fix(ci): make MCP audit writeback repeatable

This commit is contained in:
ogt
2026-07-15 11:27:36 +08:00
parent 5eb46f9421
commit dd32cbf9cd
8 changed files with 539 additions and 69 deletions

View File

@@ -22,6 +22,8 @@ on:
- scripts/security/verify_external_mcp_artifact_receipt.py
- scripts/security/tests/test_external_mcp_artifact_controller.py
- scripts/security/tests/test_verify_external_mcp_artifact_receipt.py
- scripts/ci/write-gitea-audit-receipts.sh
- scripts/ci/tests/test_write_gitea_audit_receipts.py
- .gitea/workflows/mcp-external-artifact-mirror.yaml
concurrency:
@@ -68,9 +70,11 @@ jobs:
scripts/security/external_mcp_artifact_controller.py \
scripts/security/verify_external_mcp_artifact_receipt.py \
scripts/security/tests/test_external_mcp_artifact_controller.py \
scripts/security/tests/test_verify_external_mcp_artifact_receipt.py
scripts/security/tests/test_verify_external_mcp_artifact_receipt.py \
scripts/ci/tests/test_write_gitea_audit_receipts.py
python3 scripts/security/tests/test_external_mcp_artifact_controller.py
python3 scripts/security/tests/test_verify_external_mcp_artifact_receipt.py
python3 scripts/ci/tests/test_write_gitea_audit_receipts.py
python3 - <<'PY'
import json
from pathlib import Path
@@ -194,14 +198,11 @@ jobs:
set +x
test -s "${CONTROLLER_RECEIPT_PATH}"
test -s "${VERIFIER_RECEIPT_PATH}"
WRITEBACK_DIR="$(mktemp -d)"
cp "${CONTROLLER_RECEIPT_PATH}" "${WRITEBACK_DIR}/controller.json"
cp "${VERIFIER_RECEIPT_PATH}" "${WRITEBACK_DIR}/verifier.json"
git config user.email "mcp-artifact-controller@awoooi.internal"
git config user.name "AWOOOI MCP Artifact Controller"
git add "${CONTROLLER_RECEIPT_PATH}" "${VERIFIER_RECEIPT_PATH}"
git diff --cached --quiet && {
echo "receipt_writeback=no_change"
exit 0
}
git commit -m "chore(mcp): record verified Playwright artifact mirror [skip ci] [metadata-only]"
ASKPASS_PATH="$(mktemp)"
export ASKPASS_PATH
@@ -223,31 +224,16 @@ jobs:
PY
cleanup_push_auth() {
rm -f "${ASKPASS_PATH}"
rm -rf "${WRITEBACK_DIR}"
}
trap cleanup_push_auth EXIT
export GIT_ASKPASS="${ASKPASS_PATH}"
export GIT_TERMINAL_PROMPT=0
git remote remove gitea 2>/dev/null || true
git remote add gitea "${GITEA_SOURCE_URL}"
for attempt in 1 2 3; do
if git ls-remote --exit-code --heads gitea \
"refs/heads/${RECEIPT_BRANCH}" >/dev/null 2>&1; then
git fetch --no-tags --depth=100 gitea "${RECEIPT_BRANCH}"
if ! git merge --no-edit FETCH_HEAD; then
git merge --abort || true
echo "BLOCKER receipt_writeback_merge_conflict"
exit 1
fi
fi
if git push gitea "HEAD:refs/heads/${RECEIPT_BRANCH}"; then
echo "receipt_writeback=verified_audit_branch_normal_push"
echo "receipt_branch=${RECEIPT_BRANCH}"
echo "receipt_commit_sha=$(git rev-parse HEAD)"
exit 0
fi
echo "receipt_writeback_retry=${attempt}"
sleep 5
done
echo "BLOCKER receipt_writeback_non_fast_forward_after_bounded_retry"
exit 1
export GITEA_AUDIT_REMOTE_URL="${GITEA_SOURCE_URL}"
export GITEA_AUDIT_BRANCH="${RECEIPT_BRANCH}"
export GITEA_AUDIT_CONTROLLER_SOURCE="${WRITEBACK_DIR}/controller.json"
export GITEA_AUDIT_VERIFIER_SOURCE="${WRITEBACK_DIR}/verifier.json"
export GITEA_AUDIT_CONTROLLER_TARGET="${CONTROLLER_RECEIPT_PATH}"
export GITEA_AUDIT_VERIFIER_TARGET="${VERIFIER_RECEIPT_PATH}"
export GITEA_AUDIT_COMMIT_MESSAGE="chore(mcp): record verified Playwright artifact mirror [skip ci] [metadata-only]"
bash scripts/ci/write-gitea-audit-receipts.sh

View File

@@ -22,6 +22,8 @@ on:
- scripts/security/verify_external_mcp_replay_receipt.py
- scripts/security/tests/test_external_mcp_replay_controller.py
- scripts/security/tests/test_verify_external_mcp_replay_receipt.py
- scripts/ci/write-gitea-audit-receipts.sh
- scripts/ci/tests/test_write_gitea_audit_receipts.py
- .gitea/workflows/mcp-external-replay.yaml
concurrency:
@@ -76,9 +78,11 @@ jobs:
scripts/security/external_mcp_replay_controller.py \
scripts/security/verify_external_mcp_replay_receipt.py \
scripts/security/tests/test_external_mcp_replay_controller.py \
scripts/security/tests/test_verify_external_mcp_replay_receipt.py
scripts/security/tests/test_verify_external_mcp_replay_receipt.py \
scripts/ci/tests/test_write_gitea_audit_receipts.py
python3 scripts/security/tests/test_external_mcp_replay_controller.py
python3 scripts/security/tests/test_verify_external_mcp_replay_receipt.py
python3 scripts/ci/tests/test_write_gitea_audit_receipts.py
python3 - <<'PY'
import json
from pathlib import Path
@@ -267,16 +271,11 @@ jobs:
set +x
test -s "${REPLAY_CONTROLLER_RECEIPT_PATH}"
test -s "${REPLAY_VERIFIER_RECEIPT_PATH}"
WRITEBACK_DIR="$(mktemp -d)"
cp "${REPLAY_CONTROLLER_RECEIPT_PATH}" "${WRITEBACK_DIR}/controller.json"
cp "${REPLAY_VERIFIER_RECEIPT_PATH}" "${WRITEBACK_DIR}/verifier.json"
git config user.email "mcp-replay-controller@awoooi.internal"
git config user.name "AWOOOI MCP Replay Controller"
git add \
"${REPLAY_CONTROLLER_RECEIPT_PATH}" \
"${REPLAY_VERIFIER_RECEIPT_PATH}"
git diff --cached --quiet && {
echo "receipt_writeback=no_change"
exit 0
}
git commit -m "chore(mcp): record verified Playwright protocol replay [skip ci] [metadata-only]"
ASKPASS_PATH="$(mktemp)"
export ASKPASS_PATH
@@ -298,31 +297,16 @@ jobs:
PY
cleanup_push_auth() {
rm -f "${ASKPASS_PATH}"
rm -rf "${WRITEBACK_DIR}"
}
trap cleanup_push_auth EXIT
export GIT_ASKPASS="${ASKPASS_PATH}"
export GIT_TERMINAL_PROMPT=0
git remote remove gitea 2>/dev/null || true
git remote add gitea "${GITEA_SOURCE_URL}"
for attempt in 1 2 3; do
if git ls-remote --exit-code --heads gitea \
"refs/heads/${REPLAY_RECEIPT_BRANCH}" >/dev/null 2>&1; then
git fetch --no-tags --depth=100 gitea "${REPLAY_RECEIPT_BRANCH}"
if ! git merge --no-edit FETCH_HEAD; then
git merge --abort || true
echo "BLOCKER receipt_writeback_merge_conflict"
exit 1
fi
fi
if git push gitea "HEAD:refs/heads/${REPLAY_RECEIPT_BRANCH}"; then
echo "receipt_writeback=verified_audit_branch_normal_push"
echo "receipt_branch=${REPLAY_RECEIPT_BRANCH}"
echo "receipt_commit_sha=$(git rev-parse HEAD)"
exit 0
fi
echo "receipt_writeback_retry=${attempt}"
sleep 5
done
echo "BLOCKER receipt_writeback_non_fast_forward_after_bounded_retry"
exit 1
export GITEA_AUDIT_REMOTE_URL="${GITEA_SOURCE_URL}"
export GITEA_AUDIT_BRANCH="${REPLAY_RECEIPT_BRANCH}"
export GITEA_AUDIT_CONTROLLER_SOURCE="${WRITEBACK_DIR}/controller.json"
export GITEA_AUDIT_VERIFIER_SOURCE="${WRITEBACK_DIR}/verifier.json"
export GITEA_AUDIT_CONTROLLER_TARGET="${REPLAY_CONTROLLER_RECEIPT_PATH}"
export GITEA_AUDIT_VERIFIER_TARGET="${REPLAY_VERIFIER_RECEIPT_PATH}"
export GITEA_AUDIT_COMMIT_MESSAGE="chore(mcp): record verified Playwright protocol replay [skip ci] [metadata-only]"
bash scripts/ci/write-gitea-audit-receipts.sh

View File

@@ -10,16 +10,19 @@
**source/test evidence**
- controller/verifier 本機真實雙 replay 皆通過MCP `Playwright@1.62.0-alpha-1783623505000`、protocol `2025-06-18`、24 tools、tool-name hash `12d23fea9d8d1a1de3f44863dc00cf393f0a2165323838cf93fed30a6a4cc237`、schema hash `c3737ced21147d0512b0400df5885c95a545bc915892b5c8f928cee78428ac99`,兩次均 clean exit/container removed。
- 新 replay tests `37 passed`artifact + replay supply-chain tests與 MCP control-plane/federation/version lifecycle/runner-health 聚焦回歸合計 `89 passed`。Ruff、py_compile、JSON/YAML parse、source-control owner guard與 `git diff --check` 通過。
- artifact/replay controller + independent verifier `61 passed`MCP control-plane/federation/version lifecycle/runner-health API `27 passed`、shared audit writeback `4 passed`,本輪聚焦回歸合計 `92 passed`。Ruff、py_compile、JSON/YAML parse、source-control owner guard與 `git diff --check` 通過。
- Gitea replay `#5183` 已在 source `387f841...` 完成 controller + independent verifierrun `d3b8d15e-e5c6-43c0-b657-f9f23089677c`audit commit `281d6e0058721dfbf84235d85c2f894a004ceea1`24 tools、offline clean exit、container removal與所有 runtime/write boundary false 均有 durable receipt。
- 後續 replay `#5186` 在 source `5eb46f9...` 的 controller/verifier 仍成功,但 final writeback 因 shallow main 與 audit branch `unrelated histories` 失敗scheduled artifact `#5178` 有相同 terminal。根因不是 MCP replay 或 artifact verification而是舊 workflow 把 audit branch merge 回 depth-1 main checkout。
- 新增共用 fail-closed audit writeback直接 checkout audit branch tip、只 stage兩個 allowlisted receipt、同 run identity與 branch/path/schema 綁定、最多 3 次 normal fast-forward push無 merge、無 force。真實 temporary bare-Git functional tests涵蓋連續第二/第三次線性 push與拒絕 branch/path/identity mismatch`4 passed`
- global security mirror guard另抓到 latest main 既有 `apps/web/src/app/[locale]/iwooos/page.tsx``raw_blocked_waiting_state` 敏感字樣;本 work item未修改該檔保留為獨立 drift blocker不以 replay source 綠燈覆蓋。
**尚未完成 / 不誤報**
- 目前是 source + 本機真實 replay證據Gitea replay workflow、audit branch receipt、main CD/deploy marker與 production runtime/UI readback尚未產生因此 production compatibility仍不得標 completed。
- `#5183` 證明 protocol/schema replay本身與 durable receipt可成立但最新 source `5eb46f9...` 的 repeatability writeback修正尚未 normal push / Gitea workflow terminalartifact/replay recurring lanes仍要各取得一筆新的 success terminalproduction catalog也尚未讀取該 receipt因此不得把 compatibility或週期升級 loop標 completed。
- Browser binary/runtime immutable supply chain、public-origin egress proxy與 private-network denial、prompt-injection replay、accessibility/latency shadow、canary、Gateway adapter registration、rollback execution與正式 KM/RAG learning acknowledgement仍是 promotion blocker。
- 未讀 secret value、`.env`、raw session、SQLite/auth未連線或下載 GitHub/Actions/hosted artifact未使用 `npx -y``@latest``:latest`、force push、外部 RAG write或 production mutation。
**下一步**
- normal push Gitea main並取得 replay workflow controller + standalone verifier audit receipts;再更新 MCP production read model/UI為 compatibility verified但 promotion disabled之後才建立 immutable browser runtime與隔離 public-origin shadow/canary/rollback lane。
- normal push shared writeback修正到 Gitea main要求 artifact與 replay recurring workflow各自取得線性 audit commit;再更新 MCP production read model/UI為 compatibility verified但 promotion disabled之後才建立 immutable browser runtime與隔離 public-origin shadow/canary/rollback lane。
## 2026-07-15 — P0 MCP Playwright immutable mirror 與獨立供應鏈 verifier

View File

@@ -30,6 +30,7 @@
"docs/evaluations/ai_agent_version_lifecycle_update_proposal_2026-07-10.json",
"scripts/ci/check-gitea-step-env-secrets.js",
"scripts/ci/cleanup-host-runner-workspace.sh",
"scripts/ci/write-gitea-audit-receipts.sh",
"scripts/ci/wait-host-web-build-pressure.sh",
"scripts/ci/notify-awoooi-cicd.sh",
"scripts/setup-runner-watchdog.sh",
@@ -367,6 +368,7 @@
"config/mcp/playwright-mcp-artifact-policy.json",
"scripts/security/external_mcp_artifact_controller.py",
"scripts/security/verify_external_mcp_artifact_receipt.py",
"scripts/ci/write-gitea-audit-receipts.sh",
"scripts/ci/wait-host-web-build-pressure.sh"
],
"next_action": "維持 bounded capacity wait、獨立 verifier 與 mcp-artifact-receipts audit branch不得啟動 artifact、寫 RAG、切 production route 或推回 deploy main。"
@@ -397,6 +399,7 @@
"config/mcp/playwright-mcp-replay-policy.json",
"scripts/security/external_mcp_replay_controller.py",
"scripts/security/verify_external_mcp_replay_receipt.py",
"scripts/ci/write-gitea-audit-receipts.sh",
"scripts/ci/wait-host-web-build-pressure.sh"
],
"next_action": "先取得 mcp-replay-receipts 獨立 verifier receiptbrowser runtime、public-origin egress proxy、shadow、canary、Gateway adapter registration、RAG 與 production route 仍保持停用。"

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""Functional tests for linear Gitea audit-branch receipt writeback."""
from __future__ import annotations
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "scripts/ci/write-gitea-audit-receipts.sh"
CONTROLLER_TARGET = (
"docs/operations/external-mcp-replays/fixture-controller.snapshot.json"
)
VERIFIER_TARGET = (
"docs/operations/external-mcp-replays/fixture-verifier.snapshot.json"
)
def _run(command: list[str], *, cwd: Path, env: dict[str, str] | None = None) -> str:
completed = subprocess.run(
command,
cwd=cwd,
env=env,
check=False,
capture_output=True,
text=True,
timeout=30,
)
if completed.returncode != 0:
raise AssertionError(
f"command failed: {command}\nstdout={completed.stdout}\nstderr={completed.stderr}"
)
return completed.stdout.strip()
def _receipt(schema: str, run_id: str) -> dict:
return {
"schema_version": schema,
"run_id": run_id,
"trace_id": f"mcp-replay-{run_id}",
"work_item_id": "MCP-REPLAY-PLAYWRIGHT-001",
"status": "fixture_verified",
}
def _write_receipts(directory: Path, run_id: str) -> tuple[Path, Path]:
controller = directory / "controller.json"
verifier = directory / "verifier.json"
controller.write_text(
json.dumps(
_receipt("awoooi_external_mcp_replay_receipt_v1", run_id),
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
verifier.write_text(
json.dumps(
_receipt("awoooi_external_mcp_replay_verifier_receipt_v1", run_id),
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
return controller, verifier
class AuditReceiptWritebackTests(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.remote = self.root / "remote.git"
self.seed = self.root / "seed"
_run(["git", "init", "--bare", str(self.remote)], cwd=self.root)
self.seed.mkdir()
_run(["git", "init"], cwd=self.seed)
_run(["git", "config", "user.email", "fixture@awoooi.internal"], cwd=self.seed)
_run(["git", "config", "user.name", "Fixture"], cwd=self.seed)
(self.seed / "base.txt").write_text("base\n", encoding="utf-8")
_run(["git", "add", "base.txt"], cwd=self.seed)
_run(["git", "commit", "-m", "base"], cwd=self.seed)
_run(["git", "branch", "-M", "main"], cwd=self.seed)
_run(["git", "remote", "add", "origin", f"file://{self.remote}"], cwd=self.seed)
_run(["git", "push", "origin", "main"], cwd=self.seed)
_run(["git", "checkout", "-b", "mcp-replay-receipts"], cwd=self.seed)
initial_controller, initial_verifier = _write_receipts(
self.seed, "00000000-0000-4000-8000-000000000000"
)
(self.seed / CONTROLLER_TARGET).parent.mkdir(parents=True)
(self.seed / CONTROLLER_TARGET).write_bytes(initial_controller.read_bytes())
(self.seed / VERIFIER_TARGET).write_bytes(initial_verifier.read_bytes())
_run(["git", "add", "docs"], cwd=self.seed)
_run(["git", "commit", "-m", "initial audit receipt"], cwd=self.seed)
_run(["git", "push", "origin", "mcp-replay-receipts"], cwd=self.seed)
self.initial_audit_sha = _run(["git", "rev-parse", "HEAD"], cwd=self.seed)
_run(["git", "checkout", "main"], cwd=self.seed)
(self.seed / "main-only.txt").write_text("new main\n", encoding="utf-8")
_run(["git", "add", "main-only.txt"], cwd=self.seed)
_run(["git", "commit", "-m", "advance main"], cwd=self.seed)
_run(["git", "push", "origin", "main"], cwd=self.seed)
def tearDown(self) -> None:
self.temp.cleanup()
def _fresh_main_clone(self, name: str) -> Path:
clone = self.root / name
_run(
[
"git",
"clone",
"--depth=1",
"--branch",
"main",
f"file://{self.remote}",
str(clone),
],
cwd=self.root,
)
_run(["git", "config", "user.email", "fixture@awoooi.internal"], cwd=clone)
_run(["git", "config", "user.name", "Fixture"], cwd=clone)
return clone
def _apply(self, clone: Path, run_id: str) -> str:
source_dir = self.root / f"source-{run_id}"
source_dir.mkdir()
controller, verifier = _write_receipts(source_dir, run_id)
env = {
**os.environ,
"GIT_TERMINAL_PROMPT": "0",
"GITEA_AUDIT_TEST_FILE_REMOTE": "1",
"GITEA_AUDIT_REMOTE_URL": f"file://{self.remote}",
"GITEA_AUDIT_BRANCH": "mcp-replay-receipts",
"GITEA_AUDIT_CONTROLLER_SOURCE": str(controller),
"GITEA_AUDIT_VERIFIER_SOURCE": str(verifier),
"GITEA_AUDIT_CONTROLLER_TARGET": CONTROLLER_TARGET,
"GITEA_AUDIT_VERIFIER_TARGET": VERIFIER_TARGET,
"GITEA_AUDIT_COMMIT_MESSAGE": "fixture audit receipt",
}
return _run(["bash", str(SCRIPT)], cwd=clone, env=env)
def test_second_and_third_writebacks_are_linear_normal_pushes(self) -> None:
first_clone = self._fresh_main_clone("clone-one")
first_output = self._apply(
first_clone, "11111111-1111-4111-8111-111111111111"
)
self.assertIn("receipt_writeback=verified_audit_branch_normal_push", first_output)
first_sha = _run(
["git", "--git-dir", str(self.remote), "rev-parse", "mcp-replay-receipts"],
cwd=self.root,
)
first_parent = _run(
["git", "--git-dir", str(self.remote), "rev-parse", f"{first_sha}^"],
cwd=self.root,
)
self.assertEqual(first_parent, self.initial_audit_sha)
second_clone = self._fresh_main_clone("clone-two")
second_output = self._apply(
second_clone, "22222222-2222-4222-8222-222222222222"
)
self.assertIn("receipt_writeback=verified_audit_branch_normal_push", second_output)
second_sha = _run(
["git", "--git-dir", str(self.remote), "rev-parse", "mcp-replay-receipts"],
cwd=self.root,
)
second_parent = _run(
["git", "--git-dir", str(self.remote), "rev-parse", f"{second_sha}^"],
cwd=self.root,
)
self.assertEqual(second_parent, first_sha)
latest = _run(
[
"git",
"--git-dir",
str(self.remote),
"show",
f"mcp-replay-receipts:{CONTROLLER_TARGET}",
],
cwd=self.root,
)
self.assertEqual(
json.loads(latest)["run_id"],
"22222222-2222-4222-8222-222222222222",
)
def test_non_allowlisted_branch_is_rejected_without_push(self) -> None:
clone = self._fresh_main_clone("clone-rejected")
source_dir = self.root / "source-rejected"
source_dir.mkdir()
controller, verifier = _write_receipts(
source_dir, "33333333-3333-4333-8333-333333333333"
)
env = {
**os.environ,
"GITEA_AUDIT_TEST_FILE_REMOTE": "1",
"GITEA_AUDIT_REMOTE_URL": f"file://{self.remote}",
"GITEA_AUDIT_BRANCH": "main",
"GITEA_AUDIT_CONTROLLER_SOURCE": str(controller),
"GITEA_AUDIT_VERIFIER_SOURCE": str(verifier),
"GITEA_AUDIT_CONTROLLER_TARGET": CONTROLLER_TARGET,
"GITEA_AUDIT_VERIFIER_TARGET": VERIFIER_TARGET,
"GITEA_AUDIT_COMMIT_MESSAGE": "must not commit",
}
completed = subprocess.run(
["bash", str(SCRIPT)],
cwd=clone,
env=env,
check=False,
capture_output=True,
text=True,
timeout=30,
)
self.assertEqual(completed.returncode, 2)
self.assertIn("audit_writeback_branch_not_allowlisted", completed.stdout)
def test_branch_and_receipt_directory_must_match(self) -> None:
clone = self._fresh_main_clone("clone-mismatched-target")
source_dir = self.root / "source-mismatched-target"
source_dir.mkdir()
controller, verifier = _write_receipts(
source_dir, "44444444-4444-4444-8444-444444444444"
)
env = {
**os.environ,
"GITEA_AUDIT_TEST_FILE_REMOTE": "1",
"GITEA_AUDIT_REMOTE_URL": f"file://{self.remote}",
"GITEA_AUDIT_BRANCH": "mcp-artifact-receipts",
"GITEA_AUDIT_CONTROLLER_SOURCE": str(controller),
"GITEA_AUDIT_VERIFIER_SOURCE": str(verifier),
"GITEA_AUDIT_CONTROLLER_TARGET": CONTROLLER_TARGET,
"GITEA_AUDIT_VERIFIER_TARGET": VERIFIER_TARGET,
"GITEA_AUDIT_COMMIT_MESSAGE": "must not commit",
}
completed = subprocess.run(
["bash", str(SCRIPT)],
cwd=clone,
env=env,
check=False,
capture_output=True,
text=True,
timeout=30,
)
self.assertEqual(completed.returncode, 2)
self.assertIn("audit_writeback_branch_target_mismatch", completed.stdout)
def test_controller_and_verifier_identity_must_match(self) -> None:
clone = self._fresh_main_clone("clone-mismatched-identity")
source_dir = self.root / "source-mismatched-identity"
source_dir.mkdir()
controller, verifier = _write_receipts(
source_dir, "55555555-5555-4555-8555-555555555555"
)
verifier_payload = json.loads(verifier.read_text(encoding="utf-8"))
verifier_payload["run_id"] = "66666666-6666-4666-8666-666666666666"
verifier.write_text(
json.dumps(verifier_payload, sort_keys=True) + "\n", encoding="utf-8"
)
env = {
**os.environ,
"GITEA_AUDIT_TEST_FILE_REMOTE": "1",
"GITEA_AUDIT_REMOTE_URL": f"file://{self.remote}",
"GITEA_AUDIT_BRANCH": "mcp-replay-receipts",
"GITEA_AUDIT_CONTROLLER_SOURCE": str(controller),
"GITEA_AUDIT_VERIFIER_SOURCE": str(verifier),
"GITEA_AUDIT_CONTROLLER_TARGET": CONTROLLER_TARGET,
"GITEA_AUDIT_VERIFIER_TARGET": VERIFIER_TARGET,
"GITEA_AUDIT_COMMIT_MESSAGE": "must not commit",
}
completed = subprocess.run(
["bash", str(SCRIPT)],
cwd=clone,
env=env,
check=False,
capture_output=True,
text=True,
timeout=30,
)
self.assertEqual(completed.returncode, 2)
self.assertIn("audit_writeback_receipt_identity_mismatch", completed.stdout)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,188 @@
#!/usr/bin/env bash
set -euo pipefail
set +x
# Append two verified JSON receipts to a dedicated Gitea audit branch without
# merging the shallow workflow checkout. Authentication is supplied by the
# caller through GIT_ASKPASS; this script never accepts a token argument.
required_env=(
GITEA_AUDIT_REMOTE_URL
GITEA_AUDIT_BRANCH
GITEA_AUDIT_CONTROLLER_SOURCE
GITEA_AUDIT_VERIFIER_SOURCE
GITEA_AUDIT_CONTROLLER_TARGET
GITEA_AUDIT_VERIFIER_TARGET
GITEA_AUDIT_COMMIT_MESSAGE
)
for name in "${required_env[@]}"; do
if [ -z "${!name:-}" ]; then
echo "BLOCKER audit_writeback_missing_${name}"
exit 2
fi
done
if [ "${GITEA_AUDIT_REMOTE_URL}" = "http://192.168.0.110:3001/wooo/awoooi.git" ]; then
if [ -z "${GIT_ASKPASS:-}" ] || [ ! -x "${GIT_ASKPASS}" ]; then
echo "BLOCKER audit_writeback_askpass_missing_or_not_executable"
exit 2
fi
elif [[ "${GITEA_AUDIT_REMOTE_URL}" == file://* ]] && \
[ "${GITEA_AUDIT_TEST_FILE_REMOTE:-0}" = "1" ]; then
:
else
echo "BLOCKER audit_writeback_remote_not_allowlisted"
exit 2
fi
if ! [[ "${GITEA_AUDIT_BRANCH}" =~ ^mcp-(artifact|replay)-receipts$ ]]; then
echo "BLOCKER audit_writeback_branch_not_allowlisted"
exit 2
fi
if ! [[ "${GITEA_AUDIT_CONTROLLER_TARGET}" =~ ^docs/operations/external-mcp-(artifacts|replays)/[A-Za-z0-9._-]+-controller\.snapshot\.json$ ]]; then
echo "BLOCKER audit_writeback_controller_target_not_allowlisted"
exit 2
fi
if ! [[ "${GITEA_AUDIT_VERIFIER_TARGET}" =~ ^docs/operations/external-mcp-(artifacts|replays)/[A-Za-z0-9._-]+-verifier\.snapshot\.json$ ]]; then
echo "BLOCKER audit_writeback_verifier_target_not_allowlisted"
exit 2
fi
if [ "$(dirname "${GITEA_AUDIT_CONTROLLER_TARGET}")" != "$(dirname "${GITEA_AUDIT_VERIFIER_TARGET}")" ]; then
echo "BLOCKER audit_writeback_target_directory_mismatch"
exit 2
fi
case "${GITEA_AUDIT_BRANCH}" in
mcp-artifact-receipts)
expected_target_directory="docs/operations/external-mcp-artifacts"
expected_controller_schema="awoooi_external_mcp_artifact_receipt_v1"
expected_verifier_schema="awoooi_external_mcp_artifact_verifier_receipt_v1"
;;
mcp-replay-receipts)
expected_target_directory="docs/operations/external-mcp-replays"
expected_controller_schema="awoooi_external_mcp_replay_receipt_v1"
expected_verifier_schema="awoooi_external_mcp_replay_verifier_receipt_v1"
;;
esac
if [ "$(dirname "${GITEA_AUDIT_CONTROLLER_TARGET}")" != "${expected_target_directory}" ]; then
echo "BLOCKER audit_writeback_branch_target_mismatch"
exit 2
fi
if [ "${#GITEA_AUDIT_COMMIT_MESSAGE}" -gt 200 ] || \
[[ "${GITEA_AUDIT_COMMIT_MESSAGE}" == *$'\n'* ]] || \
[[ "${GITEA_AUDIT_COMMIT_MESSAGE}" == *$'\r'* ]]; then
echo "BLOCKER audit_writeback_commit_message_invalid"
exit 2
fi
for source_path in \
"${GITEA_AUDIT_CONTROLLER_SOURCE}" \
"${GITEA_AUDIT_VERIFIER_SOURCE}"; do
if [ ! -f "${source_path}" ] || [ -L "${source_path}" ]; then
echo "BLOCKER audit_writeback_source_invalid"
exit 2
fi
source_size="$(wc -c < "${source_path}" | tr -d ' ')"
if [ "${source_size}" -le 0 ] || [ "${source_size}" -gt 4194304 ]; then
echo "BLOCKER audit_writeback_source_size_invalid"
exit 2
fi
done
python3 - \
"${GITEA_AUDIT_CONTROLLER_SOURCE}" \
"${GITEA_AUDIT_VERIFIER_SOURCE}" \
"${expected_controller_schema}" \
"${expected_verifier_schema}" <<'PY'
import json
import sys
from pathlib import Path
def fail(message: str) -> None:
print(message)
raise SystemExit(2)
try:
receipts = [
json.loads(Path(value).read_text(encoding="utf-8")) for value in sys.argv[1:3]
]
except (OSError, UnicodeError, json.JSONDecodeError):
fail("BLOCKER audit_writeback_receipt_json_invalid")
expected_schemas = sys.argv[3:5]
identity_fields = ("run_id", "trace_id", "work_item_id")
for payload, expected_schema in zip(receipts, expected_schemas):
if not isinstance(payload, dict) or payload.get("schema_version") != expected_schema:
fail("BLOCKER audit_writeback_receipt_schema_invalid")
if any(not str(payload.get(field) or "") for field in identity_fields):
fail("BLOCKER audit_writeback_receipt_identity_missing")
if any(receipts[0][field] != receipts[1][field] for field in identity_fields):
fail("BLOCKER audit_writeback_receipt_identity_mismatch")
PY
remote_name="gitea-audit"
git remote remove "${remote_name}" >/dev/null 2>&1 || true
git remote add "${remote_name}" "${GITEA_AUDIT_REMOTE_URL}"
rm -f \
"${GITEA_AUDIT_CONTROLLER_TARGET}" \
"${GITEA_AUDIT_VERIFIER_TARGET}"
for attempt in 1 2 3; do
if git ls-remote --exit-code --heads "${remote_name}" \
"refs/heads/${GITEA_AUDIT_BRANCH}" >/dev/null 2>&1; then
git fetch --no-tags --depth=1 "${remote_name}" "${GITEA_AUDIT_BRANCH}"
git checkout --force --detach FETCH_HEAD
fi
target_directory="$(dirname "${GITEA_AUDIT_CONTROLLER_TARGET}")"
for ancestor in docs docs/operations "${target_directory}"; do
if [ -L "${ancestor}" ]; then
echo "BLOCKER audit_writeback_target_symlink_rejected"
exit 2
fi
done
mkdir -p "${target_directory}"
for target_path in \
"${GITEA_AUDIT_CONTROLLER_TARGET}" \
"${GITEA_AUDIT_VERIFIER_TARGET}"; do
if [ -L "${target_path}" ]; then
echo "BLOCKER audit_writeback_target_symlink_rejected"
exit 2
fi
done
install -m 0644 \
"${GITEA_AUDIT_CONTROLLER_SOURCE}" \
"${GITEA_AUDIT_CONTROLLER_TARGET}"
install -m 0644 \
"${GITEA_AUDIT_VERIFIER_SOURCE}" \
"${GITEA_AUDIT_VERIFIER_TARGET}"
git add \
"${GITEA_AUDIT_CONTROLLER_TARGET}" \
"${GITEA_AUDIT_VERIFIER_TARGET}"
while IFS= read -r staged_path; do
if [ "${staged_path}" != "${GITEA_AUDIT_CONTROLLER_TARGET}" ] && \
[ "${staged_path}" != "${GITEA_AUDIT_VERIFIER_TARGET}" ]; then
echo "BLOCKER audit_writeback_unexpected_staged_path"
exit 2
fi
done < <(git diff --cached --name-only)
if git diff --cached --quiet; then
echo "receipt_writeback=no_change"
echo "receipt_branch=${GITEA_AUDIT_BRANCH}"
echo "receipt_commit_sha=$(git rev-parse HEAD)"
exit 0
fi
git commit -m "${GITEA_AUDIT_COMMIT_MESSAGE}"
if git push "${remote_name}" "HEAD:refs/heads/${GITEA_AUDIT_BRANCH}"; then
echo "receipt_writeback=verified_audit_branch_normal_push"
echo "receipt_branch=${GITEA_AUDIT_BRANCH}"
echo "receipt_commit_sha=$(git rev-parse HEAD)"
exit 0
fi
echo "receipt_writeback_retry=${attempt}"
sleep 5
done
echo "BLOCKER receipt_writeback_non_fast_forward_after_bounded_retry"
exit 1

View File

@@ -17,6 +17,7 @@ ROOT = Path(__file__).resolve().parents[3]
CONTROLLER_PATH = ROOT / "scripts/security/external_mcp_artifact_controller.py"
POLICY_PATH = ROOT / "config/mcp/playwright-mcp-artifact-policy.json"
WORKFLOW_PATH = ROOT / ".gitea/workflows/mcp-external-artifact-mirror.yaml"
AUDIT_WRITEBACK_PATH = ROOT / "scripts/ci/write-gitea-audit-receipts.sh"
RUN_ID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
TRACE_ID = f"mcp-artifact-{RUN_ID}"
@@ -130,8 +131,14 @@ class ExternalMcpArtifactControllerTest(unittest.TestCase):
self.assertIn("docker logout", raw)
self.assertIn("verify_external_mcp_artifact_receipt.py", raw)
self.assertIn("RECEIPT_BRANCH: mcp-artifact-receipts", raw)
self.assertIn('git merge --no-edit FETCH_HEAD', raw)
self.assertIn('HEAD:refs/heads/${RECEIPT_BRANCH}', raw)
self.assertIn("bash scripts/ci/write-gitea-audit-receipts.sh", raw)
writeback = AUDIT_WRITEBACK_PATH.read_text(encoding="utf-8")
self.assertIn("git checkout --force --detach FETCH_HEAD", writeback)
self.assertNotIn("git merge --no-edit FETCH_HEAD", writeback)
self.assertIn(
'git push "${remote_name}" "HEAD:refs/heads/${GITEA_AUDIT_BRANCH}"',
writeback,
)
self.assertNotIn("git push gitea HEAD:main", raw)
self.assertIn('cron: "13 2 * * 3"', raw)

View File

@@ -19,6 +19,7 @@ CONTROLLER_PATH = ROOT / "scripts/security/external_mcp_replay_controller.py"
POLICY_PATH = ROOT / "config/mcp/playwright-mcp-replay-policy.json"
ARTIFACT_POLICY_PATH = ROOT / "config/mcp/playwright-mcp-artifact-policy.json"
WORKFLOW_PATH = ROOT / ".gitea/workflows/mcp-external-replay.yaml"
AUDIT_WRITEBACK_PATH = ROOT / "scripts/ci/write-gitea-audit-receipts.sh"
SPEC = importlib.util.spec_from_file_location("external_mcp_replay_controller", CONTROLLER_PATH)
assert SPEC and SPEC.loader
@@ -389,6 +390,14 @@ class ReceiptAndWorkflowTests(unittest.TestCase):
self.assertIn("wait-host-web-build-pressure.sh", raw)
self.assertIn("mcp-replay-receipts", raw)
self.assertIn("verify_external_mcp_replay_receipt.py", raw)
self.assertIn("bash scripts/ci/write-gitea-audit-receipts.sh", raw)
writeback = AUDIT_WRITEBACK_PATH.read_text(encoding="utf-8").lower()
self.assertIn("git checkout --force --detach fetch_head", writeback)
self.assertNotIn("git merge --no-edit fetch_head", writeback)
self.assertIn(
'git push "${remote_name}" "head:refs/heads/${gitea_audit_branch}"',
writeback,
)
if __name__ == "__main__":