feat(mcp): add isolated protocol replay verifier

This commit is contained in:
ogt
2026-07-15 11:01:40 +08:00
parent e01db7391e
commit 7e098d8f66
9 changed files with 3376 additions and 6 deletions

View File

@@ -0,0 +1,328 @@
# AWOOOI external MCP offline protocol/schema replay.
#
# This Gitea-only high-risk controlled-apply lane pins the verified artifact
# receipt commit, artifact digest, and runtime digest. It starts only the MCP
# stdio process with network none, calls initialize and tools/list, runs a fresh
# standalone verifier, then writes receipts to a dedicated Gitea audit branch.
# Browser start, MCP tool calls, Gateway registration, RAG writes, production
# writes, shadow, canary, and promotion remain disabled.
name: MCP External Protocol Replay
on:
workflow_dispatch:
schedule:
- cron: "43 2 * * 3" # Wednesday 10:43 Asia/Taipei, after artifact mirror
push:
branches:
- main
paths:
- config/mcp/playwright-mcp-replay-policy.json
- 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
- .gitea/workflows/mcp-external-replay.yaml
concurrency:
group: awoooi-mcp-external-protocol-replay
cancel-in-progress: true
env:
GITEA_SOURCE_URL: http://192.168.0.110:3001/wooo/awoooi.git
HARBOR_REGISTRY: registry.wooo.work
POLICY_PATH: config/mcp/playwright-mcp-replay-policy.json
ARTIFACT_POLICY_PATH: config/mcp/playwright-mcp-artifact-policy.json
ARTIFACT_RECEIPT_BRANCH: mcp-artifact-receipts
ARTIFACT_RECEIPT_COMMIT: 9c08ada5c0f15bb6924304af29cd13c8376182ff
ARTIFACT_CONTROLLER_SOURCE_PATH: docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-controller.snapshot.json
ARTIFACT_VERIFIER_SOURCE_PATH: docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-verifier.snapshot.json
ARTIFACT_CONTROLLER_RECEIPT_PATH: /tmp/awoooi-mcp-replay/artifact-controller.json
ARTIFACT_VERIFIER_RECEIPT_PATH: /tmp/awoooi-mcp-replay/artifact-verifier.json
REPLAY_RECEIPT_BRANCH: mcp-replay-receipts
REPLAY_CONTROLLER_RECEIPT_PATH: docs/operations/external-mcp-replays/playwright-mcp-0.0.78-controller.snapshot.json
REPLAY_VERIFIER_RECEIPT_PATH: docs/operations/external-mcp-replays/playwright-mcp-0.0.78-verifier.snapshot.json
jobs:
replay-and-verify:
runs-on: awoooi-non110-host
timeout-minutes: 25
steps:
- name: Bootstrap bounded runner tools
run: |
set -euo pipefail
if command -v apk >/dev/null 2>&1; then
apk add --no-cache bash coreutils curl docker-cli git python3
fi
command -v bash >/dev/null
command -v curl >/dev/null
command -v docker >/dev/null
command -v git >/dev/null
command -v python3 >/dev/null
- name: Checkout exact source from Gitea
run: |
set -euo pipefail
git init .
git remote remove origin 2>/dev/null || true
git remote add origin "${GITEA_SOURCE_URL}"
git fetch --no-tags --prune --depth=1 origin "${GITHUB_SHA}"
git checkout --force -B main FETCH_HEAD
- name: Validate policy controller verifier and freeze boundaries
run: |
set -euo pipefail
python3 -m py_compile \
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
python3 scripts/security/tests/test_external_mcp_replay_controller.py
python3 scripts/security/tests/test_verify_external_mcp_replay_receipt.py
python3 - <<'PY'
import json
from pathlib import Path
paths = [
Path("config/mcp/playwright-mcp-replay-policy.json"),
Path(".gitea/workflows/mcp-external-replay.yaml"),
]
text = "\n".join(path.read_text(encoding="utf-8").lower() for path in paths)
forbidden = (
"uses" + ":",
"actions" + "/checkout",
"github" + ".com",
"api" + ".github" + ".com",
"raw" + ".githubusercontent" + ".com",
"codeload" + ".github" + ".com",
"ghcr" + ".io",
"npx" + " -y",
"@" + "latest",
":" + "latest",
)
hits = [value for value in forbidden if value in text]
if hits:
raise SystemExit(f"freeze_or_floating_reference_violation:{hits}")
policy = json.loads(paths[0].read_text(encoding="utf-8"))
if policy.get("risk_class") != "high":
raise SystemExit("replay_policy_risk_class_invalid")
if any(
value is not False
for value in (policy.get("execution_boundaries") or {}).values()
):
raise SystemExit("runtime_boundary_must_remain_disabled")
runtime_ref = (policy.get("runtime") or {}).get("image_ref", "")
artifact_ref = (policy.get("artifact_source") or {}).get("artifact_ref", "")
if "@sha256:" not in runtime_ref or "@sha256:" not in artifact_ref:
raise SystemExit("immutable_digest_pin_missing")
if (policy.get("artifact_source") or {}).get("audit_commit") != (
"9c08ada5c0f15bb6924304af29cd13c8376182ff"
):
raise SystemExit("artifact_audit_commit_drift")
print("freeze_boundary_verified=true")
print("runtime_promotion_enabled=false")
print("browser_or_tool_execution_enabled=false")
PY
git diff --check
- name: Wait for bounded host capacity
env:
HOST_WEB_BUILD_PRESSURE_ATTEMPTS: "18"
HOST_WEB_BUILD_PRESSURE_SLEEP_SECONDS: "10"
run: bash scripts/ci/wait-host-web-build-pressure.sh
- name: Read pinned artifact receipts from Gitea audit commit
run: |
set -euo pipefail
mkdir -p "$(dirname "${ARTIFACT_CONTROLLER_RECEIPT_PATH}")"
git fetch --no-tags --prune --depth=100 origin \
"refs/heads/${ARTIFACT_RECEIPT_BRANCH}:refs/remotes/origin/${ARTIFACT_RECEIPT_BRANCH}"
git cat-file -e "${ARTIFACT_RECEIPT_COMMIT}^{commit}"
git merge-base --is-ancestor \
"${ARTIFACT_RECEIPT_COMMIT}" \
"refs/remotes/origin/${ARTIFACT_RECEIPT_BRANCH}"
git show \
"${ARTIFACT_RECEIPT_COMMIT}:${ARTIFACT_CONTROLLER_SOURCE_PATH}" \
> "${ARTIFACT_CONTROLLER_RECEIPT_PATH}"
git show \
"${ARTIFACT_RECEIPT_COMMIT}:${ARTIFACT_VERIFIER_SOURCE_PATH}" \
> "${ARTIFACT_VERIFIER_RECEIPT_PATH}"
python3 - <<'PY'
import hashlib
import json
import os
from pathlib import Path
policy = json.loads(Path(os.environ["POLICY_PATH"]).read_text())
artifact = policy["artifact_source"]
rows = (
("controller_receipt_sha256", "ARTIFACT_CONTROLLER_RECEIPT_PATH"),
("verifier_receipt_sha256", "ARTIFACT_VERIFIER_RECEIPT_PATH"),
)
for policy_field, env_field in rows:
digest = hashlib.sha256(Path(os.environ[env_field]).read_bytes()).hexdigest()
if digest != artifact[policy_field]:
raise SystemExit(f"pinned_artifact_receipt_checksum_mismatch:{policy_field}")
print("artifact_receipt_commit_pinned=true")
print("artifact_receipt_checksums_verified=true")
PY
- name: Controlled offline replay and independent verifier
env:
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
run: |
set -euo pipefail
set +x
cleanup_registry_auth() {
docker logout "${HARBOR_REGISTRY}" >/dev/null 2>&1 || true
}
trap cleanup_registry_auth EXIT
registry_status="$(
curl --silent --show-error --output /dev/null \
--write-out '%{http_code}' --max-time 10 \
"https://${HARBOR_REGISTRY}/v2/" || true
)"
if [ "${registry_status}" != "200" ] && [ "${registry_status}" != "401" ]; then
echo "BLOCKER harbor_registry_readiness_status_${registry_status:-000}"
exit 1
fi
printf '%s\n' "${HARBOR_PASSWORD}" | \
docker login "${HARBOR_REGISTRY}" \
--username "${HARBOR_USERNAME}" \
--password-stdin >/dev/null
RUN_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
TRACE_ID="mcp-replay-${RUN_ID}"
python3 scripts/security/external_mcp_replay_controller.py \
--policy "${POLICY_PATH}" \
--artifact-policy "${ARTIFACT_POLICY_PATH}" \
--artifact-controller-receipt "${ARTIFACT_CONTROLLER_RECEIPT_PATH}" \
--artifact-verifier-receipt "${ARTIFACT_VERIFIER_RECEIPT_PATH}" \
--run-id "${RUN_ID}" \
--trace-id "${TRACE_ID}" \
--receipt "${REPLAY_CONTROLLER_RECEIPT_PATH}" \
replay --apply
python3 scripts/security/verify_external_mcp_replay_receipt.py \
--policy "${POLICY_PATH}" \
--artifact-controller-receipt "${ARTIFACT_CONTROLLER_RECEIPT_PATH}" \
--artifact-verifier-receipt "${ARTIFACT_VERIFIER_RECEIPT_PATH}" \
--controller-receipt "${REPLAY_CONTROLLER_RECEIPT_PATH}" \
--verifier-receipt "${REPLAY_VERIFIER_RECEIPT_PATH}"
python3 - <<'PY'
import json
import os
from pathlib import Path
controller = json.loads(
Path(os.environ["REPLAY_CONTROLLER_RECEIPT_PATH"]).read_text()
)
verifier = json.loads(
Path(os.environ["REPLAY_VERIFIER_RECEIPT_PATH"]).read_text()
)
if controller.get("status") != (
"completed_protocol_schema_replay_pending_independent_verifier"
):
raise SystemExit("controller_terminal_invalid")
if verifier.get("status") != "completed_protocol_schema_replay_verified":
raise SystemExit("independent_verifier_terminal_invalid")
if controller.get("run_id") != verifier.get("run_id"):
raise SystemExit("controller_verifier_run_id_mismatch")
if controller.get("trace_id") != verifier.get("trace_id"):
raise SystemExit("controller_verifier_trace_id_mismatch")
for payload in (controller, verifier):
if any(
payload.get(field) is not False
for field in (
"browser_started",
"tool_call_performed",
"external_rag_write_performed",
"production_write_performed",
"promotion_allowed",
)
):
raise SystemExit("replay_boundary_invalid")
post = verifier["independent_post_verifier"]
if (
post.get("status") != "passed"
or post.get("fresh_protocol_replay") is not True
or post.get("ephemeral_container_removed") is not True
):
raise SystemExit("independent_replay_verifier_invalid")
print(f"controlled_apply_run_id={verifier['run_id']}")
print(f"controlled_apply_trace_id={verifier['trace_id']}")
print(f"mcp_tool_count={post['tool_count']}")
print("mcp_process_started_and_terminated=true")
print("browser_or_tool_call_performed=false")
print("production_route_changed=false")
PY
- name: Commit verified replay receipts to Gitea audit branch
env:
CD_PUSH_TOKEN: ${{ secrets.CD_PUSH_TOKEN }}
run: |
set -euo pipefail
set +x
test -s "${REPLAY_CONTROLLER_RECEIPT_PATH}"
test -s "${REPLAY_VERIFIER_RECEIPT_PATH}"
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
python3 - <<'PY'
import os
from pathlib import Path
path = Path(os.environ["ASKPASS_PATH"])
path.write_text(
"#!/bin/sh\n"
"case \"$1\" in\n"
" *Username*) printf '%s\\n' wooo ;;\n"
" *Password*) printf '%s\\n' \"$CD_PUSH_TOKEN\" ;;\n"
" *) exit 1 ;;\n"
"esac\n",
encoding="utf-8",
)
path.chmod(0o700)
PY
cleanup_push_auth() {
rm -f "${ASKPASS_PATH}"
}
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

View File

@@ -80,5 +80,17 @@ def test_gitea_workflow_runner_health_endpoint_returns_committed_snapshot():
)
assert mcp_artifact_mirror["notification_policy"] == "read_only_no_notify"
assert mcp_artifact_mirror["notify_bridge_calls"] == 0
mcp_protocol_replay = next(
row
for row in workflow_records
if row["workflow_id"] == "mcp_external_protocol_replay"
)
assert mcp_protocol_replay["runner_labels"] == ["awoooi-non110-host"]
assert mcp_protocol_replay["runner_evidence_status"] == (
"non110_host_runner_mapped"
)
assert mcp_protocol_replay["notification_policy"] == "read_only_no_notify"
assert mcp_protocol_replay["notify_bridge_calls"] == 0
assert "schedule" in mcp_protocol_replay["triggers"]
assert "workflow 修改批准" in data["operator_contract"]["must_not_interpret_as"]
assert "Secret 已讀取或可輸出" in data["operator_contract"]["must_not_interpret_as"]

View File

@@ -0,0 +1,135 @@
{
"schema_version": "awoooi_external_mcp_replay_policy_v1",
"policy_version": "1.0.0",
"work_item_id": "MCP-REPLAY-PLAYWRIGHT-001",
"candidate_id": "external.playwright-verifier",
"adapter_id": "playwright_public_accessibility_verifier_v1",
"risk_class": "high",
"scope": "offline_protocol_and_tool_schema_compatibility_only",
"owner_lane": "awoooi_mcp_supply_chain",
"expires_at": "2026-10-15T00:00:00+08:00",
"exit_condition": "replace_with_verified_public_origin_shadow_canary_and_rollback_receipts_or_disable_candidate",
"replacement_action": "publish_a_new_exact_policy_and_replay_receipt_before_any_shadow_or_canary",
"rollback_ref": "disable_external.playwright-verifier_and_retain_immutable_artifact",
"post_verifier": "verify_external_mcp_replay_receipt.py",
"artifact_source": {
"policy_path": "config/mcp/playwright-mcp-artifact-policy.json",
"policy_checksum": "sha256:44c9f79c2af59f1d2703cbd2181b43f4c4dd4ee9f70164f606bdda70d823e78b",
"audit_branch": "mcp-artifact-receipts",
"audit_commit": "9c08ada5c0f15bb6924304af29cd13c8376182ff",
"controller_receipt_path": "docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-controller.snapshot.json",
"controller_receipt_sha256": "d59214363d9b380591b51157088f63d4ca115354d383070ba7cd4a958408641a",
"verifier_receipt_path": "docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-verifier.snapshot.json",
"verifier_receipt_sha256": "c993ba77c771b01144c962335dfd79490356b5d6797eed2bafb8c0261173f43c",
"artifact_ref": "registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp@sha256:de002c418bbb94fb4609539c4259c39ccb7f8b353f8bac8fbcba9e5742ad79ed",
"artifact_runtime_ref": "192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp@sha256:de002c418bbb94fb4609539c4259c39ccb7f8b353f8bac8fbcba9e5742ad79ed",
"bundle_manifest_sha256": "54b1d7ff24fc6f8ca89d4d36adcdb43e1cf79ed6018a4cd4f0d630c857bffb2c",
"cyclonedx_sbom_sha256": "092500242530e40f361f52cc662edda9e6c83f76edf81dfe9574235546c22be7",
"package_integrities": {
"@playwright/mcp@0.0.78": "5cb4d4780ea610df6c43e849e1d7c6f042240dbc52d0add3adcd91064509b9fd364e013615044d4ccb6afda25f87244c20db1197f61b738b317162ed167e3f4d",
"playwright@1.62.0-alpha-1783623505000": "e8a57d8783cfde1aaee0d686771c5c8a359f621f4b25c148fd1dac3f84d30b8239705a37a116b0374113956e4c904ddc40480a861a23d0a50dcf8b1fa424f44e",
"playwright-core@1.62.0-alpha-1783623505000": "08f25976c03f2864f641095e92257a5adf90950ad91d5499ea888db4e23f6d860e2152ccf237ca1964cce33422c9de1437ee340aae1c03a9719991d96ed3ef25"
}
},
"runtime": {
"image_ref": "registry.wooo.work/awoooi/ci-runner@sha256:04de9ba58445af167ef0a9f5a8d21ef20980ed82aa7818fd2cd0b64ed0aa3866",
"platform": "linux/amd64",
"user": "65534:65534",
"entrypoint": [
"node",
"node_modules/@playwright/mcp/cli.js"
],
"arguments": [
"--headless",
"--isolated",
"--allowed-origins",
"https://awoooi.wooo.work",
"--output-dir",
"/tmp/mcp-output",
"--executable-path",
"/nonexistent/browser"
],
"network_mode": "none",
"read_only_root": true,
"no_new_privileges": true,
"cap_drop": [
"ALL"
],
"pids_limit": 64,
"memory_limit": "384m",
"cpu_limit": "0.5",
"tmpfs": "/tmp:rw,noexec,nosuid,size=32m",
"startup_timeout_seconds": 20,
"shutdown_timeout_seconds": 5
},
"protocol_contract": {
"protocol_version": "2025-06-18",
"server_name": "Playwright",
"server_version": "1.62.0-alpha-1783623505000",
"tool_count": 24,
"tool_name_set_sha256": "12d23fea9d8d1a1de3f44863dc00cf393f0a2165323838cf93fed30a6a4cc237",
"tool_schema_manifest_sha256": "c3737ced21147d0512b0400df5885c95a545bc915892b5c8f928cee78428ac99",
"adapter_allowed_tools": [
"browser_close",
"browser_navigate",
"browser_snapshot",
"browser_take_screenshot"
],
"adapter_denied_tools": [
"browser_click",
"browser_console_messages",
"browser_drag",
"browser_drop",
"browser_evaluate",
"browser_file_upload",
"browser_fill_form",
"browser_find",
"browser_handle_dialog",
"browser_hover",
"browser_navigate_back",
"browser_network_request",
"browser_network_requests",
"browser_press_key",
"browser_resize",
"browser_run_code_unsafe",
"browser_select_option",
"browser_tabs",
"browser_type",
"browser_wait_for"
]
},
"execution_boundaries": {
"direct_gateway_registration_allowed": false,
"adapter_registration_allowed": false,
"browser_start_allowed": false,
"tool_call_allowed": false,
"navigation_allowed": false,
"authenticated_session_allowed": false,
"raw_request_or_response_body_storage_allowed": false,
"external_rag_write_allowed": false,
"production_write_allowed": false,
"shadow_allowed": false,
"canary_allowed": false,
"promotion_allowed": false
},
"promotion_blockers_after_replay": [
"immutable_browser_runtime_supply_chain_missing",
"public_origin_egress_proxy_and_private_network_denial_missing",
"public_origin_shadow_replay_not_verified",
"canary_verifier_and_rollback_execution_pending"
],
"prohibited": [
"github_api_or_actions",
"actions_checkout",
"github_source_or_container_download",
"npx_y",
"at_latest",
"floating_container_tag",
"direct_external_mcp_registration",
"browser_or_tool_execution_in_protocol_replay",
"secret_read_or_log",
"request_or_response_body_storage",
"external_rag_write",
"production_mutation"
]
}

View File

@@ -1,3 +1,26 @@
## 2026-07-15 — P0 MCP Playwright offline protocol/schema compatibility replay
**完成內容**
- 新增 high-risk replay policy固定 artifact policy checksum、`mcp-artifact-receipts` commit `9c08ada5c0f15bb6924304af29cd13c8376182ff`、controller/verifier receipt checksum、Harbor artifact digest與 internal runtime image digestpolicy 有 owner、expiry、exit condition、replacement、rollback 與獨立 post-verifier。
- replay controller 重新驗證 artifact receipt chain、manifest、CycloneDX SBOM、3 個 tarball SHA-512 與 package identity再以 non-root、network none、read-only root、no-new-privileges、cap-drop ALL、bounded CPU/memory/PID/tmpfs/timeout 啟動 MCP。只送 `initialize``notifications/initialized``tools/list`,不啟動 browser、不呼叫 tool。
- adapter contract 只允許 `browser_navigate``browser_snapshot``browser_take_screenshot``browser_close` 4 個未來 public verifier 能力;`browser_run_code_unsafe`、file upload、form/click/type/evaluate/network 等其餘 20 個工具明確 denied。Direct Gateway registration、adapter registration、shadow、canary、promotion、RAG write與 production write仍全部停用。
- 獨立 verifier 完全不 import controller重新 pull/readback exact artifact/runtime digest、重新解包並再次執行 protocol replaycontroller/verifier 都要求 MCP stdin EOF clean exit與 ephemeral container removal terminal。
- 新增 Gitea-only `mcp-external-replay.yaml`:每週三 10:43 Asia/Taipei、manual/push、non-110 host runner、host pressure guard、Harbor password-stdin/logout、pinned artifact audit commit、同 run/trace/work-item receipts成功後只 normal push `mcp-replay-receipts` audit branch。
- Gitea workflow runner-health snapshot納入第 15 條 workflowscheduled workflow `6 -> 7`non110 host mapped `4 -> 5`,沒有新增 runner attestation gap。
**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 `32 passed`artifact + replay supply-chain tests與 MCP control-plane/federation/version lifecycle/runner-health 聚焦回歸合計 `84 passed`。Ruff、py_compile、JSON/YAML parse、source-control owner guard與 `git diff --check` 通過。
- 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。
- 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。
## 2026-07-15 — P0 MCP Playwright immutable mirror 與獨立供應鏈 verifier
**完成內容**

View File

@@ -1,6 +1,6 @@
{
"schema_version": "gitea_workflow_runner_health_v1",
"generated_at": "2026-07-15T09:57:22+08:00",
"generated_at": "2026-07-15T11:01:19+08:00",
"program_status": {
"overall_completion_percent": 100,
"current_priority": "P1",
@@ -22,9 +22,11 @@
".gitea/workflows/e2e-health.yaml",
".gitea/workflows/harbor-110-local-repair.yaml",
".gitea/workflows/mcp-external-artifact-mirror.yaml",
".gitea/workflows/mcp-external-replay.yaml",
".gitea/workflows/run-migration.yml",
".gitea/workflows/type-sync-check.yaml",
"config/mcp/playwright-mcp-artifact-policy.json",
"config/mcp/playwright-mcp-replay-policy.json",
"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",
@@ -34,16 +36,16 @@
"scripts/ops/stop-stale-gitea-actions-jobs.sh"
],
"rollups": {
"total_workflows": 14,
"total_workflows": 15,
"by_workflow_status": {
"manifest_mapped": 14
"manifest_mapped": 15
},
"by_runner_evidence_status": {
"non110_runner_mapped": 10,
"non110_host_runner_mapped": 4
"non110_host_runner_mapped": 5
},
"workflows_with_schedule": 6,
"workflows_with_workflow_dispatch": 14,
"workflows_with_schedule": 7,
"workflows_with_workflow_dispatch": 15,
"workflows_with_notify_bridge": 6,
"workflows_with_actionable_or_failure_quiet_policy": 3,
"workflow_ids_requiring_runner_attestation": [],
@@ -369,6 +371,36 @@
],
"next_action": "維持 bounded capacity wait、獨立 verifier 與 mcp-artifact-receipts audit branch不得啟動 artifact、寫 RAG、切 production route 或推回 deploy main。"
},
{
"workflow_id": "mcp_external_protocol_replay",
"file_ref": ".gitea/workflows/mcp-external-replay.yaml",
"display_name": "MCP External Protocol Replay",
"scope": "讀取 pinned artifact receipt commit以 immutable artifact/runtime digest 在 network none、non-root、read-only container 只執行 MCP initialize 與 tools/list獨立 verifier 重跑後只寫專用 audit branch。",
"status": "manifest_mapped",
"risk_level": "high",
"triggers": [
"push:main",
"workflow_dispatch",
"schedule"
],
"schedule_cadence": "每週三 10:43 Asia/Taipeicron=43 2 * * 3 UTC排在 artifact mirror 後。",
"runner_labels": [
"awoooi-non110-host"
],
"runner_evidence_status": "non110_host_runner_mapped",
"job_count": 1,
"notification_policy": "read_only_no_notify",
"notify_bridge_calls": 0,
"secrets_policy_status": "只使用 workflow secret references 登入 Harbor 與 normal-push Gitea audit branchcontroller/verifier 不讀取、保存或顯示 secret value。",
"evidence_refs": [
".gitea/workflows/mcp-external-replay.yaml",
"config/mcp/playwright-mcp-replay-policy.json",
"scripts/security/external_mcp_replay_controller.py",
"scripts/security/verify_external_mcp_replay_receipt.py",
"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 仍保持停用。"
},
{
"workflow_id": "run_migration",
"file_ref": ".gitea/workflows/run-migration.yml",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""Tests for the bounded external MCP replay controller without mocks."""
from __future__ import annotations
import hashlib
import importlib.util
import io
import json
import sys
import tarfile
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
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"
SPEC = importlib.util.spec_from_file_location("external_mcp_replay_controller", CONTROLLER_PATH)
assert SPEC and SPEC.loader
controller = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = controller
SPEC.loader.exec_module(controller)
def _canonical(payload: object) -> bytes:
return json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def _write_json(path: Path, payload: object) -> bytes:
raw = _canonical(payload) + b"\n"
path.write_bytes(raw)
return raw
def _policy_payload() -> dict:
return json.loads(POLICY_PATH.read_text(encoding="utf-8"))
def _artifact_receipts(policy_payload: dict) -> tuple[dict, dict]:
artifact = policy_payload["artifact_source"]
artifact_run_id = "11111111-2222-4333-8444-555555555555"
artifact_trace_id = f"mcp-artifact-{artifact_run_id}"
packages = []
for key, digest in artifact["package_integrities"].items():
name, version = key.rsplit("@", 1)
packages.append(
{
"name": name,
"version": version,
"tarball_sha512_hex": digest,
}
)
artifact_controller = {
"schema_version": "awoooi_external_mcp_artifact_receipt_v1",
"status": "completed_internal_mirror_pending_independent_verifier",
"run_id": artifact_run_id,
"trace_id": artifact_trace_id,
"promotion_allowed": False,
"replay_allowed": False,
"shadow_allowed": False,
"canary_allowed": False,
"internal_mirror_ref": artifact["artifact_ref"],
"runtime_mirror_ref": artifact["artifact_runtime_ref"],
"policy_checksum": artifact["policy_checksum"],
"bundle_manifest_sha256": artifact["bundle_manifest_sha256"],
"cyclonedx_sbom_sha256": artifact["cyclonedx_sbom_sha256"],
"packages": packages,
}
artifact_verifier = {
"schema_version": "awoooi_external_mcp_artifact_verifier_receipt_v1",
"status": "completed_internal_mirror_verified",
"run_id": artifact_run_id,
"trace_id": artifact_trace_id,
"promotion_allowed": False,
"replay_allowed": False,
"shadow_allowed": False,
"canary_allowed": False,
"internal_mirror_ref": artifact["artifact_ref"],
"runtime_mirror_ref": artifact["artifact_runtime_ref"],
"policy_checksum": artifact["policy_checksum"],
"independent_post_verifier": {"status": "passed"},
}
return artifact_controller, artifact_verifier
def _source_fixture(directory: Path) -> tuple[Path, Path, Path]:
payload = _policy_payload()
artifact_controller, artifact_verifier = _artifact_receipts(payload)
controller_path = directory / "artifact-controller.json"
verifier_path = directory / "artifact-verifier.json"
controller_raw = _write_json(controller_path, artifact_controller)
artifact_verifier["controller_receipt_sha256"] = hashlib.sha256(
controller_raw
).hexdigest()
verifier_raw = _write_json(verifier_path, artifact_verifier)
payload["artifact_source"]["controller_receipt_sha256"] = hashlib.sha256(
controller_raw
).hexdigest()
payload["artifact_source"]["verifier_receipt_sha256"] = hashlib.sha256(
verifier_raw
).hexdigest()
policy_path = directory / "replay-policy.json"
_write_json(policy_path, payload)
return policy_path, controller_path, verifier_path
class ReplayPolicyTests(unittest.TestCase):
def _mutated_policy(self, mutate) -> Path:
self.temp = tempfile.TemporaryDirectory()
path = Path(self.temp.name) / "policy.json"
payload = _policy_payload()
mutate(payload)
_write_json(path, payload)
return path
def tearDown(self) -> None:
if hasattr(self, "temp"):
self.temp.cleanup()
def test_committed_policy_is_exact_and_minimal(self) -> None:
policy = controller.load_policy(POLICY_PATH)
self.assertEqual(policy.work_item_id, "MCP-REPLAY-PLAYWRIGHT-001")
self.assertEqual(policy.risk_class, "high")
self.assertEqual(policy.tool_count, 24)
self.assertEqual(len(policy.adapter_allowed_tools), 4)
self.assertEqual(len(policy.adapter_denied_tools), 20)
self.assertRegex(policy.artifact_ref, r"@sha256:[0-9a-f]{64}$")
self.assertRegex(policy.runtime_image_ref, r"@sha256:[0-9a-f]{64}$")
def test_expired_policy_fails_closed(self) -> None:
path = self._mutated_policy(
lambda payload: payload.__setitem__(
"expires_at", "2026-07-01T00:00:00+08:00"
)
)
with self.assertRaisesRegex(controller.ReplayError, "replay_policy_expired"):
controller.load_policy(path)
def test_network_must_remain_none(self) -> None:
path = self._mutated_policy(
lambda payload: payload["runtime"].__setitem__("network_mode", "bridge")
)
with self.assertRaisesRegex(controller.ReplayError, "runtime_isolation_invalid"):
controller.load_policy(path)
def test_runtime_must_remain_non_root(self) -> None:
path = self._mutated_policy(
lambda payload: payload["runtime"].__setitem__("user", "0:0")
)
with self.assertRaisesRegex(controller.ReplayError, "runtime_user_invalid"):
controller.load_policy(path)
def test_floating_runtime_reference_is_rejected(self) -> None:
path = self._mutated_policy(
lambda payload: payload["runtime"].__setitem__(
"image_ref", "registry.wooo.work/awoooi/ci-runner:current"
)
)
with self.assertRaisesRegex(controller.ReplayError, "runtime_image_ref_invalid"):
controller.load_policy(path)
def test_direct_registration_cannot_be_enabled(self) -> None:
path = self._mutated_policy(
lambda payload: payload["execution_boundaries"].__setitem__(
"direct_gateway_registration_allowed", True
)
)
with self.assertRaisesRegex(controller.ReplayError, "execution_boundary_invalid"):
controller.load_policy(path)
def test_dangerous_tool_cannot_leave_denied_partition(self) -> None:
def mutate(payload: dict) -> None:
payload["protocol_contract"]["adapter_denied_tools"].remove(
"browser_run_code_unsafe"
)
path = self._mutated_policy(mutate)
with self.assertRaises(controller.ReplayError):
controller.load_policy(path)
class SourceEvidenceTests(unittest.TestCase):
def test_verified_source_receipt_chain_passes(self) -> None:
with tempfile.TemporaryDirectory() as temp:
policy_path, controller_path, verifier_path = _source_fixture(Path(temp))
policy = controller.load_policy(policy_path)
evidence = controller.validate_source_evidence(
policy,
artifact_policy_path=ARTIFACT_POLICY_PATH,
controller_receipt_path=controller_path,
verifier_receipt_path=verifier_path,
)
self.assertEqual(
evidence.artifact_run_id, "11111111-2222-4333-8444-555555555555"
)
self.assertRegex(evidence.artifact_verifier_receipt_sha256, r"^[0-9a-f]{64}$")
def test_tampered_controller_receipt_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
policy_path, controller_path, verifier_path = _source_fixture(Path(temp))
policy = controller.load_policy(policy_path)
controller_path.write_bytes(controller_path.read_bytes() + b" ")
with self.assertRaisesRegex(
controller.ReplayError,
"artifact_controller_receipt_checksum_mismatch",
):
controller.validate_source_evidence(
policy,
artifact_policy_path=ARTIFACT_POLICY_PATH,
controller_receipt_path=controller_path,
verifier_receipt_path=verifier_path,
)
def test_artifact_policy_checksum_drift_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
policy_path, controller_path, verifier_path = _source_fixture(root)
artifact_policy = json.loads(ARTIFACT_POLICY_PATH.read_text())
artifact_policy["risk_level"] = "medium"
drift_path = root / "artifact-policy.json"
_write_json(drift_path, artifact_policy)
with self.assertRaisesRegex(
controller.ReplayError, "artifact_policy_checksum_mismatch"
):
controller.validate_source_evidence(
controller.load_policy(policy_path),
artifact_policy_path=drift_path,
controller_receipt_path=controller_path,
verifier_receipt_path=verifier_path,
)
class ArchiveAndProtocolTests(unittest.TestCase):
def test_archive_traversal_is_rejected(self) -> None:
with self.assertRaisesRegex(controller.ReplayError, "artifact_archive_path_invalid"):
controller._safe_relative_member("package/../../escape")
def test_archive_symlink_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
tarball = root / "bad.tgz"
with tarfile.open(tarball, "w:gz") as archive:
member = tarfile.TarInfo("package/link")
member.type = tarfile.SYMTYPE
member.linkname = "/etc/passwd"
archive.addfile(member)
with self.assertRaisesRegex(
controller.ReplayError, "artifact_archive_member_type_invalid"
):
controller._extract_package(tarball, root / "out")
def test_regular_archive_extracts_with_exact_size(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
tarball = root / "good.tgz"
content = b'{"name":"fixture"}'
with tarfile.open(tarball, "w:gz") as archive:
member = tarfile.TarInfo("package/package.json")
member.size = len(content)
archive.addfile(member, io.BytesIO(content))
files, size = controller._extract_package(tarball, root / "out")
self.assertEqual((files, size), (1, len(content)))
self.assertEqual((root / "out/package.json").read_bytes(), content)
def test_tool_hashes_are_order_independent(self) -> None:
tools = [
{
"name": "beta",
"inputSchema": {"type": "object"},
"annotations": {"readOnlyHint": True},
},
{"name": "alpha", "inputSchema": {"type": "object"}},
]
first = controller._tool_hashes(tools)
second = controller._tool_hashes(list(reversed(tools)))
self.assertEqual(first, second)
self.assertEqual(first[0], 2)
self.assertEqual(first[3], ("alpha", "beta"))
def test_duplicate_tool_names_are_rejected(self) -> None:
tools = [
{"name": "same", "inputSchema": {}},
{"name": "same", "inputSchema": {}},
]
with self.assertRaisesRegex(controller.ReplayError, "mcp_tool_names_not_unique"):
controller._tool_hashes(tools)
class ReceiptAndWorkflowTests(unittest.TestCase):
def test_success_receipt_preserves_no_browser_no_write_boundary(self) -> None:
policy = controller.load_policy(POLICY_PATH)
source = controller.SourceEvidence("a", "b", "c", "d")
observation = controller.ReplayObservation(
policy.server_name,
policy.server_version,
policy.protocol_version,
policy.tool_count,
policy.tool_name_set_sha256,
policy.tool_schema_manifest_sha256,
0,
"stdin_eof_clean_exit",
True,
)
receipt = controller.build_receipt(
policy=policy,
source=source,
run_id="11111111-2222-4333-8444-555555555555",
trace_id="mcp-replay-11111111-2222-4333-8444-555555555555",
mode="apply",
observation=observation,
package_file_count=10,
package_unpacked_bytes=20,
)
self.assertEqual(
receipt["status"],
"completed_protocol_schema_replay_pending_independent_verifier",
)
for field in (
"browser_started",
"tool_call_performed",
"external_rag_write_performed",
"production_write_performed",
"promotion_allowed",
):
self.assertIs(receipt[field], False)
def test_failure_receipt_keeps_candidate_disabled(self) -> None:
policy = controller.load_policy(POLICY_PATH)
receipt = controller._failure_receipt(
policy=policy,
run_id="11111111-2222-4333-8444-555555555555",
trace_id="mcp-replay-11111111-2222-4333-8444-555555555555",
error_class="fixture_failure",
)
self.assertEqual(receipt["status"], "blocked_with_safe_next_action")
terminal = receipt["stage_receipts"]["rollback_or_no_write_terminal"]
self.assertEqual(terminal["candidate_registration_state"], "disabled")
self.assertIs(receipt["promotion_allowed"], False)
def test_gitea_workflow_has_no_floating_or_external_source_controls(self) -> None:
raw = WORKFLOW_PATH.read_text(encoding="utf-8").lower()
forbidden = (
"uses" + ":",
"actions" + "/checkout",
"github" + ".com",
"api" + ".github" + ".com",
"raw" + ".githubusercontent" + ".com",
"codeload" + ".github" + ".com",
"ghcr" + ".io",
"npx" + " -y",
"@" + "latest",
":" + "latest",
)
self.assertFalse([value for value in forbidden if value in raw])
self.assertIn("runs-on: awoooi-non110-host", raw)
self.assertIn("wait-host-web-build-pressure.sh", raw)
self.assertIn("mcp-replay-receipts", raw)
self.assertIn("verify_external_mcp_replay_receipt.py", raw)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,325 @@
#!/usr/bin/env python3
"""Tests for the standalone external MCP replay verifier without mocks."""
from __future__ import annotations
import hashlib
import importlib.util
import json
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
VERIFIER_PATH = ROOT / "scripts/security/verify_external_mcp_replay_receipt.py"
CONTROLLER_PATH = ROOT / "scripts/security/external_mcp_replay_controller.py"
POLICY_PATH = ROOT / "config/mcp/playwright-mcp-replay-policy.json"
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
verifier = _load("verify_external_mcp_replay_receipt", VERIFIER_PATH)
controller = _load("external_mcp_replay_controller_for_verifier_test", CONTROLLER_PATH)
def _canonical(payload: object) -> bytes:
return json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def _write(path: Path, payload: object) -> bytes:
raw = _canonical(payload) + b"\n"
path.write_bytes(raw)
return raw
def _policy_payload() -> dict:
return json.loads(POLICY_PATH.read_text(encoding="utf-8"))
def _valid_controller_receipt() -> tuple[dict, dict, str]:
policy = controller.load_policy(POLICY_PATH)
source = controller.SourceEvidence(
artifact_run_id="aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
artifact_trace_id="mcp-artifact-aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
artifact_controller_receipt_sha256="a" * 64,
artifact_verifier_receipt_sha256="b" * 64,
)
observation = controller.ReplayObservation(
server_name=policy.server_name,
server_version=policy.server_version,
protocol_version=policy.protocol_version,
tool_count=policy.tool_count,
tool_name_set_sha256=policy.tool_name_set_sha256,
tool_schema_manifest_sha256=policy.tool_schema_manifest_sha256,
process_exit_code=0,
shutdown_mode="stdin_eof_clean_exit",
ephemeral_container_removed=True,
)
receipt = controller.build_receipt(
policy=policy,
source=source,
run_id="11111111-2222-4333-8444-555555555555",
trace_id="mcp-replay-11111111-2222-4333-8444-555555555555",
mode="apply",
observation=observation,
package_file_count=173,
package_unpacked_bytes=1_000,
)
payload, checksum = verifier.load_and_validate_policy(POLICY_PATH)
return payload, receipt, checksum
def _artifact_chain(policy: dict) -> tuple[dict, dict]:
artifact = policy["artifact_source"]
run_id = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
controller_receipt = {
"schema_version": "awoooi_external_mcp_artifact_receipt_v1",
"status": "completed_internal_mirror_pending_independent_verifier",
"run_id": run_id,
"trace_id": f"mcp-artifact-{run_id}",
"internal_mirror_ref": artifact["artifact_ref"],
"policy_checksum": artifact["policy_checksum"],
"promotion_allowed": False,
"replay_allowed": False,
"shadow_allowed": False,
"canary_allowed": False,
}
verifier_receipt = {
"schema_version": "awoooi_external_mcp_artifact_verifier_receipt_v1",
"status": "completed_internal_mirror_verified",
"run_id": run_id,
"trace_id": f"mcp-artifact-{run_id}",
"internal_mirror_ref": artifact["artifact_ref"],
"policy_checksum": artifact["policy_checksum"],
"promotion_allowed": False,
"replay_allowed": False,
"shadow_allowed": False,
"canary_allowed": False,
"independent_post_verifier": {
"status": "passed",
"container_or_mcp_started": False,
},
}
return controller_receipt, verifier_receipt
class StandalonePolicyTests(unittest.TestCase):
def test_committed_policy_passes_independent_validation(self) -> None:
policy, checksum = verifier.load_and_validate_policy(POLICY_PATH)
self.assertEqual(policy["work_item_id"], "MCP-REPLAY-PLAYWRIGHT-001")
self.assertRegex(checksum, r"^sha256:[0-9a-f]{64}$")
def test_any_enabled_execution_boundary_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
payload = _policy_payload()
payload["execution_boundaries"]["browser_start_allowed"] = True
path = Path(temp) / "policy.json"
_write(path, payload)
with self.assertRaisesRegex(verifier.VerifierError, "boundary_must_remain_false"):
verifier.load_and_validate_policy(path)
def test_adapter_tool_partition_overlap_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
payload = _policy_payload()
payload["protocol_contract"]["adapter_denied_tools"].append(
"browser_snapshot"
)
path = Path(temp) / "policy.json"
_write(path, payload)
with self.assertRaisesRegex(verifier.VerifierError, "adapter_tool_partition_invalid"):
verifier.load_and_validate_policy(path)
def test_tagged_artifact_reference_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as temp:
payload = _policy_payload()
payload["artifact_source"]["artifact_ref"] = (
"registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp:0.0.78"
)
path = Path(temp) / "policy.json"
_write(path, payload)
with self.assertRaisesRegex(verifier.VerifierError, "artifact_ref_invalid"):
verifier.load_and_validate_policy(path)
class ControllerReceiptTests(unittest.TestCase):
def test_valid_controller_receipt_passes_standalone_validation(self) -> None:
policy, receipt, checksum = _valid_controller_receipt()
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "controller.json"
raw = _write(path, receipt)
observed, observed_sha = verifier.validate_controller_receipt(
policy, checksum, path
)
self.assertEqual(observed["run_id"], receipt["run_id"])
self.assertEqual(observed_sha, hashlib.sha256(raw).hexdigest())
def test_controller_browser_start_is_rejected(self) -> None:
policy, receipt, checksum = _valid_controller_receipt()
receipt["browser_started"] = True
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "controller.json"
_write(path, receipt)
with self.assertRaisesRegex(verifier.VerifierError, "controller_boundary_invalid"):
verifier.validate_controller_receipt(policy, checksum, path)
def test_controller_missing_cleanup_terminal_is_rejected(self) -> None:
policy, receipt, checksum = _valid_controller_receipt()
receipt["stage_receipts"]["rollback_or_no_write_terminal"][
"ephemeral_container_removed"
] = False
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "controller.json"
_write(path, receipt)
with self.assertRaisesRegex(verifier.VerifierError, "controller_rollback_invalid"):
verifier.validate_controller_receipt(policy, checksum, path)
def test_controller_protocol_hash_drift_is_rejected(self) -> None:
policy, receipt, checksum = _valid_controller_receipt()
receipt["protocol_observation"]["tool_name_set_sha256"] = "0" * 64
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "controller.json"
_write(path, receipt)
with self.assertRaisesRegex(
verifier.VerifierError, "controller_protocol_observation_mismatch"
):
verifier.validate_controller_receipt(policy, checksum, path)
class ArtifactChainAndToolTests(unittest.TestCase):
def test_valid_artifact_chain_passes(self) -> None:
policy = _policy_payload()
artifact = policy["artifact_source"]
artifact_controller, artifact_verifier = _artifact_chain(policy)
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
controller_path = root / "artifact-controller.json"
verifier_path = root / "artifact-verifier.json"
controller_raw = _write(controller_path, artifact_controller)
artifact_verifier["controller_receipt_sha256"] = hashlib.sha256(
controller_raw
).hexdigest()
verifier_raw = _write(verifier_path, artifact_verifier)
artifact["controller_receipt_sha256"] = hashlib.sha256(
controller_raw
).hexdigest()
artifact["verifier_receipt_sha256"] = hashlib.sha256(
verifier_raw
).hexdigest()
observed = verifier.validate_artifact_receipt_chain(
policy, controller_path, verifier_path
)
self.assertEqual(observed[0], artifact["controller_receipt_sha256"])
self.assertEqual(observed[1], artifact["verifier_receipt_sha256"])
def test_artifact_chain_tamper_is_rejected(self) -> None:
policy = _policy_payload()
artifact = policy["artifact_source"]
artifact_controller, artifact_verifier = _artifact_chain(policy)
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
controller_path = root / "artifact-controller.json"
verifier_path = root / "artifact-verifier.json"
controller_raw = _write(controller_path, artifact_controller)
artifact_verifier["controller_receipt_sha256"] = hashlib.sha256(
controller_raw
).hexdigest()
verifier_raw = _write(verifier_path, artifact_verifier)
artifact["controller_receipt_sha256"] = hashlib.sha256(
controller_raw
).hexdigest()
artifact["verifier_receipt_sha256"] = hashlib.sha256(
verifier_raw
).hexdigest()
verifier_path.write_bytes(verifier_raw + b" ")
with self.assertRaisesRegex(verifier.VerifierError, "artifact_receipt_chain_invalid"):
verifier.validate_artifact_receipt_chain(
policy, controller_path, verifier_path
)
def test_independent_tool_hashes_are_order_independent(self) -> None:
tools = [
{"name": "zeta", "inputSchema": {"type": "object"}},
{
"name": "alpha",
"inputSchema": {"type": "object"},
"annotations": {"readOnlyHint": True},
},
]
first = verifier._independent_tool_hashes(tools)
second = verifier._independent_tool_hashes(list(reversed(tools)))
self.assertEqual(first, second)
self.assertEqual(first[3], {"alpha", "zeta"})
def test_duplicate_tool_names_are_rejected(self) -> None:
tools = [
{"name": "duplicate", "inputSchema": {}},
{"name": "duplicate", "inputSchema": {}},
]
with self.assertRaisesRegex(verifier.VerifierError, "tool_names_not_unique"):
verifier._independent_tool_hashes(tools)
class VerifierReceiptTests(unittest.TestCase):
def test_completed_verifier_receipt_keeps_promotion_disabled(self) -> None:
policy, controller_receipt, policy_checksum = _valid_controller_receipt()
receipt = verifier.build_verifier_receipt(
policy=policy,
policy_checksum=policy_checksum,
controller=controller_receipt,
controller_sha256="c" * 64,
artifact_controller_sha256="a" * 64,
artifact_verifier_sha256="b" * 64,
observation={
"protocol_version": "2025-06-18",
"server_name": "Playwright",
"server_version": "1.62.0-alpha-1783623505000",
"tool_count": 24,
"tool_name_set_sha256": "d" * 64,
"tool_schema_manifest_sha256": "e" * 64,
"process_exit_code": 0,
"shutdown_mode": "stdin_eof_clean_exit",
"ephemeral_container_removed": True,
},
package_file_count=173,
package_unpacked_bytes=17_000_000,
)
self.assertEqual(receipt["status"], "completed_protocol_schema_replay_verified")
self.assertEqual(
receipt["independent_post_verifier"]["implementation"],
"standalone_no_controller_import",
)
for field in (
"browser_started",
"tool_call_performed",
"external_rag_write_performed",
"production_write_performed",
"promotion_allowed",
):
self.assertIs(receipt[field], False)
def test_failure_receipt_has_rollback_terminal(self) -> None:
policy = _policy_payload()
receipt = verifier._failure_receipt(policy, {}, "fixture_failure")
self.assertEqual(receipt["status"], "blocked_with_safe_next_action")
self.assertEqual(
receipt["rollback_terminal"]["candidate_registration_state"], "disabled"
)
self.assertIs(receipt["promotion_allowed"], False)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,898 @@
#!/usr/bin/env python3
"""Independently verify an external MCP protocol/schema replay receipt.
This standalone verifier intentionally does not import the replay controller.
It revalidates the committed policy and artifact receipt chain, rematerializes
the immutable bundle, and repeats initialize plus tools/list in a fresh offline
container. No browser or MCP tool is invoked.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import select
import shutil
import subprocess
import sys
import tarfile
import tempfile
import uuid
from datetime import datetime, timezone
from pathlib import Path, PurePosixPath
from typing import Any
POLICY_SCHEMA = "awoooi_external_mcp_replay_policy_v1"
CONTROLLER_SCHEMA = "awoooi_external_mcp_replay_receipt_v1"
VERIFIER_SCHEMA = "awoooi_external_mcp_replay_verifier_receipt_v1"
ARTIFACT_CONTROLLER_SCHEMA = "awoooi_external_mcp_artifact_receipt_v1"
ARTIFACT_VERIFIER_SCHEMA = "awoooi_external_mcp_artifact_verifier_receipt_v1"
ARTIFACT_BUNDLE_SCHEMA = "awoooi_external_mcp_artifact_bundle_v1"
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
SHA512_PATTERN = re.compile(r"^[0-9a-f]{128}$")
OCI_REF_PATTERN = re.compile(
r"^[a-z0-9.-]+(?::[0-9]+)?/[a-z0-9._/-]+@sha256:[0-9a-f]{64}$"
)
MAX_JSON_BYTES = 4 * 1024 * 1024
MAX_PROTOCOL_LINE_BYTES = 2 * 1024 * 1024
MAX_FILES = 4_096
MAX_UNPACKED_BYTES = 40 * 1024 * 1024
class VerifierError(RuntimeError):
"""Fail-closed public-safe verifier error class."""
def _canonical(payload: Any) -> bytes:
return json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def _sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _read_json(path: Path, error: str) -> tuple[bytes, dict[str, Any]]:
try:
raw = path.read_bytes()
except OSError as exc:
raise VerifierError(error) from exc
if not raw or len(raw) > MAX_JSON_BYTES:
raise VerifierError(error)
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise VerifierError(error) from exc
if not isinstance(payload, dict):
raise VerifierError(error)
return raw, payload
def _dict(value: Any, error: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise VerifierError(error)
return value
def _text(value: Any, error: str) -> str:
if not isinstance(value, str) or not value.strip():
raise VerifierError(error)
return value.strip()
def _exact_digest(value: Any, error: str, *, prefixed: bool = False) -> str:
text = _text(value, error)
digest = text.removeprefix("sha256:") if prefixed else text
if not SHA256_PATTERN.fullmatch(digest):
raise VerifierError(error)
if prefixed and not text.startswith("sha256:"):
raise VerifierError(error)
return text
def _oci_ref(value: Any, error: str) -> str:
ref = _text(value, error)
if not OCI_REF_PATTERN.fullmatch(ref):
raise VerifierError(error)
return ref
def load_and_validate_policy(path: Path) -> tuple[dict[str, Any], str]:
_, policy = _read_json(path, "policy_unreadable")
if (
policy.get("schema_version") != POLICY_SCHEMA
or policy.get("policy_version") != "1.0.0"
or policy.get("risk_class") != "high"
or policy.get("scope")
!= "offline_protocol_and_tool_schema_compatibility_only"
):
raise VerifierError("policy_contract_invalid")
try:
expires = datetime.fromisoformat(_text(policy.get("expires_at"), "policy_expiry_invalid"))
except ValueError as exc:
raise VerifierError("policy_expiry_invalid") from exc
if expires.tzinfo is None or expires <= datetime.now(timezone.utc):
raise VerifierError("policy_expired")
artifact = _dict(policy.get("artifact_source"), "artifact_source_invalid")
runtime = _dict(policy.get("runtime"), "runtime_invalid")
protocol = _dict(policy.get("protocol_contract"), "protocol_invalid")
boundaries = _dict(policy.get("execution_boundaries"), "boundaries_invalid")
if any(value is not False for value in boundaries.values()):
raise VerifierError("boundary_must_remain_false")
if (
runtime.get("network_mode") != "none"
or runtime.get("read_only_root") is not True
or runtime.get("no_new_privileges") is not True
or runtime.get("cap_drop") != ["ALL"]
or runtime.get("user") != "65534:65534"
or runtime.get("platform") != "linux/amd64"
):
raise VerifierError("runtime_isolation_invalid")
artifact_ref = _oci_ref(artifact.get("artifact_ref"), "artifact_ref_invalid")
runtime_ref = _oci_ref(runtime.get("image_ref"), "runtime_ref_invalid")
if artifact_ref == runtime_ref:
raise VerifierError("artifact_runtime_separation_invalid")
for field in (
"controller_receipt_sha256",
"verifier_receipt_sha256",
"bundle_manifest_sha256",
"cyclonedx_sbom_sha256",
"tool_name_set_sha256",
"tool_schema_manifest_sha256",
):
source = artifact if field in artifact else protocol
_exact_digest(source.get(field), f"{field}_invalid")
_exact_digest(artifact.get("policy_checksum"), "artifact_policy_checksum_invalid", prefixed=True)
integrities = _dict(artifact.get("package_integrities"), "integrities_invalid")
if len(integrities) != 3:
raise VerifierError("integrities_invalid")
for digest in integrities.values():
if not isinstance(digest, str) or not SHA512_PATTERN.fullmatch(digest):
raise VerifierError("integrities_invalid")
allowed = protocol.get("adapter_allowed_tools")
denied = protocol.get("adapter_denied_tools")
if (
not isinstance(allowed, list)
or not isinstance(denied, list)
or len(allowed) != 4
or len(denied) != 20
or set(allowed) & set(denied)
or set(allowed)
!= {
"browser_close",
"browser_navigate",
"browser_snapshot",
"browser_take_screenshot",
}
or "browser_run_code_unsafe" not in denied
or "browser_file_upload" not in denied
):
raise VerifierError("adapter_tool_partition_invalid")
return policy, f"sha256:{_sha256(_canonical(policy))}"
def validate_controller_receipt(
policy: dict[str, Any], policy_checksum: str, path: Path
) -> tuple[dict[str, Any], str]:
raw, receipt = _read_json(path, "controller_receipt_unreadable")
if (
receipt.get("schema_version") != CONTROLLER_SCHEMA
or receipt.get("status")
!= "completed_protocol_schema_replay_pending_independent_verifier"
or receipt.get("mode") != "apply"
or receipt.get("policy_checksum") != policy_checksum
or receipt.get("work_item_id") != policy.get("work_item_id")
or receipt.get("candidate_id") != policy.get("candidate_id")
or receipt.get("adapter_id") != policy.get("adapter_id")
):
raise VerifierError("controller_receipt_terminal_invalid")
try:
normalized_run_id = str(uuid.UUID(_text(receipt.get("run_id"), "run_id_invalid")))
except ValueError as exc:
raise VerifierError("run_id_invalid") from exc
if (
receipt.get("run_id") != normalized_run_id
or receipt.get("trace_id") != f"mcp-replay-{normalized_run_id}"
):
raise VerifierError("run_trace_identity_invalid")
for field in (
"direct_gateway_registration_allowed",
"adapter_registration_allowed",
"browser_started",
"tool_call_performed",
"navigation_performed",
"raw_request_or_response_body_stored",
"external_rag_write_performed",
"production_write_performed",
"shadow_allowed",
"canary_allowed",
"promotion_allowed",
):
if receipt.get(field) is not False:
raise VerifierError("controller_boundary_invalid")
artifact = _dict(policy.get("artifact_source"), "artifact_source_invalid")
runtime = _dict(policy.get("runtime"), "runtime_invalid")
if (
receipt.get("artifact_ref") != artifact.get("artifact_ref")
or receipt.get("runtime_image_ref") != runtime.get("image_ref")
or receipt.get("artifact_audit_commit") != artifact.get("audit_commit")
):
raise VerifierError("controller_source_identity_invalid")
protocol = _dict(policy.get("protocol_contract"), "protocol_invalid")
observed = _dict(receipt.get("protocol_observation"), "protocol_observation_invalid")
expected = {
"protocol_version": protocol.get("protocol_version"),
"server_name": protocol.get("server_name"),
"server_version": protocol.get("server_version"),
"tool_count": protocol.get("tool_count"),
"tool_name_set_sha256": protocol.get("tool_name_set_sha256"),
"tool_schema_manifest_sha256": protocol.get("tool_schema_manifest_sha256"),
}
if any(observed.get(field) != value for field, value in expected.items()):
raise VerifierError("controller_protocol_observation_mismatch")
if observed.get("shutdown_mode") != "stdin_eof_clean_exit":
raise VerifierError("controller_shutdown_terminal_invalid")
stages = _dict(receipt.get("stage_receipts"), "controller_stages_invalid")
rollback = _dict(
stages.get("rollback_or_no_write_terminal"), "controller_rollback_invalid"
)
if (
rollback.get("status")
!= "mcp_process_terminated_and_ephemeral_container_removed"
or rollback.get("ephemeral_container_removed") is not True
or rollback.get("production_route_changed") is not False
):
raise VerifierError("controller_rollback_invalid")
return receipt, _sha256(raw)
def validate_artifact_receipt_chain(
policy: dict[str, Any], controller_path: Path, verifier_path: Path
) -> tuple[str, str]:
artifact = _dict(policy.get("artifact_source"), "artifact_source_invalid")
controller_raw, controller = _read_json(
controller_path, "artifact_controller_receipt_unreadable"
)
verifier_raw, verifier = _read_json(
verifier_path, "artifact_verifier_receipt_unreadable"
)
controller_sha = _sha256(controller_raw)
verifier_sha = _sha256(verifier_raw)
if (
controller_sha != artifact.get("controller_receipt_sha256")
or verifier_sha != artifact.get("verifier_receipt_sha256")
or controller.get("schema_version") != ARTIFACT_CONTROLLER_SCHEMA
or controller.get("status")
!= "completed_internal_mirror_pending_independent_verifier"
or verifier.get("schema_version") != ARTIFACT_VERIFIER_SCHEMA
or verifier.get("status") != "completed_internal_mirror_verified"
or verifier.get("controller_receipt_sha256") != controller_sha
):
raise VerifierError("artifact_receipt_chain_invalid")
post = _dict(verifier.get("independent_post_verifier"), "artifact_verifier_invalid")
if (
post.get("status") != "passed"
or post.get("container_or_mcp_started") is not False
or controller.get("run_id") != verifier.get("run_id")
or controller.get("trace_id") != verifier.get("trace_id")
):
raise VerifierError("artifact_independent_verifier_invalid")
for receipt in (controller, verifier):
if (
receipt.get("internal_mirror_ref") != artifact.get("artifact_ref")
or receipt.get("policy_checksum") != artifact.get("policy_checksum")
or any(
receipt.get(field) is not False
for field in (
"promotion_allowed",
"replay_allowed",
"shadow_allowed",
"canary_allowed",
)
)
):
raise VerifierError("artifact_receipt_boundary_invalid")
return controller_sha, verifier_sha
def _docker() -> str:
executable = shutil.which("docker")
if not executable:
raise VerifierError("docker_cli_unavailable")
return executable
def _run(command: list[str], *, timeout: int, error: str) -> str:
try:
result = subprocess.run(
command,
check=False,
capture_output=True,
text=True,
timeout=timeout,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise VerifierError(error) from exc
if result.returncode != 0:
raise VerifierError(error)
return result.stdout
def _pull_and_readback(docker: str, ref: str, platform: str) -> None:
_run(
[docker, "pull", "--platform", platform, ref],
timeout=300,
error="image_pull_failed",
)
output = _run(
[docker, "image", "inspect", "--format", "{{json .RepoDigests}}", ref],
timeout=30,
error="image_readback_failed",
)
try:
digests = json.loads(output.strip())
except json.JSONDecodeError as exc:
raise VerifierError("image_readback_invalid") from exc
if not isinstance(digests, list) or ref not in digests:
raise VerifierError("image_digest_readback_mismatch")
def _safe_member(name: str) -> Path | None:
pure = PurePosixPath(name)
if pure.is_absolute() or not pure.parts or ".." in pure.parts:
raise VerifierError("archive_path_invalid")
if pure.parts[0] != "package":
raise VerifierError("archive_root_invalid")
if len(pure.parts) == 1:
return None
return Path(*pure.parts[1:])
def _unpack(tarball: Path, destination: Path) -> tuple[int, int]:
files = 0
size = 0
try:
archive = tarfile.open(tarball, "r:gz")
except (OSError, tarfile.TarError) as exc:
raise VerifierError("tarball_invalid") from exc
with archive:
for member in archive.getmembers():
relative = _safe_member(member.name)
if relative is None:
continue
target = destination / relative
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
continue
if not member.isfile() or member.issym() or member.islnk():
raise VerifierError("archive_member_type_invalid")
files += 1
size += member.size
if files > MAX_FILES or size > MAX_UNPACKED_BYTES:
raise VerifierError("archive_limits_exceeded")
source = archive.extractfile(member)
if source is None:
raise VerifierError("archive_member_unreadable")
content = source.read(member.size + 1)
if len(content) != member.size:
raise VerifierError("archive_member_size_mismatch")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
return files, size
def independently_materialize(
policy: dict[str, Any], directory: Path
) -> tuple[Path, int, int]:
artifact = _dict(policy.get("artifact_source"), "artifact_source_invalid")
runtime = _dict(policy.get("runtime"), "runtime_invalid")
docker = _docker()
artifact_ref = _oci_ref(artifact.get("artifact_ref"), "artifact_ref_invalid")
platform = _text(runtime.get("platform"), "platform_invalid")
_pull_and_readback(docker, artifact_ref, platform)
bundle = directory / "bundle"
modules = directory / "node_modules"
bundle.mkdir()
modules.mkdir()
container = f"awoooi-mcp-verifier-artifact-{uuid.uuid4().hex[:16]}"
try:
_run(
[
docker,
"create",
"--platform",
platform,
"--name",
container,
artifact_ref,
"/nonexistent/no-start",
],
timeout=60,
error="artifact_container_create_failed",
)
_run(
[docker, "cp", f"{container}:/artifact/.", str(bundle)],
timeout=60,
error="artifact_copy_failed",
)
finally:
subprocess.run(
[docker, "rm", "-f", container],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=30,
)
manifest_raw, manifest = _read_json(
bundle / "bundle-manifest.json", "bundle_manifest_invalid"
)
sbom_raw, sbom = _read_json(bundle / "sbom.cdx.json", "bundle_sbom_invalid")
if (
_sha256(manifest_raw) != artifact.get("bundle_manifest_sha256")
or _sha256(sbom_raw) != artifact.get("cyclonedx_sbom_sha256")
or manifest.get("schema_version") != ARTIFACT_BUNDLE_SCHEMA
or manifest.get("candidate_id") != policy.get("candidate_id")
or manifest.get("policy_checksum") != artifact.get("policy_checksum")
or sbom.get("bomFormat") != "CycloneDX"
or sbom.get("specVersion") != "1.5"
):
raise VerifierError("bundle_identity_invalid")
bundle_boundaries = _dict(
manifest.get("operation_boundaries"), "bundle_boundaries_invalid"
)
if any(
bundle_boundaries.get(field) is not False
for field in (
"mcp_server_started",
"browser_started",
"external_tool_call_performed",
"external_rag_write_performed",
"production_write_performed",
"runtime_request_or_response_body_stored",
)
):
raise VerifierError("bundle_boundary_invalid")
integrities = _dict(artifact.get("package_integrities"), "integrities_invalid")
rows = manifest.get("packages")
if not isinstance(rows, list) or len(rows) != 3:
raise VerifierError("bundle_package_set_invalid")
observed: set[str] = set()
total_files = 0
total_size = 0
for row in rows:
if not isinstance(row, dict):
raise VerifierError("bundle_package_set_invalid")
name = _text(row.get("name"), "package_name_invalid")
version = _text(row.get("version"), "package_version_invalid")
key = f"{name}@{version}"
tarball = bundle / _text(row.get("tarball_path"), "tarball_path_invalid")
try:
raw = tarball.read_bytes()
except OSError as exc:
raise VerifierError("tarball_unreadable") from exc
if (
key in observed
or integrities.get(key) != row.get("tarball_sha512_hex")
or hashlib.sha512(raw).hexdigest() != integrities.get(key)
):
raise VerifierError("tarball_integrity_mismatch")
observed.add(key)
destination = (
modules / "@playwright" / "mcp"
if name == "@playwright/mcp"
else modules / name
)
destination.mkdir(parents=True)
files, size = _unpack(tarball, destination)
total_files += files
total_size += size
_, package_json = _read_json(destination / "package.json", "package_json_invalid")
if package_json.get("name") != name or package_json.get("version") != version:
raise VerifierError("package_json_identity_mismatch")
if observed != set(integrities):
raise VerifierError("bundle_package_set_mismatch")
for path in modules.rglob("*"):
path.chmod(0o755 if path.is_dir() else 0o444)
modules.chmod(0o755)
directory.chmod(0o755)
return modules, total_files, total_size
def _send(process: subprocess.Popen[str], payload: dict[str, Any]) -> None:
if process.stdin is None:
raise VerifierError("mcp_stdin_unavailable")
try:
process.stdin.write(_canonical(payload).decode("utf-8") + "\n")
process.stdin.flush()
except (BrokenPipeError, OSError) as exc:
raise VerifierError("mcp_write_failed") from exc
def _receive(
process: subprocess.Popen[str], timeout_seconds: int, expected_id: int
) -> dict[str, Any]:
if process.stdout is None:
raise VerifierError("mcp_stdout_unavailable")
ready, _, _ = select.select([process.stdout], [], [], timeout_seconds)
if not ready:
raise VerifierError("mcp_response_timeout")
line = process.stdout.readline(MAX_PROTOCOL_LINE_BYTES + 1)
if not line or len(line.encode("utf-8")) > MAX_PROTOCOL_LINE_BYTES:
raise VerifierError("mcp_response_invalid")
try:
response = json.loads(line)
except json.JSONDecodeError as exc:
raise VerifierError("mcp_response_invalid") from exc
if (
not isinstance(response, dict)
or response.get("jsonrpc") != "2.0"
or response.get("id") != expected_id
or "error" in response
or not isinstance(response.get("result"), dict)
):
raise VerifierError("mcp_response_invalid")
return response["result"]
def _independent_tool_hashes(tools: Any) -> tuple[int, str, str, set[str]]:
if not isinstance(tools, list):
raise VerifierError("tool_list_invalid")
names: list[str] = []
schema_rows: list[dict[str, Any]] = []
for tool in tools:
if not isinstance(tool, dict):
raise VerifierError("tool_schema_invalid")
name = _text(tool.get("name"), "tool_name_invalid")
schema = tool.get("inputSchema")
if not isinstance(schema, dict):
raise VerifierError("tool_schema_invalid")
row: dict[str, Any] = {"name": name, "inputSchema": schema}
if "annotations" in tool:
annotations = tool["annotations"]
if not isinstance(annotations, dict):
raise VerifierError("tool_annotations_invalid")
row["annotations"] = annotations
names.append(name)
schema_rows.append(row)
if len(names) != len(set(names)):
raise VerifierError("tool_names_not_unique")
schema_rows.sort(key=lambda row: row["name"])
return (
len(tools),
_sha256(_canonical(sorted(names))),
_sha256(_canonical(schema_rows)),
set(names),
)
def independently_replay(
policy: dict[str, Any], modules: Path
) -> dict[str, Any]:
runtime = _dict(policy.get("runtime"), "runtime_invalid")
protocol = _dict(policy.get("protocol_contract"), "protocol_invalid")
docker = _docker()
runtime_ref = _oci_ref(runtime.get("image_ref"), "runtime_ref_invalid")
platform = _text(runtime.get("platform"), "platform_invalid")
_pull_and_readback(docker, runtime_ref, platform)
container = f"awoooi-mcp-replay-verifier-{uuid.uuid4().hex[:16]}"
command = [
docker,
"run",
"--rm",
"--platform",
platform,
"--name",
container,
"--network",
"none",
"--read-only",
"--security-opt",
"no-new-privileges",
"--cap-drop",
"ALL",
"--pids-limit",
str(runtime.get("pids_limit")),
"--memory",
_text(runtime.get("memory_limit"), "memory_limit_invalid"),
"--cpus",
_text(runtime.get("cpu_limit"), "cpu_limit_invalid"),
"--user",
_text(runtime.get("user"), "runtime_user_invalid"),
"--env",
"HOME=/tmp",
"--env",
"CI=1",
"--tmpfs",
_text(runtime.get("tmpfs"), "tmpfs_invalid"),
"--stop-timeout",
str(runtime.get("shutdown_timeout_seconds")),
"-i",
"-v",
f"{modules.resolve()}:/work/node_modules:ro",
"-w",
"/work",
runtime_ref,
*runtime.get("entrypoint", []),
*runtime.get("arguments", []),
]
stderr_file = tempfile.TemporaryFile(mode="w+t", encoding="utf-8")
process: subprocess.Popen[str] | None = None
try:
try:
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=stderr_file,
text=True,
bufsize=1,
env={**os.environ, "DOCKER_CLI_HINTS": "false"},
)
except OSError as exc:
raise VerifierError("mcp_process_start_failed") from exc
_send(
process,
{
"jsonrpc": "2.0",
"id": 41,
"method": "initialize",
"params": {
"protocolVersion": protocol.get("protocol_version"),
"capabilities": {},
"clientInfo": {
"name": "awoooi-external-mcp-replay-verifier",
"version": "1",
},
},
},
)
timeout = int(runtime.get("startup_timeout_seconds"))
initialized = _receive(process, timeout, 41)
server = _dict(initialized.get("serverInfo"), "server_info_invalid")
if (
initialized.get("protocolVersion") != protocol.get("protocol_version")
or server.get("name") != protocol.get("server_name")
or server.get("version") != protocol.get("server_version")
):
raise VerifierError("initialize_contract_mismatch")
_send(
process,
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {},
},
)
_send(
process,
{"jsonrpc": "2.0", "id": 42, "method": "tools/list", "params": {}},
)
listed = _receive(process, timeout, 42)
count, names_hash, schemas_hash, names = _independent_tool_hashes(
listed.get("tools")
)
if (
count != protocol.get("tool_count")
or names_hash != protocol.get("tool_name_set_sha256")
or schemas_hash != protocol.get("tool_schema_manifest_sha256")
or names
!= set(protocol.get("adapter_allowed_tools", []))
| set(protocol.get("adapter_denied_tools", []))
):
raise VerifierError("tool_contract_mismatch")
if process.stdin is None:
raise VerifierError("mcp_stdin_unavailable")
process.stdin.close()
try:
exit_code = process.wait(timeout=int(runtime.get("shutdown_timeout_seconds")))
except subprocess.TimeoutExpired as exc:
raise VerifierError("mcp_shutdown_timeout") from exc
if exit_code != 0:
raise VerifierError("mcp_exit_invalid")
finally:
if process is not None and process.poll() is None:
process.terminate()
try:
process.wait(timeout=int(runtime.get("shutdown_timeout_seconds", 5)))
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
subprocess.run(
[docker, "rm", "-f", container],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=30,
)
stderr_file.close()
inspect = subprocess.run(
[docker, "container", "inspect", container],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=30,
)
if inspect.returncode == 0:
raise VerifierError("container_cleanup_failed")
return {
"protocol_version": protocol.get("protocol_version"),
"server_name": protocol.get("server_name"),
"server_version": protocol.get("server_version"),
"tool_count": count,
"tool_name_set_sha256": names_hash,
"tool_schema_manifest_sha256": schemas_hash,
"process_exit_code": 0,
"shutdown_mode": "stdin_eof_clean_exit",
"ephemeral_container_removed": True,
}
def build_verifier_receipt(
*,
policy: dict[str, Any],
policy_checksum: str,
controller: dict[str, Any],
controller_sha256: str,
artifact_controller_sha256: str,
artifact_verifier_sha256: str,
observation: dict[str, Any],
package_file_count: int,
package_unpacked_bytes: int,
) -> dict[str, Any]:
artifact = _dict(policy.get("artifact_source"), "artifact_source_invalid")
runtime = _dict(policy.get("runtime"), "runtime_invalid")
return {
"schema_version": VERIFIER_SCHEMA,
"run_id": controller["run_id"],
"trace_id": controller["trace_id"],
"work_item_id": controller["work_item_id"],
"candidate_id": controller["candidate_id"],
"adapter_id": controller["adapter_id"],
"risk_class": "high",
"status": "completed_protocol_schema_replay_verified",
"observed_at": datetime.now(timezone.utc).isoformat(),
"policy_checksum": policy_checksum,
"controller_receipt_sha256": controller_sha256,
"artifact_controller_receipt_sha256": artifact_controller_sha256,
"artifact_verifier_receipt_sha256": artifact_verifier_sha256,
"artifact_ref": artifact.get("artifact_ref"),
"runtime_image_ref": runtime.get("image_ref"),
"independent_post_verifier": {
"status": "passed",
"implementation": "standalone_no_controller_import",
"fresh_artifact_digest_readback": True,
"fresh_runtime_digest_readback": True,
"fresh_protocol_replay": True,
"package_file_count": package_file_count,
"package_unpacked_bytes": package_unpacked_bytes,
**observation,
},
"direct_gateway_registration_allowed": False,
"adapter_registration_allowed": False,
"browser_started": False,
"tool_call_performed": False,
"navigation_performed": False,
"raw_request_or_response_body_stored": False,
"external_rag_write_performed": False,
"production_write_performed": False,
"shadow_allowed": False,
"canary_allowed": False,
"promotion_allowed": False,
"promotion_blockers": policy.get("promotion_blockers_after_replay"),
"rollback_terminal": {
"status": "mcp_process_terminated_and_ephemeral_container_removed",
"candidate_registration_state": "disabled",
"ephemeral_container_removed": True,
"production_route_changed": False,
},
"learning_writeback": {
"status": "verified_receipt_ready_for_gitea_audit_branch_writeback",
"external_rag_write_performed": False,
"km_write_performed": False,
},
}
def _write(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(_canonical(payload) + b"\n")
def _failure_receipt(
policy: dict[str, Any], controller: dict[str, Any], error: str
) -> dict[str, Any]:
return {
"schema_version": VERIFIER_SCHEMA,
"run_id": controller.get("run_id"),
"trace_id": controller.get("trace_id"),
"work_item_id": policy.get("work_item_id"),
"candidate_id": policy.get("candidate_id"),
"status": "blocked_with_safe_next_action",
"error_class": error,
"observed_at": datetime.now(timezone.utc).isoformat(),
"browser_started": False,
"tool_call_performed": False,
"external_rag_write_performed": False,
"production_write_performed": False,
"promotion_allowed": False,
"rollback_terminal": {
"status": "candidate_disabled_ephemeral_process_cleanup_attempted",
"candidate_registration_state": "disabled",
"production_route_changed": False,
},
}
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--policy", type=Path, required=True)
parser.add_argument("--artifact-controller-receipt", type=Path, required=True)
parser.add_argument("--artifact-verifier-receipt", type=Path, required=True)
parser.add_argument("--controller-receipt", type=Path, required=True)
parser.add_argument("--verifier-receipt", type=Path, required=True)
return parser
def main(argv: list[str] | None = None) -> int:
args = _parser().parse_args(argv)
policy, policy_checksum = load_and_validate_policy(args.policy)
controller: dict[str, Any] = {}
try:
controller, controller_sha256 = validate_controller_receipt(
policy, policy_checksum, args.controller_receipt
)
artifact_controller_sha256, artifact_verifier_sha256 = (
validate_artifact_receipt_chain(
policy,
args.artifact_controller_receipt,
args.artifact_verifier_receipt,
)
)
with tempfile.TemporaryDirectory(prefix="awoooi-mcp-replay-verifier-") as temp:
modules, file_count, unpacked_bytes = independently_materialize(
policy, Path(temp)
)
observation = independently_replay(policy, modules)
receipt = build_verifier_receipt(
policy=policy,
policy_checksum=policy_checksum,
controller=controller,
controller_sha256=controller_sha256,
artifact_controller_sha256=artifact_controller_sha256,
artifact_verifier_sha256=artifact_verifier_sha256,
observation=observation,
package_file_count=file_count,
package_unpacked_bytes=unpacked_bytes,
)
except VerifierError as exc:
_write(args.verifier_receipt, _failure_receipt(policy, controller, str(exc)))
raise
_write(args.verifier_receipt, receipt)
print(json.dumps(receipt, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except VerifierError as exc:
print(
json.dumps(
{
"schema_version": VERIFIER_SCHEMA,
"status": "blocked_with_safe_next_action",
"error_class": str(exc),
},
sort_keys=True,
),
file=sys.stderr,
)
raise SystemExit(1) from None