- Alerting
+ {t('eyebrow')}
{t('title')}
{t('subtitle')}
@@ -93,12 +165,167 @@ export default function NotificationsPage({ params }: { params: { locale: string
+ {/* Canonical Telegram routing cockpit */}
+
+
+
+
+
+
{t('routing.title')}
+
+
+ {t('routing.subtitle')}
+
+
+ {routing?.runtime_enforcement.gateway_pre_send_gate_active && (
+
+
+ {t('routing.gateActive')}
+
+ )}
+
+
+ {routingLoading && (
+
+ {[0, 1, 2, 3].map(item =>
)}
+
+ )}
+
+ {!routingLoading && routingError && (
+
+
+
{t('routing.unavailable')}
+
+ )}
+
+ {!routingLoading && routing && (
+
+
+ {[
+ { key: 'products', label: t('routing.metrics.products'), value: routing.rollups.product_count, tone: 'text-[#141413]' },
+ { key: 'routes', label: t('routing.metrics.routes'), value: routing.rollups.route_count, tone: 'text-[#141413]' },
+ { key: 'allowed', label: t('routing.metrics.allowed'), value: routing.rollups.runtime_allowed_route_count, tone: 'text-[#17602a]' },
+ { key: 'blocked', label: t('routing.metrics.blocked'), value: routing.rollups.runtime_blocked_route_count, tone: 'text-[#9f2f25]' },
+ ].map(({ key, label, value, tone }) => (
+
+ ))}
+
+
+
+
{t('routing.sharedTitle')}
+
+ {t('routing.sharedDetail')}
+
+
+
+
+
+ {t('routing.rolesSummary', { count: routing.destination_roles.length })}
+
+
+ {routing.destination_roles.map(role => (
+
+
+
{role.visible_label}
+
+ {role.monitoring_owner ? t('routing.monitoringOwner') : t('routing.nonMonitoringOwner')}
+
+
+
+ {role.bot_alias} → {role.chat_alias}
+
+
{role.role}
+
+ ))}
+
+
+
+
+
+ {t('routing.routesSummary', {
+ products: routing.products.length,
+ routes: routing.routes.length,
+ })}
+
+
+ {routing.products.map(product => {
+ const productRoutes = routing.routes.filter(route => route.product_id === product.product_id)
+ return (
+
+
+
+
{product.display_name}
+
{product.product_id}
+
+
+ {product.egress_status}
+
+
+
+ {productRoutes.map(route => (
+
+
+
{route.signal_family}
+
+ {route.decision === 'allow' ? t('routing.allow') : t('routing.noSend')}
+
+
+
+ {route.severity.join('/')} · {route.bot_alias} → {route.chat_alias}
+
+
{route.decision_reason}
+
+ ))}
+
+
+ )
+ })}
+
+
+
+
+ {t('routing.footer', { runtime: routing.status, source: routing.source_status })}
+
+
+ {t('routing.generatedAt', {
+ time: new Intl.DateTimeFormat(params.locale, {
+ dateStyle: 'medium',
+ timeStyle: 'medium',
+ timeZone: 'Asia/Taipei',
+ }).format(new Date(routing.runtime_generated_at)),
+ })}
+
+
+ )}
+
+
{/* Summary */}
{!loading && channels.length > 0 && (
{activeCount}
- Active Channels
+ {t('activeChannels')}
0 ? 'text-[#9f2f25]' : 'text-[#77736a]')}>
{errorCount}
- Errors
+ {t('errors')}
)}
diff --git a/config/mcp/playwright-mcp-artifact-policy.json b/config/mcp/playwright-mcp-artifact-policy.json
new file mode 100644
index 000000000..daead51a9
--- /dev/null
+++ b/config/mcp/playwright-mcp-artifact-policy.json
@@ -0,0 +1,149 @@
+{
+ "schema_version": "awoooi_external_mcp_artifact_policy_v1",
+ "work_item_id": "MCP-ARTIFACT-PLAYWRIGHT-001",
+ "candidate_id": "external.playwright-verifier",
+ "risk_level": "high",
+ "catalog_source": {
+ "kind": "discovery_only",
+ "name": "mcp-so",
+ "url": "https://mcp.so/servers/playwright-mcp",
+ "executable_supply_chain_authority": false
+ },
+ "registry_contract": {
+ "registry_origin": "https://registry.npmjs.org",
+ "keys_url": "https://registry.npmjs.org/-/npm/v1/keys",
+ "allowed_hosts": [
+ "registry.npmjs.org"
+ ],
+ "redirects_allowed": false,
+ "maximum_metadata_bytes": 1048576,
+ "maximum_tarball_bytes": 8388608,
+ "maximum_unpacked_bytes": 33554432,
+ "signature_key": {
+ "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
+ "keytype": "ecdsa-sha2-nistp256",
+ "scheme": "ecdsa-sha2-nistp256",
+ "expires": null,
+ "public_key_der_base64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=="
+ }
+ },
+ "packages": [
+ {
+ "name": "@playwright/mcp",
+ "version": "0.0.78",
+ "metadata_url": "https://registry.npmjs.org/@playwright%2Fmcp/0.0.78",
+ "tarball_url": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.78.tgz",
+ "integrity": "sha512-XLTUeA6mEN9sQ+hJ4dfG8EIkDbxS0K3Trc2RBkUJuf02TgE2FQRNTMtq/aJfhyRMINsRl/Ybc4sxcWLtFn4/TQ==",
+ "shasum_sha1": "305c96c4ac0179bd37622fe4ed4162493513b33e",
+ "signature": "MEQCIFFsdei1ioeXzOhsBHdVv1XBungfkFzNx6eHS+igHlDaAiBfcETN9LM6vcv8GU8ch9KtODfcx3NXW0HW775r66Rzag==",
+ "attestations_url": "https://registry.npmjs.org/-/npm/v1/attestations/@playwright%2fmcp@0.0.78",
+ "slsa_subject_name": "pkg:npm/%40playwright/mcp@0.0.78",
+ "slsa_subject_sha512_hex": "5cb4d4780ea610df6c43e849e1d7c6f042240dbc52d0add3adcd91064509b9fd364e013615044d4ccb6afda25f87244c20db1197f61b738b317162ed167e3f4d",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.62.0-alpha-1783623505000",
+ "playwright-core": "1.62.0-alpha-1783623505000"
+ }
+ },
+ {
+ "name": "playwright",
+ "version": "1.62.0-alpha-1783623505000",
+ "metadata_url": "https://registry.npmjs.org/playwright/1.62.0-alpha-1783623505000",
+ "tarball_url": "https://registry.npmjs.org/playwright/-/playwright-1.62.0-alpha-1783623505000.tgz",
+ "integrity": "sha512-6KV9h4PP3hqu4NaGdxxcijWfYh9LJcFI/R2sP4TTC4I5cFo3oRawN0ETlW5MkE3cQEgKhhoj0KUNz4sfpCT0Tg==",
+ "shasum_sha1": "d04815de36073de29cc0b02c69bfd657231ec0c1",
+ "signature": "MEUCIQDw1UimZ6bCEYoTrl+bW4mopC7zomhYdObstPBijNCbMgIgUYnCgVS8CaC3sIfjJwe4RL3RY3SO3Zxy92PcP63vr7w=",
+ "attestations_url": "https://registry.npmjs.org/-/npm/v1/attestations/playwright@1.62.0-alpha-1783623505000",
+ "slsa_subject_name": "pkg:npm/playwright@1.62.0-alpha-1783623505000",
+ "slsa_subject_sha512_hex": "e8a57d8783cfde1aaee0d686771c5c8a359f621f4b25c148fd1dac3f84d30b8239705a37a116b0374113956e4c904ddc40480a861a23d0a50dcf8b1fa424f44e",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.62.0-alpha-1783623505000"
+ },
+ "optional_dependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ {
+ "name": "playwright-core",
+ "version": "1.62.0-alpha-1783623505000",
+ "metadata_url": "https://registry.npmjs.org/playwright-core/1.62.0-alpha-1783623505000",
+ "tarball_url": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0-alpha-1783623505000.tgz",
+ "integrity": "sha512-CPJZdsA/KGT2QQlekiV6Wt+QlQrZHVSZ6oiNtOI/bYYOIVLM8jfKGWTM4zQiyd4UN+40Cq4cA6lxmZHZbtPvJQ==",
+ "shasum_sha1": "3984745e615ad065b24a89edeeec1a653addc807",
+ "signature": "MEUCIFhV50jjIBy0rF0fm8SO5i0BHs7OULVIVg64Z+KOng7EAiEAy8QtBfSmtVqDHBXb+1x2VdWuChhvd2GWWaJHGm8O9eU=",
+ "attestations_url": "https://registry.npmjs.org/-/npm/v1/attestations/playwright-core@1.62.0-alpha-1783623505000",
+ "slsa_subject_name": "pkg:npm/playwright-core@1.62.0-alpha-1783623505000",
+ "slsa_subject_sha512_hex": "08f25976c03f2864f641095e92257a5adf90950ad91d5499ea888db4e23f6d860e2152ccf237ca1964cce33422c9de1437ee340aae1c03a9719991d96ed3ef25",
+ "license": "Apache-2.0",
+ "dependencies": {}
+ }
+ ],
+ "internal_mirror": {
+ "artifact_kind": "oci_scratch_bundle",
+ "push_repository": "registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp",
+ "runtime_repository": "192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp",
+ "tag": "0.0.78-bundle-v1",
+ "digest_pin_required": true,
+ "sbom_required": true,
+ "upstream_signature_required": true,
+ "slsa_subject_match_required": true,
+ "vulnerability_scan_required": true,
+ "vulnerability_db_max_age_hours": 168,
+ "allowed_licenses": [
+ "Apache-2.0"
+ ]
+ },
+ "replay_contract": {
+ "registered_adapter": "playwright_public_accessibility_verifier_v1",
+ "adapter_implemented": false,
+ "allowed_origins": [
+ "https://awoooi.wooo.work"
+ ],
+ "first_canary_url": "https://awoooi.wooo.work/zh-TW/automation/mcp",
+ "authenticated_session_allowed": false,
+ "browser_profile_persistence_allowed": false,
+ "file_upload_allowed": false,
+ "download_allowed": false,
+ "pdf_write_allowed": false,
+ "browser_install_allowed": false,
+ "arbitrary_navigation_allowed": false,
+ "production_form_submit_allowed": false,
+ "production_write_allowed": false,
+ "screenshot_and_accessibility_receipts_only": true
+ },
+ "promotion_contract": {
+ "deployment_allowed": false,
+ "shadow_allowed": false,
+ "canary_allowed": false,
+ "required_before_shadow": [
+ "internal_mirror_digest_verified",
+ "cyclonedx_sbom_verified",
+ "npm_registry_signatures_verified",
+ "slsa_subjects_verified",
+ "vulnerability_decision_passed",
+ "license_decision_passed",
+ "registered_replay_adapter_verified",
+ "rollback_receipt_verified"
+ ],
+ "rollback": {
+ "action": "disable_candidate_and_retain_immutable_bundle",
+ "fallback_verifier": "awoooi_repo_owned_playwright_test_runner",
+ "external_artifact_delete_allowed": false,
+ "production_route_change_allowed": false
+ }
+ },
+ "prohibited": [
+ "github_api_or_github_actions",
+ "actions_checkout",
+ "github_source_archive_or_ghcr",
+ "npx_y",
+ "at_latest",
+ "floating_container_tag",
+ "unverified_redirect",
+ "secret_read_or_log",
+ "request_or_response_body_storage",
+ "external_rag_write",
+ "production_mutation"
+ ]
+}
diff --git a/config/security/runtime-image-mirror-policy.json b/config/security/runtime-image-mirror-policy.json
index b3a3cb5b7..4a75d9c28 100644
--- a/config/security/runtime-image-mirror-policy.json
+++ b/config/security/runtime-image-mirror-policy.json
@@ -213,6 +213,71 @@
"name": "node-problem-detector",
"container": "node-problem-detector"
}
+ },
+ {
+ "asset_id": "runtime-image:vpa-recommender",
+ "source_index_digest": "sha256:4af365370c187e4eb60302e937b18a188774698b049a1bfdd77d43e51b30abec",
+ "platform_digest": "sha256:f7da718d281d5e880e966d0182ffb2160b82517c7bc3a38f8002b49e7a4bd0a1",
+ "target_registry_digest": "sha256:6ae9b8c6ac256fe034f4e2741c2fef2c2ba2d64778c071d6d93c99cd86ba7c89",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/vpa-recommender:1.6.0-linux-amd64",
+ "runtime_ref": "192.168.0.110:5000/awoooi/runtime-mirror/vpa-recommender@sha256:6ae9b8c6ac256fe034f4e2741c2fef2c2ba2d64778c071d6d93c99cd86ba7c89",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "vpa-recommender",
+ "container": "recommender"
+ },
+ "availability_guard": {
+ "minimum_ready_replicas": 1,
+ "maximum_effective_unavailable": 0,
+ "minimum_effective_surge": 1
+ }
+ },
+ {
+ "asset_id": "runtime-image:metrics-server",
+ "source_index_digest": "sha256:b2d2efaf5ac3b366ed0f839d2412a2c4279d4fc2a2a733f12c52133faed36c41",
+ "platform_digest": "sha256:6231fb0a1ffab76c92ab880f51a0d11b290f688373647bcedff85af025dfd8a9",
+ "target_registry_digest": "sha256:4a4376379ab80d3e1fd5edbf8c3ddcc0b9be8ccb56167e4f8c9423e5e11b0cfe",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/metrics-server:0.8.1-linux-amd64",
+ "runtime_ref": "192.168.0.110:5000/awoooi/runtime-mirror/metrics-server@sha256:4a4376379ab80d3e1fd5edbf8c3ddcc0b9be8ccb56167e4f8c9423e5e11b0cfe",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "metrics-server",
+ "container": "metrics-server"
+ },
+ "availability_guard": {
+ "minimum_ready_replicas": 1,
+ "maximum_effective_unavailable": 0,
+ "minimum_effective_surge": 1,
+ "enforced_strategy": {
+ "max_unavailable": 0,
+ "max_surge": 1
+ }
+ }
+ },
+ {
+ "asset_id": "runtime-image:local-path-provisioner",
+ "source_index_digest": "sha256:6ff68ebe98bc623b45ad22c28be84f8a08214982710f3247d5862e9bccce73ef",
+ "platform_digest": "sha256:90745b5457d61b84362ea2aeeb06caef4d576c99861eaff4c7bfe9b36e1cf8d7",
+ "target_registry_digest": "sha256:8ea3128d48a78b376d094366101d54180cd5293839cabd8568b1dce064413619",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/local-path-provisioner:v0.0.34-linux-amd64",
+ "runtime_ref": "192.168.0.110:5000/awoooi/runtime-mirror/local-path-provisioner@sha256:8ea3128d48a78b376d094366101d54180cd5293839cabd8568b1dce064413619",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "local-path-provisioner",
+ "container": "local-path-provisioner"
+ },
+ "availability_guard": {
+ "minimum_ready_replicas": 1,
+ "maximum_effective_unavailable": 0,
+ "minimum_effective_surge": 1,
+ "enforced_strategy": {
+ "max_unavailable": 0,
+ "max_surge": 1
+ }
+ }
}
]
}
diff --git a/config/security/runtime-image-mirror-staging-policy.json b/config/security/runtime-image-mirror-staging-policy.json
index e0c9d2370..0242a73ed 100644
--- a/config/security/runtime-image-mirror-staging-policy.json
+++ b/config/security/runtime-image-mirror-staging-policy.json
@@ -41,6 +41,58 @@
"name": "argocd-redis",
"container": "redis"
}
+ },
+ {
+ "asset_id": "runtime-image:vpa-recommender-1.6.0-canary",
+ "source_index_digest": "sha256:4af365370c187e4eb60302e937b18a188774698b049a1bfdd77d43e51b30abec",
+ "platform_digest": "sha256:f7da718d281d5e880e966d0182ffb2160b82517c7bc3a38f8002b49e7a4bd0a1",
+ "source_config_digest": "sha256:05526683ba80f70887ef5622c4ee7d78e1e0482d2a9e2f0e29ad92c32cb73819",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/vpa-recommender:1.6.0-linux-amd64",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "vpa-recommender",
+ "container": "recommender"
+ }
+ },
+ {
+ "asset_id": "runtime-image:metrics-server-0.8.1-canary",
+ "source_index_digest": "sha256:b2d2efaf5ac3b366ed0f839d2412a2c4279d4fc2a2a733f12c52133faed36c41",
+ "platform_digest": "sha256:6231fb0a1ffab76c92ab880f51a0d11b290f688373647bcedff85af025dfd8a9",
+ "source_config_digest": "sha256:e76b3f3568b7f440dfd477c1d6de638d7769ba34c93eef999dee418eb72bc0e3",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/metrics-server:0.8.1-linux-amd64",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "metrics-server",
+ "container": "metrics-server"
+ }
+ },
+ {
+ "asset_id": "runtime-image:local-path-provisioner-0.0.34-canary",
+ "source_index_digest": "sha256:6ff68ebe98bc623b45ad22c28be84f8a08214982710f3247d5862e9bccce73ef",
+ "platform_digest": "sha256:90745b5457d61b84362ea2aeeb06caef4d576c99861eaff4c7bfe9b36e1cf8d7",
+ "source_config_digest": "sha256:acccaf97bcb578daff51c6246e47babbca6e998e4225aa230544684cf147d6c1",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/local-path-provisioner:v0.0.34-linux-amd64",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "local-path-provisioner",
+ "container": "local-path-provisioner"
+ }
+ },
+ {
+ "asset_id": "runtime-image:sealed-secrets-controller-0.26.0-canary",
+ "source_index_digest": "sha256:74cd190f424b591dd82a234685c8c81d50b33af807e03470bc12b23d202e0106",
+ "platform_digest": "sha256:1827b36853e124ac0dd2554f7cd55c04952ecb7c38ff19a4a8d93d633811ed68",
+ "source_config_digest": "sha256:bbfc8314cb4978b252fb313b84e1d51a7b0c040befd79ecc903ea952485a3348",
+ "push_ref": "registry.wooo.work/awoooi/runtime-mirror/sealed-secrets-controller:0.26.0-linux-amd64",
+ "workload": {
+ "kind": "deployment",
+ "namespace": "kube-system",
+ "name": "sealed-secrets-controller",
+ "container": "sealed-secrets-controller"
+ }
}
]
}
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md
index ef07354ab..8f4eb97c4 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -1,3 +1,26 @@
+## 2026-07-15 — P0 MCP EwoooC/MOMO durable production federation source closure
+
+**完成內容**:
+- 沿用既有 AWOOOI MCP Gateway、Provider Registry、PostgreSQL/pgvector 與 Redis;沒有新增第二套 Gateway、RAG database、權限或外部自動寫入平面。
+- 新增固定來源 `mo.wooo.work`、固定 HTTPS path 的 read-only federation controller。它拒絕 redirect、credential、過期/無 timezone receipt、額外欄位、prompt payload、URL/private endpoint、敏感 key、schema drift 與 SHA-256 fingerprint mismatch;raw source payload 只存在 bounded memory,不落 DB/LOG/RAG。
+- 新增 additive `awooop_mcp_federation_run` 與 `awooop_mcp_federated_runtime_receipt`,只允許 `ewoooc`、`momo-pro-system` 兩個 canonical identity,啟用 FORCE RLS。每次 run 使用同一 `run_id/trace_id/work_item_id`,並從 durable normalized columns 重建 source hash scope;不只比較來源聲稱的 hash。
+- signal worker 以 Redis atomic lease 每 6 小時執行,startup delay 45 秒;K8s manifest 明確啟用。CD 在 worker rollout 前套用 migration,驗證兩張 table、2 個 RLS policy、3 個 identity/source constraint 與 runtime DB role read access。
+- MCP overview 以真實 fresh receipt 取代硬編碼 `0`。兩筆 receipt 上線後只會把 EwoooC/MOMO 改為 federated partial/verified;全產品仍維持 `2/12`,`cross_product_runtime_federation_receipts_missing` 不會被誤關閉。
+- `/automation/mcp` 新增繁中/英文 production federation cockpit,顯示兩個產品的 MCP server IDs、server/tool counts、runtime version、RAG model/version state、PixelRAG platforms、freshness、durable run 與 blockers;mobile/desktop 採 responsive grid、empty/degraded state 與語意化 heading/status。
+
+**source/test/build evidence**:
+- API Ruff、py_compile、JSON/YAML parse 與 `git diff --check` 通過;MCP control plane、federation、version lifecycle、signal-worker binding focused pytest `30 passed`。
+- Web `typecheck` 通過;以 CD 同值 `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work` 執行 production build 成功,`/[locale]/automation/mcp` route 為 `6.92 kB`、First Load JS `240 kB`。既有 Sentry migration / webpack cache 提示未由本工作新增。
+- EwoooC/MOMO source contract 在獨立 canonical Gitea worktree 完成 `7 passed`,endpoint 只輸出 aggregate metadata 與 verifier-recomputable fingerprint;既有 authenticated AI automation routes 不放寬。
+
+**尚未完成 / 不誤報**:
+- 本筆建立時兩個 repo 都還沒有本 feature 的 Gitea main、CD/deploy marker 或 production runtime receipt;source/test/build 綠燈不等於 runtime closure。
+- 第一筆 production federation run 尚未產生,因此目前不能宣稱 EwoooC/MOMO `2/2` durable receipt、12 產品 `12/12`、embedding model immutable、MCP/RAG production query canary 或 PixelRAG formal promotion 完成。
+- 未安裝/呼叫外部 MCP,未寫外部 RAG 或正式商品價格,未保存 request/response body、tool output、raw catalog/page,未讀 secret/token/`.env`/raw sessions/SQLite/auth,未使用 GitHub/`gh`/Actions,也未使用 `npx -y`、`@latest` 或 `:latest` 進行安裝/升級。
+
+**下一步**:
+- 依序 normal push EwoooC 與 AWOOOI Gitea main、等待各自 CD terminal,讀回 V10.796 public receipt、AWOOOI migration/runtime-role verifier、signal-worker first run、production API `2/12` 與 desktop/mobile visible UI;任一層失敗維持 partial/degraded 並走 bounded retry/rollback,不倒推完成。
+
## 2026-07-15 — P0-OBS-002 post-closure runtime regression 與 guard 修復
**runtime evidence**:
diff --git a/docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-check.snapshot.json b/docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-check.snapshot.json
new file mode 100644
index 000000000..0b97de270
--- /dev/null
+++ b/docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-check.snapshot.json
@@ -0,0 +1 @@
+{"bundle_manifest_sha256":"54b1d7ff24fc6f8ca89d4d36adcdb43e1cf79ed6018a4cd4f0d630c857bffb2c","canary_allowed":false,"candidate_id":"external.playwright-verifier","cyclonedx_sbom_sha256":"092500242530e40f361f52cc662edda9e6c83f76edf81dfe9574235546c22be7","internal_mirror_ref":null,"mode":"check","observed_at":"2026-07-15T01:42:22.303666+00:00","package_count":3,"packages":[{"archive_file_count":7,"archive_safety_verified":true,"attestation_sha256":"4830a73d5f551fefc969e297b042cbd956f88c954fe0f02c11101a995b03ede1","license_decision":"passed","metadata_sha256":"8e6bbce53663e1c9488e02a748e2b20e8898fb58cd4ec745e09339bc546599b3","name":"@playwright/mcp","npm_registry_signature_verified":true,"slsa_subject_verified":true,"tarball_sha1":"305c96c4ac0179bd37622fe4ed4162493513b33e","tarball_sha512_hex":"5cb4d4780ea610df6c43e849e1d7c6f042240dbc52d0add3adcd91064509b9fd364e013615044d4ccb6afda25f87244c20db1197f61b738b317162ed167e3f4d","unpacked_bytes":84517,"version":"0.0.78","vulnerability_decision":"passed_no_known_osv_records","vulnerability_ids":[]},{"archive_file_count":62,"archive_safety_verified":true,"attestation_sha256":"f9188fc1fc80c15cb8cc0c962f13c56cf38ebba5e5cf5092565c258b464d7df3","license_decision":"passed","metadata_sha256":"032e79d40dd69d8f573b6b2a6b7bb48dbf331e8dc642db625d33598dd5c478d4","name":"playwright","npm_registry_signature_verified":true,"slsa_subject_verified":true,"tarball_sha1":"d04815de36073de29cc0b02c69bfd657231ec0c1","tarball_sha512_hex":"e8a57d8783cfde1aaee0d686771c5c8a359f621f4b25c148fd1dac3f84d30b8239705a37a116b0374113956e4c904ddc40480a861a23d0a50dcf8b1fa424f44e","unpacked_bytes":5062430,"version":"1.62.0-alpha-1783623505000","vulnerability_decision":"passed_no_known_osv_records","vulnerability_ids":[]},{"archive_file_count":104,"archive_safety_verified":true,"attestation_sha256":"99df3734d80709dc8b7826c661e4bc05a57bf3fbe61c4c838921d5c04df9e02b","license_decision":"passed","metadata_sha256":"1477249209c785ce155309e4e4d210aaa1e74ec6ef172115da310763b8024505","name":"playwright-core","npm_registry_signature_verified":true,"slsa_subject_verified":true,"tarball_sha1":"3984745e615ad065b24a89edeeec1a653addc807","tarball_sha512_hex":"08f25976c03f2864f641095e92257a5adf90950ad91d5499ea888db4e23f6d860e2152ccf237ca1964cce33422c9de1437ee340aae1c03a9719991d96ed3ef25","unpacked_bytes":12815408,"version":"1.62.0-alpha-1783623505000","vulnerability_decision":"passed_no_known_osv_records","vulnerability_ids":[]}],"policy_checksum":"sha256:44c9f79c2af59f1d2703cbd2181b43f4c4dd4ee9f70164f606bdda70d823e78b","promotion_allowed":false,"replay_allowed":false,"risk_level":"high","run_id":"26aa5be0-b71c-40fd-8244-ac74c53fdd86","runtime_mirror_ref":null,"schema_version":"awoooi_external_mcp_artifact_receipt_v1","shadow_allowed":false,"stage_receipts":{"bounded_execution":{"external_runtime_mutation_performed":false,"internal_registry_write_performed":false,"production_route_changed":false,"status":"not_executed"},"check_mode":{"browser_started":false,"mcp_process_started":false,"production_write_performed":false,"status":"passed"},"decision":{"candidate_action":"run_independent_check_verifier_then_mirror_exact_bundle","runtime_promotion_allowed":false},"independent_post_verifier":{"bundle_manifest_sha256":"54b1d7ff24fc6f8ca89d4d36adcdb43e1cf79ed6018a4cd4f0d630c857bffb2c","controller_registry_digest_check":false,"cyclonedx_sbom_sha256":"092500242530e40f361f52cc662edda9e6c83f76edf81dfe9574235546c22be7","internal_digest_ref_verified":false,"package_digest_count":3,"status":"pending_external_verifier"},"learning_writeback":{"km_write_performed":false,"rag_write_performed":false,"status":"receipt_ready_for_committed_writeback"},"normalized_asset_identity":{"candidate_id":"external.playwright-verifier","exact_root_version":"0.0.78","package_count":3},"risk_policy":{"exact_versions":true,"immutable_upstream_digests":true,"license_decision":"passed","npm_registry_signatures_verified":true,"risk_level":"high","slsa_subjects_verified":true,"vulnerability_decision":"passed_no_known_osv_records"},"rollback_or_no_write_terminal":{"external_artifact_delete_allowed":false,"production_route_change_allowed":false,"rollback_action":"disable_candidate_and_retain_immutable_bundle","status":"no_write_pending_external_verifier"},"sensor_source_receipt":{"osv":"api.osv.dev","redirects_followed":0,"registry":"registry.npmjs.org","runtime_request_or_response_body_stored":false,"upstream_supply_chain_attestation_bundled":true},"source_of_truth_diff":{"change_state":"baseline_created","policy_checksum":"sha256:44c9f79c2af59f1d2703cbd2181b43f4c4dd4ee9f70164f606bdda70d823e78b"}},"status":"completed_check_mode_pending_independent_verifier","trace_id":"mcp-artifact-26aa5be0-b71c-40fd-8244-ac74c53fdd86","work_item_id":"MCP-ARTIFACT-PLAYWRIGHT-001"}
diff --git a/docs/operations/mcp-control-plane-catalog.snapshot.json b/docs/operations/mcp-control-plane-catalog.snapshot.json
index 67186d1af..312253008 100644
--- a/docs/operations/mcp-control-plane-catalog.snapshot.json
+++ b/docs/operations/mcp-control-plane-catalog.snapshot.json
@@ -294,22 +294,26 @@
"title": "Playwright UI verifier MCP",
"source_type": "external",
"catalog_source": "mcp-so",
- "catalog_url": "https://mcp.so/server/playwright-mcp/microsoft",
+ "catalog_url": "https://mcp.so/servers/playwright-mcp",
"decision": "internalize_then_canary",
"risk_tier": "high",
"scope": "allowlisted_origins_only",
"data_boundary": "public_and_test_surfaces",
"deployment_allowed": false,
- "version_state": "unresolved",
- "digest_state": "missing",
- "sbom_state": "missing",
- "signature_state": "missing",
+ "version_state": "exact_0.0.78_check_verified",
+ "digest_state": "upstream_sha512_pinned_internal_mirror_pending",
+ "sbom_state": "cyclonedx_1.5_check_verified_internal_mirror_pending",
+ "signature_state": "npm_registry_ecdsa_and_slsa_subject_verified",
"rag_policy": "screenshots_and_accessibility_receipts_only",
"blockers": [
- "floating_install_examples_rejected",
- "origin_allowlist_and_session_redaction_verifier_missing"
+ "internal_mirror_and_independent_digest_readback_pending",
+ "registered_public_origin_replay_adapter_missing",
+ "shadow_canary_verifier_and_rollback_execution_pending"
],
- "evidence_refs": []
+ "evidence_refs": [
+ "config/mcp/playwright-mcp-artifact-policy.json",
+ "docs/operations/external-mcp-artifacts/playwright-mcp-0.0.78-check.snapshot.json"
+ ]
},
{
"capability_id": "external.google-search-console-readonly",
diff --git a/docs/security/TELEGRAM-CANONICAL-ROUTING-INVENTORY.md b/docs/security/TELEGRAM-CANONICAL-ROUTING-INVENTORY.md
index 22200319a..ff05da340 100644
--- a/docs/security/TELEGRAM-CANONICAL-ROUTING-INVENTORY.md
+++ b/docs/security/TELEGRAM-CANONICAL-ROUTING-INVENTORY.md
@@ -1,7 +1,8 @@
# Telegram 監控告警路由總帳
-> 狀態:`source_policy_ready_runtime_not_applied`
-> 真相邊界:本表把「已證明現況」、「建議目的地」、「目前阻擋」、「source risk」與「receipt 強度」分開。建議不等於 live route;alias 不是 numeric chat ID;本變更沒有送 Telegram、讀 token/chat ID、改 runtime route 或部署。
+> Source snapshot 狀態:`source_policy_ready_runtime_not_applied`(保留 2026-07-14 產生當下的歷史邊界,不回寫成 runtime 綠燈)
+> Runtime projection 狀態:`runtime_policy_enforced_product_delivery_receipts_partial`(由 production gateway 相同 fail-closed resolver 即時計算;4 條 allow、16 條 block)
+> 真相邊界:本表把「已證明現況」、「建議目的地」、「目前阻擋」、「source risk」與「receipt 強度」分開。建議不等於 delivery receipt;alias 不是 numeric chat ID;readback 不送 Telegram、不讀 token/chat ID、不改 runtime route。是否已部署仍以 exact-SHA CD、deploy marker 與 production API/UI readback 為準。
## 判讀規則
@@ -44,7 +45,7 @@
- Alertmanager 產品規則現在帶 `awoooi_product_id`、`awoooi_signal_family`、`awoooi_route_severity`;gateway 以這組明確 context 覆蓋 legacy `TYPE-3`,所以 blocked product 不會再落到 shared SRE。
- SignOz public receiver 尚無 source authentication,因此即使 payload 自稱 canonical labels 也固定 no-egress;Sentry(signature verified)與 HMAC generic webhook 缺完整 namespaced context 時一律 `UNKNOWN / blocked_no_egress`。Alertmanager 另要求 direct peer 與完整 forwarded chain 都是 private IP,不再信任單一可偽造的 forwarded value。
- `docker-compose`、production ConfigMap、docker-health scripts 與 7 個 Gitea workflows 的 11 個 raw destination defaults 已移除;runtime 只可由既有 secret/env alias 注入,缺值即 fail closed。白名單也不再內嵌 numeric default。
-- 本次只改 source policy,沒有讀 token、呼叫 Bot API、改 Telegram 群組、送測試訊息或偽造 delivery receipt。
+- Runtime readback 只透過與 gateway pre-send gate 相同的 resolver 投影 symbolic policy;不讀 token、呼叫 Bot API、改 Telegram 群組、送測試訊息或偽造 delivery receipt。
## 11 產品路由清冊
@@ -89,6 +90,8 @@
- Registry:`docs/security/telegram-canonical-routing-registry.snapshot.json`
- Schema:`docs/schemas/telegram_canonical_routing_registry_v1.schema.json`
- Resolver:`apps/api/src/services/notification_matrix.py`
-- Tests:`apps/api/tests/test_telegram_canonical_routing_registry.py`
+- API readback:`GET /api/v1/agents/telegram-canonical-routing-runtime-readback`
+- UI cockpit:`/{locale}/notifications`
+- Tests:`apps/api/tests/test_telegram_canonical_routing_registry.py`、`apps/api/tests/test_telegram_canonical_routing_readback_api.py`
-Production apply 前仍需同一 route change run 留下 source diff、check-mode、bounded apply、durable delivery receipt、post-verifier、rollback/no-write terminal 與 learning/writeback;本 source-only 交付不代表 runtime route 已更動。
+Source snapshot 的 `runtime_not_applied` 是歷史產生狀態;API 會另以目前 resolver 重算 runtime enforcement,兩者不得互相覆寫。任何 route change 仍需同一 run 留下 source diff、check-mode、bounded apply、durable delivery receipt、post-verifier、rollback/no-write terminal 與 learning/writeback;API/UI 綠燈本身不代表所有產品 delivery 已閉環。
diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml
index b0691e6c8..3af3f9487 100644
--- a/k8s/awoooi-prod/06-deployment-api.yaml
+++ b/k8s/awoooi-prod/06-deployment-api.yaml
@@ -160,12 +160,12 @@ spec:
- name: AWOOOI_BUILD_COMMIT_SHA
# 2026-06-29 Codex: CD rewrites this to the deployed image tag so
# production deploy readback does not rely on a stale static snapshot.
- value: "af4cb2fd432e0f1098a072034637de90c6b6964f"
+ value: "0f2ddc0ce81ab52c7fbe33cb2e14dc54d0d33ed5"
- name: AWOOOI_DESIRED_API_IMAGE_TAG
# 2026-06-30 Codex: CD rewrites this alongside AWOOOI_BUILD_COMMIT_SHA.
# Production readback compares runtime image truth against this
# GitOps desired tag instead of doing a slow Gitea raw fetch.
- value: "af4cb2fd432e0f1098a072034637de90c6b6964f"
+ value: "0f2ddc0ce81ab52c7fbe33cb2e14dc54d0d33ed5"
- name: DATABASE_POOL_SIZE
# 2026-07-01 Codex: production role `awoooi` currently has a low
# connection limit. Keep API pool conservative until DB role
diff --git a/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml b/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml
index 50953fc60..643fc8be7 100644
--- a/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml
+++ b/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml
@@ -66,7 +66,7 @@ spec:
- name: DATABASE_NULL_POOL
value: "true"
- name: AWOOOI_BUILD_COMMIT_SHA
- value: "af4cb2fd432e0f1098a072034637de90c6b6964f"
+ value: "0f2ddc0ce81ab52c7fbe33cb2e14dc54d0d33ed5"
- name: ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER
value: "false"
- name: ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER
diff --git a/k8s/awoooi-prod/08-deployment-worker.yaml b/k8s/awoooi-prod/08-deployment-worker.yaml
index 90643e6c4..edad2bbb6 100644
--- a/k8s/awoooi-prod/08-deployment-worker.yaml
+++ b/k8s/awoooi-prod/08-deployment-worker.yaml
@@ -31,9 +31,9 @@ spec:
strategy:
type: RollingUpdate
rollingUpdate:
- # 2026-07-01 Codex: DB role `awoooi` is capped at two connections and
- # worker imports the same API DB stack. Do not run old+new worker pods
- # during rollout until the DB role/pool budget is raised and verified.
+ # 2026-07-15 Codex: keep one worker owner during rollout. This prevents
+ # duplicate consumers and keeps the isolated worker DB role inside its
+ # verified connection budget while the old pod terminates.
maxSurge: 0
maxUnavailable: 1
template:
@@ -163,21 +163,33 @@ spec:
fieldRef:
fieldPath: metadata.name
- name: DATABASE_POOL_SIZE
- # 2026-07-01 Codex: keep worker DB usage inside the current
- # production role connection budget during reboot rollouts.
+ # NullPool releases each session; the explicit concurrency cap
+ # below preserves the intended one-connection worker budget.
value: "1"
- name: DATABASE_MAX_OVERFLOW
value: "0"
- name: DATABASE_NULL_POOL
value: "true"
+ - name: DATABASE_NULL_POOL_CONCURRENCY_LIMIT
+ # NullPool releases each connection after use, while this cap
+ # keeps concurrent background loops inside the worker role budget.
+ value: "1"
- name: DATABASE_BOOTSTRAP_DDL_ENABLED
value: "false"
- name: AWOOOI_BUILD_COMMIT_SHA
- value: "af4cb2fd432e0f1098a072034637de90c6b6964f"
+ value: "0f2ddc0ce81ab52c7fbe33cb2e14dc54d0d33ed5"
- name: ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER
value: "true"
- name: ENABLE_SECURITY_CONTROL_PLANE_MAINTENANCE_WORKER
value: "true"
+ - name: ENABLE_MCP_FEDERATION_RECEIPT_WORKER
+ value: "true"
+ - name: MCP_FEDERATION_INTERVAL_SECONDS
+ value: "21600"
+ - name: MCP_FEDERATION_STARTUP_SLEEP_SECONDS
+ value: "45"
+ - name: MCP_FEDERATION_SOURCE_TIMEOUT_SECONDS
+ value: "10"
- name: AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_INTERVAL_SECONDS
value: "600"
- name: AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_BATCH_LIMIT
diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml
index 0b67f7c82..2d19b025a 100644
--- a/k8s/awoooi-prod/kustomization.yaml
+++ b/k8s/awoooi-prod/kustomization.yaml
@@ -40,9 +40,9 @@ resources:
# ⚠️ 重要: name 必須與 deployment YAML 中的 image 完全匹配 (含 tag)
# newName + newTag 由 CI 透過 kustomize edit set image 注入
images:
-- digest: sha256:171844f7b67fa1be9f477611ced536638b349f4163c5747264cefe72c8761896
+- digest: sha256:1d195016008e4f5ec795170055aca2194161f74fc44fb373d387f26417f0b4af
name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/api
-- digest: sha256:c5e0142481d0d91f5b2033300c8824d84a2b11c5f5a62aee03db267e9c76f132
+- digest: sha256:a1422caaefe56ce45d185cb85147ab3591e25251dc421cf738f1439565348c4d
name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/web
diff --git a/ops/alertmanager/alertmanager.yml b/ops/alertmanager/alertmanager.yml
index 760140cb2..44fcbe0e1 100644
--- a/ops/alertmanager/alertmanager.yml
+++ b/ops/alertmanager/alertmanager.yml
@@ -93,7 +93,7 @@ inhibit_rules:
- source_matchers:
- alertname="HostDown"
target_matchers:
- - alertname=~"HostHighCpuLoad|HostOutOfMemory|HostOutOfDiskSpace"
+ - alertname=~"HostHighCpuLoad|HostMemoryUsageHigh|HostOutOfMemory|HostOutOfDiskSpace"
equal: ['host']
- source_matchers:
diff --git a/ops/monitoring/alerts-unified.yml b/ops/monitoring/alerts-unified.yml
index c453077fe..42a2dd517 100644
--- a/ops/monitoring/alerts-unified.yml
+++ b/ops/monitoring/alerts-unified.yml
@@ -104,20 +104,23 @@ groups:
auto_repair_action: "ssh 192.168.0.{{ $labels.host }} '/home/wooo/scripts/host-sustained-load-controller.py --host {{ $labels.host }} --metrics-file /home/wooo/node_exporter_textfiles/host_runaway_process.prom --docker-stats-file /home/wooo/node_exporter_textfiles/docker_stats.prom --json'"
runbook: "交給 host-sustained-load-controller 產生 AI controlled packet:orphan browser 走 host-runaway-process-remediation.py dry-run → controlled SIGTERM → verifier;合法 CI/BuildKit 走 runner pressure fail-closed 與 drain/cancel packet;unknown 先跑 host-sustained-load-evidence.py 只讀脫敏證據再選服務專屬 PlayBook;swap 走服務專屬記憶體 PlayBook。禁止直接 docker/systemd/nginx/firewall/reboot。"
- - alert: HostOutOfMemory
+ - alert: HostMemoryUsageHigh
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
- layer: systemd-188
+ layer: 'systemd-{{ $labels.host }}'
team: ops
auto_repair: "true"
mcp_provider: "ssh_host"
host_type: "bare_metal"
alert_category: "host_resource"
annotations:
- summary: "主機 {{ $labels.host }} 記憶體不足"
- description: "記憶體使用率超過 85%"
+ summary: "主機 {{ $labels.host }} 記憶體使用率偏高"
+ description: "MemAvailable 推算使用率超過 85% 持續 5 分鐘;這是容量壓力訊號,不代表已發生 OOM、服務停止或主機關機。"
+ auto_repair_action: >-
+ ssh {{ if eq $labels.host "112" }}kali@{{ end }}192.168.0.{{ $labels.host }} 'free -h; grep "^oom_kill " /proc/vmstat; ps aux --sort=-%mem | head -20'
+ runbook: "唯讀確認 MemAvailable、swap、/proc/vmstat oom_kill、主要記憶體程序與服務健康;禁止 drop_caches、restart、reboot。若 oom_kill 未增加且服務健康,維持 degraded/capacity-pressure,不得誤報 OOM 或主機離線。"
- alert: HostOutOfDiskSpace
expr: (1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})) * 100 > 85
diff --git a/ops/monitoring/alerts.yml b/ops/monitoring/alerts.yml
index b437fc24f..aedfd6577 100644
--- a/ops/monitoring/alerts.yml
+++ b/ops/monitoring/alerts.yml
@@ -72,23 +72,23 @@ groups:
auto_repair_action: "ssh 192.168.0.{{ $labels.host }} '/home/wooo/scripts/host-sustained-load-controller.py --host {{ $labels.host }} --metrics-file /home/wooo/node_exporter_textfiles/host_runaway_process.prom --docker-stats-file /home/wooo/node_exporter_textfiles/docker_stats.prom --json'"
runbook: "交給 host-sustained-load-controller 產生 AI controlled packet:orphan browser 走 host-runaway-process-remediation.py dry-run → controlled SIGTERM → verifier;合法 CI/BuildKit 走 runner pressure fail-closed 與 drain/cancel packet;unknown 先跑 host-sustained-load-evidence.py 只讀脫敏證據再選服務專屬 PlayBook;swap 走服務專屬記憶體 PlayBook。禁止直接 docker/systemd/nginx/firewall/reboot。"
- - alert: HostOutOfMemory
+ - alert: HostMemoryUsageHigh
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
- layer: systemd-188
+ layer: 'systemd-{{ $labels.host }}'
team: ops
auto_repair: "true"
mcp_provider: "ssh_host"
host_type: "bare_metal"
alert_category: "host_resource"
annotations:
- summary: "主機 {{ $labels.host }} 記憶體不足"
- description: "記憶體使用率超過 85%"
- # 2026-05-02 ogt + Claude Sonnet 4.6: 引導 LLM 走 SSH 診斷
- auto_repair_action: "ssh {{ $labels.instance }} 'ps aux --sort=-%mem | head -20' (host 記憶體診斷;禁 kubectl restart — 主因常為第三方服務)"
- runbook: "host 記憶體不足排查:SSH 看 top 進程;若為第三方服務需擴容或調 limit"
+ summary: "主機 {{ $labels.host }} 記憶體使用率偏高"
+ description: "MemAvailable 推算使用率超過 85% 持續 5 分鐘;這是容量壓力訊號,不代表已發生 OOM、服務停止或主機關機。"
+ auto_repair_action: >-
+ ssh {{ if eq $labels.host "112" }}kali@{{ end }}192.168.0.{{ $labels.host }} 'free -h; grep "^oom_kill " /proc/vmstat; ps aux --sort=-%mem | head -20'
+ runbook: "唯讀確認 MemAvailable、swap、/proc/vmstat oom_kill、主要記憶體程序與服務健康;禁止 drop_caches、restart、reboot。若 oom_kill 未增加且服務健康,維持 degraded/capacity-pressure,不得誤報 OOM 或主機離線。"
- alert: HostOutOfDiskSpace
expr: (1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})) * 100 > 85
diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py
index 321cd1dad..e50ce4ce9 100644
--- a/ops/runner/test_cd_controlled_runtime_profile.py
+++ b/ops/runner/test_cd_controlled_runtime_profile.py
@@ -1149,6 +1149,21 @@ def test_b5_full_profile_uses_outer_docker_for_db_without_inner_socket_mount() -
)
+def test_runtime_image_mirror_sources_stay_on_controlled_runtime_profile() -> None:
+ text = _workflow_text()
+ expected_sources = [
+ "config/security/runtime-image-mirror-policy.json)",
+ "config/security/runtime-image-mirror-staging-policy.json)",
+ "scripts/security/runtime_image_mirror_controller.py)",
+ "scripts/security/tests/test_runtime_image_mirror_controller.py)",
+ ]
+ for source in expected_sources:
+ assert source in text
+
+ assert "../../scripts/security/runtime_image_mirror_controller.py" in text
+ assert "../../scripts/security/tests/test_runtime_image_mirror_controller.py" in text
+
+
def test_controlled_runtime_pytest_paths_exist() -> None:
text = _workflow_text()
block = text.split("PYTHONFAULTHANDLER=1 python3.11 -m pytest", 1)[1]
diff --git a/scripts/backup/backup-awoooi.sh b/scripts/backup/backup-awoooi.sh
index fc1799714..c5a7cd2c3 100755
--- a/scripts/backup/backup-awoooi.sh
+++ b/scripts/backup/backup-awoooi.sh
@@ -21,6 +21,70 @@ AWOOOI_DB_PORT="5432"
K3S_DB_USER="postgres"
LOCAL_REPO="${BACKUP_BASE}/awoooi"
DUMP_DIR="/tmp/awoooi-backup-$$"
+AWOOOI_K8S_HOST="${AWOOOI_K8S_HOST:-192.168.0.120}"
+AWOOOI_K8S_HOSTS="${AWOOOI_K8S_HOSTS:-${AWOOOI_K8S_HOST} 192.168.0.121 192.168.0.125}"
+AWOOOI_K8S_SECRET_NAME="${AWOOOI_K8S_SECRET_NAME:-awoooi-secrets}"
+AWOOOI_K8S_NAMESPACE="${AWOOOI_K8S_NAMESPACE:-awoooi-prod}"
+AWOOOI_K8S_DATABASE_URL_KEYS="${AWOOOI_K8S_DATABASE_URL_KEYS:-AWOOOI_BACKUP_DATABASE_URL BACKUP_DATABASE_URL DATABASE_URL}"
+FORCE_RLS_RESTORE_SQL=""
+FORCE_RLS_RESTORE_DB=""
+
+resolve_database_url() {
+ if [ -n "${AWOOOI_DATABASE_URL:-}" ]; then
+ printf '%s\n' "${AWOOOI_DATABASE_URL}"
+ return 0
+ fi
+ if [ -n "${DATABASE_URL:-}" ]; then
+ printf '%s\n' "${DATABASE_URL}"
+ return 0
+ fi
+
+ # Resolve only inside the runtime backup process. The value is never
+ # written to logs, source, artifacts, or command output.
+ local k8s_host key encoded decoded
+ for k8s_host in ${AWOOOI_K8S_HOSTS}; do
+ for key in ${AWOOOI_K8S_DATABASE_URL_KEYS}; do
+ encoded="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 "wooo@${k8s_host}" \
+ "sudo -n kubectl get secret ${AWOOOI_K8S_SECRET_NAME} -n ${AWOOOI_K8S_NAMESPACE} -o jsonpath='{.data.${key}}' 2>/dev/null || kubectl get secret ${AWOOOI_K8S_SECRET_NAME} -n ${AWOOOI_K8S_NAMESPACE} -o jsonpath='{.data.${key}}'" \
+ 2>/dev/null || true)"
+ decoded="$(printf '%s' "${encoded}" | base64 -d 2>/dev/null || true)"
+ if [ -n "${decoded}" ]; then
+ printf '%s\n' "${decoded}"
+ return 0
+ fi
+ done
+ done
+ return 1
+}
+
+load_database_config() {
+ local database_url
+ database_url="$(resolve_database_url || true)"
+ if [ -z "${database_url}" ]; then
+ log_error "無法解析 AWOOOI DATABASE_URL;拒絕使用舊硬編密碼"
+ return 1
+ fi
+
+ eval "$(
+ python3 - 3<<< "${database_url}" <<'PY'
+import shlex
+from urllib.parse import unquote, urlparse
+
+with open(3) as source:
+ url = source.read().strip()
+parsed = urlparse(url)
+
+values = {
+ "AWOOOI_DB_USER": unquote(parsed.username or "awoooi"),
+ "AWOOOI_DB_PASS": unquote(parsed.password or ""),
+ "AWOOOI_DB_HOST": parsed.hostname or "localhost",
+ "AWOOOI_DB_PORT": str(parsed.port or 5432),
+}
+for key, value in values.items():
+ print(f"{key}={shlex.quote(value)}")
+PY
+ )"
+}
quote_remote() {
printf "%q" "$1"
@@ -45,26 +109,95 @@ pgpass_line() {
remote_pgpass_wrapper() {
local command="$1"
- printf 'umask 077; pgpass=$(mktemp "${TMPDIR:-/tmp}/awoooi-pgpass.XXXXXX") || exit 1; cleanup() { rm -f "$pgpass"; }; trap cleanup EXIT HUP INT TERM; cat > "$pgpass"; PGPASSFILE="$pgpass" %s' "${command}"
+ printf 'umask 077; pgpass=$(mktemp "${TMPDIR:-/tmp}/awoooi-pgpass.XXXXXX") || exit 1; cleanup() { rm -f "$pgpass"; }; trap cleanup EXIT HUP INT TERM; cat > "$pgpass"; PGOPTIONS="-c statement_timeout=0 -c max_parallel_workers_per_gather=0" PGPASSFILE="$pgpass" %s' "${command}"
}
-run_remote_pg_dump() {
+remote_psql_command() {
+ local database="$1"
+ printf "psql --no-password -U %s -h %s -p %s -d %s -v ON_ERROR_STOP=1" \
+ "$(quote_remote "${AWOOOI_DB_USER}")" \
+ "$(quote_remote "${AWOOOI_DB_HOST}")" \
+ "$(quote_remote "${AWOOOI_DB_PORT}")" \
+ "$(quote_remote "${database}")"
+}
+
+run_remote_pgpass_command() {
+ local database="$1"
+ local command="$2"
+ pgpass_line "${database}" | ssh "ollama@${AWOOOI_HOST}" "$(remote_pgpass_wrapper "${command}")"
+}
+
+collect_force_rls_sql() {
+ local database="$1"
+ local mode="$2"
+ local query
+
+ query="
+select format('ALTER TABLE %I.%I ${mode} ROW LEVEL SECURITY;', n.nspname, c.relname)
+from pg_class c
+join pg_namespace n on n.oid = c.relnamespace
+where c.relkind in ('r', 'p')
+ and c.relforcerowsecurity
+ and pg_get_userbyid(c.relowner) = current_user
+order by 1;
+"
+ run_remote_pgpass_command "${database}" "$(remote_psql_command "${database}") -At -c $(quote_remote "${query}")"
+}
+
+apply_remote_sql() {
+ local database="$1"
+ local sql="$2"
+ [ -n "${sql}" ] || return 0
+ run_remote_pgpass_command "${database}" "$(remote_psql_command "${database}") -c $(quote_remote "${sql}")" >/dev/null
+}
+
+restore_force_rls() {
+ if [ -n "${FORCE_RLS_RESTORE_DB}" ] && [ -n "${FORCE_RLS_RESTORE_SQL}" ]; then
+ if apply_remote_sql "${FORCE_RLS_RESTORE_DB}" "${FORCE_RLS_RESTORE_SQL}"; then
+ log_info "FORCE ROW LEVEL SECURITY 已恢復 (${FORCE_RLS_RESTORE_DB})"
+ else
+ log_error "FORCE ROW LEVEL SECURITY 恢復失敗 (${FORCE_RLS_RESTORE_DB})"
+ return 1
+ fi
+ FORCE_RLS_RESTORE_DB=""
+ FORCE_RLS_RESTORE_SQL=""
+ fi
+}
+
+trap restore_force_rls EXIT
+
+dump_database_with_rls_guard() {
local database="$1"
local output_file="$2"
local stderr_file="${output_file}.stderr"
- local command
+ local noforce_sql force_sql dump_rc
- command="pg_dump --no-password -U $(quote_remote "${AWOOOI_DB_USER}") -h $(quote_remote "${AWOOOI_DB_HOST}") -p $(quote_remote "${AWOOOI_DB_PORT}") $(quote_remote "${database}")"
- if pgpass_line "${database}" \
- | ssh "ollama@${AWOOOI_HOST}" "$(remote_pgpass_wrapper "${command}")" \
- >"${output_file}" 2>"${stderr_file}"; then
+ noforce_sql="$(collect_force_rls_sql "${database}" "NO FORCE")"
+ force_sql="$(printf '%s\n' "${noforce_sql}" | sed 's/NO FORCE/FORCE/')"
+ if [ -n "${noforce_sql}" ]; then
+ FORCE_RLS_RESTORE_DB="${database}"
+ FORCE_RLS_RESTORE_SQL="${force_sql}"
+ log_info "暫時解除 FORCE RLS 以完成完整 pg_dump (${database}, tables=$(printf '%s\n' "${noforce_sql}" | awk 'NF {count++} END {print count+0}'))"
+ apply_remote_sql "${database}" "${noforce_sql}"
+ fi
+
+ set +e
+ run_remote_pgpass_command "${database}" "pg_dump --no-password \
+ -U $(quote_remote "${AWOOOI_DB_USER}") -h $(quote_remote "${AWOOOI_DB_HOST}") -p $(quote_remote "${AWOOOI_DB_PORT}") \
+ $(quote_remote "${database}")" >"${output_file}" 2>"${stderr_file}"
+ dump_rc=$?
+ set -e
+
+ restore_force_rls
+ if [ "${dump_rc}" -eq 0 ]; then
rm -f "${stderr_file}"
return 0
fi
+ log_error "${database} dump 失敗,pg_dump stderr 尾端如下(已避免輸出 credential):"
tail -40 "${stderr_file}" | sed -E 's/(password=)[^ ]+/\1REDACTED/g' >&2 || true
rm -f "${stderr_file}"
- return 1
+ return "${dump_rc}"
}
# 保留策略覆寫(比其他服務更長)
@@ -76,8 +209,7 @@ main() {
local start_time=$(date +%s)
local failed=0
- if [ -z "${AWOOOI_DB_PASS}" ]; then
- log_error "AWOOOI_DB_PASS is unavailable; refusing an unauthenticated or hardcoded fallback"
+ if ! load_database_config; then
notify_clawbot "failed" "${SERVICE}" "AWOOOI DB backup credential unavailable"
exit 1
fi
@@ -89,7 +221,7 @@ main() {
log_info "Dump awoooi_prod..."
local timestamp=$(date "+%Y%m%d_%H%M%S")
- if run_remote_pg_dump \
+ if dump_database_with_rls_guard \
"awoooi_prod" "${DUMP_DIR}/awoooi_prod_${timestamp}.sql"; then
local size=$(du -h "${DUMP_DIR}/awoooi_prod_${timestamp}.sql" | cut -f1)
log_success "awoooi_prod dump 完成 (${size})"
@@ -100,7 +232,7 @@ main() {
# Step 2: awoooi_dev dump
log_info "Dump awoooi_dev..."
- if run_remote_pg_dump \
+ if dump_database_with_rls_guard \
"awoooi_dev" "${DUMP_DIR}/awoooi_dev_${timestamp}.sql"; then
local size=$(du -h "${DUMP_DIR}/awoooi_dev_${timestamp}.sql" | cut -f1)
log_success "awoooi_dev dump 完成 (${size})"
@@ -110,7 +242,7 @@ main() {
# Step 3: k3s_datastore dump(Kine 後端)
log_info "Dump k3s_datastore..."
- if run_remote_pg_dump \
+ if dump_database_with_rls_guard \
"k3s_datastore" "${DUMP_DIR}/k3s_datastore_${timestamp}.sql"; then
local size=$(du -h "${DUMP_DIR}/k3s_datastore_${timestamp}.sql" | cut -f1)
log_success "k3s_datastore dump 完成 (${size})"
diff --git a/scripts/backup/tests/test_backup_awoooi_secret_contract.py b/scripts/backup/tests/test_backup_awoooi_secret_contract.py
index e513cb955..a31b48bc6 100644
--- a/scripts/backup/tests/test_backup_awoooi_secret_contract.py
+++ b/scripts/backup/tests/test_backup_awoooi_secret_contract.py
@@ -18,8 +18,22 @@ def test_daily_backup_has_no_hardcoded_database_password() -> None:
assert "pg_dump --no-password" in text
-def test_daily_backup_fails_closed_without_runtime_credential() -> None:
+def test_daily_backup_resolves_runtime_database_url_and_fails_closed() -> None:
text = SCRIPT.read_text(encoding="utf-8")
- assert 'if [ -z "${AWOOOI_DB_PASS}" ]; then' in text
- assert "refusing an unauthenticated or hardcoded fallback" in text
+ assert "resolve_database_url()" in text
+ assert "load_database_config()" in text
+ assert "AWOOOI_BACKUP_DATABASE_URL BACKUP_DATABASE_URL DATABASE_URL" in text
+ assert "無法解析 AWOOOI DATABASE_URL;拒絕使用舊硬編密碼" in text
+ assert "base64 -d" in text
+ assert 'printf \'%s\\n\' "${decoded}"' in text
+
+
+def test_daily_backup_uses_same_rls_safe_pg_dump_contract_as_frequent_lane() -> None:
+ text = SCRIPT.read_text(encoding="utf-8")
+
+ assert "dump_database_with_rls_guard()" in text
+ assert 'PGOPTIONS="-c statement_timeout=0 -c max_parallel_workers_per_gather=0"' in text
+ assert "ALTER TABLE %I.%I ${mode} ROW LEVEL SECURITY" in text
+ assert "trap restore_force_rls EXIT" in text
+ assert 'dump_database_with_rls_guard \\\n "awoooi_prod"' in text
diff --git a/scripts/cold_start_playbooks.py b/scripts/cold_start_playbooks.py
index ce89e2d47..48708d695 100644
--- a/scripts/cold_start_playbooks.py
+++ b/scripts/cold_start_playbooks.py
@@ -104,18 +104,18 @@ PLAYBOOK_TEMPLATES = [
"severity_range": ["P2", "P3"],
},
{
- "name": "節點記憶體不足 — 清理 Cache",
- "alert_type": "HostOutOfMemory",
- "description": "主機記憶體不足,清理 Page Cache 釋放空間",
+ "name": "節點記憶體壓力 — 唯讀診斷",
+ "alert_type": "HostMemoryUsageHigh",
+ "description": "主機 MemAvailable 偏低;先驗證 OOM、swap、程序與服務狀態,不做破壞性快取清理",
"category": "infrastructure",
"repair_steps": [
- {"command": "ssh {target} 'free -h'", "purpose": "確認記憶體狀況"},
- {"command": "ssh {target} 'sudo sync && sudo sysctl vm.drop_caches=3'", "purpose": "清理 Page Cache"},
- {"command": "ssh {target} 'free -h'", "purpose": "確認記憶體已釋放"},
+ {"command": "ssh {target} 'free -h'", "purpose": "讀取 MemAvailable 與 swap 狀況"},
+ {"command": "ssh {target} \"awk '$1 == \\\"oom_kill\\\" { print }' /proc/vmstat\"", "purpose": "確認 kernel OOM kill 累計值"},
+ {"command": "ssh {target} 'ps aux --sort=-%mem | head -20'", "purpose": "讀取主要記憶體程序"},
],
"estimated_minutes": 5,
- "symptom_alertnames": ["HostOutOfMemory", "NodeMemoryPressure"],
- "severity_range": ["P1", "P2"],
+ "symptom_alertnames": ["HostMemoryUsageHigh", "HostOutOfMemory", "NodeMemoryPressure"],
+ "severity_range": ["P2", "P3"],
},
{
"name": "磁碟使用率過高 — 清理舊日誌",
diff --git a/scripts/ops/tests/test_host_pressure_alert_contract.py b/scripts/ops/tests/test_host_pressure_alert_contract.py
index d18282895..92808411d 100644
--- a/scripts/ops/tests/test_host_pressure_alert_contract.py
+++ b/scripts/ops/tests/test_host_pressure_alert_contract.py
@@ -7,6 +7,8 @@ import yaml
ROOT = Path(__file__).resolve().parents[3]
ALERTS = ROOT / "ops" / "monitoring" / "alerts-unified.yml"
+LEGACY_ALERTS = ROOT / "ops" / "monitoring" / "alerts.yml"
+COLD_START_PLAYBOOKS = ROOT / "scripts" / "cold_start_playbooks.py"
def load_alerts() -> dict[str, dict]:
@@ -54,6 +56,44 @@ def test_critical_sustained_load_alert_uses_deployed_controller_path() -> None:
assert "scripts/ops/host-sustained-load-controller.py" not in action
+def test_host_memory_pressure_is_not_mislabeled_as_oom_or_host_188() -> None:
+ alerts = load_alerts()
+ rule = alerts["HostMemoryUsageHigh"]
+ annotations = rule["annotations"]
+
+ assert "HostOutOfMemory" not in alerts
+ assert rule["labels"]["layer"] == "systemd-{{ $labels.host }}"
+ assert rule["labels"]["auto_repair"] == "true"
+ assert "不代表已發生 OOM" in annotations["description"]
+ assert annotations["auto_repair_action"].startswith(
+ 'ssh {{ if eq $labels.host "112" }}kali@{{ end }}192.168.0.{{ $labels.host }} '
+ )
+ assert "{{ $labels.instance }}" not in annotations["auto_repair_action"]
+ assert "oom_kill" in annotations["auto_repair_action"]
+ assert "drop_caches" not in annotations["auto_repair_action"]
+ assert "restart" not in annotations["auto_repair_action"]
+ assert "reboot" not in annotations["auto_repair_action"]
+ assert "禁止 drop_caches、restart、reboot" in annotations["runbook"]
+
+
+def test_legacy_rule_and_seed_playbook_use_same_read_only_memory_contract() -> None:
+ legacy_payload = yaml.safe_load(LEGACY_ALERTS.read_text(encoding="utf-8"))
+ legacy_alerts = {
+ rule["alert"]: rule
+ for group in legacy_payload["groups"]
+ for rule in group.get("rules", [])
+ if "alert" in rule
+ }
+ playbook_source = COLD_START_PLAYBOOKS.read_text(encoding="utf-8")
+
+ assert "HostMemoryUsageHigh" in legacy_alerts
+ assert "HostOutOfMemory" not in legacy_alerts
+ assert legacy_alerts["HostMemoryUsageHigh"]["labels"]["layer"] == "systemd-{{ $labels.host }}"
+ assert "sudo sysctl vm.drop_caches" not in playbook_source
+ assert '"alert_type": "HostMemoryUsageHigh"' in playbook_source
+ assert '"HostOutOfMemory"' in playbook_source # historical persisted-event alias only
+
+
def test_backup_aggregate_alert_excludes_old_wrapper_noise() -> None:
alerts = load_alerts()
expr = str(alerts["BackupAggregateRunFailed"]["expr"])
diff --git a/scripts/reboot-recovery/tests/test_agent99_recovery_coordinator_contract.py b/scripts/reboot-recovery/tests/test_agent99_recovery_coordinator_contract.py
index b668a3df8..dc6f54d6f 100644
--- a/scripts/reboot-recovery/tests/test_agent99_recovery_coordinator_contract.py
+++ b/scripts/reboot-recovery/tests/test_agent99_recovery_coordinator_contract.py
@@ -209,6 +209,26 @@ def test_live_preflight_adapts_inbox_evidence_without_raw_message_readback() ->
assert forbidden not in preflight
+def test_agent99_relay_requires_identity_bound_queue_receipt_before_promotion() -> None:
+ relay = read("agent99-sre-alert-relay.ps1")
+ inbox = read("agent99-sre-alert-inbox.ps1")
+
+ assert 'schemaVersion = "agent99_sre_queue_receipt_v1"' in inbox
+ assert 'queueAccepted = $QueueAccepted' in inbox
+ assert 'storesRawPayload = $false' in inbox
+ assert 'Write-AgentRelayQueueReceipt $alert $alertId "queued" $true' in inbox
+ assert 'Write-AgentRelayQueueReceipt $alert $alertId "duplicate_suppressed" $false' in inbox
+ assert 'Wait-AgentRelayQueueReceipt $relayReceiptId $alert' in relay
+ assert '$queueReceipt.queueAccepted -and $queueReceipt.dispatchIdentityMatched' in relay
+ assert 'queueReceiptFound = [bool]$queueReceipt.found' in relay
+ assert 'dispatchIdentityMatched = [bool]$queueReceipt.dispatchIdentityMatched' in relay
+ response_block = relay.split("Send-AgentRelayResponse $context 202 @{", 1)[1]
+ response_block = response_block.split("Write-AgentRelayEvent", 1)[0]
+ assert "incomingFile" not in response_block
+ assert "inboxProcessId" not in response_block
+ assert "storesRawPayload = $false" in response_block
+
+
def test_recover_receipt_readback_requires_verifier_and_notification() -> None:
readback = read("scripts/reboot-recovery/agent99-recover-receipt-readback.ps1")
diff --git a/scripts/security/external_mcp_artifact_controller.py b/scripts/security/external_mcp_artifact_controller.py
new file mode 100644
index 000000000..fce2da11b
--- /dev/null
+++ b/scripts/security/external_mcp_artifact_controller.py
@@ -0,0 +1,1330 @@
+#!/usr/bin/env python3
+"""Verify and mirror an exact external MCP artifact bundle.
+
+The controller treats marketplace pages as discovery-only input. Executable
+artifacts are accepted only from the exact allowlisted registry URLs committed
+in policy, after digest, npm registry signature, SLSA subject, dependency,
+license, archive-safety, and OSV checks pass. The mirror target is a scratch OCI
+bundle in the internal Harbor registry; this controller never starts the MCP
+server, a browser, a replay, or a production canary.
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import binascii
+import hashlib
+import io
+import json
+import re
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+import uuid
+from dataclasses import dataclass
+from datetime import datetime, timezone
+from pathlib import Path, PurePosixPath
+from typing import Any, Protocol
+from urllib.error import HTTPError, URLError
+from urllib.parse import quote, urlparse
+from urllib.request import HTTPRedirectHandler, Request, build_opener
+
+
+POLICY_SCHEMA_VERSION = "awoooi_external_mcp_artifact_policy_v1"
+RECEIPT_SCHEMA_VERSION = "awoooi_external_mcp_artifact_receipt_v1"
+SBOM_SPEC_VERSION = "1.5"
+SLSA_PREDICATE_TYPE = "https://slsa.dev/provenance/v1"
+IN_TOTO_STATEMENT_TYPE = "https://in-toto.io/Statement/v1"
+OSV_QUERY_URL = "https://api.osv.dev/v1/query"
+OSV_HOST = "api.osv.dev"
+TRACE_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{3,160}$")
+SHA1_PATTERN = re.compile(r"^[0-9a-f]{40}$")
+SHA256_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
+SHA512_HEX_PATTERN = re.compile(r"^[0-9a-f]{128}$")
+VERSION_PATTERN = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$")
+VULNERABILITY_ID_PATTERN = re.compile(r"^[A-Za-z0-9._:+-]{1,160}$")
+REQUIRED_PACKAGE_NAMES = {
+ "@playwright/mcp",
+ "playwright",
+ "playwright-core",
+}
+
+
+class ControllerError(RuntimeError):
+ """Fail-closed error containing only a public-safe error class."""
+
+
+class HTTPClient(Protocol):
+ def get_bytes(self, url: str, *, maximum_bytes: int) -> bytes: ...
+
+ def post_json(
+ self,
+ url: str,
+ payload: dict[str, Any],
+ *,
+ maximum_bytes: int,
+ ) -> bytes: ...
+
+
+class _NoRedirect(HTTPRedirectHandler):
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001
+ return None
+
+
+class BoundedHTTPClient:
+ """HTTPS client with redirects disabled and bounded response reads."""
+
+ def __init__(self, allowed_hosts: frozenset[str], timeout_seconds: int = 20):
+ self._allowed_hosts = allowed_hosts
+ self._timeout_seconds = timeout_seconds
+ self._opener = build_opener(_NoRedirect())
+
+ def get_bytes(self, url: str, *, maximum_bytes: int) -> bytes:
+ self._validate_url(url)
+ request = Request(
+ url,
+ method="GET",
+ headers={
+ "Accept": "application/json, application/octet-stream",
+ "User-Agent": "awoooi-external-mcp-artifact-controller/1",
+ },
+ )
+ return self._read(request, maximum_bytes)
+
+ def post_json(
+ self,
+ url: str,
+ payload: dict[str, Any],
+ *,
+ maximum_bytes: int,
+ ) -> bytes:
+ self._validate_url(url)
+ request = Request(
+ url,
+ method="POST",
+ data=_canonical_json_bytes(payload),
+ headers={
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "User-Agent": "awoooi-external-mcp-artifact-controller/1",
+ },
+ )
+ return self._read(request, maximum_bytes)
+
+ def _validate_url(self, url: str) -> None:
+ parsed = urlparse(url)
+ try:
+ port = parsed.port
+ except ValueError as exc:
+ raise ControllerError("source_url_invalid") from exc
+ if (
+ parsed.scheme != "https"
+ or (parsed.hostname or "").lower() not in self._allowed_hosts
+ or parsed.username is not None
+ or parsed.password is not None
+ or port not in {None, 443}
+ or parsed.query
+ or parsed.fragment
+ ):
+ raise ControllerError("source_url_not_allowlisted")
+
+ def _read(self, request: Request, maximum_bytes: int) -> bytes:
+ try:
+ with self._opener.open(request, timeout=self._timeout_seconds) as response:
+ if response.status != 200:
+ raise ControllerError("source_http_status_invalid")
+ data = response.read(maximum_bytes + 1)
+ except HTTPError as exc:
+ if 300 <= exc.code < 400:
+ raise ControllerError("source_redirect_rejected") from exc
+ raise ControllerError(f"source_http_{exc.code}") from exc
+ except (URLError, TimeoutError, OSError) as exc:
+ raise ControllerError("source_unavailable") from exc
+ if len(data) > maximum_bytes:
+ raise ControllerError("source_payload_too_large")
+ return data
+
+
+@dataclass(frozen=True)
+class PackagePolicy:
+ name: str
+ version: str
+ metadata_url: str
+ tarball_url: str
+ integrity: str
+ sha512_hex: str
+ shasum_sha1: str
+ signature: str
+ attestations_url: str
+ slsa_subject_name: str
+ slsa_subject_sha512_hex: str
+ license: str
+ dependencies: dict[str, str]
+ optional_dependencies: dict[str, str]
+
+ @property
+ def filename(self) -> str:
+ safe_name = self.name.replace("@", "").replace("/", "-")
+ return f"{safe_name}-{self.version}.tgz"
+
+ @property
+ def bom_ref(self) -> str:
+ return f"pkg:npm/{quote(self.name, safe='/')}@{self.version}"
+
+
+@dataclass(frozen=True)
+class Policy:
+ work_item_id: str
+ candidate_id: str
+ risk_level: str
+ allowed_hosts: frozenset[str]
+ maximum_metadata_bytes: int
+ maximum_tarball_bytes: int
+ maximum_unpacked_bytes: int
+ signature_keyid: str
+ signature_keytype: str
+ signature_scheme: str
+ signature_public_key_der_base64: str
+ packages: tuple[PackagePolicy, ...]
+ push_repository: str
+ runtime_repository: str
+ mirror_tag: str
+ allowed_licenses: frozenset[str]
+ checksum: str
+
+ @property
+ def push_ref(self) -> str:
+ return f"{self.push_repository}:{self.mirror_tag}"
+
+
+@dataclass(frozen=True)
+class VerifiedPackage:
+ policy: PackagePolicy
+ tarball: bytes
+ attestation: bytes
+ metadata_sha256: str
+ tarball_sha512_hex: str
+ tarball_sha1: str
+ attestation_sha256: str
+ unpacked_bytes: int
+ archive_file_count: int
+ license_file_count: int
+ osv_response_sha256: str
+ vulnerability_ids: tuple[str, ...]
+
+
+def _canonical_json_bytes(payload: Any) -> bytes:
+ return json.dumps(
+ payload,
+ ensure_ascii=False,
+ sort_keys=True,
+ separators=(",", ":"),
+ ).encode("utf-8")
+
+
+def _sha256_hex(data: bytes) -> str:
+ return hashlib.sha256(data).hexdigest()
+
+
+def _require_text(value: Any, label: str) -> str:
+ if not isinstance(value, str) or not value.strip():
+ raise ControllerError(f"invalid_{label}")
+ return value.strip()
+
+
+def _require_positive_int(value: Any, label: str) -> int:
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
+ raise ControllerError(f"invalid_{label}")
+ return value
+
+
+def _parse_json(data: bytes, error_class: str) -> dict[str, Any]:
+ try:
+ payload = json.loads(data.decode("utf-8"))
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
+ raise ControllerError(error_class) from exc
+ if not isinstance(payload, dict):
+ raise ControllerError(error_class)
+ return payload
+
+
+def _parse_sha512_integrity(value: Any, label: str) -> tuple[str, str]:
+ integrity = _require_text(value, label)
+ if not integrity.startswith("sha512-"):
+ raise ControllerError(f"invalid_{label}")
+ try:
+ digest = base64.b64decode(integrity.removeprefix("sha512-"), validate=True)
+ except (ValueError, binascii.Error) as exc:
+ raise ControllerError(f"invalid_{label}") from exc
+ if len(digest) != 64:
+ raise ControllerError(f"invalid_{label}")
+ return integrity, digest.hex()
+
+
+def _validate_exact_version(value: Any, label: str) -> str:
+ version = _require_text(value, label)
+ if (
+ not VERSION_PATTERN.fullmatch(version)
+ or "latest" in version.lower()
+ or version.startswith(("^", "~", ">", "<", "*"))
+ ):
+ raise ControllerError(f"invalid_{label}")
+ return version
+
+
+def _validate_https_url(url: str, allowed_hosts: frozenset[str], label: str) -> None:
+ parsed = urlparse(url)
+ try:
+ port = parsed.port
+ except ValueError as exc:
+ raise ControllerError(f"invalid_{label}") from exc
+ if (
+ parsed.scheme != "https"
+ or (parsed.hostname or "").lower() not in allowed_hosts
+ or parsed.username is not None
+ or parsed.password is not None
+ or port not in {None, 443}
+ or parsed.query
+ or parsed.fragment
+ ):
+ raise ControllerError(f"invalid_{label}")
+
+
+def load_policy(path: Path) -> Policy:
+ try:
+ payload = json.loads(path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as exc:
+ raise ControllerError("policy_unreadable") from exc
+ if not isinstance(payload, dict) or payload.get("schema_version") != POLICY_SCHEMA_VERSION:
+ raise ControllerError("policy_schema_invalid")
+
+ work_item_id = _require_text(payload.get("work_item_id"), "work_item_id")
+ candidate_id = _require_text(payload.get("candidate_id"), "candidate_id")
+ if candidate_id != "external.playwright-verifier":
+ raise ControllerError("candidate_id_not_allowlisted")
+ if payload.get("risk_level") != "high":
+ raise ControllerError("risk_level_invalid")
+
+ catalog = payload.get("catalog_source")
+ if not isinstance(catalog, dict):
+ raise ControllerError("catalog_source_invalid")
+ if (
+ catalog.get("kind") != "discovery_only"
+ or catalog.get("executable_supply_chain_authority") is not False
+ ):
+ raise ControllerError("catalog_must_be_discovery_only")
+
+ registry = payload.get("registry_contract")
+ if not isinstance(registry, dict):
+ raise ControllerError("registry_contract_invalid")
+ allowed_hosts = registry.get("allowed_hosts")
+ if allowed_hosts != ["registry.npmjs.org"]:
+ raise ControllerError("registry_hosts_invalid")
+ host_set = frozenset(allowed_hosts)
+ if registry.get("redirects_allowed") is not False:
+ raise ControllerError("registry_redirect_policy_invalid")
+ registry_origin = _require_text(registry.get("registry_origin"), "registry_origin")
+ keys_url = _require_text(registry.get("keys_url"), "keys_url")
+ _validate_https_url(registry_origin, host_set, "registry_origin")
+ _validate_https_url(keys_url, host_set, "keys_url")
+ if urlparse(registry_origin).path not in {"", "/"}:
+ raise ControllerError("registry_origin_invalid")
+ if urlparse(keys_url).path != "/-/npm/v1/keys":
+ raise ControllerError("keys_url_invalid")
+
+ signature_key = registry.get("signature_key")
+ if not isinstance(signature_key, dict):
+ raise ControllerError("signature_key_invalid")
+ signature_keyid = _require_text(signature_key.get("keyid"), "signature_keyid")
+ signature_keytype = _require_text(
+ signature_key.get("keytype"), "signature_keytype"
+ )
+ signature_scheme = _require_text(
+ signature_key.get("scheme"), "signature_scheme"
+ )
+ if signature_keytype != "ecdsa-sha2-nistp256" or signature_scheme != signature_keytype:
+ raise ControllerError("signature_scheme_not_allowlisted")
+ public_key = _require_text(
+ signature_key.get("public_key_der_base64"), "signature_public_key"
+ )
+ try:
+ decoded_key = base64.b64decode(public_key, validate=True)
+ except (ValueError, binascii.Error) as exc:
+ raise ControllerError("signature_public_key_invalid") from exc
+ if len(decoded_key) < 80:
+ raise ControllerError("signature_public_key_invalid")
+
+ rows = payload.get("packages")
+ if not isinstance(rows, list) or len(rows) != len(REQUIRED_PACKAGE_NAMES):
+ raise ControllerError("package_set_invalid")
+ packages: list[PackagePolicy] = []
+ seen: set[str] = set()
+ for row in rows:
+ if not isinstance(row, dict):
+ raise ControllerError("package_policy_invalid")
+ name = _require_text(row.get("name"), "package_name")
+ if name in seen:
+ raise ControllerError("duplicate_package_name")
+ seen.add(name)
+ version = _validate_exact_version(row.get("version"), "package_version")
+ integrity, sha512_hex = _parse_sha512_integrity(
+ row.get("integrity"), "package_integrity"
+ )
+ shasum = _require_text(row.get("shasum_sha1"), "package_shasum")
+ if not SHA1_PATTERN.fullmatch(shasum):
+ raise ControllerError("package_shasum_invalid")
+ slsa_sha512 = _require_text(
+ row.get("slsa_subject_sha512_hex"), "slsa_subject_sha512"
+ )
+ if not SHA512_HEX_PATTERN.fullmatch(slsa_sha512) or slsa_sha512 != sha512_hex:
+ raise ControllerError("slsa_subject_digest_mismatch")
+ metadata_url = _require_text(row.get("metadata_url"), "metadata_url")
+ tarball_url = _require_text(row.get("tarball_url"), "tarball_url")
+ attestations_url = _require_text(
+ row.get("attestations_url"), "attestations_url"
+ )
+ for url, label in (
+ (metadata_url, "metadata_url"),
+ (tarball_url, "tarball_url"),
+ (attestations_url, "attestations_url"),
+ ):
+ _validate_https_url(url, host_set, label)
+ dependencies = _validate_dependency_map(row.get("dependencies", {}))
+ optional_dependencies = _validate_dependency_map(
+ row.get("optional_dependencies", {})
+ )
+ packages.append(
+ PackagePolicy(
+ name=name,
+ version=version,
+ metadata_url=metadata_url,
+ tarball_url=tarball_url,
+ integrity=integrity,
+ sha512_hex=sha512_hex,
+ shasum_sha1=shasum,
+ signature=_require_text(row.get("signature"), "package_signature"),
+ attestations_url=attestations_url,
+ slsa_subject_name=_require_text(
+ row.get("slsa_subject_name"), "slsa_subject_name"
+ ),
+ slsa_subject_sha512_hex=slsa_sha512,
+ license=_require_text(row.get("license"), "package_license"),
+ dependencies=dependencies,
+ optional_dependencies=optional_dependencies,
+ )
+ )
+ if seen != REQUIRED_PACKAGE_NAMES:
+ raise ControllerError("package_set_invalid")
+ package_versions = {item.name: item.version for item in packages}
+ for package in packages:
+ for dependency, version in package.dependencies.items():
+ if package_versions.get(dependency) != version:
+ raise ControllerError("dependency_closure_invalid")
+
+ mirror = payload.get("internal_mirror")
+ if not isinstance(mirror, dict):
+ raise ControllerError("internal_mirror_invalid")
+ if (
+ mirror.get("artifact_kind") != "oci_scratch_bundle"
+ or mirror.get("digest_pin_required") is not True
+ or mirror.get("sbom_required") is not True
+ or mirror.get("upstream_signature_required") is not True
+ or mirror.get("slsa_subject_match_required") is not True
+ or mirror.get("vulnerability_scan_required") is not True
+ ):
+ raise ControllerError("internal_mirror_gate_invalid")
+ push_repository = _require_text(
+ mirror.get("push_repository"), "push_repository"
+ )
+ runtime_repository = _require_text(
+ mirror.get("runtime_repository"), "runtime_repository"
+ )
+ if push_repository != "registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp":
+ raise ControllerError("push_repository_not_internal")
+ if runtime_repository != "192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp":
+ raise ControllerError("runtime_repository_not_internal")
+ mirror_tag = _require_text(mirror.get("tag"), "mirror_tag")
+ if mirror_tag != "0.0.78-bundle-v1" or "latest" in mirror_tag.lower():
+ raise ControllerError("mirror_tag_invalid")
+ allowed_licenses = mirror.get("allowed_licenses")
+ if allowed_licenses != ["Apache-2.0"]:
+ raise ControllerError("allowed_licenses_invalid")
+ for package in packages:
+ if package.license not in allowed_licenses:
+ raise ControllerError("package_license_not_allowed")
+
+ replay = payload.get("replay_contract")
+ promotion = payload.get("promotion_contract")
+ if not isinstance(replay, dict) or not isinstance(promotion, dict):
+ raise ControllerError("promotion_contract_invalid")
+ required_false = (
+ "authenticated_session_allowed",
+ "browser_profile_persistence_allowed",
+ "file_upload_allowed",
+ "download_allowed",
+ "pdf_write_allowed",
+ "browser_install_allowed",
+ "arbitrary_navigation_allowed",
+ "production_form_submit_allowed",
+ "production_write_allowed",
+ )
+ if any(replay.get(field) is not False for field in required_false):
+ raise ControllerError("replay_boundary_invalid")
+ if any(
+ promotion.get(field) is not False
+ for field in ("deployment_allowed", "shadow_allowed", "canary_allowed")
+ ):
+ raise ControllerError("promotion_must_remain_disabled")
+ prohibited = set(payload.get("prohibited") or [])
+ if not {
+ "github_api_or_github_actions",
+ "actions_checkout",
+ "github_source_archive_or_ghcr",
+ "npx_y",
+ "at_latest",
+ "floating_container_tag",
+ "secret_read_or_log",
+ "request_or_response_body_storage",
+ "external_rag_write",
+ "production_mutation",
+ }.issubset(prohibited):
+ raise ControllerError("prohibited_actions_incomplete")
+
+ return Policy(
+ work_item_id=work_item_id,
+ candidate_id=candidate_id,
+ risk_level="high",
+ allowed_hosts=host_set,
+ maximum_metadata_bytes=_require_positive_int(
+ registry.get("maximum_metadata_bytes"), "maximum_metadata_bytes"
+ ),
+ maximum_tarball_bytes=_require_positive_int(
+ registry.get("maximum_tarball_bytes"), "maximum_tarball_bytes"
+ ),
+ maximum_unpacked_bytes=_require_positive_int(
+ registry.get("maximum_unpacked_bytes"), "maximum_unpacked_bytes"
+ ),
+ signature_keyid=signature_keyid,
+ signature_keytype=signature_keytype,
+ signature_scheme=signature_scheme,
+ signature_public_key_der_base64=public_key,
+ packages=tuple(packages),
+ push_repository=push_repository,
+ runtime_repository=runtime_repository,
+ mirror_tag=mirror_tag,
+ allowed_licenses=frozenset(allowed_licenses),
+ checksum="sha256:" + _sha256_hex(_canonical_json_bytes(payload)),
+ )
+
+
+def _validate_dependency_map(value: Any) -> dict[str, str]:
+ if not isinstance(value, dict):
+ raise ControllerError("dependency_map_invalid")
+ result: dict[str, str] = {}
+ for name, version in value.items():
+ dependency = _require_text(name, "dependency_name")
+ result[dependency] = _validate_exact_version(version, "dependency_version")
+ return dict(sorted(result.items()))
+
+
+def _verify_registry_key(policy: Policy, payload: dict[str, Any]) -> None:
+ rows = payload.get("keys")
+ if not isinstance(rows, list):
+ raise ControllerError("registry_keys_invalid")
+ matches = [
+ row
+ for row in rows
+ if isinstance(row, dict) and row.get("keyid") == policy.signature_keyid
+ ]
+ if len(matches) != 1:
+ raise ControllerError("registry_signature_key_missing")
+ key = matches[0]
+ if (
+ key.get("keytype") != policy.signature_keytype
+ or key.get("scheme") != policy.signature_scheme
+ or key.get("key") != policy.signature_public_key_der_base64
+ or key.get("expires") is not None
+ ):
+ raise ControllerError("registry_signature_key_drift")
+
+
+def verify_npm_signature(
+ *,
+ public_key_der_base64: str,
+ signature_base64: str,
+ message: bytes,
+) -> None:
+ try:
+ key_bytes = base64.b64decode(public_key_der_base64, validate=True)
+ signature = base64.b64decode(signature_base64, validate=True)
+ except (ValueError, binascii.Error) as exc:
+ raise ControllerError("npm_signature_encoding_invalid") from exc
+ try:
+ from cryptography.hazmat.primitives import hashes, serialization
+ from cryptography.hazmat.primitives.asymmetric import ec
+
+ public_key = serialization.load_der_public_key(key_bytes)
+ public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
+ return
+ except ImportError:
+ pass
+ except Exception as exc: # cryptography exposes several backend error types
+ raise ControllerError("npm_signature_verification_failed") from exc
+
+ openssl = shutil.which("openssl")
+ if not openssl:
+ raise ControllerError("npm_signature_verifier_unavailable")
+ with tempfile.TemporaryDirectory(prefix="awoooi-npm-signature-") as directory:
+ root = Path(directory)
+ der_path = root / "public.der"
+ pem_path = root / "public.pem"
+ signature_path = root / "signature.der"
+ message_path = root / "message.bin"
+ der_path.write_bytes(key_bytes)
+ signature_path.write_bytes(signature)
+ message_path.write_bytes(message)
+ convert = subprocess.run(
+ [
+ openssl,
+ "pkey",
+ "-pubin",
+ "-inform",
+ "DER",
+ "-in",
+ str(der_path),
+ "-out",
+ str(pem_path),
+ ],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ if convert.returncode != 0:
+ raise ControllerError("npm_signature_public_key_invalid")
+ verified = subprocess.run(
+ [
+ openssl,
+ "dgst",
+ "-sha256",
+ "-verify",
+ str(pem_path),
+ "-signature",
+ str(signature_path),
+ str(message_path),
+ ],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ if verified.returncode != 0:
+ raise ControllerError("npm_signature_verification_failed")
+
+
+def _validate_metadata(
+ package: PackagePolicy,
+ metadata: dict[str, Any],
+ policy: Policy,
+) -> None:
+ if metadata.get("name") != package.name or metadata.get("version") != package.version:
+ raise ControllerError("registry_package_identity_mismatch")
+ if metadata.get("license") != package.license:
+ raise ControllerError("registry_package_license_mismatch")
+ if _validate_dependency_map(metadata.get("dependencies", {})) != package.dependencies:
+ raise ControllerError("registry_dependency_drift")
+ if (
+ _validate_dependency_map(metadata.get("optionalDependencies", {}))
+ != package.optional_dependencies
+ ):
+ raise ControllerError("registry_optional_dependency_drift")
+ dist = metadata.get("dist")
+ if not isinstance(dist, dict):
+ raise ControllerError("registry_dist_missing")
+ if (
+ dist.get("integrity") != package.integrity
+ or dist.get("shasum") != package.shasum_sha1
+ or dist.get("tarball") != package.tarball_url
+ ):
+ raise ControllerError("registry_dist_drift")
+ signatures = dist.get("signatures")
+ expected_signature = {
+ "keyid": policy.signature_keyid,
+ "sig": package.signature,
+ }
+ if not isinstance(signatures, list) or expected_signature not in signatures:
+ raise ControllerError("registry_package_signature_drift")
+ attestations = dist.get("attestations")
+ if not isinstance(attestations, dict):
+ raise ControllerError("registry_attestations_missing")
+ if (
+ attestations.get("url") != package.attestations_url
+ or not isinstance(attestations.get("provenance"), dict)
+ or attestations["provenance"].get("predicateType") != SLSA_PREDICATE_TYPE
+ ):
+ raise ControllerError("registry_attestation_drift")
+ message = f"{package.name}@{package.version}:{package.integrity}".encode()
+ verify_npm_signature(
+ public_key_der_base64=policy.signature_public_key_der_base64,
+ signature_base64=package.signature,
+ message=message,
+ )
+
+
+def validate_slsa_attestation(
+ package: PackagePolicy,
+ payload: dict[str, Any],
+) -> None:
+ rows = payload.get("attestations")
+ if not isinstance(rows, list):
+ raise ControllerError("slsa_attestation_invalid")
+ slsa_rows = [
+ row
+ for row in rows
+ if isinstance(row, dict) and row.get("predicateType") == SLSA_PREDICATE_TYPE
+ ]
+ if len(slsa_rows) != 1:
+ raise ControllerError("slsa_attestation_missing")
+ bundle = slsa_rows[0].get("bundle")
+ envelope = bundle.get("dsseEnvelope") if isinstance(bundle, dict) else None
+ encoded = envelope.get("payload") if isinstance(envelope, dict) else None
+ if not isinstance(encoded, str):
+ raise ControllerError("slsa_dsse_payload_missing")
+ try:
+ statement = json.loads(base64.b64decode(encoded, validate=True))
+ except (ValueError, binascii.Error, json.JSONDecodeError) as exc:
+ raise ControllerError("slsa_dsse_payload_invalid") from exc
+ if not isinstance(statement, dict):
+ raise ControllerError("slsa_statement_invalid")
+ if (
+ statement.get("_type") != IN_TOTO_STATEMENT_TYPE
+ or statement.get("predicateType") != SLSA_PREDICATE_TYPE
+ ):
+ raise ControllerError("slsa_statement_type_invalid")
+ subjects = statement.get("subject")
+ expected_subject = {
+ "name": package.slsa_subject_name,
+ "digest": {"sha512": package.slsa_subject_sha512_hex},
+ }
+ if subjects != [expected_subject]:
+ raise ControllerError("slsa_subject_mismatch")
+
+
+def inspect_tarball(
+ package: PackagePolicy,
+ data: bytes,
+ *,
+ maximum_unpacked_bytes: int,
+) -> tuple[int, int, int]:
+ try:
+ archive = tarfile.open(fileobj=io.BytesIO(data), mode="r:gz")
+ except (tarfile.TarError, OSError) as exc:
+ raise ControllerError("package_tarball_invalid") from exc
+ unpacked_bytes = 0
+ file_count = 0
+ license_file_count = 0
+ seen_files: set[str] = set()
+ package_json: dict[str, Any] | None = None
+ with archive:
+ members = archive.getmembers()
+ if not members or len(members) > 1024:
+ raise ControllerError("package_archive_entry_count_invalid")
+ for member in members:
+ path = PurePosixPath(member.name)
+ if (
+ path.is_absolute()
+ or ".." in path.parts
+ or not path.parts
+ or path.parts[0] != "package"
+ or member.issym()
+ or member.islnk()
+ or member.isdev()
+ ):
+ raise ControllerError("package_archive_unsafe_entry")
+ if member.isfile():
+ normalized_path = str(path)
+ if member.size < 0 or normalized_path in seen_files:
+ raise ControllerError("package_archive_duplicate_or_invalid_file")
+ seen_files.add(normalized_path)
+ file_count += 1
+ unpacked_bytes += member.size
+ if unpacked_bytes > maximum_unpacked_bytes:
+ raise ControllerError("package_archive_unpacked_size_exceeded")
+ basename = path.name.lower()
+ if basename.startswith(("license", "licence", "copying")):
+ license_file_count += 1
+ if path == PurePosixPath("package/package.json"):
+ extracted = archive.extractfile(member)
+ if extracted is None:
+ raise ControllerError("package_manifest_unreadable")
+ package_json = _parse_json(
+ extracted.read(262_145), "package_manifest_invalid"
+ )
+ if package_json is None:
+ raise ControllerError("package_manifest_missing")
+ if (
+ package_json.get("name") != package.name
+ or package_json.get("version") != package.version
+ or package_json.get("license") != package.license
+ ):
+ raise ControllerError("package_manifest_identity_mismatch")
+ if _validate_dependency_map(package_json.get("dependencies", {})) != package.dependencies:
+ raise ControllerError("package_manifest_dependency_drift")
+ if (
+ _validate_dependency_map(package_json.get("optionalDependencies", {}))
+ != package.optional_dependencies
+ ):
+ raise ControllerError("package_manifest_optional_dependency_drift")
+ if license_file_count == 0:
+ raise ControllerError("package_license_file_missing")
+ return unpacked_bytes, file_count, license_file_count
+
+
+def _query_osv(
+ package: PackagePolicy,
+ http: HTTPClient,
+ maximum_bytes: int,
+) -> tuple[str, tuple[str, ...]]:
+ data = http.post_json(
+ OSV_QUERY_URL,
+ {
+ "package": {"ecosystem": "npm", "name": package.name},
+ "version": package.version,
+ },
+ maximum_bytes=maximum_bytes,
+ )
+ payload = _parse_json(data, "osv_response_invalid")
+ rows = payload.get("vulns", [])
+ if not isinstance(rows, list):
+ raise ControllerError("osv_response_invalid")
+ vulnerability_ids: list[str] = []
+ for row in rows:
+ identifier = row.get("id") if isinstance(row, dict) else None
+ if not isinstance(identifier, str) or not VULNERABILITY_ID_PATTERN.fullmatch(
+ identifier
+ ):
+ raise ControllerError("osv_vulnerability_id_invalid")
+ vulnerability_ids.append(identifier)
+ return _sha256_hex(data), tuple(sorted(set(vulnerability_ids)))
+
+
+def verify_upstream_bundle(policy: Policy, http: HTTPClient) -> tuple[VerifiedPackage, ...]:
+ keys_bytes = http.get_bytes(
+ "https://registry.npmjs.org/-/npm/v1/keys",
+ maximum_bytes=policy.maximum_metadata_bytes,
+ )
+ _verify_registry_key(policy, _parse_json(keys_bytes, "registry_keys_invalid"))
+ verified: list[VerifiedPackage] = []
+ for package in policy.packages:
+ metadata_bytes = http.get_bytes(
+ package.metadata_url,
+ maximum_bytes=policy.maximum_metadata_bytes,
+ )
+ metadata = _parse_json(metadata_bytes, "registry_metadata_invalid")
+ _validate_metadata(package, metadata, policy)
+ attestation_bytes = http.get_bytes(
+ package.attestations_url,
+ maximum_bytes=policy.maximum_metadata_bytes,
+ )
+ validate_slsa_attestation(
+ package,
+ _parse_json(attestation_bytes, "slsa_attestation_invalid"),
+ )
+ tarball = http.get_bytes(
+ package.tarball_url,
+ maximum_bytes=policy.maximum_tarball_bytes,
+ )
+ tarball_sha512 = hashlib.sha512(tarball).hexdigest()
+ tarball_sha1 = hashlib.sha1(tarball, usedforsecurity=False).hexdigest()
+ if (
+ tarball_sha512 != package.sha512_hex
+ or tarball_sha1 != package.shasum_sha1
+ ):
+ raise ControllerError("package_tarball_digest_mismatch")
+ unpacked_bytes, archive_file_count, license_file_count = inspect_tarball(
+ package,
+ tarball,
+ maximum_unpacked_bytes=policy.maximum_unpacked_bytes,
+ )
+ osv_sha256, vulnerability_ids = _query_osv(
+ package,
+ http,
+ policy.maximum_metadata_bytes,
+ )
+ if vulnerability_ids:
+ raise ControllerError("vulnerability_decision_blocked")
+ verified.append(
+ VerifiedPackage(
+ policy=package,
+ tarball=tarball,
+ attestation=attestation_bytes,
+ metadata_sha256=_sha256_hex(metadata_bytes),
+ tarball_sha512_hex=tarball_sha512,
+ tarball_sha1=tarball_sha1,
+ attestation_sha256=_sha256_hex(attestation_bytes),
+ unpacked_bytes=unpacked_bytes,
+ archive_file_count=archive_file_count,
+ license_file_count=license_file_count,
+ osv_response_sha256=osv_sha256,
+ vulnerability_ids=vulnerability_ids,
+ )
+ )
+ return tuple(verified)
+
+
+def build_cyclonedx_sbom(
+ policy: Policy,
+ verified: tuple[VerifiedPackage, ...],
+) -> dict[str, Any]:
+ by_name = {item.policy.name: item for item in verified}
+ components: list[dict[str, Any]] = []
+ dependencies: list[dict[str, Any]] = []
+ for package in policy.packages:
+ evidence = by_name[package.name]
+ components.append(
+ {
+ "type": "library",
+ "bom-ref": package.bom_ref,
+ "group": package.name.split("/", 1)[0].removeprefix("@")
+ if package.name.startswith("@")
+ else "",
+ "name": package.name.split("/", 1)[-1],
+ "version": package.version,
+ "purl": package.bom_ref,
+ "hashes": [
+ {"alg": "SHA-1", "content": evidence.tarball_sha1},
+ {"alg": "SHA-512", "content": evidence.tarball_sha512_hex},
+ ],
+ "licenses": [{"license": {"id": package.license}}],
+ "externalReferences": [
+ {"type": "distribution", "url": package.tarball_url},
+ {"type": "build-meta", "url": package.attestations_url},
+ ],
+ }
+ )
+ dependencies.append(
+ {
+ "ref": package.bom_ref,
+ "dependsOn": [
+ by_name[name].policy.bom_ref
+ for name in sorted(package.dependencies)
+ ],
+ }
+ )
+ serial = uuid.uuid5(uuid.NAMESPACE_URL, f"{policy.candidate_id}:{policy.checksum}")
+ return {
+ "bomFormat": "CycloneDX",
+ "specVersion": SBOM_SPEC_VERSION,
+ "serialNumber": f"urn:uuid:{serial}",
+ "version": 1,
+ "metadata": {
+ "component": {
+ "type": "application",
+ "bom-ref": f"awoooi:mcp-artifact:{policy.candidate_id}",
+ "name": policy.candidate_id,
+ "version": policy.packages[0].version,
+ },
+ "tools": {
+ "components": [
+ {
+ "type": "application",
+ "name": "awoooi-external-mcp-artifact-controller",
+ "version": "1",
+ }
+ ]
+ },
+ },
+ "components": components,
+ "dependencies": dependencies,
+ }
+
+
+def _write_bundle(
+ directory: Path,
+ policy: Policy,
+ verified: tuple[VerifiedPackage, ...],
+ sbom: dict[str, Any],
+) -> tuple[str, str]:
+ bundle = directory / "bundle"
+ tarballs = bundle / "tarballs"
+ attestations = bundle / "attestations"
+ tarballs.mkdir(parents=True)
+ attestations.mkdir(parents=True)
+ package_rows: list[dict[str, Any]] = []
+ for item in verified:
+ package = item.policy
+ tarball_path = tarballs / package.filename
+ attestation_path = attestations / f"{package.filename}.attestations.json"
+ tarball_path.write_bytes(item.tarball)
+ attestation_path.write_bytes(item.attestation)
+ package_rows.append(
+ {
+ "name": package.name,
+ "version": package.version,
+ "tarball_path": f"tarballs/{package.filename}",
+ "tarball_sha512_hex": item.tarball_sha512_hex,
+ "tarball_sha1": item.tarball_sha1,
+ "metadata_sha256": item.metadata_sha256,
+ "attestation_path": (
+ f"attestations/{package.filename}.attestations.json"
+ ),
+ "attestation_sha256": item.attestation_sha256,
+ "npm_registry_signature_verified": True,
+ "slsa_subject_verified": True,
+ "archive_safety_verified": True,
+ "license": package.license,
+ "license_file_count": item.license_file_count,
+ "osv_response_sha256": item.osv_response_sha256,
+ "vulnerability_ids": list(item.vulnerability_ids),
+ }
+ )
+ sbom_bytes = _canonical_json_bytes(sbom) + b"\n"
+ sbom_path = bundle / "sbom.cdx.json"
+ sbom_path.write_bytes(sbom_bytes)
+ manifest = {
+ "schema_version": "awoooi_external_mcp_artifact_bundle_v1",
+ "candidate_id": policy.candidate_id,
+ "work_item_id": policy.work_item_id,
+ "policy_checksum": policy.checksum,
+ "package_count": len(package_rows),
+ "packages": package_rows,
+ "sbom": {
+ "path": "sbom.cdx.json",
+ "format": "CycloneDX",
+ "spec_version": SBOM_SPEC_VERSION,
+ "sha256": _sha256_hex(sbom_bytes),
+ },
+ "operation_boundaries": {
+ "mcp_server_started": False,
+ "browser_started": False,
+ "external_tool_call_performed": False,
+ "external_rag_write_performed": False,
+ "production_write_performed": False,
+ "runtime_request_or_response_body_stored": False,
+ "upstream_supply_chain_attestation_bundled": True,
+ },
+ }
+ manifest_bytes = _canonical_json_bytes(manifest) + b"\n"
+ (bundle / "bundle-manifest.json").write_bytes(manifest_bytes)
+ dockerfile = (
+ "FROM scratch\n"
+ "COPY bundle/ /artifact/\n"
+ f'LABEL org.opencontainers.image.title="{policy.candidate_id}"\n'
+ f'LABEL org.opencontainers.image.version="{policy.packages[0].version}"\n'
+ f'LABEL io.awoooi.policy-checksum="{policy.checksum}"\n'
+ )
+ (directory / "Dockerfile").write_text(dockerfile, encoding="utf-8")
+ return _sha256_hex(manifest_bytes), _sha256_hex(sbom_bytes)
+
+
+def _run_command(command: list[str], *, cwd: Path, timeout: int) -> str:
+ try:
+ completed = subprocess.run(
+ command,
+ cwd=cwd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=timeout,
+ )
+ except (OSError, subprocess.TimeoutExpired) as exc:
+ raise ControllerError("bounded_execution_command_failed") from exc
+ if completed.returncode != 0:
+ raise ControllerError("bounded_execution_command_failed")
+ return completed.stdout + completed.stderr
+
+
+def mirror_bundle(
+ policy: Policy,
+ verified: tuple[VerifiedPackage, ...],
+ sbom: dict[str, Any],
+) -> tuple[str, str, str, str]:
+ docker = shutil.which("docker")
+ if not docker:
+ raise ControllerError("docker_cli_unavailable")
+ with tempfile.TemporaryDirectory(prefix="awoooi-playwright-mcp-bundle-") as temp:
+ context = Path(temp)
+ manifest_sha256, sbom_sha256 = _write_bundle(
+ context,
+ policy,
+ verified,
+ sbom,
+ )
+ _run_command(
+ [
+ docker,
+ "build",
+ "--network=none",
+ "--pull=false",
+ "--provenance=false",
+ "--sbom=false",
+ "--file",
+ str(context / "Dockerfile"),
+ "--tag",
+ policy.push_ref,
+ ".",
+ ],
+ cwd=context,
+ timeout=300,
+ )
+ push_output = _run_command(
+ [docker, "push", policy.push_ref],
+ cwd=context,
+ timeout=300,
+ )
+ matches = re.findall(r"digest:\s+(sha256:[0-9a-f]{64})", push_output)
+ if not matches:
+ inspected = _run_command(
+ [
+ docker,
+ "image",
+ "inspect",
+ "--format",
+ "{{json .RepoDigests}}",
+ policy.push_ref,
+ ],
+ cwd=context,
+ timeout=30,
+ )
+ try:
+ repo_digests = json.loads(inspected.strip())
+ except json.JSONDecodeError as exc:
+ raise ControllerError("internal_mirror_digest_missing") from exc
+ matches = [
+ value.rsplit("@", 1)[1]
+ for value in repo_digests
+ if isinstance(value, str)
+ and value.startswith(policy.push_repository + "@sha256:")
+ ]
+ digest = matches[-1] if matches else ""
+ if not SHA256_PATTERN.fullmatch(digest):
+ raise ControllerError("internal_mirror_digest_invalid")
+ digest_ref = f"{policy.push_repository}@{digest}"
+ _run_command(
+ [docker, "buildx", "imagetools", "inspect", digest_ref],
+ cwd=context,
+ timeout=60,
+ )
+ runtime_ref = f"{policy.runtime_repository}@{digest}"
+ return digest_ref, runtime_ref, manifest_sha256, sbom_sha256
+
+
+def build_receipt(
+ *,
+ policy: Policy,
+ verified: tuple[VerifiedPackage, ...],
+ sbom_sha256: str,
+ bundle_manifest_sha256: str,
+ run_id: str,
+ trace_id: str,
+ mode: str,
+ internal_mirror_ref: str | None,
+ runtime_mirror_ref: str | None,
+) -> dict[str, Any]:
+ observed_at = datetime.now(timezone.utc).isoformat()
+ apply_performed = internal_mirror_ref is not None
+ terminal = (
+ "completed_internal_mirror_pending_independent_verifier"
+ if apply_performed
+ else "completed_check_mode_pending_independent_verifier"
+ )
+ package_rows = [
+ {
+ "name": item.policy.name,
+ "version": item.policy.version,
+ "metadata_sha256": item.metadata_sha256,
+ "tarball_sha512_hex": item.tarball_sha512_hex,
+ "tarball_sha1": item.tarball_sha1,
+ "attestation_sha256": item.attestation_sha256,
+ "npm_registry_signature_verified": True,
+ "slsa_subject_verified": True,
+ "archive_safety_verified": True,
+ "license_decision": "passed",
+ "vulnerability_decision": "passed_no_known_osv_records",
+ "vulnerability_ids": list(item.vulnerability_ids),
+ "unpacked_bytes": item.unpacked_bytes,
+ "archive_file_count": item.archive_file_count,
+ }
+ for item in verified
+ ]
+ return {
+ "schema_version": RECEIPT_SCHEMA_VERSION,
+ "run_id": run_id,
+ "trace_id": trace_id,
+ "work_item_id": policy.work_item_id,
+ "candidate_id": policy.candidate_id,
+ "risk_level": policy.risk_level,
+ "mode": mode,
+ "status": terminal,
+ "observed_at": observed_at,
+ "policy_checksum": policy.checksum,
+ "package_count": len(package_rows),
+ "packages": package_rows,
+ "internal_mirror_ref": internal_mirror_ref,
+ "runtime_mirror_ref": runtime_mirror_ref,
+ "bundle_manifest_sha256": bundle_manifest_sha256,
+ "cyclonedx_sbom_sha256": sbom_sha256,
+ "promotion_allowed": False,
+ "replay_allowed": False,
+ "shadow_allowed": False,
+ "canary_allowed": False,
+ "stage_receipts": {
+ "sensor_source_receipt": {
+ "registry": "registry.npmjs.org",
+ "osv": "api.osv.dev",
+ "redirects_followed": 0,
+ "runtime_request_or_response_body_stored": False,
+ "upstream_supply_chain_attestation_bundled": True,
+ },
+ "normalized_asset_identity": {
+ "candidate_id": policy.candidate_id,
+ "exact_root_version": policy.packages[0].version,
+ "package_count": len(package_rows),
+ },
+ "source_of_truth_diff": {
+ "change_state": "baseline_created",
+ "policy_checksum": policy.checksum,
+ },
+ "decision": {
+ "candidate_action": (
+ "run_independent_mirror_verifier_then_implement_replay_adapter"
+ if apply_performed
+ else "run_independent_check_verifier_then_mirror_exact_bundle"
+ ),
+ "runtime_promotion_allowed": False,
+ },
+ "risk_policy": {
+ "risk_level": "high",
+ "exact_versions": True,
+ "immutable_upstream_digests": True,
+ "npm_registry_signatures_verified": True,
+ "slsa_subjects_verified": True,
+ "license_decision": "passed",
+ "vulnerability_decision": "passed_no_known_osv_records",
+ },
+ "check_mode": {
+ "status": "passed",
+ "mcp_process_started": False,
+ "browser_started": False,
+ "production_write_performed": False,
+ },
+ "bounded_execution": {
+ "status": "internal_oci_bundle_pushed" if apply_performed else "not_executed",
+ "internal_registry_write_performed": apply_performed,
+ "external_runtime_mutation_performed": False,
+ "production_route_changed": False,
+ },
+ "independent_post_verifier": {
+ "status": "pending_external_verifier",
+ "controller_registry_digest_check": apply_performed,
+ "internal_digest_ref_verified": False,
+ "bundle_manifest_sha256": bundle_manifest_sha256,
+ "cyclonedx_sbom_sha256": sbom_sha256,
+ "package_digest_count": len(package_rows),
+ },
+ "rollback_or_no_write_terminal": {
+ "status": (
+ "pending_external_verifier"
+ if apply_performed
+ else "no_write_pending_external_verifier"
+ ),
+ "rollback_action": "disable_candidate_and_retain_immutable_bundle",
+ "external_artifact_delete_allowed": False,
+ "production_route_change_allowed": False,
+ },
+ "learning_writeback": {
+ "status": "receipt_ready_for_committed_writeback",
+ "rag_write_performed": False,
+ "km_write_performed": False,
+ },
+ },
+ }
+
+
+def _write_receipt(path: Path, receipt: dict[str, Any]) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_bytes(_canonical_json_bytes(receipt) + b"\n")
+
+
+def _build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--policy", type=Path, required=True)
+ parser.add_argument("--run-id", required=True)
+ parser.add_argument("--trace-id", required=True)
+ parser.add_argument("--receipt", type=Path)
+ parser.add_argument("command", choices=("check", "mirror"))
+ parser.add_argument("--apply", action="store_true")
+ return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = _build_parser().parse_args(argv)
+ try:
+ normalized_run_id = str(uuid.UUID(args.run_id))
+ except (ValueError, AttributeError) as exc:
+ raise ControllerError("run_id_invalid") from exc
+ if normalized_run_id != args.run_id:
+ raise ControllerError("run_id_not_canonical")
+ if not TRACE_PATTERN.fullmatch(args.trace_id):
+ raise ControllerError("trace_id_invalid")
+ if args.trace_id != f"mcp-artifact-{normalized_run_id}":
+ raise ControllerError("run_trace_identity_mismatch")
+ if args.command == "check" and args.apply:
+ raise ControllerError("check_mode_cannot_apply")
+ if args.command == "mirror" and args.apply and args.receipt is None:
+ raise ControllerError("apply_receipt_path_required")
+ policy = load_policy(args.policy)
+ http = BoundedHTTPClient(policy.allowed_hosts | frozenset({OSV_HOST}))
+ verified = verify_upstream_bundle(policy, http)
+ sbom = build_cyclonedx_sbom(policy, verified)
+ with tempfile.TemporaryDirectory(prefix="awoooi-playwright-mcp-check-") as temp:
+ manifest_sha256, sbom_sha256 = _write_bundle(
+ Path(temp),
+ policy,
+ verified,
+ sbom,
+ )
+ internal_ref = None
+ runtime_ref = None
+ if args.command == "mirror" and args.apply:
+ internal_ref, runtime_ref, manifest_sha256, sbom_sha256 = mirror_bundle(
+ policy,
+ verified,
+ sbom,
+ )
+ receipt = build_receipt(
+ policy=policy,
+ verified=verified,
+ sbom_sha256=sbom_sha256,
+ bundle_manifest_sha256=manifest_sha256,
+ run_id=normalized_run_id,
+ trace_id=args.trace_id,
+ mode=("apply" if args.command == "mirror" and args.apply else "check"),
+ internal_mirror_ref=internal_ref,
+ runtime_mirror_ref=runtime_ref,
+ )
+ if args.receipt is not None:
+ _write_receipt(args.receipt, receipt)
+ print(json.dumps(receipt, ensure_ascii=False, sort_keys=True))
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(main())
+ except ControllerError as exc:
+ print(
+ json.dumps(
+ {
+ "schema_version": RECEIPT_SCHEMA_VERSION,
+ "status": "blocked_with_safe_next_action",
+ "error_class": str(exc),
+ },
+ sort_keys=True,
+ ),
+ file=sys.stderr,
+ )
+ raise SystemExit(1) from None
diff --git a/scripts/security/runtime_image_mirror_controller.py b/scripts/security/runtime_image_mirror_controller.py
index 6438bf040..4b441df17 100644
--- a/scripts/security/runtime_image_mirror_controller.py
+++ b/scripts/security/runtime_image_mirror_controller.py
@@ -6,6 +6,7 @@ from __future__ import annotations
import argparse
import hashlib
import json
+import math
import re
import shlex
import subprocess
@@ -41,6 +42,15 @@ class Workload:
container_kind: str
+@dataclass(frozen=True)
+class AvailabilityGuard:
+ minimum_ready_replicas: int
+ maximum_effective_unavailable: int
+ minimum_effective_surge: int
+ enforced_max_unavailable: int | None = None
+ enforced_max_surge: int | None = None
+
+
@dataclass(frozen=True)
class ImagePolicy:
asset_id: str
@@ -50,6 +60,7 @@ class ImagePolicy:
push_ref: str
runtime_ref: str
workload: Workload
+ availability_guard: AvailabilityGuard | None
@dataclass(frozen=True)
@@ -112,6 +123,19 @@ def _require_name(value: Any, label: str) -> str:
return name
+def _require_nonnegative_int(value: Any, label: str) -> int:
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
+ raise ControllerError(f"invalid_{label}")
+ return value
+
+
+def _require_positive_int(value: Any, label: str) -> int:
+ number = _require_nonnegative_int(value, label)
+ if number == 0:
+ raise ControllerError(f"invalid_{label}")
+ return number
+
+
def _policy_checksum(payload: dict[str, Any]) -> str:
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return "sha256:" + hashlib.sha256(canonical).hexdigest()
@@ -201,6 +225,54 @@ def load_policy(path: Path) -> Policy:
if workload_key in workloads:
raise ControllerError("duplicate_workload_target")
workloads.add(workload_key)
+ availability_guard = None
+ guard_row = row.get("availability_guard")
+ if guard_row is not None:
+ if not isinstance(guard_row, dict):
+ raise ControllerError("availability_guard_invalid")
+ if workload.kind != "deployment":
+ raise ControllerError("availability_guard_requires_deployment")
+ minimum_ready_replicas = _require_positive_int(
+ guard_row.get("minimum_ready_replicas"),
+ "minimum_ready_replicas",
+ )
+ maximum_effective_unavailable = _require_nonnegative_int(
+ guard_row.get("maximum_effective_unavailable"),
+ "maximum_effective_unavailable",
+ )
+ minimum_effective_surge = _require_positive_int(
+ guard_row.get("minimum_effective_surge"),
+ "minimum_effective_surge",
+ )
+ enforced_max_unavailable = None
+ enforced_max_surge = None
+ strategy_row = guard_row.get("enforced_strategy")
+ if strategy_row is not None:
+ if not isinstance(strategy_row, dict):
+ raise ControllerError("availability_enforced_strategy_invalid")
+ enforced_max_unavailable = _require_nonnegative_int(
+ strategy_row.get("max_unavailable"),
+ "enforced_max_unavailable",
+ )
+ enforced_max_surge = _require_positive_int(
+ strategy_row.get("max_surge"),
+ "enforced_max_surge",
+ )
+ if enforced_max_unavailable > maximum_effective_unavailable:
+ raise ControllerError(
+ "availability_enforced_unavailable_exceeds_guard"
+ )
+ if enforced_max_surge < minimum_effective_surge:
+ raise ControllerError(
+ "availability_enforced_surge_below_guard"
+ )
+ availability_guard = AvailabilityGuard(
+ minimum_ready_replicas=minimum_ready_replicas,
+ maximum_effective_unavailable=maximum_effective_unavailable,
+ minimum_effective_surge=minimum_effective_surge,
+ enforced_max_unavailable=enforced_max_unavailable,
+ enforced_max_surge=enforced_max_surge,
+ )
images.append(
ImagePolicy(
asset_id=asset_id,
@@ -210,6 +282,7 @@ def load_policy(path: Path) -> Policy:
push_ref=push_ref,
runtime_ref=runtime_ref,
workload=workload,
+ availability_guard=availability_guard,
)
)
@@ -372,6 +445,7 @@ class RuntimeCache:
self, policy: Policy | StagingPolicy, ssh_key: Path, known_hosts: Path
) -> None:
self.policy = policy
+ self._verified_sources: dict[tuple[str, str], str] = {}
self.ssh = [
"ssh",
"-i",
@@ -403,6 +477,10 @@ class RuntimeCache:
return sorted(set(matches))[0]
def verify_source(self, image: ImagePolicy | StagingImage) -> str:
+ cache_key = (image.source_index_digest, image.platform_digest)
+ cached = self._verified_sources.get(cache_key)
+ if cached is not None:
+ return cached
source_ref = self.source_ref(image.source_index_digest)
check = _run(self._command("images", "check", f"name=={source_ref}"))
if "complete" not in (check.stdout or ""):
@@ -423,6 +501,7 @@ class RuntimeCache:
]
if selected != [image.platform_digest]:
raise ControllerError("runtime_cache_platform_digest_mismatch")
+ self._verified_sources[cache_key] = source_ref
return source_ref
def source_config_digest(self, platform_digest: str) -> str:
@@ -524,9 +603,21 @@ def _registry_descriptor(image_ref: str) -> RegistryDescriptor | None:
)
-def _local_config_digest(image_ref: str) -> str:
- output = _run(["docker", "image", "inspect", "--format={{.Id}}", image_ref]).stdout
- return _require_digest((output or "").strip(), "local_config_digest")
+def _local_platform_config_digest(image_ref: str, platform: str) -> str:
+ output = _run(
+ [
+ "docker",
+ "image",
+ "inspect",
+ "--platform",
+ platform,
+ "--format={{.Id}}",
+ image_ref,
+ ]
+ ).stdout
+ return _require_digest(
+ (output or "").strip(), "local_platform_config_digest"
+ )
def _local_image_present(image_ref: str) -> bool:
@@ -596,14 +687,30 @@ def mirror_images(
if not _archive_contains_digest(archive, image.platform_digest):
raise ControllerError("runtime_cache_export_digest_missing")
_run(
- ["docker", "load", "--input", str(archive)],
+ [
+ "docker",
+ "load",
+ "--platform",
+ policy.platform,
+ "--input",
+ str(archive),
+ ],
quiet=True,
)
_run(
["docker", "tag", source_ref, image.push_ref],
quiet=True,
)
- _run(["docker", "push", image.push_ref], quiet=True)
+ _run(
+ [
+ "docker",
+ "push",
+ "--platform",
+ policy.platform,
+ image.push_ref,
+ ],
+ quiet=True,
+ )
finally:
_remove_local_image(image.push_ref)
if not source_preexisting:
@@ -684,17 +791,42 @@ def stage_images(
cache.export(source_ref, archive)
if not _archive_contains_digest(archive, image.platform_digest):
raise ControllerError("runtime_cache_export_digest_missing")
+ if not _archive_contains_digest(archive, source_config_digest):
+ raise ControllerError(
+ "runtime_cache_export_config_digest_missing"
+ )
_run(
- ["docker", "load", "--input", str(archive)],
+ [
+ "docker",
+ "load",
+ "--platform",
+ policy.platform,
+ "--input",
+ str(archive),
+ ],
quiet=True,
)
- if _local_config_digest(source_ref) != source_config_digest:
+ if (
+ _local_platform_config_digest(
+ source_ref, policy.platform
+ )
+ != source_config_digest
+ ):
raise ControllerError("local_image_config_digest_mismatch")
_run(
["docker", "tag", source_ref, image.push_ref],
quiet=True,
)
- _run(["docker", "push", image.push_ref], quiet=True)
+ _run(
+ [
+ "docker",
+ "push",
+ "--platform",
+ policy.platform,
+ image.push_ref,
+ ],
+ quiet=True,
+ )
finally:
_remove_local_image(image.push_ref)
if not source_preexisting:
@@ -840,20 +972,144 @@ class Kubernetes:
raise ControllerError("workload_container_not_found")
return values[0]
- def patch_image(self, item: ImagePolicy, image_ref: str, *, dry_run: bool) -> None:
- container_field = self._spec_container_field(item)
- patch = json.dumps(
- {
- "spec": {
- "template": {
- "spec": {
- container_field: [
- {"name": item.workload.container, "image": image_ref}
- ]
- }
- }
- }
+ def current_strategy(self, item: ImagePolicy) -> dict[str, Any]:
+ strategy = self.workload(item).get("spec", {}).get("strategy")
+ if not isinstance(strategy, dict):
+ raise ControllerError("workload_strategy_missing")
+ return strategy
+
+ @staticmethod
+ def enforced_strategy(item: ImagePolicy) -> dict[str, Any] | None:
+ guard = item.availability_guard
+ if (
+ guard is None
+ or guard.enforced_max_unavailable is None
+ or guard.enforced_max_surge is None
+ ):
+ return None
+ return {
+ "type": "RollingUpdate",
+ "rollingUpdate": {
+ "maxUnavailable": guard.enforced_max_unavailable,
+ "maxSurge": guard.enforced_max_surge,
},
+ }
+
+ @staticmethod
+ def _effective_replicas(
+ value: Any, replicas: int, *, round_up: bool, label: str
+ ) -> int:
+ if isinstance(value, bool):
+ raise ControllerError(f"{label}_invalid")
+ if isinstance(value, int) and value >= 0:
+ return value
+ if isinstance(value, str) and value.endswith("%"):
+ try:
+ percent = int(value[:-1])
+ except ValueError as exc:
+ raise ControllerError(f"{label}_invalid") from exc
+ if percent < 0 or percent > 100:
+ raise ControllerError(f"{label}_invalid")
+ scaled = replicas * percent / 100
+ return math.ceil(scaled) if round_up else math.floor(scaled)
+ raise ControllerError(f"{label}_invalid")
+
+ def verify_availability(
+ self,
+ item: ImagePolicy,
+ *,
+ strategy_override: dict[str, Any] | None = None,
+ ) -> None:
+ guard = item.availability_guard
+ if guard is None:
+ return
+ workload = self.workload(item)
+ metadata = workload.get("metadata", {})
+ spec = workload.get("spec", {})
+ status = workload.get("status", {})
+ generation = metadata.get("generation")
+ observed_generation = status.get("observedGeneration")
+ replicas = spec.get("replicas", 1)
+ if (
+ isinstance(replicas, bool)
+ or not isinstance(replicas, int)
+ or replicas < guard.minimum_ready_replicas
+ ):
+ raise ControllerError("availability_replicas_insufficient")
+ if (
+ isinstance(generation, bool)
+ or not isinstance(generation, int)
+ or isinstance(observed_generation, bool)
+ or not isinstance(observed_generation, int)
+ or observed_generation < generation
+ ):
+ raise ControllerError("availability_generation_not_observed")
+ ready = status.get("readyReplicas", 0)
+ available = status.get("availableReplicas", 0)
+ unavailable = status.get("unavailableReplicas", 0)
+ if (
+ isinstance(ready, bool)
+ or not isinstance(ready, int)
+ or isinstance(available, bool)
+ or not isinstance(available, int)
+ or ready < guard.minimum_ready_replicas
+ or available < guard.minimum_ready_replicas
+ ):
+ raise ControllerError("availability_ready_replicas_insufficient")
+ if (
+ isinstance(unavailable, bool)
+ or not isinstance(unavailable, int)
+ or unavailable > guard.maximum_effective_unavailable
+ ):
+ raise ControllerError("availability_unavailable_replicas_exceeded")
+
+ strategy = strategy_override if strategy_override is not None else spec.get(
+ "strategy", {}
+ )
+ if not isinstance(strategy, dict) or strategy.get("type") != "RollingUpdate":
+ raise ControllerError("availability_rolling_update_required")
+ rolling = strategy.get("rollingUpdate", {})
+ if not isinstance(rolling, dict):
+ raise ControllerError("availability_rolling_update_invalid")
+ effective_unavailable = self._effective_replicas(
+ rolling.get("maxUnavailable", "25%"),
+ replicas,
+ round_up=False,
+ label="availability_max_unavailable",
+ )
+ effective_surge = self._effective_replicas(
+ rolling.get("maxSurge", "25%"),
+ replicas,
+ round_up=True,
+ label="availability_max_surge",
+ )
+ if effective_unavailable > guard.maximum_effective_unavailable:
+ raise ControllerError("availability_effective_unavailable_exceeded")
+ if effective_surge < guard.minimum_effective_surge:
+ raise ControllerError("availability_effective_surge_insufficient")
+
+ def patch_image(
+ self,
+ item: ImagePolicy,
+ image_ref: str,
+ *,
+ dry_run: bool,
+ strategy: dict[str, Any] | None = None,
+ ) -> None:
+ container_field = self._spec_container_field(item)
+ spec_patch: dict[str, Any] = {
+ "template": {
+ "spec": {
+ container_field: [
+ {"name": item.workload.container, "image": image_ref}
+ ]
+ }
+ }
+ }
+ if strategy is not None:
+ spec_patch["strategy"] = strategy
+ patch = json.dumps(
+ {"spec": spec_patch},
separators=(",", ":"),
)
args = [
@@ -991,37 +1247,102 @@ def apply_policy(
mirror_receipt = _read_mirror_receipt(mirror_receipt_path, policy, trace_id)
kubernetes = Kubernetes(use_sudo=use_sudo, kubeconfig=kubeconfig, server=server)
previous: dict[str, str] = {}
+ previous_strategies: dict[str, dict[str, Any] | None] = {}
+ desired_strategies: dict[str, dict[str, Any] | None] = {}
+ strategy_diffs: dict[str, bool] = {}
+ requires_changes: dict[str, bool] = {}
changed: list[ImagePolicy] = []
+ failure_reason_code: str | None = None
+ rollback_performed = False
+ rollback_verified = False
+ rollback_verified_workload_count = 0
for item in policy.images:
previous[item.asset_id] = kubernetes.current_image(item)
- kubernetes.patch_image(item, item.runtime_ref, dry_run=True)
+ desired_strategy = kubernetes.enforced_strategy(item)
+ desired_strategies[item.asset_id] = desired_strategy
+ previous_strategy = (
+ kubernetes.current_strategy(item) if desired_strategy is not None else None
+ )
+ previous_strategies[item.asset_id] = previous_strategy
+ strategy_diffs[item.asset_id] = (
+ desired_strategy is not None and previous_strategy != desired_strategy
+ )
+ requires_changes[item.asset_id] = (
+ previous[item.asset_id] != item.runtime_ref
+ or strategy_diffs[item.asset_id]
+ )
+ kubernetes.verify_availability(
+ item,
+ strategy_override=desired_strategy,
+ )
+ kubernetes.patch_image(
+ item,
+ item.runtime_ref,
+ dry_run=True,
+ strategy=desired_strategy,
+ )
if apply:
try:
for item in policy.images:
- if previous[item.asset_id] == item.runtime_ref:
+ if not requires_changes[item.asset_id]:
continue
- kubernetes.patch_image(item, item.runtime_ref, dry_run=False)
+ kubernetes.patch_image(
+ item,
+ item.runtime_ref,
+ dry_run=False,
+ strategy=desired_strategies[item.asset_id],
+ )
changed.append(item)
for item in policy.images:
kubernetes.rollout(item)
- except ControllerError:
+ kubernetes.verify_availability(item)
+ for item in policy.images:
+ if kubernetes.current_image(item) != item.runtime_ref:
+ raise ControllerError("workload_image_not_applied")
+ kubernetes.verify_pods(item)
+ except ControllerError as exc:
+ failure_reason_code = str(exc)
+ rollback_performed = bool(changed)
for item in reversed(changed):
try:
- kubernetes.patch_image(item, previous[item.asset_id], dry_run=False)
+ kubernetes.patch_image(
+ item,
+ previous[item.asset_id],
+ dry_run=False,
+ strategy=previous_strategies[item.asset_id],
+ )
kubernetes.rollout(item)
+ image_restored = (
+ kubernetes.current_image(item) == previous[item.asset_id]
+ )
+ strategy_restored = (
+ previous_strategies[item.asset_id] is None
+ or kubernetes.current_strategy(item)
+ == previous_strategies[item.asset_id]
+ )
except ControllerError:
- pass
- raise
+ continue
+ if image_restored and strategy_restored:
+ rollback_verified_workload_count += 1
+ rollback_verified = (
+ not changed or rollback_verified_workload_count == len(changed)
+ )
pod_count = 0
compliant_count = 0
for item in policy.images:
- if kubernetes.current_image(item) == item.runtime_ref:
- compliant_count += 1
- pod_count += kubernetes.verify_pods(item)
+ try:
+ if kubernetes.current_image(item) == item.runtime_ref:
+ compliant_count += 1
+ pod_count += kubernetes.verify_pods(item)
+ except ControllerError as exc:
+ if failure_reason_code is None:
+ failure_reason_code = str(exc)
- verified = compliant_count == len(policy.images)
+ verified = (
+ failure_reason_code is None and compliant_count == len(policy.images)
+ )
receipt = {
"schema_version": RECEIPT_SCHEMA_VERSION,
"trace_id": trace_id,
@@ -1033,22 +1354,41 @@ def apply_policy(
"sensor_receipt_present": True,
"normalized_asset_count": len(policy.images),
"source_of_truth_diff_count": sum(
- 1 for item in policy.images if previous[item.asset_id] != item.runtime_ref
+ 1 for item in policy.images if requires_changes[item.asset_id]
),
"ai_candidate_action": "replace_forbidden_registry_with_internal_digest",
"policy_decision": "controlled_apply_allowed",
"server_dry_run_passed": True,
+ "availability_guard_count": sum(
+ 1 for item in policy.images if item.availability_guard is not None
+ ),
+ "availability_guard_verified_count": sum(
+ 1 for item in policy.images if item.availability_guard is not None
+ ),
+ "availability_strategy_diff_count": sum(strategy_diffs.values()),
+ "availability_strategy_changed_count": sum(
+ 1 for item in changed if strategy_diffs[item.asset_id]
+ ),
+ "availability_strategy_verified_count": (
+ sum(1 for value in desired_strategies.values() if value is not None)
+ if apply and verified
+ else 0
+ ),
"execution_changed_count": len(changed),
"independent_verifier_passed": verified,
"verified_workload_count": compliant_count,
"verified_pod_count": pod_count,
- "rollback_performed": False,
+ "rollback_performed": rollback_performed,
+ "rollback_verified": rollback_verified,
+ "rollback_verified_workload_count": rollback_verified_workload_count,
+ "no_write_terminal": failure_reason_code is not None and not changed,
+ "failure_reason_code": failure_reason_code,
"external_pull_allowed": False,
"mirror_stage_verified": mirror_receipt.get("state") == "verified",
"durable_writeback": "kubernetes_configmap",
"state": "verified" if verified else "blocked_with_safe_next_action",
}
- if apply and verified:
+ if apply:
kubernetes.write_receipt(receipt)
return receipt
diff --git a/scripts/security/tests/test_external_mcp_artifact_controller.py b/scripts/security/tests/test_external_mcp_artifact_controller.py
new file mode 100644
index 000000000..cfce6178c
--- /dev/null
+++ b/scripts/security/tests/test_external_mcp_artifact_controller.py
@@ -0,0 +1,393 @@
+from __future__ import annotations
+
+import base64
+import importlib.util
+import io
+import json
+import sys
+import tarfile
+import tempfile
+import unittest
+from contextlib import redirect_stdout
+from pathlib import Path
+from unittest.mock import patch
+
+
+ROOT = Path(__file__).resolve().parents[3]
+CONTROLLER_PATH = ROOT / "scripts/security/external_mcp_artifact_controller.py"
+POLICY_PATH = ROOT / "config/mcp/playwright-mcp-artifact-policy.json"
+WORKFLOW_PATH = ROOT / ".gitea/workflows/mcp-external-artifact-mirror.yaml"
+RUN_ID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
+TRACE_ID = f"mcp-artifact-{RUN_ID}"
+
+
+def _load_controller():
+ spec = importlib.util.spec_from_file_location(
+ "external_mcp_artifact_controller", CONTROLLER_PATH
+ )
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+controller = _load_controller()
+
+
+def _verified_packages(policy):
+ return tuple(
+ controller.VerifiedPackage(
+ policy=package,
+ tarball=b"tarball",
+ attestation=b"{}",
+ metadata_sha256="a" * 64,
+ tarball_sha512_hex=package.sha512_hex,
+ tarball_sha1=package.shasum_sha1,
+ attestation_sha256="b" * 64,
+ unpacked_bytes=1024,
+ archive_file_count=3,
+ license_file_count=1,
+ osv_response_sha256="c" * 64,
+ vulnerability_ids=(),
+ )
+ for package in policy.packages
+ )
+
+
+def _tarball(
+ package,
+ *,
+ unsafe_symlink: bool = False,
+ duplicate_manifest: bool = False,
+) -> bytes:
+ package_json = json.dumps(
+ {
+ "name": package.name,
+ "version": package.version,
+ "license": package.license,
+ "dependencies": package.dependencies,
+ "optionalDependencies": package.optional_dependencies,
+ }
+ ).encode()
+ output = io.BytesIO()
+ with tarfile.open(fileobj=output, mode="w:gz") as archive:
+ for name, data in (
+ ("package/package.json", package_json),
+ ("package/LICENSE", b"Apache License 2.0"),
+ ):
+ member = tarfile.TarInfo(name)
+ member.size = len(data)
+ archive.addfile(member, io.BytesIO(data))
+ if duplicate_manifest:
+ member = tarfile.TarInfo("package/package.json")
+ member.size = len(package_json)
+ archive.addfile(member, io.BytesIO(package_json))
+ if unsafe_symlink:
+ member = tarfile.TarInfo("package/escape")
+ member.type = tarfile.SYMTYPE
+ member.linkname = "../../outside"
+ archive.addfile(member)
+ return output.getvalue()
+
+
+class ExternalMcpArtifactControllerTest(unittest.TestCase):
+ def test_policy_is_exact_discovery_only_and_fail_closed(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ raw = POLICY_PATH.read_text(encoding="utf-8").lower()
+
+ self.assertEqual(policy.candidate_id, "external.playwright-verifier")
+ self.assertEqual(policy.risk_level, "high")
+ self.assertEqual(len(policy.packages), 3)
+ self.assertEqual(policy.mirror_tag, "0.0.78-bundle-v1")
+ self.assertNotIn("@latest", raw)
+ self.assertNotIn(":latest", raw)
+ self.assertNotIn("npx -y", raw)
+ self.assertNotIn("github.com", raw)
+ self.assertNotIn("ghcr.io", raw)
+ self.assertTrue(all(package.version for package in policy.packages))
+ self.assertTrue(all(package.sha512_hex for package in policy.packages))
+
+ def test_gitea_workflow_has_no_remote_action_or_floating_supply_chain(self) -> None:
+ raw = WORKFLOW_PATH.read_text(encoding="utf-8")
+ lowered = raw.lower()
+
+ self.assertNotIn("uses:", lowered)
+ self.assertNotIn("actions/checkout", lowered)
+ self.assertNotIn("github.com", lowered)
+ self.assertNotIn("api.github.com", lowered)
+ self.assertNotIn("raw.githubusercontent.com", lowered)
+ self.assertNotIn("codeload.github.com", lowered)
+ self.assertNotIn("ghcr.io", lowered)
+ self.assertNotIn("npx -y", lowered)
+ self.assertNotIn("@latest", lowered)
+ self.assertNotIn(":latest", lowered)
+ self.assertNotIn("git push --force", lowered)
+ self.assertIn("runs-on: awoooi-non110-host", raw)
+ self.assertIn("docker login", raw)
+ self.assertIn("--password-stdin", raw)
+ self.assertIn("docker logout", raw)
+ self.assertIn("verify_external_mcp_artifact_receipt.py", raw)
+ self.assertIn("git merge --no-edit gitea/main", raw)
+ self.assertIn("git push gitea HEAD:main", raw)
+ self.assertIn('cron: "13 2 * * 3"', raw)
+
+ def test_dependency_closure_rejects_drift(self) -> None:
+ payload = json.loads(POLICY_PATH.read_text(encoding="utf-8"))
+ payload["packages"][0]["dependencies"]["playwright"] = "1.2.3"
+ with tempfile.TemporaryDirectory() as temp:
+ path = Path(temp) / "policy.json"
+ path.write_text(json.dumps(payload), encoding="utf-8")
+ with self.assertRaisesRegex(
+ controller.ControllerError, "dependency_closure_invalid"
+ ):
+ controller.load_policy(path)
+
+ def test_run_and_trace_identity_must_match(self) -> None:
+ with self.assertRaisesRegex(
+ controller.ControllerError, "run_trace_identity_mismatch"
+ ):
+ controller.main(
+ [
+ "--policy",
+ str(POLICY_PATH),
+ "--run-id",
+ RUN_ID,
+ "--trace-id",
+ "mcp-artifact-00000000-0000-4000-8000-000000000000",
+ "check",
+ ]
+ )
+
+ def test_noncanonical_run_id_is_rejected(self) -> None:
+ with self.assertRaisesRegex(controller.ControllerError, "run_id_not_canonical"):
+ controller.main(
+ [
+ "--policy",
+ str(POLICY_PATH),
+ "--run-id",
+ RUN_ID.upper(),
+ "--trace-id",
+ TRACE_ID,
+ "check",
+ ]
+ )
+
+ def test_check_mode_emits_bound_no_write_receipt(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ verified = _verified_packages(policy)
+ with (
+ patch.object(controller, "verify_upstream_bundle", return_value=verified),
+ patch.object(controller, "build_cyclonedx_sbom", return_value={}),
+ patch.object(
+ controller,
+ "_write_bundle",
+ return_value=("d" * 64, "e" * 64),
+ ),
+ io.StringIO() as output,
+ redirect_stdout(output),
+ ):
+ result = controller.main(
+ [
+ "--policy",
+ str(POLICY_PATH),
+ "--run-id",
+ RUN_ID,
+ "--trace-id",
+ TRACE_ID,
+ "check",
+ ]
+ )
+ receipt = json.loads(output.getvalue())
+
+ self.assertEqual(result, 0)
+ self.assertEqual(receipt["run_id"], RUN_ID)
+ self.assertEqual(receipt["trace_id"], TRACE_ID)
+ self.assertEqual(
+ receipt["status"], "completed_check_mode_pending_independent_verifier"
+ )
+ self.assertFalse(receipt["promotion_allowed"])
+ self.assertFalse(receipt["canary_allowed"])
+ self.assertFalse(
+ receipt["stage_receipts"]["sensor_source_receipt"]
+ ["runtime_request_or_response_body_stored"]
+ )
+ self.assertTrue(
+ receipt["stage_receipts"]["sensor_source_receipt"]
+ ["upstream_supply_chain_attestation_bundled"]
+ )
+ self.assertEqual(
+ receipt["stage_receipts"]["rollback_or_no_write_terminal"]["status"],
+ "no_write_pending_external_verifier",
+ )
+ self.assertEqual(
+ receipt["stage_receipts"]["independent_post_verifier"]["status"],
+ "pending_external_verifier",
+ )
+
+ def test_mirror_apply_requires_receipt_path(self) -> None:
+ with self.assertRaisesRegex(
+ controller.ControllerError, "apply_receipt_path_required"
+ ):
+ controller.main(
+ [
+ "--policy",
+ str(POLICY_PATH),
+ "--run-id",
+ RUN_ID,
+ "--trace-id",
+ TRACE_ID,
+ "mirror",
+ "--apply",
+ ]
+ )
+
+ def test_tarball_inspection_accepts_exact_manifest(self) -> None:
+ package = controller.load_policy(POLICY_PATH).packages[0]
+
+ unpacked, files, licenses = controller.inspect_tarball(
+ package,
+ _tarball(package),
+ maximum_unpacked_bytes=1024 * 1024,
+ )
+
+ self.assertGreater(unpacked, 0)
+ self.assertEqual(files, 2)
+ self.assertEqual(licenses, 1)
+
+ def test_tarball_inspection_rejects_symlink(self) -> None:
+ package = controller.load_policy(POLICY_PATH).packages[0]
+ with self.assertRaisesRegex(
+ controller.ControllerError, "package_archive_unsafe_entry"
+ ):
+ controller.inspect_tarball(
+ package,
+ _tarball(package, unsafe_symlink=True),
+ maximum_unpacked_bytes=1024 * 1024,
+ )
+
+ def test_tarball_inspection_rejects_duplicate_manifest(self) -> None:
+ package = controller.load_policy(POLICY_PATH).packages[0]
+ with self.assertRaisesRegex(
+ controller.ControllerError,
+ "package_archive_duplicate_or_invalid_file",
+ ):
+ controller.inspect_tarball(
+ package,
+ _tarball(package, duplicate_manifest=True),
+ maximum_unpacked_bytes=1024 * 1024,
+ )
+
+ def test_slsa_subject_must_match_exact_tarball_digest(self) -> None:
+ package = controller.load_policy(POLICY_PATH).packages[0]
+ statement = {
+ "_type": controller.IN_TOTO_STATEMENT_TYPE,
+ "predicateType": controller.SLSA_PREDICATE_TYPE,
+ "subject": [
+ {
+ "name": package.slsa_subject_name,
+ "digest": {"sha512": package.slsa_subject_sha512_hex},
+ }
+ ],
+ }
+ payload = {
+ "attestations": [
+ {
+ "predicateType": controller.SLSA_PREDICATE_TYPE,
+ "bundle": {
+ "dsseEnvelope": {
+ "payload": base64.b64encode(
+ json.dumps(statement).encode()
+ ).decode()
+ }
+ },
+ }
+ ]
+ }
+
+ controller.validate_slsa_attestation(package, payload)
+ bad_statement = dict(statement)
+ bad_statement["subject"] = [
+ {
+ "name": package.slsa_subject_name,
+ "digest": {"sha512": "0" * 128},
+ }
+ ]
+ payload["attestations"][0]["bundle"]["dsseEnvelope"]["payload"] = (
+ base64.b64encode(json.dumps(bad_statement).encode()).decode()
+ )
+ with self.assertRaisesRegex(controller.ControllerError, "slsa_subject_mismatch"):
+ controller.validate_slsa_attestation(package, payload)
+
+ def test_sbom_has_exact_npm_purls_and_dependency_edges(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+
+ sbom = controller.build_cyclonedx_sbom(
+ policy,
+ _verified_packages(policy),
+ )
+
+ refs = {row["bom-ref"] for row in sbom["components"]}
+ self.assertIn("pkg:npm/%40playwright/mcp@0.0.78", refs)
+ self.assertNotIn("pkg:npm/%40playwright%2Fmcp@0.0.78", refs)
+ root = next(
+ row
+ for row in sbom["dependencies"]
+ if row["ref"] == "pkg:npm/%40playwright/mcp@0.0.78"
+ )
+ self.assertEqual(len(root["dependsOn"]), 2)
+
+ def test_osv_records_fail_closed(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ package = policy.packages[0]
+
+ class OsvClient:
+ def post_json(self, *_args, **_kwargs):
+ return b'{"vulns":[{"id":"OSV-TEST-1"}]}'
+
+ response_hash, vulnerabilities = controller._query_osv(
+ package,
+ OsvClient(),
+ policy.maximum_metadata_bytes,
+ )
+ self.assertEqual(len(response_hash), 64)
+ self.assertEqual(vulnerabilities, ("OSV-TEST-1",))
+
+ def test_mirror_receipt_never_promotes_runtime(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ verified = _verified_packages(policy)
+
+ receipt = controller.build_receipt(
+ policy=policy,
+ verified=verified,
+ sbom_sha256="a" * 64,
+ bundle_manifest_sha256="b" * 64,
+ run_id=RUN_ID,
+ trace_id=TRACE_ID,
+ mode="apply",
+ internal_mirror_ref=(
+ "registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp@sha256:"
+ + "c" * 64
+ ),
+ runtime_mirror_ref=(
+ "192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp@sha256:"
+ + "c" * 64
+ ),
+ )
+
+ self.assertEqual(
+ receipt["status"],
+ "completed_internal_mirror_pending_independent_verifier",
+ )
+ self.assertFalse(receipt["promotion_allowed"])
+ self.assertFalse(receipt["replay_allowed"])
+ self.assertFalse(receipt["shadow_allowed"])
+ self.assertFalse(receipt["canary_allowed"])
+ self.assertFalse(
+ receipt["stage_receipts"]["bounded_execution"]
+ ["production_route_changed"]
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/security/tests/test_runtime_image_mirror_controller.py b/scripts/security/tests/test_runtime_image_mirror_controller.py
index d0b3f742e..8107e2496 100644
--- a/scripts/security/tests/test_runtime_image_mirror_controller.py
+++ b/scripts/security/tests/test_runtime_image_mirror_controller.py
@@ -46,13 +46,42 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
policy = controller.load_staging_policy(STAGING_POLICY_PATH)
return replace(policy, images=(policy.images[0],))
+ def _image_with_availability_guard(self):
+ image = controller.load_policy(POLICY_PATH).images[0]
+ return replace(
+ image,
+ availability_guard=controller.AvailabilityGuard(
+ minimum_ready_replicas=1,
+ maximum_effective_unavailable=0,
+ minimum_effective_surge=1,
+ ),
+ )
+
+ @staticmethod
+ def _write_verified_mirror_receipt(
+ path: Path, policy, trace_id: str
+ ) -> None:
+ path.write_text(
+ json.dumps(
+ {
+ "schema_version": controller.RECEIPT_SCHEMA_VERSION,
+ "trace_id": trace_id,
+ "work_item_id": policy.work_item_id,
+ "policy_checksum": policy.checksum,
+ "state": "verified",
+ "verified_count": len(policy.images),
+ }
+ ),
+ encoding="utf-8",
+ )
+
def test_policy_is_internal_digest_pinned_and_runtime_cache_only(self) -> None:
policy = controller.load_policy(POLICY_PATH)
raw = POLICY_PATH.read_text(encoding="utf-8")
self.assertEqual(policy.work_item_id, "AIA-P0-006-02A")
self.assertEqual(policy.risk_level, "high")
- self.assertEqual(len(policy.images), 14)
+ self.assertEqual(len(policy.images), 17)
self.assertNotIn("github.com", raw)
self.assertNotIn("ghcr.io", raw)
self.assertIn('"external_pull_allowed": false', raw)
@@ -77,9 +106,76 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
)
)
container_kinds = [image.workload.container_kind for image in policy.images]
- self.assertEqual(container_kinds.count("container"), 11)
+ self.assertEqual(container_kinds.count("container"), 14)
self.assertEqual(container_kinds.count("init_container"), 3)
+ vpa = next(
+ image
+ for image in policy.images
+ if image.asset_id == "runtime-image:vpa-recommender"
+ )
+ self.assertEqual(
+ vpa.target_registry_digest,
+ "sha256:6ae9b8c6ac256fe034f4e2741c2fef2c2ba2d64778c071d6d93c99cd86ba7c89",
+ )
+ self.assertEqual(vpa.workload.namespace, "kube-system")
+ self.assertEqual(vpa.workload.name, "vpa-recommender")
+ self.assertEqual(vpa.workload.container, "recommender")
+ self.assertEqual(
+ vpa.availability_guard,
+ controller.AvailabilityGuard(
+ minimum_ready_replicas=1,
+ maximum_effective_unavailable=0,
+ minimum_effective_surge=1,
+ ),
+ )
+
+ metrics = next(
+ image
+ for image in policy.images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+ self.assertEqual(
+ metrics.target_registry_digest,
+ "sha256:4a4376379ab80d3e1fd5edbf8c3ddcc0b9be8ccb56167e4f8c9423e5e11b0cfe",
+ )
+ self.assertEqual(metrics.workload.namespace, "kube-system")
+ self.assertEqual(metrics.workload.name, "metrics-server")
+ self.assertEqual(metrics.workload.container, "metrics-server")
+ self.assertEqual(
+ metrics.availability_guard,
+ controller.AvailabilityGuard(
+ minimum_ready_replicas=1,
+ maximum_effective_unavailable=0,
+ minimum_effective_surge=1,
+ enforced_max_unavailable=0,
+ enforced_max_surge=1,
+ ),
+ )
+
+ local_path = next(
+ image
+ for image in policy.images
+ if image.asset_id == "runtime-image:local-path-provisioner"
+ )
+ self.assertEqual(
+ local_path.target_registry_digest,
+ "sha256:8ea3128d48a78b376d094366101d54180cd5293839cabd8568b1dce064413619",
+ )
+ self.assertEqual(local_path.workload.namespace, "kube-system")
+ self.assertEqual(local_path.workload.name, "local-path-provisioner")
+ self.assertEqual(local_path.workload.container, "local-path-provisioner")
+ self.assertEqual(
+ local_path.availability_guard,
+ controller.AvailabilityGuard(
+ minimum_ready_replicas=1,
+ maximum_effective_unavailable=0,
+ minimum_effective_surge=1,
+ enforced_max_unavailable=0,
+ enforced_max_surge=1,
+ ),
+ )
+
def test_policy_loads_and_validates_init_container_kind(self) -> None:
image = self._image_with_container_kind("init_container")
@@ -90,6 +186,50 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
):
self._image_with_container_kind("ephemeral_container")
+ def test_availability_guard_cannot_be_configured_as_noop(self) -> None:
+ payload = json.loads(POLICY_PATH.read_text(encoding="utf-8"))
+ vpa = next(
+ image
+ for image in payload["images"]
+ if image["asset_id"] == "runtime-image:vpa-recommender"
+ )
+ metrics = next(
+ image
+ for image in payload["images"]
+ if image["asset_id"] == "runtime-image:metrics-server"
+ )
+ with tempfile.TemporaryDirectory() as temp:
+ path = Path(temp) / "policy.json"
+ for field in ("minimum_ready_replicas", "minimum_effective_surge"):
+ with self.subTest(field=field):
+ vpa["availability_guard"][field] = 0
+ path.write_text(json.dumps(payload), encoding="utf-8")
+ with self.assertRaisesRegex(
+ controller.ControllerError, f"invalid_{field}"
+ ):
+ controller.load_policy(path)
+ vpa["availability_guard"][field] = 1
+
+ invalid_strategies = (
+ (
+ "max_unavailable",
+ 1,
+ "availability_enforced_unavailable_exceeds_guard",
+ ),
+ ("max_surge", 0, "invalid_enforced_max_surge"),
+ )
+ for field, value, error_code in invalid_strategies:
+ with self.subTest(field=field):
+ strategy = metrics["availability_guard"]["enforced_strategy"]
+ original = strategy[field]
+ strategy[field] = value
+ path.write_text(json.dumps(payload), encoding="utf-8")
+ with self.assertRaisesRegex(
+ controller.ControllerError, error_code
+ ):
+ controller.load_policy(path)
+ strategy[field] = original
+
def test_staging_policy_is_runtime_cache_only_and_bounded_canary_scoped(
self,
) -> None:
@@ -98,13 +238,17 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
self.assertEqual(policy.work_item_id, "AIA-P0-006-02B")
self.assertEqual(policy.risk_level, "high")
- self.assertEqual(len(policy.images), 2)
+ self.assertEqual(len(policy.images), 6)
by_asset = {image.asset_id: image for image in policy.images}
self.assertEqual(
set(by_asset),
{
"runtime-image:argocd-v3.3.6-canary",
+ "runtime-image:local-path-provisioner-0.0.34-canary",
+ "runtime-image:metrics-server-0.8.1-canary",
"runtime-image:redis-8.2.3-canary",
+ "runtime-image:sealed-secrets-controller-0.26.0-canary",
+ "runtime-image:vpa-recommender-1.6.0-canary",
},
)
argocd = by_asset["runtime-image:argocd-v3.3.6-canary"]
@@ -130,6 +274,94 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
self.assertEqual(redis.workload.name, "argocd-redis")
self.assertEqual(redis.workload.container, "redis")
self.assertEqual(redis.workload.container_kind, "container")
+
+ vpa = by_asset["runtime-image:vpa-recommender-1.6.0-canary"]
+ self.assertEqual(
+ vpa.source_index_digest,
+ "sha256:4af365370c187e4eb60302e937b18a188774698b049a1bfdd77d43e51b30abec",
+ )
+ self.assertEqual(
+ vpa.platform_digest,
+ "sha256:f7da718d281d5e880e966d0182ffb2160b82517c7bc3a38f8002b49e7a4bd0a1",
+ )
+ self.assertEqual(
+ vpa.source_config_digest,
+ "sha256:05526683ba80f70887ef5622c4ee7d78e1e0482d2a9e2f0e29ad92c32cb73819",
+ )
+ self.assertEqual(vpa.runtime_repository, "vpa-recommender")
+ self.assertEqual(vpa.workload.kind, "deployment")
+ self.assertEqual(vpa.workload.namespace, "kube-system")
+ self.assertEqual(vpa.workload.name, "vpa-recommender")
+ self.assertEqual(vpa.workload.container, "recommender")
+ self.assertEqual(vpa.workload.container_kind, "container")
+
+ metrics = by_asset["runtime-image:metrics-server-0.8.1-canary"]
+ self.assertEqual(
+ metrics.source_index_digest,
+ "sha256:b2d2efaf5ac3b366ed0f839d2412a2c4279d4fc2a2a733f12c52133faed36c41",
+ )
+ self.assertEqual(
+ metrics.platform_digest,
+ "sha256:6231fb0a1ffab76c92ab880f51a0d11b290f688373647bcedff85af025dfd8a9",
+ )
+ self.assertEqual(
+ metrics.source_config_digest,
+ "sha256:e76b3f3568b7f440dfd477c1d6de638d7769ba34c93eef999dee418eb72bc0e3",
+ )
+ self.assertEqual(metrics.runtime_repository, "metrics-server")
+ self.assertEqual(metrics.workload.kind, "deployment")
+ self.assertEqual(metrics.workload.namespace, "kube-system")
+ self.assertEqual(metrics.workload.name, "metrics-server")
+ self.assertEqual(metrics.workload.container, "metrics-server")
+ self.assertEqual(metrics.workload.container_kind, "container")
+
+ local_path = by_asset[
+ "runtime-image:local-path-provisioner-0.0.34-canary"
+ ]
+ self.assertEqual(
+ local_path.source_index_digest,
+ "sha256:6ff68ebe98bc623b45ad22c28be84f8a08214982710f3247d5862e9bccce73ef",
+ )
+ self.assertEqual(
+ local_path.platform_digest,
+ "sha256:90745b5457d61b84362ea2aeeb06caef4d576c99861eaff4c7bfe9b36e1cf8d7",
+ )
+ self.assertEqual(
+ local_path.source_config_digest,
+ "sha256:acccaf97bcb578daff51c6246e47babbca6e998e4225aa230544684cf147d6c1",
+ )
+ self.assertEqual(local_path.runtime_repository, "local-path-provisioner")
+ self.assertEqual(local_path.workload.kind, "deployment")
+ self.assertEqual(local_path.workload.namespace, "kube-system")
+ self.assertEqual(local_path.workload.name, "local-path-provisioner")
+ self.assertEqual(local_path.workload.container, "local-path-provisioner")
+ self.assertEqual(local_path.workload.container_kind, "container")
+
+ sealed_secrets = by_asset[
+ "runtime-image:sealed-secrets-controller-0.26.0-canary"
+ ]
+ self.assertEqual(
+ sealed_secrets.source_index_digest,
+ "sha256:74cd190f424b591dd82a234685c8c81d50b33af807e03470bc12b23d202e0106",
+ )
+ self.assertEqual(
+ sealed_secrets.platform_digest,
+ "sha256:1827b36853e124ac0dd2554f7cd55c04952ecb7c38ff19a4a8d93d633811ed68",
+ )
+ self.assertEqual(
+ sealed_secrets.source_config_digest,
+ "sha256:bbfc8314cb4978b252fb313b84e1d51a7b0c040befd79ecc903ea952485a3348",
+ )
+ self.assertEqual(
+ sealed_secrets.runtime_repository, "sealed-secrets-controller"
+ )
+ self.assertEqual(sealed_secrets.workload.kind, "deployment")
+ self.assertEqual(sealed_secrets.workload.namespace, "kube-system")
+ self.assertEqual(sealed_secrets.workload.name, "sealed-secrets-controller")
+ self.assertEqual(
+ sealed_secrets.workload.container, "sealed-secrets-controller"
+ )
+ self.assertEqual(sealed_secrets.workload.container_kind, "container")
self.assertNotIn("github.com", raw)
self.assertNotIn("ghcr.io", raw)
self.assertIn('"external_pull_allowed": false', raw)
@@ -155,6 +387,86 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
canary.runtime_ref.endswith("@" + canary.target_registry_digest)
)
+ def test_vpa_stable_policy_reuses_verified_staging_provenance(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ staging = next(
+ image
+ for image in controller.load_staging_policy(STAGING_POLICY_PATH).images
+ if image.asset_id == "runtime-image:vpa-recommender-1.6.0-canary"
+ )
+ stable = next(
+ image
+ for image in policy.images
+ if image.asset_id == "runtime-image:vpa-recommender"
+ )
+
+ self.assertEqual(stable.source_index_digest, staging.source_index_digest)
+ self.assertEqual(stable.platform_digest, staging.platform_digest)
+ self.assertEqual(stable.push_ref, staging.push_ref)
+ self.assertEqual(stable.workload, staging.workload)
+ self.assertEqual(
+ stable.target_registry_digest,
+ "sha256:6ae9b8c6ac256fe034f4e2741c2fef2c2ba2d64778c071d6d93c99cd86ba7c89",
+ )
+ self.assertTrue(
+ stable.runtime_ref.endswith("@" + stable.target_registry_digest)
+ )
+
+ def test_metrics_server_stable_policy_reuses_verified_staging_provenance(
+ self,
+ ) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ staging = next(
+ image
+ for image in controller.load_staging_policy(STAGING_POLICY_PATH).images
+ if image.asset_id == "runtime-image:metrics-server-0.8.1-canary"
+ )
+ stable = next(
+ image
+ for image in policy.images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+
+ self.assertEqual(stable.source_index_digest, staging.source_index_digest)
+ self.assertEqual(stable.platform_digest, staging.platform_digest)
+ self.assertEqual(stable.push_ref, staging.push_ref)
+ self.assertEqual(stable.workload, staging.workload)
+ self.assertEqual(
+ stable.target_registry_digest,
+ "sha256:4a4376379ab80d3e1fd5edbf8c3ddcc0b9be8ccb56167e4f8c9423e5e11b0cfe",
+ )
+ self.assertTrue(
+ stable.runtime_ref.endswith("@" + stable.target_registry_digest)
+ )
+
+ def test_local_path_stable_policy_reuses_verified_staging_provenance(
+ self,
+ ) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ staging = next(
+ image
+ for image in controller.load_staging_policy(STAGING_POLICY_PATH).images
+ if image.asset_id
+ == "runtime-image:local-path-provisioner-0.0.34-canary"
+ )
+ stable = next(
+ image
+ for image in policy.images
+ if image.asset_id == "runtime-image:local-path-provisioner"
+ )
+
+ self.assertEqual(stable.source_index_digest, staging.source_index_digest)
+ self.assertEqual(stable.platform_digest, staging.platform_digest)
+ self.assertEqual(stable.push_ref, staging.push_ref)
+ self.assertEqual(stable.workload, staging.workload)
+ self.assertEqual(
+ stable.target_registry_digest,
+ "sha256:8ea3128d48a78b376d094366101d54180cd5293839cabd8568b1dce064413619",
+ )
+ self.assertTrue(
+ stable.runtime_ref.endswith("@" + stable.target_registry_digest)
+ )
+
def test_argocd_controller_batch_reuses_verified_staging_artifact(self) -> None:
policy = controller.load_policy(POLICY_PATH)
staging = controller.load_staging_policy(STAGING_POLICY_PATH).images[0]
@@ -426,6 +738,28 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
)
)
+ def test_local_platform_config_digest_reads_image_id(self) -> None:
+ digest = "sha256:" + "1" * 64
+ completed = controller.subprocess.CompletedProcess(
+ args=[],
+ returncode=0,
+ stdout=digest + "\n",
+ stderr="",
+ )
+ with patch.object(controller, "_run", return_value=completed) as run:
+ self.assertEqual(
+ controller._local_platform_config_digest(
+ "source:tag", "linux/amd64"
+ ),
+ digest,
+ )
+
+ command = run.call_args.args[0]
+ self.assertEqual(command[:3], ["docker", "image", "inspect"])
+ self.assertIn("--platform", command)
+ self.assertIn("linux/amd64", command)
+ self.assertIn("--format={{.Id}}", command)
+
def test_target_digest_verifier_uses_internal_registry_transport(self) -> None:
image = controller.load_policy(POLICY_PATH).images[0]
with patch.object(controller.subprocess, "run") as run:
@@ -438,6 +772,52 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
self.assertIn("--insecure", command)
self.assertEqual(command[-1], controller._target_digest_ref(image))
+ def test_runtime_cache_verifies_shared_source_digest_once(self) -> None:
+ policy = controller.load_policy(POLICY_PATH)
+ shared = [
+ image
+ for image in policy.images
+ if image.source_index_digest
+ == "sha256:16b92ba472fbb9287459cc52e0ecff07288dff461209955098edb56ce866fe49"
+ ]
+ self.assertGreaterEqual(len(shared), 2)
+ source_ref = "quay.io/argoproj/argocd:v3.3.6"
+ source_list = (
+ f"{source_ref} application/vnd.oci.image.index.v1+json "
+ f"{shared[0].source_index_digest}\n"
+ )
+ source_index = json.dumps(
+ {
+ "manifests": [
+ {
+ "digest": shared[0].platform_digest,
+ "platform": {"os": "linux", "architecture": "amd64"},
+ }
+ ]
+ }
+ )
+ cache = controller.RuntimeCache(
+ policy,
+ Path("/unused/key"),
+ Path("/unused/known-hosts"),
+ )
+ completed = controller.subprocess.CompletedProcess
+ with patch.object(
+ controller,
+ "_run",
+ side_effect=[
+ completed([], 0, stdout=source_list),
+ completed([], 0, stdout="complete"),
+ completed([], 0, stdout=source_index),
+ ],
+ ) as run:
+ first = cache.verify_source(shared[0])
+ second = cache.verify_source(shared[1])
+
+ self.assertEqual(first, source_ref)
+ self.assertEqual(second, source_ref)
+ self.assertEqual(run.call_count, 3)
+
def test_source_manifests_match_internal_policy(self) -> None:
policy = controller.load_policy(POLICY_PATH)
by_asset = {image.asset_id: image for image in policy.images}
@@ -557,10 +937,12 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
side_effect=[None, descriptor],
),
patch.object(controller, "_local_image_present", return_value=False),
- patch.object(controller, "_archive_contains_digest", return_value=True),
+ patch.object(
+ controller, "_archive_contains_digest", return_value=True
+ ) as archive_contains,
patch.object(
controller,
- "_local_config_digest",
+ "_local_platform_config_digest",
return_value=image.source_config_digest,
),
patch.object(controller, "_registry_digest_present", return_value=True),
@@ -585,6 +967,11 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
export.assert_called_once()
commands = [call.args[0] for call in run.call_args_list]
self.assertEqual([command[1] for command in commands], ["load", "tag", "push"])
+ self.assertEqual(archive_contains.call_count, 2)
+ self.assertIn("--platform", commands[0])
+ self.assertIn(policy.platform, commands[0])
+ self.assertIn("--platform", commands[2])
+ self.assertIn(policy.platform, commands[2])
self.assertTrue(all("pull" not in command for command in commands))
self.assertEqual(remove.call_count, 2)
@@ -684,6 +1071,85 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
)
self.assertIn("--dry-run=server", args)
+ def test_enforced_strategy_is_included_in_server_dry_run_patch(self) -> None:
+ image = next(
+ image
+ for image in controller.load_policy(POLICY_PATH).images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+ strategy = kubernetes.enforced_strategy(image)
+ self.assertEqual(
+ strategy,
+ {
+ "type": "RollingUpdate",
+ "rollingUpdate": {"maxUnavailable": 0, "maxSurge": 1},
+ },
+ )
+
+ with patch.object(kubernetes, "run") as run:
+ kubernetes.patch_image(
+ image,
+ image.runtime_ref,
+ dry_run=True,
+ strategy=strategy,
+ )
+
+ args = run.call_args.args[0]
+ patch_payload = json.loads(args[args.index("--patch") + 1])
+ self.assertEqual(patch_payload["spec"]["strategy"], strategy)
+ self.assertEqual(
+ patch_payload["spec"]["template"]["spec"]["containers"],
+ [{"name": "metrics-server", "image": image.runtime_ref}],
+ )
+ self.assertIn("--dry-run=server", args)
+
+ def test_safe_strategy_override_repairs_unsafe_single_replica_plan(self) -> None:
+ image = next(
+ image
+ for image in controller.load_policy(POLICY_PATH).images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+ workload = {
+ "metadata": {"generation": 1},
+ "spec": {
+ "replicas": 1,
+ "strategy": {
+ "type": "RollingUpdate",
+ "rollingUpdate": {"maxUnavailable": 1, "maxSurge": "25%"},
+ },
+ },
+ "status": {
+ "observedGeneration": 1,
+ "readyReplicas": 1,
+ "availableReplicas": 1,
+ "unavailableReplicas": 0,
+ },
+ }
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+ with (
+ patch.object(kubernetes, "workload", return_value=workload),
+ self.assertRaisesRegex(
+ controller.ControllerError,
+ "availability_effective_unavailable_exceeded",
+ ),
+ ):
+ kubernetes.verify_availability(image)
+
+ with patch.object(kubernetes, "workload", return_value=workload):
+ kubernetes.verify_availability(
+ image,
+ strategy_override=kubernetes.enforced_strategy(image),
+ )
+
def test_init_container_verifier_accepts_completed_digest_pinned_status(
self,
) -> None:
@@ -763,6 +1229,319 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
run.return_value.stdout = json.dumps(pods)
kubernetes.verify_pods(image)
+ def test_single_replica_rolling_update_passes_availability_guard(self) -> None:
+ image = self._image_with_availability_guard()
+ workload = {
+ "metadata": {"generation": 4},
+ "spec": {
+ "replicas": 1,
+ "strategy": {
+ "type": "RollingUpdate",
+ "rollingUpdate": {
+ "maxUnavailable": "25%",
+ "maxSurge": "25%",
+ },
+ },
+ },
+ "status": {
+ "observedGeneration": 4,
+ "readyReplicas": 1,
+ "availableReplicas": 1,
+ "unavailableReplicas": 0,
+ },
+ }
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+
+ with patch.object(kubernetes, "workload", return_value=workload):
+ kubernetes.verify_availability(image)
+
+ self.assertEqual(
+ kubernetes._effective_replicas(
+ "25%", 1, round_up=False, label="unavailable"
+ ),
+ 0,
+ )
+ self.assertEqual(
+ kubernetes._effective_replicas(
+ "25%", 1, round_up=True, label="surge"
+ ),
+ 1,
+ )
+
+ def test_availability_guard_fails_closed_on_unsafe_rollout_state(self) -> None:
+ image = self._image_with_availability_guard()
+ safe = {
+ "metadata": {"generation": 4},
+ "spec": {
+ "replicas": 1,
+ "strategy": {
+ "type": "RollingUpdate",
+ "rollingUpdate": {"maxUnavailable": 0, "maxSurge": 1},
+ },
+ },
+ "status": {
+ "observedGeneration": 4,
+ "readyReplicas": 1,
+ "availableReplicas": 1,
+ "unavailableReplicas": 0,
+ },
+ }
+ cases = (
+ (
+ {**safe, "metadata": {"generation": 5}},
+ "availability_generation_not_observed",
+ ),
+ (
+ {
+ **safe,
+ "status": {
+ **safe["status"],
+ "readyReplicas": 0,
+ "availableReplicas": 0,
+ },
+ },
+ "availability_ready_replicas_insufficient",
+ ),
+ (
+ {
+ **safe,
+ "spec": {
+ **safe["spec"],
+ "strategy": {
+ "type": "RollingUpdate",
+ "rollingUpdate": {
+ "maxUnavailable": 1,
+ "maxSurge": 0,
+ },
+ },
+ },
+ },
+ "availability_effective_unavailable_exceeded",
+ ),
+ (
+ {
+ **safe,
+ "spec": {**safe["spec"], "strategy": {"type": "Recreate"}},
+ },
+ "availability_rolling_update_required",
+ ),
+ )
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+
+ for workload, error_code in cases:
+ with (
+ self.subTest(error_code=error_code),
+ patch.object(kubernetes, "workload", return_value=workload),
+ self.assertRaisesRegex(controller.ControllerError, error_code),
+ ):
+ kubernetes.verify_availability(image)
+
+ def test_strategy_only_diff_is_applied_and_recorded(self) -> None:
+ full_policy = controller.load_policy(POLICY_PATH)
+ image = next(
+ image
+ for image in full_policy.images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+ policy = replace(full_policy, images=(image,))
+ trace_id = "aia-p0-006-02a-strategy-only"
+ previous_strategy = {
+ "type": "RollingUpdate",
+ "rollingUpdate": {"maxUnavailable": 1, "maxSurge": "25%"},
+ }
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+ with tempfile.TemporaryDirectory() as temp:
+ receipt_path = Path(temp) / "mirror.json"
+ self._write_verified_mirror_receipt(receipt_path, policy, trace_id)
+ with (
+ patch.object(controller, "Kubernetes", return_value=kubernetes),
+ patch.object(
+ kubernetes, "current_image", return_value=image.runtime_ref
+ ),
+ patch.object(
+ kubernetes,
+ "current_strategy",
+ return_value=previous_strategy,
+ ),
+ patch.object(kubernetes, "verify_availability"),
+ patch.object(kubernetes, "patch_image") as patch_image,
+ patch.object(kubernetes, "rollout"),
+ patch.object(kubernetes, "verify_pods", return_value=1),
+ patch.object(kubernetes, "write_receipt") as write_receipt,
+ ):
+ receipt = controller.apply_policy(
+ policy,
+ trace_id=trace_id,
+ mirror_receipt_path=receipt_path,
+ apply=True,
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+
+ self.assertEqual(receipt["source_of_truth_diff_count"], 1)
+ self.assertEqual(receipt["execution_changed_count"], 1)
+ self.assertEqual(receipt["availability_strategy_diff_count"], 1)
+ self.assertEqual(receipt["availability_strategy_changed_count"], 1)
+ self.assertEqual(receipt["availability_strategy_verified_count"], 1)
+ self.assertTrue(receipt["independent_verifier_passed"])
+ self.assertEqual(patch_image.call_count, 2)
+ self.assertTrue(patch_image.call_args_list[0].kwargs["dry_run"])
+ self.assertFalse(patch_image.call_args_list[1].kwargs["dry_run"])
+ self.assertEqual(
+ patch_image.call_args_list[1].kwargs["strategy"],
+ kubernetes.enforced_strategy(image),
+ )
+ write_receipt.assert_called_once()
+
+ def test_failed_rollout_restores_previous_image_and_strategy(self) -> None:
+ full_policy = controller.load_policy(POLICY_PATH)
+ image = next(
+ image
+ for image in full_policy.images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+ policy = replace(full_policy, images=(image,))
+ trace_id = "aia-p0-006-02a-strategy-rollback"
+ previous_image = "rancher/mirrored-metrics-server:v0.8.1"
+ previous_strategy = {
+ "type": "RollingUpdate",
+ "rollingUpdate": {"maxUnavailable": 1, "maxSurge": "25%"},
+ }
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+ with tempfile.TemporaryDirectory() as temp:
+ receipt_path = Path(temp) / "mirror.json"
+ self._write_verified_mirror_receipt(receipt_path, policy, trace_id)
+ with (
+ patch.object(controller, "Kubernetes", return_value=kubernetes),
+ patch.object(
+ kubernetes, "current_image", return_value=previous_image
+ ),
+ patch.object(
+ kubernetes,
+ "current_strategy",
+ return_value=previous_strategy,
+ ),
+ patch.object(kubernetes, "verify_availability"),
+ patch.object(kubernetes, "patch_image") as patch_image,
+ patch.object(
+ kubernetes,
+ "rollout",
+ side_effect=[controller.ControllerError("rollout_failed"), None],
+ ),
+ patch.object(kubernetes, "write_receipt") as write_receipt,
+ ):
+ receipt = controller.apply_policy(
+ policy,
+ trace_id=trace_id,
+ mirror_receipt_path=receipt_path,
+ apply=True,
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+
+ self.assertEqual(patch_image.call_count, 3)
+ rollback = patch_image.call_args_list[-1]
+ self.assertEqual(rollback.args[1], previous_image)
+ self.assertFalse(rollback.kwargs["dry_run"])
+ self.assertEqual(rollback.kwargs["strategy"], previous_strategy)
+ self.assertEqual(receipt["state"], "blocked_with_safe_next_action")
+ self.assertEqual(receipt["failure_reason_code"], "rollout_failed")
+ self.assertTrue(receipt["rollback_performed"])
+ self.assertTrue(receipt["rollback_verified"])
+ self.assertEqual(receipt["rollback_verified_workload_count"], 1)
+ self.assertFalse(receipt["no_write_terminal"])
+ self.assertFalse(receipt["independent_verifier_passed"])
+ write_receipt.assert_called_once_with(receipt)
+
+ def test_failed_independent_verifier_triggers_durable_rollback(self) -> None:
+ full_policy = controller.load_policy(POLICY_PATH)
+ image = next(
+ image
+ for image in full_policy.images
+ if image.asset_id == "runtime-image:metrics-server"
+ )
+ policy = replace(full_policy, images=(image,))
+ trace_id = "aia-p0-006-02a-verifier-rollback"
+ previous_image = "rancher/mirrored-metrics-server:v0.8.1"
+ previous_strategy = {
+ "type": "RollingUpdate",
+ "rollingUpdate": {"maxUnavailable": 1, "maxSurge": "25%"},
+ }
+ kubernetes = controller.Kubernetes(
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+ with tempfile.TemporaryDirectory() as temp:
+ receipt_path = Path(temp) / "mirror.json"
+ self._write_verified_mirror_receipt(receipt_path, policy, trace_id)
+ with (
+ patch.object(controller, "Kubernetes", return_value=kubernetes),
+ patch.object(
+ kubernetes,
+ "current_image",
+ side_effect=[
+ previous_image,
+ image.runtime_ref,
+ previous_image,
+ previous_image,
+ ],
+ ),
+ patch.object(
+ kubernetes,
+ "current_strategy",
+ return_value=previous_strategy,
+ ),
+ patch.object(kubernetes, "verify_availability"),
+ patch.object(kubernetes, "patch_image") as patch_image,
+ patch.object(kubernetes, "rollout"),
+ patch.object(
+ kubernetes,
+ "verify_pods",
+ side_effect=controller.ControllerError(
+ "pod_image_digest_mismatch"
+ ),
+ ),
+ patch.object(kubernetes, "write_receipt") as write_receipt,
+ ):
+ receipt = controller.apply_policy(
+ policy,
+ trace_id=trace_id,
+ mirror_receipt_path=receipt_path,
+ apply=True,
+ use_sudo=False,
+ kubeconfig=None,
+ server=None,
+ )
+
+ self.assertEqual(patch_image.call_count, 3)
+ self.assertEqual(receipt["state"], "blocked_with_safe_next_action")
+ self.assertEqual(
+ receipt["failure_reason_code"], "pod_image_digest_mismatch"
+ )
+ self.assertTrue(receipt["rollback_performed"])
+ self.assertTrue(receipt["rollback_verified"])
+ self.assertEqual(receipt["rollback_verified_workload_count"], 1)
+ write_receipt.assert_called_once_with(receipt)
+
if __name__ == "__main__":
unittest.main()
diff --git a/scripts/security/tests/test_verify_external_mcp_artifact_receipt.py b/scripts/security/tests/test_verify_external_mcp_artifact_receipt.py
new file mode 100644
index 000000000..d99a1ce95
--- /dev/null
+++ b/scripts/security/tests/test_verify_external_mcp_artifact_receipt.py
@@ -0,0 +1,471 @@
+from __future__ import annotations
+
+import base64
+import hashlib
+import importlib.util
+import io
+import json
+import sys
+import tarfile
+import tempfile
+import unittest
+import subprocess
+from copy import deepcopy
+from pathlib import Path
+from urllib.parse import quote
+
+
+ROOT = Path(__file__).resolve().parents[3]
+VERIFIER_PATH = ROOT / "scripts/security/verify_external_mcp_artifact_receipt.py"
+RUN_ID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
+TRACE_ID = f"mcp-artifact-{RUN_ID}"
+
+
+def _load_verifier():
+ spec = importlib.util.spec_from_file_location(
+ "verify_external_mcp_artifact_receipt", VERIFIER_PATH
+ )
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+verifier = _load_verifier()
+
+
+def _canonical(payload) -> bytes:
+ return json.dumps(
+ payload,
+ ensure_ascii=False,
+ sort_keys=True,
+ separators=(",", ":"),
+ ).encode()
+
+
+def _fixture():
+ package_data = {
+ "@playwright/mcp": b"root-package",
+ "playwright": b"playwright-package",
+ "playwright-core": b"playwright-core-package",
+ }
+ versions = {
+ "@playwright/mcp": "0.0.78",
+ "playwright": "1.62.0-alpha-1783623505000",
+ "playwright-core": "1.62.0-alpha-1783623505000",
+ }
+ packages = []
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ private_key = root / "private.pem"
+ public_key = root / "public.der"
+ subprocess.run(
+ [
+ "openssl",
+ "ecparam",
+ "-name",
+ "prime256v1",
+ "-genkey",
+ "-noout",
+ "-out",
+ str(private_key),
+ ],
+ check=True,
+ capture_output=True,
+ )
+ subprocess.run(
+ [
+ "openssl",
+ "pkey",
+ "-in",
+ str(private_key),
+ "-pubout",
+ "-outform",
+ "DER",
+ "-out",
+ str(public_key),
+ ],
+ check=True,
+ capture_output=True,
+ )
+ public_key_base64 = base64.b64encode(public_key.read_bytes()).decode()
+ for index, (name, data) in enumerate(package_data.items()):
+ integrity = "sha512-" + base64.b64encode(
+ hashlib.sha512(data).digest()
+ ).decode()
+ message = f"{name}@{versions[name]}:{integrity}".encode()
+ message_path = root / f"message-{index}.bin"
+ signature_path = root / f"signature-{index}.der"
+ message_path.write_bytes(message)
+ subprocess.run(
+ [
+ "openssl",
+ "dgst",
+ "-sha256",
+ "-sign",
+ str(private_key),
+ "-out",
+ str(signature_path),
+ str(message_path),
+ ],
+ check=True,
+ capture_output=True,
+ )
+ packages.append(
+ {
+ "name": name,
+ "version": versions[name],
+ "integrity": integrity,
+ "signature": base64.b64encode(
+ signature_path.read_bytes()
+ ).decode(),
+ "slsa_subject_name": (
+ f"pkg:npm/{quote(name, safe='/')}@{versions[name]}"
+ ),
+ "slsa_subject_sha512_hex": hashlib.sha512(data).hexdigest(),
+ "shasum_sha1": hashlib.sha1(
+ data, usedforsecurity=False
+ ).hexdigest(),
+ }
+ )
+ policy = {
+ "schema_version": verifier.POLICY_SCHEMA_VERSION,
+ "work_item_id": "MCP-ARTIFACT-PLAYWRIGHT-TEST",
+ "candidate_id": "external.playwright-verifier",
+ "risk_level": "high",
+ "registry_contract": {
+ "signature_key": {
+ "public_key_der_base64": public_key_base64,
+ }
+ },
+ "packages": packages,
+ "internal_mirror": {
+ "push_repository": (
+ "registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp"
+ ),
+ "runtime_repository": (
+ "192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp"
+ ),
+ },
+ }
+ policy_checksum = "sha256:" + hashlib.sha256(_canonical(policy)).hexdigest()
+ sbom = {
+ "bomFormat": "CycloneDX",
+ "specVersion": "1.5",
+ "components": [
+ {
+ "bom-ref": f"pkg:npm/{quote(row['name'], safe='/')}@{row['version']}"
+ }
+ for row in packages
+ ],
+ }
+ sbom_bytes = _canonical(sbom) + b"\n"
+ files = {"artifact/sbom.cdx.json": sbom_bytes}
+ manifest_rows = []
+ receipt_rows = []
+ for row in packages:
+ safe_name = row["name"].replace("@", "").replace("/", "-")
+ filename = f"{safe_name}-{row['version']}.tgz"
+ statement = {
+ "_type": verifier.IN_TOTO_STATEMENT_TYPE,
+ "predicateType": verifier.SLSA_PREDICATE_TYPE,
+ "subject": [
+ {
+ "name": row["slsa_subject_name"],
+ "digest": {"sha512": row["slsa_subject_sha512_hex"]},
+ }
+ ],
+ }
+ attestation = _canonical(
+ {
+ "attestations": [
+ {
+ "predicateType": verifier.SLSA_PREDICATE_TYPE,
+ "bundle": {
+ "dsseEnvelope": {
+ "payload": base64.b64encode(
+ _canonical(statement)
+ ).decode()
+ }
+ },
+ }
+ ]
+ }
+ )
+ files[f"artifact/tarballs/{filename}"] = package_data[row["name"]]
+ files[f"artifact/attestations/{filename}.attestations.json"] = attestation
+ manifest_rows.append(
+ {
+ "name": row["name"],
+ "version": row["version"],
+ "tarball_path": f"tarballs/{filename}",
+ "tarball_sha512_hex": row["slsa_subject_sha512_hex"],
+ "tarball_sha1": row["shasum_sha1"],
+ "attestation_path": (
+ f"attestations/{filename}.attestations.json"
+ ),
+ "attestation_sha256": hashlib.sha256(attestation).hexdigest(),
+ "npm_registry_signature_verified": True,
+ "slsa_subject_verified": True,
+ "archive_safety_verified": True,
+ "vulnerability_ids": [],
+ }
+ )
+ receipt_rows.append(
+ {
+ "name": row["name"],
+ "version": row["version"],
+ "tarball_sha512_hex": row["slsa_subject_sha512_hex"],
+ "tarball_sha1": row["shasum_sha1"],
+ "npm_registry_signature_verified": True,
+ "slsa_subject_verified": True,
+ "archive_safety_verified": True,
+ "license_decision": "passed",
+ "vulnerability_decision": "passed_no_known_osv_records",
+ "vulnerability_ids": [],
+ }
+ )
+ manifest = {
+ "candidate_id": policy["candidate_id"],
+ "work_item_id": policy["work_item_id"],
+ "policy_checksum": policy_checksum,
+ "package_count": 3,
+ "packages": manifest_rows,
+ "sbom": {"sha256": hashlib.sha256(sbom_bytes).hexdigest()},
+ "operation_boundaries": {
+ "mcp_server_started": False,
+ "browser_started": False,
+ "runtime_request_or_response_body_stored": False,
+ },
+ }
+ manifest_bytes = _canonical(manifest) + b"\n"
+ files["artifact/bundle-manifest.json"] = manifest_bytes
+ digest = "sha256:" + "c" * 64
+ receipt = {
+ "schema_version": verifier.CONTROLLER_RECEIPT_SCHEMA_VERSION,
+ "run_id": RUN_ID,
+ "trace_id": TRACE_ID,
+ "work_item_id": policy["work_item_id"],
+ "candidate_id": policy["candidate_id"],
+ "risk_level": "high",
+ "mode": "apply",
+ "status": "completed_internal_mirror_pending_independent_verifier",
+ "policy_checksum": policy_checksum,
+ "packages": receipt_rows,
+ "internal_mirror_ref": (
+ policy["internal_mirror"]["push_repository"] + "@" + digest
+ ),
+ "runtime_mirror_ref": (
+ policy["internal_mirror"]["runtime_repository"] + "@" + digest
+ ),
+ "bundle_manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
+ "cyclonedx_sbom_sha256": hashlib.sha256(sbom_bytes).hexdigest(),
+ "promotion_allowed": False,
+ "replay_allowed": False,
+ "shadow_allowed": False,
+ "canary_allowed": False,
+ "stage_receipts": {
+ "sensor_source_receipt": {
+ "redirects_followed": 0,
+ "runtime_request_or_response_body_stored": False,
+ "upstream_supply_chain_attestation_bundled": True,
+ },
+ "check_mode": {
+ "status": "passed",
+ "mcp_process_started": False,
+ "browser_started": False,
+ "production_write_performed": False,
+ },
+ "bounded_execution": {
+ "status": "internal_oci_bundle_pushed",
+ "internal_registry_write_performed": True,
+ "external_runtime_mutation_performed": False,
+ "production_route_changed": False,
+ },
+ "independent_post_verifier": {
+ "status": "pending_external_verifier",
+ "internal_digest_ref_verified": False,
+ },
+ },
+ }
+ return policy, receipt, files
+
+
+def _write_image_archive(path: Path, files: dict[str, bytes], *, symlink=False):
+ layer_buffer = io.BytesIO()
+ with tarfile.open(fileobj=layer_buffer, mode="w") as layer:
+ for name, data in files.items():
+ member = tarfile.TarInfo(name)
+ member.size = len(data)
+ layer.addfile(member, io.BytesIO(data))
+ if symlink:
+ member = tarfile.TarInfo("artifact/unsafe-link")
+ member.type = tarfile.SYMTYPE
+ member.linkname = "../../outside"
+ layer.addfile(member)
+ layer_bytes = layer_buffer.getvalue()
+ manifest_bytes = json.dumps([{"Layers": ["layer.tar"]}]).encode()
+ with tarfile.open(path, mode="w") as outer:
+ for name, data in (
+ ("manifest.json", manifest_bytes),
+ ("layer.tar", layer_bytes),
+ ):
+ member = tarfile.TarInfo(name)
+ member.size = len(data)
+ outer.addfile(member, io.BytesIO(data))
+
+
+class ExternalMcpArtifactIndependentVerifierTest(unittest.TestCase):
+ def test_controller_receipt_identity_and_boundaries_are_validated(self) -> None:
+ policy, receipt, _ = _fixture()
+
+ identity = verifier.validate_controller_receipt(policy, receipt)
+
+ self.assertEqual(identity["run_id"], RUN_ID)
+ self.assertEqual(identity["trace_id"], TRACE_ID)
+ self.assertEqual(identity["digest"], "sha256:" + "c" * 64)
+
+ def test_controller_receipt_rejects_promotion_claim(self) -> None:
+ policy, receipt, _ = _fixture()
+ receipt["canary_allowed"] = True
+
+ with self.assertRaisesRegex(
+ verifier.VerifierError,
+ "controller_receipt_promotion_boundary_invalid",
+ ):
+ verifier.validate_controller_receipt(policy, receipt)
+
+ def test_bundle_files_are_rehashed_independently(self) -> None:
+ policy, receipt, files = _fixture()
+
+ evidence = verifier.verify_bundle_files(
+ files,
+ policy=policy,
+ controller_receipt=receipt,
+ )
+
+ self.assertEqual(evidence["verified_package_count"], 3)
+ self.assertEqual(evidence["artifact_file_count"], 8)
+
+ def test_bundle_tarball_tamper_is_rejected(self) -> None:
+ policy, receipt, files = _fixture()
+ tampered = deepcopy(files)
+ root_tarball = next(
+ key
+ for key in tampered
+ if key.startswith("artifact/tarballs/playwright-mcp-")
+ )
+ tampered[root_tarball] += b"tamper"
+
+ with self.assertRaisesRegex(
+ verifier.VerifierError,
+ "bundle_package_evidence_mismatch",
+ ):
+ verifier.verify_bundle_files(
+ tampered,
+ policy=policy,
+ controller_receipt=receipt,
+ )
+
+ def test_bundle_extra_file_is_rejected(self) -> None:
+ policy, receipt, files = _fixture()
+ files["artifact/unexpected.bin"] = b"unexpected"
+
+ with self.assertRaisesRegex(
+ verifier.VerifierError,
+ "bundle_file_set_invalid",
+ ):
+ verifier.verify_bundle_files(
+ files,
+ policy=policy,
+ controller_receipt=receipt,
+ )
+
+ def test_independent_npm_signature_rejects_tamper(self) -> None:
+ policy, _, files = _fixture()
+ package = deepcopy(policy["packages"][0])
+ package["signature"] = base64.b64encode(b"invalid-signature").decode()
+ tarball_path = next(
+ key
+ for key in files
+ if key.startswith("artifact/tarballs/playwright-mcp-")
+ )
+
+ with self.assertRaisesRegex(
+ verifier.VerifierError,
+ "bundled_npm_signature_verification_failed",
+ ):
+ verifier.verify_bundled_npm_signature(
+ policy,
+ package,
+ files[tarball_path],
+ )
+
+ def test_independent_slsa_subject_rejects_tamper(self) -> None:
+ policy, _, files = _fixture()
+ package = deepcopy(policy["packages"][0])
+ package["slsa_subject_sha512_hex"] = "0" * 128
+ attestation_path = next(
+ key
+ for key in files
+ if key.startswith("artifact/attestations/playwright-mcp-")
+ )
+
+ with self.assertRaisesRegex(
+ verifier.VerifierError,
+ "bundled_slsa_subject_mismatch",
+ ):
+ verifier.verify_bundled_slsa_subject(
+ package,
+ files[attestation_path],
+ )
+
+ def test_image_archive_is_inspected_as_data(self) -> None:
+ _, _, files = _fixture()
+ with tempfile.TemporaryDirectory() as temp:
+ archive = Path(temp) / "image.tar"
+ _write_image_archive(archive, files)
+
+ extracted = verifier.extract_image_files(archive)
+
+ self.assertEqual(extracted, files)
+
+ def test_image_archive_rejects_symlink(self) -> None:
+ _, _, files = _fixture()
+ with tempfile.TemporaryDirectory() as temp:
+ archive = Path(temp) / "image.tar"
+ _write_image_archive(archive, files, symlink=True)
+ with self.assertRaisesRegex(
+ verifier.VerifierError,
+ "image_layer_unsafe_entry",
+ ):
+ verifier.extract_image_files(archive)
+
+ def test_verifier_receipt_keeps_runtime_promotion_disabled(self) -> None:
+ policy, receipt, files = _fixture()
+ identity = verifier.validate_controller_receipt(policy, receipt)
+ evidence = verifier.verify_bundle_files(
+ files,
+ policy=policy,
+ controller_receipt=receipt,
+ )
+
+ result = verifier.build_verifier_receipt(
+ identity=identity,
+ controller_receipt_bytes=_canonical(receipt) + b"\n",
+ bundle_evidence=evidence,
+ )
+
+ self.assertEqual(result["status"], "completed_internal_mirror_verified")
+ self.assertFalse(result["promotion_allowed"])
+ self.assertFalse(result["replay_allowed"])
+ self.assertFalse(result["shadow_allowed"])
+ self.assertFalse(result["canary_allowed"])
+ self.assertFalse(
+ result["independent_post_verifier"]["container_or_mcp_started"]
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/security/verify_external_mcp_artifact_receipt.py b/scripts/security/verify_external_mcp_artifact_receipt.py
new file mode 100644
index 000000000..3c24f61f0
--- /dev/null
+++ b/scripts/security/verify_external_mcp_artifact_receipt.py
@@ -0,0 +1,688 @@
+#!/usr/bin/env python3
+"""Independently verify an internal external-MCP artifact mirror receipt.
+
+This verifier does not import the artifact controller. It validates the policy
+and controller receipt through a separate code path, reads the Harbor image back
+by immutable digest, and inspects its layers as data. It never starts a container,
+MCP server, browser, replay, or production canary.
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import binascii
+import hashlib
+import io
+import json
+import re
+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
+from urllib.parse import quote
+
+
+POLICY_SCHEMA_VERSION = "awoooi_external_mcp_artifact_policy_v1"
+CONTROLLER_RECEIPT_SCHEMA_VERSION = "awoooi_external_mcp_artifact_receipt_v1"
+VERIFIER_RECEIPT_SCHEMA_VERSION = "awoooi_external_mcp_artifact_verifier_receipt_v1"
+SHA256_HEX_PATTERN = re.compile(r"^[0-9a-f]{64}$")
+SHA1_HEX_PATTERN = re.compile(r"^[0-9a-f]{40}$")
+SHA512_HEX_PATTERN = re.compile(r"^[0-9a-f]{128}$")
+OCI_DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
+MAX_IMAGE_ARCHIVE_BYTES = 96 * 1024 * 1024
+MAX_LAYER_UNPACKED_BYTES = 64 * 1024 * 1024
+MAX_LAYER_FILES = 2_048
+SLSA_PREDICATE_TYPE = "https://slsa.dev/provenance/v1"
+IN_TOTO_STATEMENT_TYPE = "https://in-toto.io/Statement/v1"
+
+
+class VerifierError(RuntimeError):
+ """Fail-closed public-safe verifier error class."""
+
+
+def _canonical_json_bytes(payload: Any) -> bytes:
+ return json.dumps(
+ payload,
+ ensure_ascii=False,
+ sort_keys=True,
+ separators=(",", ":"),
+ ).encode("utf-8")
+
+
+def _sha256_hex(data: bytes) -> str:
+ return hashlib.sha256(data).hexdigest()
+
+
+def _read_json(path: Path, error_class: str) -> dict[str, Any]:
+ try:
+ data = path.read_bytes()
+ except OSError as exc:
+ raise VerifierError(error_class) from exc
+ if len(data) > 4 * 1024 * 1024:
+ raise VerifierError(error_class)
+ try:
+ payload = json.loads(data)
+ except json.JSONDecodeError as exc:
+ raise VerifierError(error_class) from exc
+ if not isinstance(payload, dict):
+ raise VerifierError(error_class)
+ return payload
+
+
+def _require_dict(value: Any, error_class: str) -> dict[str, Any]:
+ if not isinstance(value, dict):
+ raise VerifierError(error_class)
+ return value
+
+
+def _expected_packages(policy: dict[str, Any]) -> dict[str, dict[str, Any]]:
+ rows = policy.get("packages")
+ if not isinstance(rows, list) or len(rows) != 3:
+ raise VerifierError("policy_package_set_invalid")
+ result: dict[str, dict[str, Any]] = {}
+ for row in rows:
+ if not isinstance(row, dict):
+ raise VerifierError("policy_package_set_invalid")
+ name = row.get("name")
+ version = row.get("version")
+ sha512_hex = row.get("slsa_subject_sha512_hex")
+ sha1_hex = row.get("shasum_sha1")
+ if (
+ not isinstance(name, str)
+ or not isinstance(version, str)
+ or not isinstance(sha512_hex, str)
+ or not SHA512_HEX_PATTERN.fullmatch(sha512_hex)
+ or not isinstance(sha1_hex, str)
+ or not SHA1_HEX_PATTERN.fullmatch(sha1_hex)
+ or name in result
+ ):
+ raise VerifierError("policy_package_set_invalid")
+ result[name] = row
+ if set(result) != {"@playwright/mcp", "playwright", "playwright-core"}:
+ raise VerifierError("policy_package_set_invalid")
+ return result
+
+
+def validate_controller_receipt(
+ policy: dict[str, Any],
+ receipt: dict[str, Any],
+) -> dict[str, Any]:
+ """Validate the controller receipt without using controller code."""
+ if policy.get("schema_version") != POLICY_SCHEMA_VERSION:
+ raise VerifierError("policy_schema_invalid")
+ if receipt.get("schema_version") != CONTROLLER_RECEIPT_SCHEMA_VERSION:
+ raise VerifierError("controller_receipt_schema_invalid")
+ policy_checksum = "sha256:" + _sha256_hex(_canonical_json_bytes(policy))
+ if receipt.get("policy_checksum") != policy_checksum:
+ raise VerifierError("controller_receipt_policy_checksum_mismatch")
+ if (
+ receipt.get("work_item_id") != policy.get("work_item_id")
+ or receipt.get("candidate_id") != policy.get("candidate_id")
+ or receipt.get("risk_level") != "high"
+ or receipt.get("mode") != "apply"
+ or receipt.get("status")
+ != "completed_internal_mirror_pending_independent_verifier"
+ ):
+ raise VerifierError("controller_receipt_identity_or_state_invalid")
+
+ run_id = receipt.get("run_id")
+ trace_id = receipt.get("trace_id")
+ try:
+ normalized_run_id = str(uuid.UUID(run_id))
+ except (ValueError, AttributeError) as exc:
+ raise VerifierError("controller_receipt_run_id_invalid") from exc
+ if run_id != normalized_run_id or trace_id != f"mcp-artifact-{run_id}":
+ raise VerifierError("controller_receipt_run_trace_mismatch")
+ if any(
+ receipt.get(field) is not False
+ for field in (
+ "promotion_allowed",
+ "replay_allowed",
+ "shadow_allowed",
+ "canary_allowed",
+ )
+ ):
+ raise VerifierError("controller_receipt_promotion_boundary_invalid")
+
+ expected = _expected_packages(policy)
+ rows = receipt.get("packages")
+ if not isinstance(rows, list) or len(rows) != len(expected):
+ raise VerifierError("controller_receipt_package_set_invalid")
+ seen: set[str] = set()
+ for row in rows:
+ if not isinstance(row, dict) or row.get("name") not in expected:
+ raise VerifierError("controller_receipt_package_set_invalid")
+ package = expected[row["name"]]
+ if row["name"] in seen:
+ raise VerifierError("controller_receipt_package_set_invalid")
+ seen.add(row["name"])
+ if (
+ row.get("version") != package.get("version")
+ or row.get("tarball_sha512_hex")
+ != package.get("slsa_subject_sha512_hex")
+ or row.get("tarball_sha1") != package.get("shasum_sha1")
+ or row.get("npm_registry_signature_verified") is not True
+ or row.get("slsa_subject_verified") is not True
+ or row.get("archive_safety_verified") is not True
+ or row.get("license_decision") != "passed"
+ or row.get("vulnerability_decision")
+ != "passed_no_known_osv_records"
+ or row.get("vulnerability_ids") != []
+ ):
+ raise VerifierError("controller_receipt_package_evidence_invalid")
+ if seen != set(expected):
+ raise VerifierError("controller_receipt_package_set_invalid")
+
+ stages = _require_dict(receipt.get("stage_receipts"), "stage_receipts_invalid")
+ sensor = _require_dict(
+ stages.get("sensor_source_receipt"), "sensor_source_receipt_invalid"
+ )
+ check_mode = _require_dict(stages.get("check_mode"), "check_mode_invalid")
+ execution = _require_dict(
+ stages.get("bounded_execution"), "bounded_execution_invalid"
+ )
+ pending_verifier = _require_dict(
+ stages.get("independent_post_verifier"),
+ "independent_post_verifier_state_invalid",
+ )
+ if (
+ sensor.get("redirects_followed") != 0
+ or sensor.get("runtime_request_or_response_body_stored") is not False
+ or sensor.get("upstream_supply_chain_attestation_bundled") is not True
+ or check_mode.get("status") != "passed"
+ or check_mode.get("mcp_process_started") is not False
+ or check_mode.get("browser_started") is not False
+ or check_mode.get("production_write_performed") is not False
+ or execution.get("status") != "internal_oci_bundle_pushed"
+ or execution.get("internal_registry_write_performed") is not True
+ or execution.get("external_runtime_mutation_performed") is not False
+ or execution.get("production_route_changed") is not False
+ or pending_verifier.get("status") != "pending_external_verifier"
+ or pending_verifier.get("internal_digest_ref_verified") is not False
+ ):
+ raise VerifierError("controller_receipt_stage_boundary_invalid")
+
+ mirror = _require_dict(policy.get("internal_mirror"), "internal_mirror_invalid")
+ internal_ref = receipt.get("internal_mirror_ref")
+ runtime_ref = receipt.get("runtime_mirror_ref")
+ push_prefix = f"{mirror.get('push_repository')}@"
+ runtime_prefix = f"{mirror.get('runtime_repository')}@"
+ if (
+ not isinstance(internal_ref, str)
+ or not internal_ref.startswith(push_prefix)
+ or not isinstance(runtime_ref, str)
+ or not runtime_ref.startswith(runtime_prefix)
+ ):
+ raise VerifierError("controller_receipt_internal_ref_invalid")
+ internal_digest = internal_ref.removeprefix(push_prefix)
+ runtime_digest = runtime_ref.removeprefix(runtime_prefix)
+ if (
+ internal_digest != runtime_digest
+ or not OCI_DIGEST_PATTERN.fullmatch(internal_digest)
+ ):
+ raise VerifierError("controller_receipt_internal_digest_invalid")
+ for field in ("bundle_manifest_sha256", "cyclonedx_sbom_sha256"):
+ if not isinstance(receipt.get(field), str) or not SHA256_HEX_PATTERN.fullmatch(
+ receipt[field]
+ ):
+ raise VerifierError("controller_receipt_bundle_hash_invalid")
+ return {
+ "run_id": run_id,
+ "trace_id": trace_id,
+ "work_item_id": receipt["work_item_id"],
+ "candidate_id": receipt["candidate_id"],
+ "policy_checksum": policy_checksum,
+ "internal_ref": internal_ref,
+ "runtime_ref": runtime_ref,
+ "digest": internal_digest,
+ "expected_packages": expected,
+ }
+
+
+def _run_command(command: list[str], *, cwd: Path, timeout: int) -> None:
+ try:
+ completed = subprocess.run(
+ command,
+ cwd=cwd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=timeout,
+ )
+ except (OSError, subprocess.TimeoutExpired) as exc:
+ raise VerifierError("registry_readback_command_failed") from exc
+ if completed.returncode != 0:
+ raise VerifierError("registry_readback_command_failed")
+
+
+def verify_bundled_npm_signature(
+ policy: dict[str, Any],
+ package: dict[str, Any],
+ tarball: bytes,
+) -> None:
+ """Verify the npm ECDSA signature with OpenSSL through a separate path."""
+ integrity = package.get("integrity")
+ signature_base64 = package.get("signature")
+ registry = _require_dict(policy.get("registry_contract"), "registry_contract_invalid")
+ signature_key = _require_dict(
+ registry.get("signature_key"), "registry_signature_key_invalid"
+ )
+ public_key_base64 = signature_key.get("public_key_der_base64")
+ if (
+ not isinstance(integrity, str)
+ or not integrity.startswith("sha512-")
+ or not isinstance(signature_base64, str)
+ or not isinstance(public_key_base64, str)
+ ):
+ raise VerifierError("bundled_npm_signature_policy_invalid")
+ try:
+ integrity_digest = base64.b64decode(
+ integrity.removeprefix("sha512-"), validate=True
+ )
+ signature = base64.b64decode(signature_base64, validate=True)
+ public_key = base64.b64decode(public_key_base64, validate=True)
+ except (ValueError, binascii.Error) as exc:
+ raise VerifierError("bundled_npm_signature_encoding_invalid") from exc
+ if integrity_digest != hashlib.sha512(tarball).digest():
+ raise VerifierError("bundled_npm_integrity_mismatch")
+ openssl = shutil.which("openssl")
+ if not openssl:
+ raise VerifierError("openssl_signature_verifier_unavailable")
+ message = f"{package.get('name')}@{package.get('version')}:{integrity}".encode()
+ with tempfile.TemporaryDirectory(prefix="awoooi-mcp-npm-verifier-") as temp:
+ root = Path(temp)
+ der_path = root / "public.der"
+ pem_path = root / "public.pem"
+ signature_path = root / "signature.der"
+ message_path = root / "message.bin"
+ der_path.write_bytes(public_key)
+ signature_path.write_bytes(signature)
+ message_path.write_bytes(message)
+ convert = subprocess.run(
+ [
+ openssl,
+ "pkey",
+ "-pubin",
+ "-inform",
+ "DER",
+ "-in",
+ str(der_path),
+ "-out",
+ str(pem_path),
+ ],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ if convert.returncode != 0:
+ raise VerifierError("bundled_npm_public_key_invalid")
+ verified = subprocess.run(
+ [
+ openssl,
+ "dgst",
+ "-sha256",
+ "-verify",
+ str(pem_path),
+ "-signature",
+ str(signature_path),
+ str(message_path),
+ ],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ if verified.returncode != 0:
+ raise VerifierError("bundled_npm_signature_verification_failed")
+
+
+def verify_bundled_slsa_subject(
+ package: dict[str, Any],
+ attestation: bytes,
+) -> None:
+ """Decode the bundled DSSE statement and independently bind its subject."""
+ try:
+ payload = json.loads(attestation)
+ except json.JSONDecodeError as exc:
+ raise VerifierError("bundled_slsa_attestation_invalid") from exc
+ rows = payload.get("attestations") if isinstance(payload, dict) else None
+ if not isinstance(rows, list):
+ raise VerifierError("bundled_slsa_attestation_invalid")
+ matches = [
+ row
+ for row in rows
+ if isinstance(row, dict) and row.get("predicateType") == SLSA_PREDICATE_TYPE
+ ]
+ if len(matches) != 1:
+ raise VerifierError("bundled_slsa_attestation_invalid")
+ bundle = matches[0].get("bundle")
+ envelope = bundle.get("dsseEnvelope") if isinstance(bundle, dict) else None
+ encoded = envelope.get("payload") if isinstance(envelope, dict) else None
+ if not isinstance(encoded, str):
+ raise VerifierError("bundled_slsa_dsse_payload_missing")
+ try:
+ statement = json.loads(base64.b64decode(encoded, validate=True))
+ except (ValueError, binascii.Error, json.JSONDecodeError) as exc:
+ raise VerifierError("bundled_slsa_dsse_payload_invalid") from exc
+ expected_subject = {
+ "name": package.get("slsa_subject_name"),
+ "digest": {"sha512": package.get("slsa_subject_sha512_hex")},
+ }
+ if (
+ not isinstance(statement, dict)
+ or statement.get("_type") != IN_TOTO_STATEMENT_TYPE
+ or statement.get("predicateType") != SLSA_PREDICATE_TYPE
+ or statement.get("subject") != [expected_subject]
+ ):
+ raise VerifierError("bundled_slsa_subject_mismatch")
+
+
+def read_image_files(internal_ref: str) -> dict[str, bytes]:
+ """Pull and inspect an OCI image as tar data without creating a container."""
+ docker = shutil.which("docker")
+ if not docker:
+ raise VerifierError("docker_cli_unavailable")
+ with tempfile.TemporaryDirectory(prefix="awoooi-mcp-artifact-verifier-") as temp:
+ root = Path(temp)
+ archive_path = root / "image.tar"
+ _run_command(
+ [docker, "buildx", "imagetools", "inspect", internal_ref],
+ cwd=root,
+ timeout=60,
+ )
+ _run_command([docker, "pull", internal_ref], cwd=root, timeout=180)
+ _run_command(
+ [docker, "image", "save", "--output", str(archive_path), internal_ref],
+ cwd=root,
+ timeout=120,
+ )
+ return extract_image_files(archive_path)
+
+
+def extract_image_files(archive_path: Path) -> dict[str, bytes]:
+ """Extract bounded artifact files from a Docker image archive."""
+ try:
+ if archive_path.stat().st_size > MAX_IMAGE_ARCHIVE_BYTES:
+ raise VerifierError("image_archive_size_exceeded")
+ with tarfile.open(archive_path, mode="r:") as outer:
+ manifest_member = outer.getmember("manifest.json")
+ manifest_file = outer.extractfile(manifest_member)
+ if manifest_file is None:
+ raise VerifierError("image_manifest_missing")
+ manifests = json.loads(manifest_file.read(1_048_577))
+ if not isinstance(manifests, list) or len(manifests) != 1:
+ raise VerifierError("image_manifest_invalid")
+ layers = manifests[0].get("Layers")
+ if not isinstance(layers, list) or not layers:
+ raise VerifierError("image_layers_missing")
+ files: dict[str, bytes] = {}
+ total_bytes = 0
+ file_count = 0
+ for layer_name in layers:
+ if not isinstance(layer_name, str):
+ raise VerifierError("image_layer_name_invalid")
+ layer_path = PurePosixPath(layer_name)
+ if layer_path.is_absolute() or ".." in layer_path.parts:
+ raise VerifierError("image_layer_name_invalid")
+ layer_member = outer.getmember(layer_name)
+ layer_file = outer.extractfile(layer_member)
+ if layer_file is None:
+ raise VerifierError("image_layer_missing")
+ layer_bytes = layer_file.read(MAX_IMAGE_ARCHIVE_BYTES + 1)
+ if len(layer_bytes) > MAX_IMAGE_ARCHIVE_BYTES:
+ raise VerifierError("image_layer_size_exceeded")
+ with tarfile.open(fileobj=io.BytesIO(layer_bytes)) as layer:
+ for member in layer.getmembers():
+ path = PurePosixPath(member.name)
+ if (
+ path.is_absolute()
+ or ".." in path.parts
+ or member.issym()
+ or member.islnk()
+ or member.isdev()
+ ):
+ raise VerifierError("image_layer_unsafe_entry")
+ if member.isdir():
+ continue
+ if not member.isfile():
+ raise VerifierError("image_layer_unsupported_entry")
+ if any(part.startswith(".wh.") for part in path.parts):
+ raise VerifierError("image_layer_whiteout_not_allowed")
+ normalized = str(path).lstrip("./")
+ if not normalized.startswith("artifact/"):
+ raise VerifierError("image_layer_file_outside_artifact")
+ extracted = layer.extractfile(member)
+ if extracted is None:
+ raise VerifierError("image_layer_file_unreadable")
+ data = extracted.read(member.size + 1)
+ if len(data) != member.size:
+ raise VerifierError("image_layer_file_size_mismatch")
+ total_bytes += len(data)
+ file_count += 1
+ if (
+ total_bytes > MAX_LAYER_UNPACKED_BYTES
+ or file_count > MAX_LAYER_FILES
+ ):
+ raise VerifierError("image_layer_bounds_exceeded")
+ files[normalized] = data
+ except (OSError, KeyError, tarfile.TarError, json.JSONDecodeError) as exc:
+ raise VerifierError("image_archive_invalid") from exc
+ return files
+
+
+def verify_bundle_files(
+ files: dict[str, bytes],
+ *,
+ policy: dict[str, Any],
+ controller_receipt: dict[str, Any],
+) -> dict[str, Any]:
+ """Recompute immutable bundle evidence from image layer files."""
+ manifest_bytes = files.get("artifact/bundle-manifest.json")
+ sbom_bytes = files.get("artifact/sbom.cdx.json")
+ if manifest_bytes is None or sbom_bytes is None:
+ raise VerifierError("bundle_core_files_missing")
+ if _sha256_hex(manifest_bytes) != controller_receipt.get(
+ "bundle_manifest_sha256"
+ ) or _sha256_hex(sbom_bytes) != controller_receipt.get("cyclonedx_sbom_sha256"):
+ raise VerifierError("bundle_core_hash_mismatch")
+ try:
+ manifest = json.loads(manifest_bytes)
+ sbom = json.loads(sbom_bytes)
+ except json.JSONDecodeError as exc:
+ raise VerifierError("bundle_core_json_invalid") from exc
+ if not isinstance(manifest, dict) or not isinstance(sbom, dict):
+ raise VerifierError("bundle_core_json_invalid")
+ policy_checksum = "sha256:" + _sha256_hex(_canonical_json_bytes(policy))
+ if (
+ manifest.get("candidate_id") != policy.get("candidate_id")
+ or manifest.get("work_item_id") != policy.get("work_item_id")
+ or manifest.get("policy_checksum") != policy_checksum
+ or manifest.get("package_count") != 3
+ or manifest.get("operation_boundaries", {}).get("mcp_server_started")
+ is not False
+ or manifest.get("operation_boundaries", {}).get("browser_started")
+ is not False
+ or manifest.get("operation_boundaries", {}).get(
+ "runtime_request_or_response_body_stored"
+ )
+ is not False
+ ):
+ raise VerifierError("bundle_manifest_boundary_invalid")
+ expected = _expected_packages(policy)
+ package_rows = manifest.get("packages")
+ if not isinstance(package_rows, list) or len(package_rows) != len(expected):
+ raise VerifierError("bundle_package_set_invalid")
+ verified_package_names: list[str] = []
+ expected_file_paths = {
+ "artifact/bundle-manifest.json",
+ "artifact/sbom.cdx.json",
+ }
+ for row in package_rows:
+ if not isinstance(row, dict) or row.get("name") not in expected:
+ raise VerifierError("bundle_package_set_invalid")
+ package = expected[row["name"]]
+ tarball_path = row.get("tarball_path")
+ attestation_path = row.get("attestation_path")
+ if not isinstance(tarball_path, str) or not isinstance(attestation_path, str):
+ raise VerifierError("bundle_package_path_invalid")
+ tarball = files.get(f"artifact/{tarball_path}")
+ attestation = files.get(f"artifact/{attestation_path}")
+ if tarball is None or attestation is None:
+ raise VerifierError("bundle_package_file_missing")
+ expected_file_paths.update(
+ {f"artifact/{tarball_path}", f"artifact/{attestation_path}"}
+ )
+ if (
+ hashlib.sha512(tarball).hexdigest()
+ != package.get("slsa_subject_sha512_hex")
+ or hashlib.sha1(tarball, usedforsecurity=False).hexdigest()
+ != package.get("shasum_sha1")
+ or _sha256_hex(attestation) != row.get("attestation_sha256")
+ or row.get("npm_registry_signature_verified") is not True
+ or row.get("slsa_subject_verified") is not True
+ or row.get("archive_safety_verified") is not True
+ or row.get("vulnerability_ids") != []
+ ):
+ raise VerifierError("bundle_package_evidence_mismatch")
+ verify_bundled_npm_signature(policy, package, tarball)
+ verify_bundled_slsa_subject(package, attestation)
+ verified_package_names.append(row["name"])
+ if len(set(verified_package_names)) != len(expected):
+ raise VerifierError("bundle_package_set_invalid")
+ if set(files) != expected_file_paths:
+ raise VerifierError("bundle_file_set_invalid")
+
+ expected_refs = {
+ f"pkg:npm/{quote(name, safe='/')}@{row['version']}"
+ for name, row in expected.items()
+ }
+ components = sbom.get("components")
+ if (
+ sbom.get("bomFormat") != "CycloneDX"
+ or sbom.get("specVersion") != "1.5"
+ or not isinstance(components, list)
+ or {row.get("bom-ref") for row in components if isinstance(row, dict)}
+ != expected_refs
+ ):
+ raise VerifierError("bundle_sbom_invalid")
+ manifest_sbom = _require_dict(manifest.get("sbom"), "bundle_sbom_invalid")
+ if manifest_sbom.get("sha256") != _sha256_hex(sbom_bytes):
+ raise VerifierError("bundle_sbom_hash_mismatch")
+ return {
+ "artifact_file_count": len(files),
+ "verified_package_count": len(verified_package_names),
+ "bundle_manifest_sha256": _sha256_hex(manifest_bytes),
+ "cyclonedx_sbom_sha256": _sha256_hex(sbom_bytes),
+ }
+
+
+def build_verifier_receipt(
+ *,
+ identity: dict[str, Any],
+ controller_receipt_bytes: bytes,
+ bundle_evidence: dict[str, Any],
+) -> dict[str, Any]:
+ return {
+ "schema_version": VERIFIER_RECEIPT_SCHEMA_VERSION,
+ "run_id": identity["run_id"],
+ "trace_id": identity["trace_id"],
+ "work_item_id": identity["work_item_id"],
+ "candidate_id": identity["candidate_id"],
+ "status": "completed_internal_mirror_verified",
+ "observed_at": datetime.now(timezone.utc).isoformat(),
+ "policy_checksum": identity["policy_checksum"],
+ "controller_receipt_sha256": _sha256_hex(controller_receipt_bytes),
+ "internal_mirror_ref": identity["internal_ref"],
+ "runtime_mirror_ref": identity["runtime_ref"],
+ "internal_digest": identity["digest"],
+ "promotion_allowed": False,
+ "replay_allowed": False,
+ "shadow_allowed": False,
+ "canary_allowed": False,
+ "independent_post_verifier": {
+ "status": "passed",
+ "implementation": "standalone_no_controller_import",
+ "registry_digest_readback_verified": True,
+ "npm_registry_signatures_reverified": True,
+ "slsa_subjects_reverified": True,
+ "container_or_mcp_started": False,
+ **bundle_evidence,
+ },
+ "rollback_terminal": {
+ "status": "rollback_ready_not_promoted",
+ "action": "disable_candidate_and_retain_immutable_bundle",
+ "production_route_changed": False,
+ "external_artifact_delete_allowed": False,
+ },
+ "learning_writeback": {
+ "status": "verifier_receipt_ready_for_committed_writeback",
+ "external_rag_write_performed": False,
+ "raw_runtime_request_or_response_body_stored": False,
+ },
+ }
+
+
+def _write_receipt(path: Path, receipt: dict[str, Any]) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_bytes(_canonical_json_bytes(receipt) + b"\n")
+
+
+def _build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--policy", required=True, type=Path)
+ parser.add_argument("--controller-receipt", required=True, type=Path)
+ parser.add_argument("--verifier-receipt", required=True, type=Path)
+ return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = _build_parser().parse_args(argv)
+ policy = _read_json(args.policy, "policy_unreadable")
+ controller_receipt_bytes = args.controller_receipt.read_bytes()
+ if len(controller_receipt_bytes) > 4 * 1024 * 1024:
+ raise VerifierError("controller_receipt_too_large")
+ controller_receipt = _read_json(
+ args.controller_receipt,
+ "controller_receipt_unreadable",
+ )
+ identity = validate_controller_receipt(policy, controller_receipt)
+ files = read_image_files(identity["internal_ref"])
+ bundle_evidence = verify_bundle_files(
+ files,
+ policy=policy,
+ controller_receipt=controller_receipt,
+ )
+ receipt = build_verifier_receipt(
+ identity=identity,
+ controller_receipt_bytes=controller_receipt_bytes,
+ bundle_evidence=bundle_evidence,
+ )
+ _write_receipt(args.verifier_receipt, receipt)
+ print(json.dumps(receipt, ensure_ascii=False, sort_keys=True))
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(main())
+ except (OSError, VerifierError) as exc:
+ error_class = str(exc) if isinstance(exc, VerifierError) else "verifier_io_error"
+ print(
+ json.dumps(
+ {
+ "schema_version": VERIFIER_RECEIPT_SCHEMA_VERSION,
+ "status": "blocked_with_safe_next_action",
+ "error_class": error_class,
+ },
+ sort_keys=True,
+ ),
+ file=sys.stderr,
+ )
+ raise SystemExit(1) from None