From 8a314a83737c0f15a2d51159e1cd3cb8e0e21a49 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 11:56:08 +0800 Subject: [PATCH] feat(mcp): expose verified replay receipts --- .../src/services/mcp_control_plane_service.py | 70 +++++++++++++++++++ apps/api/tests/test_mcp_control_plane_api.py | 59 ++++++++++++++++ apps/web/messages/en.json | 10 ++- apps/web/messages/zh-TW.json | 10 ++- .../src/app/[locale]/automation/mcp/page.tsx | 35 +++++++++- docs/LOGBOOK.md | 11 ++- .../mcp-control-plane-catalog.snapshot.json | 46 ++++++++++-- 7 files changed, 231 insertions(+), 10 deletions(-) diff --git a/apps/api/src/services/mcp_control_plane_service.py b/apps/api/src/services/mcp_control_plane_service.py index 09d212a84..2176dc2d4 100644 --- a/apps/api/src/services/mcp_control_plane_service.py +++ b/apps/api/src/services/mcp_control_plane_service.py @@ -497,6 +497,68 @@ def _validate_catalog(payload: dict[str, Any], label: str) -> None: raise ValueError( f"{label}: external capability {row.get('capability_id')} cannot deploy without immutable receipts" ) + if row.get("compatibility_state") != "protocol_schema_compatibility_verified": + continue + if row.get("replay_status") != "completed_protocol_schema_replay_verified": + raise ValueError( + f"{label}: verified compatibility requires a completed replay verifier receipt" + ) + if row.get("replay_independent_verifier") is not True: + raise ValueError( + f"{label}: verified compatibility requires an independent verifier" + ) + if not isinstance(row.get("replay_tool_count"), int) or int( + row["replay_tool_count"] + ) <= 0: + raise ValueError( + f"{label}: verified compatibility requires a positive replay tool count" + ) + if row.get("replay_process_exit_code") != 0 or row.get( + "replay_shutdown_mode" + ) != "stdin_eof_clean_exit": + raise ValueError( + f"{label}: verified compatibility requires a clean MCP process terminal" + ) + if row.get("replay_ephemeral_container_removed") is not True: + raise ValueError( + f"{label}: verified compatibility requires ephemeral container removal" + ) + no_effect_fields = ( + "replay_browser_started", + "replay_tool_call_performed", + "replay_navigation_performed", + "replay_external_rag_write_performed", + "replay_production_write_performed", + "replay_promotion_allowed", + ) + if any(row.get(field) is not False for field in no_effect_fields): + raise ValueError( + f"{label}: compatibility replay must remain no-browser, no-tool, no-write, and no-promotion" + ) + if row.get("deployment_allowed") is not False: + raise ValueError( + f"{label}: compatibility verification alone cannot enable deployment" + ) + fixed_hex_fields = { + "replay_audit_commit": 40, + "replay_artifact_audit_commit": 40, + "artifact_latest_revalidation_audit_commit": 40, + "replay_controller_receipt_sha256": 64, + "replay_verifier_receipt_sha256": 64, + "replay_tool_name_set_sha256": 64, + "replay_tool_schema_manifest_sha256": 64, + } + if any( + not _is_lower_hex(row.get(field), length) + for field, length in fixed_hex_fields.items() + ): + raise ValueError( + f"{label}: verified compatibility requires immutable audit and replay hashes" + ) + if "compatibility_replay_not_executed" in _strings(row.get("blockers")): + raise ValueError( + f"{label}: verified compatibility cannot retain replay-not-executed blocker" + ) products = _dict_rows(payload.get("products")) product_ids = {str(row.get("product_id") or "") for row in products} @@ -529,6 +591,14 @@ def _capability_summary(row: dict[str, Any]) -> dict[str, Any]: } +def _is_lower_hex(value: Any, length: int) -> bool: + return ( + isinstance(value, str) + and len(value) == length + and all(character in "0123456789abcdef" for character in value) + ) + + def _dict_rows(value: Any) -> list[dict[str, Any]]: if not isinstance(value, list): return [] diff --git a/apps/api/tests/test_mcp_control_plane_api.py b/apps/api/tests/test_mcp_control_plane_api.py index d8fc57116..4536a5890 100644 --- a/apps/api/tests/test_mcp_control_plane_api.py +++ b/apps/api/tests/test_mcp_control_plane_api.py @@ -52,6 +52,32 @@ def test_catalog_contains_exact_12_products_and_explicit_missing_rows() -> None: "tool_output_contains_cross_tool_prompt_injection" in capabilities["quarantine.agent-bounty-mcp"]["blockers"] ) + playwright = capabilities["external.playwright-verifier"] + assert playwright["compatibility_state"] == ( + "protocol_schema_compatibility_verified" + ) + assert playwright["replay_status"] == ( + "completed_protocol_schema_replay_verified" + ) + assert playwright["replay_protocol_version"] == "2025-06-18" + assert playwright["replay_tool_count"] == 24 + assert playwright["replay_independent_verifier"] is True + assert playwright["replay_ephemeral_container_removed"] is True + assert playwright["replay_browser_started"] is False + assert playwright["replay_tool_call_performed"] is False + assert playwright["replay_external_rag_write_performed"] is False + assert playwright["replay_production_write_performed"] is False + assert playwright["replay_promotion_allowed"] is False + assert playwright["deployment_allowed"] is False + assert "compatibility_replay_not_executed" not in playwright["blockers"] + assert playwright["replay_audit_commit"] == ( + "4a15edb8309a550b6d77f8c1f88b6b1203206045" + ) + assert any( + "mcp-replay-receipts@4a15edb8309a550b6d77f8c1f88b6b1203206045" + in ref + for ref in playwright["evidence_refs"] + ) assert not any( row["deployment_allowed"] for row in catalog["capabilities"] @@ -107,6 +133,29 @@ def test_catalog_rejects_external_deployment_without_immutable_receipts( raise AssertionError("unsafe external deployment must be rejected") +def test_catalog_rejects_compatibility_replay_with_side_effect( + tmp_path: Path, +) -> None: + catalog = load_mcp_control_plane_catalog(_OPERATIONS_DIR) + playwright = next( + row + for row in catalog["capabilities"] + if row["capability_id"] == "external.playwright-verifier" + ) + playwright["replay_tool_call_performed"] = True + (tmp_path / "mcp-control-plane-catalog.snapshot.json").write_text( + json.dumps(catalog), + encoding="utf-8", + ) + + try: + load_mcp_control_plane_catalog(tmp_path) + except ValueError as exc: + assert "no-browser, no-tool, no-write, and no-promotion" in str(exc) + else: + raise AssertionError("compatibility replay side effects must fail closed") + + def test_overview_endpoint_returns_real_committed_catalog_without_runtime() -> None: app = FastAPI() app.include_router(router, prefix="/api/v1") @@ -127,6 +176,16 @@ def test_overview_endpoint_returns_real_committed_catalog_without_runtime() -> N data["operation_boundaries"]["operator_auth_required_for_future_mutation"] is True ) + playwright = next( + row + for row in data["capabilities"] + if row["capability_id"] == "external.playwright-verifier" + ) + assert playwright["replay_status"] == ( + "completed_protocol_schema_replay_verified" + ) + assert playwright["replay_run_id"] == "90d0515a-0812-4b04-9913-b210313accff" + assert playwright["deployment_allowed"] is False def test_overview_promotes_durable_lifecycle_receipt_not_static_policy() -> None: diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9958eaf6f..8cc647cce 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -202,10 +202,18 @@ "scope": "Scope", "version": "Version", "supply": "Digest / SBOM / signature", + "replay": "Compatibility replay", "promote": "Deployable" }, "allowed": "Deployment allowed", - "blocked": "Deployment not allowed" + "blocked": "Deployment not allowed", + "blockerCount": "{count} blockers remain", + "replay": { + "verified": "Protocol / schema verified", + "pending": "Compatibility replay pending", + "protocolTools": "MCP {protocol} · {tools} tools", + "receipt": "receipt {id}" + } }, "lifecycle": { "title": "Version discovery to rollback loop", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 6b60e85ae..a86239adf 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -202,10 +202,18 @@ "scope": "Scope", "version": "版本", "supply": "Digest / SBOM / 簽章", + "replay": "相容性重播", "promote": "可部署" }, "allowed": "已允許部署", - "blocked": "尚未允許部署" + "blocked": "尚未允許部署", + "blockerCount": "仍有 {count} 個 blocker", + "replay": { + "verified": "Protocol / schema 已驗證", + "pending": "尚未完成 compatibility replay", + "protocolTools": "MCP {protocol} · {tools} tools", + "receipt": "receipt {id}" + } }, "lifecycle": { "title": "版本發現到回滾循環", diff --git a/apps/web/src/app/[locale]/automation/mcp/page.tsx b/apps/web/src/app/[locale]/automation/mcp/page.tsx index 8420d9a3f..fd4e25de7 100644 --- a/apps/web/src/app/[locale]/automation/mcp/page.tsx +++ b/apps/web/src/app/[locale]/automation/mcp/page.tsx @@ -38,6 +38,14 @@ interface Capability extends CapabilitySummary { digest_state: string sbom_state: string signature_state: string + compatibility_state?: string + replay_status?: string + replay_protocol_version?: string + replay_server_version?: string + replay_tool_count?: number + replay_run_id?: string + replay_independent_verifier?: boolean + replay_promotion_allowed?: boolean rag_policy: string blockers: string[] } @@ -633,7 +641,7 @@ export default function McpControlPlanePage({ params }: { params: { locale: stri
- +
@@ -643,6 +651,7 @@ export default function McpControlPlanePage({ params }: { params: { locale: stri + @@ -662,8 +671,32 @@ export default function McpControlPlanePage({ params }: { params: { locale: stri ) : null} + ))} diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 04276b79f..d22a7a56d 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -10,19 +10,24 @@ **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。 -- 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` 通過。 +- artifact/replay controller + independent verifier `61 passed`、MCP control-plane/federation/version lifecycle/runner-health API `28 passed`、shared audit writeback `4 passed`,本輪聚焦回歸合計 `93 passed`。Ruff、py_compile、JSON/YAML parse、source-control owner guard與 `git diff --check` 通過。 - Gitea replay `#5183` 已在 source `387f841...` 完成 controller + independent verifier,run `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`。 +- 修正由 source `7cd0f69475c4828c46c249d33d327a01bc5cb941` normal push後,artifact `#5191` 完整成功;audit branch從 `9c08ada...` 線性前進到 `226a733b022072d13fb4a4d69b81b7c2fac11cc3`,parent正是舊 audit tip,沒有 merge或 force。 +- replay `#5192` 隨後完整成功;run `90d0515a-0812-4b04-9913-b210313accff`、audit commit `4a15edb8309a550b6d77f8c1f88b6b1203206045` 的 parent為舊 tip `281d6e0...`。controller/verifier receipt SHA-256分別為 `279cf92ca06d418537b54921df7deab1377eac290cc885102f608f832cd013f5` / `161d41484cddab750e1904099c3adcb0909756262331351b09f28b632194cbad`。 +- production read model新增 compatibility state、protocol/server/tool schema、audit/run/receipt identity與 no-effect boundaries;API fail-closed拒絕缺 hash、非獨立 verifier、非 clean exit、container未移除或任何 browser/tool/navigation/RAG/production write/promotion side effect。`deployment_allowed`持續 false。 +- `/automation/mcp` 外部候選表新增 compatibility replay欄;Playwright列顯示 protocol/schema verified、MCP `2025-06-18`、24 tools與短 receipt ID,同列仍顯示 promotion disabled與5個剩餘 blockers。繁中/英文、desktop/mobile與內部水平 scroller同步完成。 - global security mirror guard另抓到 latest main 既有 `apps/web/src/app/[locale]/iwooos/page.tsx` 的 `raw_blocked_waiting_state` 敏感字樣;本 work item未修改該檔,保留為獨立 drift blocker,不以 replay source 綠燈覆蓋。 +- catalog/API `7 passed`、Web typecheck與 production build成功;`/[locale]/automation/mcp` 為 `7.14 kB` / First Load JS `240 kB`。Browser桌機 `1440x1000`與手機 `390x844`均讀到12產品、replay receipt與 promotion disabled;頁面水平溢位 `0`、手機表格 overflow限制在內部 scroller、console error `0`。 **尚未完成 / 不誤報**: -- `#5183` 證明 protocol/schema replay本身與 durable receipt可成立,但最新 source `5eb46f9...` 的 repeatability writeback修正尚未 normal push / Gitea workflow terminal;artifact/replay recurring lanes仍要各取得一筆新的 success terminal,production catalog也尚未讀取該 receipt,因此不得把 compatibility或週期升級 loop標 completed。 +- recurring artifact/replay audit writeback已由 `#5191/#5192`關閉;但 catalog/API/UI投影仍只在本地 source,尚未 normal push、CD、deploy marker與 production readback。先前 writeback source的 CD `#5190` 仍在 build-and-deploy,兩個 source generation不得混為同一 production closure。 - 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 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。 +- 等待 `#5190` terminal後 normal push catalog/API/UI,取得新的 Gitea CD/deploy marker與 production API/mobile/desktop visible readback;再建立 immutable browser runtime與隔離 public-origin shadow/canary/rollback lane。 ## 2026-07-15 — P0 MCP Playwright immutable mirror 與獨立供應鏈 verifier diff --git a/docs/operations/mcp-control-plane-catalog.snapshot.json b/docs/operations/mcp-control-plane-catalog.snapshot.json index 7b5d6576b..23040150e 100644 --- a/docs/operations/mcp-control-plane-catalog.snapshot.json +++ b/docs/operations/mcp-control-plane-catalog.snapshot.json @@ -1,6 +1,6 @@ { "schema_version": "mcp_control_plane_catalog_v1", - "generated_at": "2026-07-15T18:30:00+08:00", + "generated_at": "2026-07-15T11:53:26+08:00", "status": "source_inventory_ready_runtime_federation_partial", "architecture": { "decision": "single_control_plane_reuse_existing_gateway_registry_rag", @@ -300,21 +300,59 @@ "scope": "allowlisted_origins_only", "data_boundary": "public_and_test_surfaces", "deployment_allowed": false, - "version_state": "exact_0.0.78_check_verified", + "version_state": "exact_0.0.78_protocol_schema_replay_verified", "digest_state": "internal_mirror_digest_verified_sha256_de002c418bbb94fb4609539c4259c39ccb7f8b353f8bac8fbcba9e5742ad79ed", "sbom_state": "cyclonedx_1.5_mirror_readback_verified", "signature_state": "npm_registry_ecdsa_and_slsa_subject_independently_reverified", + "compatibility_state": "protocol_schema_compatibility_verified", + "replay_status": "completed_protocol_schema_replay_verified", + "replay_protocol_version": "2025-06-18", + "replay_server_name": "Playwright", + "replay_server_version": "1.62.0-alpha-1783623505000", + "replay_tool_count": 24, + "replay_tool_name_set_sha256": "12d23fea9d8d1a1de3f44863dc00cf393f0a2165323838cf93fed30a6a4cc237", + "replay_tool_schema_manifest_sha256": "c3737ced21147d0512b0400df5885c95a545bc915892b5c8f928cee78428ac99", + "replay_adapter_id": "playwright_public_accessibility_verifier_v1", + "replay_adapter_allowed_tool_count": 4, + "replay_adapter_denied_tool_count": 20, + "replay_run_id": "90d0515a-0812-4b04-9913-b210313accff", + "replay_trace_id": "mcp-replay-90d0515a-0812-4b04-9913-b210313accff", + "replay_work_item_id": "MCP-REPLAY-PLAYWRIGHT-001", + "replay_observed_at": "2026-07-15T03:37:08.023613+00:00", + "replay_audit_commit": "4a15edb8309a550b6d77f8c1f88b6b1203206045", + "replay_artifact_audit_commit": "9c08ada5c0f15bb6924304af29cd13c8376182ff", + "artifact_latest_revalidation_audit_commit": "226a733b022072d13fb4a4d69b81b7c2fac11cc3", + "replay_controller_receipt_sha256": "279cf92ca06d418537b54921df7deab1377eac290cc885102f608f832cd013f5", + "replay_verifier_receipt_sha256": "161d41484cddab750e1904099c3adcb0909756262331351b09f28b632194cbad", + "replay_independent_verifier": true, + "replay_process_exit_code": 0, + "replay_shutdown_mode": "stdin_eof_clean_exit", + "replay_ephemeral_container_removed": true, + "replay_browser_started": false, + "replay_tool_call_performed": false, + "replay_navigation_performed": false, + "replay_external_rag_write_performed": false, + "replay_production_write_performed": false, + "replay_promotion_allowed": false, "rag_policy": "screenshots_and_accessibility_receipts_only", "blockers": [ "registered_public_origin_replay_adapter_missing", - "compatibility_replay_not_executed", - "shadow_canary_verifier_and_rollback_execution_pending" + "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" ], "evidence_refs": [ "config/mcp/playwright-mcp-artifact-policy.json", + "config/mcp/playwright-mcp-replay-policy.json", + ".gitea/workflows/mcp-external-replay.yaml", "docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-check.snapshot.json", "gitea:mcp-artifact-receipts@9c08ada5c0f15bb6924304af29cd13c8376182ff:docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-controller.snapshot.json", "gitea:mcp-artifact-receipts@9c08ada5c0f15bb6924304af29cd13c8376182ff:docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-verifier.snapshot.json", + "gitea:mcp-artifact-receipts@226a733b022072d13fb4a4d69b81b7c2fac11cc3:docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-verifier.snapshot.json", + "gitea:mcp-replay-receipts@4a15edb8309a550b6d77f8c1f88b6b1203206045:docs/operations/external-mcp-replays/playwright-mcp-0.0.78-controller.snapshot.json", + "gitea:mcp-replay-receipts@4a15edb8309a550b6d77f8c1f88b6b1203206045:docs/operations/external-mcp-replays/playwright-mcp-0.0.78-verifier.snapshot.json", + "gitea-run:wooo/awoooi/actions/runs/5192", "registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp@sha256:de002c418bbb94fb4609539c4259c39ccb7f8b353f8bac8fbcba9e5742ad79ed" ] },
{t('external.caption')}
{t('external.columns.scope')} {t('external.columns.version')} {t('external.columns.supply')}{t('external.columns.replay')} {t('external.columns.promote')}
{item.digest_state} / {item.sbom_state} / {item.signature_state} + {item.compatibility_state === 'protocol_schema_compatibility_verified' + && item.replay_status === 'completed_protocol_schema_replay_verified' + && item.replay_independent_verifier === true ? ( +
+
{t('external.replay.verified')}
+
+ {t('external.replay.protocolTools', { + protocol: item.replay_protocol_version ?? '--', + tools: item.replay_tool_count ?? 0, + })} +
+
+ {t('external.replay.receipt', { id: item.replay_run_id?.slice(0, 8) ?? '--' })} +
+
+ ) : ( + {t('external.replay.pending')} + )} +
{item.deployment_allowed ? : } +

{t('external.blockerCount', { count: item.blockers.length })}