Merge remote-tracking branch 'origin/main' into codex/p0-obs-002-20260715
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
**source/test evidence**:
|
||||
- controller/verifier 本機真實雙 replay 皆通過:MCP `Playwright@1.62.0-alpha-1783623505000`、protocol `2025-06-18`、24 tools、tool-name hash `12d23fea9d8d1a1de3f44863dc00cf393f0a2165323838cf93fed30a6a4cc237`、schema hash `c3737ced21147d0512b0400df5885c95a545bc915892b5c8f928cee78428ac99`,兩次均 clean exit/container removed。
|
||||
- 新 replay tests `32 passed`;artifact + replay supply-chain tests與 MCP control-plane/federation/version lifecycle/runner-health 聚焦回歸合計 `84 passed`。Ruff、py_compile、JSON/YAML parse、source-control owner guard與 `git diff --check` 通過。
|
||||
- 新 replay tests `37 passed`;artifact + replay supply-chain tests與 MCP control-plane/federation/version lifecycle/runner-health 聚焦回歸合計 `89 passed`。Ruff、py_compile、JSON/YAML parse、source-control owner guard與 `git diff --check` 通過。
|
||||
- global security mirror guard另抓到 latest main 既有 `apps/web/src/app/[locale]/iwooos/page.tsx` 的 `raw_blocked_waiting_state` 敏感字樣;本 work item未修改該檔,保留為獨立 drift blocker,不以 replay source 綠燈覆蓋。
|
||||
|
||||
**尚未完成 / 不誤報**:
|
||||
|
||||
@@ -52,6 +52,27 @@ EXPECTED_PACKAGE_KEYS = {
|
||||
"playwright@1.62.0-alpha-1783623505000",
|
||||
"playwright-core@1.62.0-alpha-1783623505000",
|
||||
}
|
||||
EXPECTED_WORK_ITEM_ID = "MCP-REPLAY-PLAYWRIGHT-001"
|
||||
EXPECTED_CANDIDATE_ID = "external.playwright-verifier"
|
||||
EXPECTED_ADAPTER_ID = "playwright_public_accessibility_verifier_v1"
|
||||
EXPECTED_OWNER_LANE = "awoooi_mcp_supply_chain"
|
||||
EXPECTED_ARTIFACT_REPOSITORY = (
|
||||
"registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp@"
|
||||
)
|
||||
EXPECTED_ARTIFACT_RUNTIME_REPOSITORY = (
|
||||
"192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp@"
|
||||
)
|
||||
EXPECTED_RUNTIME_REPOSITORY = "registry.wooo.work/awoooi/ci-runner@"
|
||||
EXPECTED_RUNTIME_ARGUMENTS = (
|
||||
"--headless",
|
||||
"--isolated",
|
||||
"--allowed-origins",
|
||||
"https://awoooi.wooo.work",
|
||||
"--output-dir",
|
||||
"/tmp/mcp-output",
|
||||
"--executable-path",
|
||||
"/nonexistent/browser",
|
||||
)
|
||||
|
||||
|
||||
class ReplayError(RuntimeError):
|
||||
@@ -203,6 +224,13 @@ def load_policy(path: Path) -> ReplayPolicy:
|
||||
raise ReplayError("replay_scope_invalid")
|
||||
if payload.get("risk_class") != "high":
|
||||
raise ReplayError("replay_risk_class_invalid")
|
||||
if (
|
||||
payload.get("work_item_id") != EXPECTED_WORK_ITEM_ID
|
||||
or payload.get("candidate_id") != EXPECTED_CANDIDATE_ID
|
||||
or payload.get("adapter_id") != EXPECTED_ADAPTER_ID
|
||||
or payload.get("owner_lane") != EXPECTED_OWNER_LANE
|
||||
):
|
||||
raise ReplayError("replay_asset_identity_invalid")
|
||||
|
||||
expiry_text = _require_text(payload.get("expires_at"), "replay_expiry_invalid")
|
||||
try:
|
||||
@@ -220,6 +248,8 @@ def load_policy(path: Path) -> ReplayPolicy:
|
||||
_require_text(payload.get(field), f"replay_{field}_invalid")
|
||||
|
||||
artifact = _require_dict(payload.get("artifact_source"), "artifact_source_invalid")
|
||||
if artifact.get("audit_branch") != "mcp-artifact-receipts":
|
||||
raise ReplayError("audit_branch_invalid")
|
||||
audit_commit = _require_text(artifact.get("audit_commit"), "audit_commit_invalid")
|
||||
if not GIT_SHA_PATTERN.fullmatch(audit_commit):
|
||||
raise ReplayError("audit_commit_invalid")
|
||||
@@ -235,32 +265,42 @@ def load_policy(path: Path) -> ReplayPolicy:
|
||||
raise ReplayError("package_integrity_invalid")
|
||||
normalized_integrities[key] = digest
|
||||
|
||||
artifact_ref = _validate_oci_ref(artifact.get("artifact_ref"), "artifact_ref_invalid")
|
||||
artifact_runtime_ref = _validate_oci_ref(
|
||||
artifact.get("artifact_runtime_ref"), "artifact_runtime_ref_invalid"
|
||||
)
|
||||
if not artifact_ref.startswith(EXPECTED_ARTIFACT_REPOSITORY):
|
||||
raise ReplayError("artifact_repository_not_allowlisted")
|
||||
if not artifact_runtime_ref.startswith(EXPECTED_ARTIFACT_RUNTIME_REPOSITORY):
|
||||
raise ReplayError("artifact_runtime_repository_not_allowlisted")
|
||||
|
||||
runtime = _require_dict(payload.get("runtime"), "runtime_invalid")
|
||||
runtime_image_ref = _validate_oci_ref(
|
||||
runtime.get("image_ref"), "runtime_image_ref_invalid"
|
||||
)
|
||||
if not runtime_image_ref.startswith(EXPECTED_RUNTIME_REPOSITORY):
|
||||
raise ReplayError("runtime_repository_not_allowlisted")
|
||||
if (
|
||||
runtime.get("network_mode") != "none"
|
||||
or runtime.get("read_only_root") is not True
|
||||
or runtime.get("no_new_privileges") is not True
|
||||
or runtime.get("cap_drop") != ["ALL"]
|
||||
or runtime.get("platform") != "linux/amd64"
|
||||
or runtime.get("user") != "65534:65534"
|
||||
or runtime.get("pids_limit") != 64
|
||||
or runtime.get("memory_limit") != "384m"
|
||||
or runtime.get("cpu_limit") != "0.5"
|
||||
or runtime.get("tmpfs") != "/tmp:rw,noexec,nosuid,size=32m"
|
||||
or runtime.get("startup_timeout_seconds") != 20
|
||||
or runtime.get("shutdown_timeout_seconds") != 5
|
||||
):
|
||||
raise ReplayError("runtime_isolation_invalid")
|
||||
entrypoint = _require_text_list(runtime.get("entrypoint"), "entrypoint_invalid")
|
||||
arguments = _require_text_list(runtime.get("arguments"), "arguments_invalid")
|
||||
if entrypoint != ("node", "node_modules/@playwright/mcp/cli.js"):
|
||||
raise ReplayError("entrypoint_not_allowlisted")
|
||||
required_arguments = {
|
||||
"--headless",
|
||||
"--isolated",
|
||||
"--allowed-origins",
|
||||
"https://awoooi.wooo.work",
|
||||
"--output-dir",
|
||||
"/tmp/mcp-output",
|
||||
"--executable-path",
|
||||
"/nonexistent/browser",
|
||||
}
|
||||
if set(arguments) != required_arguments or len(arguments) != 8:
|
||||
if arguments != EXPECTED_RUNTIME_ARGUMENTS:
|
||||
raise ReplayError("arguments_not_allowlisted")
|
||||
if runtime.get("user") != "65534:65534":
|
||||
raise ReplayError("runtime_user_invalid")
|
||||
|
||||
protocol = _require_dict(
|
||||
payload.get("protocol_contract"), "protocol_contract_invalid"
|
||||
@@ -332,10 +372,8 @@ def load_policy(path: Path) -> ReplayPolicy:
|
||||
artifact.get("verifier_receipt_sha256"),
|
||||
"verifier_receipt_sha256_invalid",
|
||||
),
|
||||
artifact_ref=_validate_oci_ref(artifact.get("artifact_ref"), "artifact_ref_invalid"),
|
||||
artifact_runtime_ref=_validate_oci_ref(
|
||||
artifact.get("artifact_runtime_ref"), "artifact_runtime_ref_invalid"
|
||||
),
|
||||
artifact_ref=artifact_ref,
|
||||
artifact_runtime_ref=artifact_runtime_ref,
|
||||
bundle_manifest_sha256=_validate_sha256(
|
||||
artifact.get("bundle_manifest_sha256"), "bundle_manifest_sha256_invalid"
|
||||
),
|
||||
@@ -343,9 +381,7 @@ def load_policy(path: Path) -> ReplayPolicy:
|
||||
artifact.get("cyclonedx_sbom_sha256"), "cyclonedx_sbom_sha256_invalid"
|
||||
),
|
||||
package_integrities=normalized_integrities,
|
||||
runtime_image_ref=_validate_oci_ref(
|
||||
runtime.get("image_ref"), "runtime_image_ref_invalid"
|
||||
),
|
||||
runtime_image_ref=runtime_image_ref,
|
||||
runtime_platform=_require_text(
|
||||
runtime.get("platform"), "runtime_platform_invalid"
|
||||
),
|
||||
|
||||
@@ -157,7 +157,23 @@ class ReplayPolicyTests(unittest.TestCase):
|
||||
path = self._mutated_policy(
|
||||
lambda payload: payload["runtime"].__setitem__("user", "0:0")
|
||||
)
|
||||
with self.assertRaisesRegex(controller.ReplayError, "runtime_user_invalid"):
|
||||
with self.assertRaisesRegex(controller.ReplayError, "runtime_isolation_invalid"):
|
||||
controller.load_policy(path)
|
||||
|
||||
def test_runtime_memory_limit_cannot_be_weakened(self) -> None:
|
||||
path = self._mutated_policy(
|
||||
lambda payload: payload["runtime"].__setitem__("memory_limit", "2g")
|
||||
)
|
||||
with self.assertRaisesRegex(controller.ReplayError, "runtime_isolation_invalid"):
|
||||
controller.load_policy(path)
|
||||
|
||||
def test_runtime_argument_order_is_part_of_allowlist(self) -> None:
|
||||
def mutate(payload: dict) -> None:
|
||||
arguments = payload["runtime"]["arguments"]
|
||||
arguments[2], arguments[4] = arguments[4], arguments[2]
|
||||
|
||||
path = self._mutated_policy(mutate)
|
||||
with self.assertRaisesRegex(controller.ReplayError, "arguments_not_allowlisted"):
|
||||
controller.load_policy(path)
|
||||
|
||||
def test_floating_runtime_reference_is_rejected(self) -> None:
|
||||
@@ -178,6 +194,13 @@ class ReplayPolicyTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(controller.ReplayError, "execution_boundary_invalid"):
|
||||
controller.load_policy(path)
|
||||
|
||||
def test_candidate_identity_cannot_be_retargeted(self) -> None:
|
||||
path = self._mutated_policy(
|
||||
lambda payload: payload.__setitem__("candidate_id", "external.other")
|
||||
)
|
||||
with self.assertRaisesRegex(controller.ReplayError, "replay_asset_identity_invalid"):
|
||||
controller.load_policy(path)
|
||||
|
||||
def test_dangerous_tool_cannot_leave_denied_partition(self) -> None:
|
||||
def mutate(payload: dict) -> None:
|
||||
payload["protocol_contract"]["adapter_denied_tools"].remove(
|
||||
|
||||
@@ -154,6 +154,26 @@ class StandalonePolicyTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(verifier.VerifierError, "artifact_ref_invalid"):
|
||||
verifier.load_and_validate_policy(path)
|
||||
|
||||
def test_runtime_resource_limits_cannot_be_weakened(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
payload = _policy_payload()
|
||||
payload["runtime"]["pids_limit"] = 512
|
||||
path = Path(temp) / "policy.json"
|
||||
_write(path, payload)
|
||||
with self.assertRaisesRegex(verifier.VerifierError, "runtime_isolation_invalid"):
|
||||
verifier.load_and_validate_policy(path)
|
||||
|
||||
def test_runtime_argument_order_drift_is_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
payload = _policy_payload()
|
||||
payload["runtime"]["arguments"] = list(
|
||||
reversed(payload["runtime"]["arguments"])
|
||||
)
|
||||
path = Path(temp) / "policy.json"
|
||||
_write(path, payload)
|
||||
with self.assertRaisesRegex(verifier.VerifierError, "runtime_isolation_invalid"):
|
||||
verifier.load_and_validate_policy(path)
|
||||
|
||||
|
||||
class ControllerReceiptTests(unittest.TestCase):
|
||||
def test_valid_controller_receipt_passes_standalone_validation(self) -> None:
|
||||
|
||||
@@ -41,6 +41,16 @@ MAX_JSON_BYTES = 4 * 1024 * 1024
|
||||
MAX_PROTOCOL_LINE_BYTES = 2 * 1024 * 1024
|
||||
MAX_FILES = 4_096
|
||||
MAX_UNPACKED_BYTES = 40 * 1024 * 1024
|
||||
EXPECTED_RUNTIME_ARGUMENTS = [
|
||||
"--headless",
|
||||
"--isolated",
|
||||
"--allowed-origins",
|
||||
"https://awoooi.wooo.work",
|
||||
"--output-dir",
|
||||
"/tmp/mcp-output",
|
||||
"--executable-path",
|
||||
"/nonexistent/browser",
|
||||
]
|
||||
|
||||
|
||||
class VerifierError(RuntimeError):
|
||||
@@ -113,6 +123,11 @@ def load_and_validate_policy(path: Path) -> tuple[dict[str, Any], str]:
|
||||
or policy.get("risk_class") != "high"
|
||||
or policy.get("scope")
|
||||
!= "offline_protocol_and_tool_schema_compatibility_only"
|
||||
or policy.get("work_item_id") != "MCP-REPLAY-PLAYWRIGHT-001"
|
||||
or policy.get("candidate_id") != "external.playwright-verifier"
|
||||
or policy.get("adapter_id")
|
||||
!= "playwright_public_accessibility_verifier_v1"
|
||||
or policy.get("owner_lane") != "awoooi_mcp_supply_chain"
|
||||
):
|
||||
raise VerifierError("policy_contract_invalid")
|
||||
try:
|
||||
@@ -134,12 +149,34 @@ def load_and_validate_policy(path: Path) -> tuple[dict[str, Any], str]:
|
||||
or runtime.get("cap_drop") != ["ALL"]
|
||||
or runtime.get("user") != "65534:65534"
|
||||
or runtime.get("platform") != "linux/amd64"
|
||||
or runtime.get("pids_limit") != 64
|
||||
or runtime.get("memory_limit") != "384m"
|
||||
or runtime.get("cpu_limit") != "0.5"
|
||||
or runtime.get("tmpfs") != "/tmp:rw,noexec,nosuid,size=32m"
|
||||
or runtime.get("startup_timeout_seconds") != 20
|
||||
or runtime.get("shutdown_timeout_seconds") != 5
|
||||
or runtime.get("entrypoint")
|
||||
!= ["node", "node_modules/@playwright/mcp/cli.js"]
|
||||
or runtime.get("arguments") != EXPECTED_RUNTIME_ARGUMENTS
|
||||
):
|
||||
raise VerifierError("runtime_isolation_invalid")
|
||||
artifact_ref = _oci_ref(artifact.get("artifact_ref"), "artifact_ref_invalid")
|
||||
runtime_ref = _oci_ref(runtime.get("image_ref"), "runtime_ref_invalid")
|
||||
if artifact_ref == runtime_ref:
|
||||
raise VerifierError("artifact_runtime_separation_invalid")
|
||||
artifact_runtime_ref = _oci_ref(
|
||||
artifact.get("artifact_runtime_ref"), "artifact_runtime_ref_invalid"
|
||||
)
|
||||
if (
|
||||
artifact_ref == runtime_ref
|
||||
or not artifact_ref.startswith(
|
||||
"registry.wooo.work/awoooi/mcp-artifacts/playwright-mcp@"
|
||||
)
|
||||
or not artifact_runtime_ref.startswith(
|
||||
"192.168.0.110:5000/awoooi/mcp-artifacts/playwright-mcp@"
|
||||
)
|
||||
or not runtime_ref.startswith("registry.wooo.work/awoooi/ci-runner@")
|
||||
or artifact.get("audit_branch") != "mcp-artifact-receipts"
|
||||
):
|
||||
raise VerifierError("artifact_runtime_repository_contract_invalid")
|
||||
for field in (
|
||||
"controller_receipt_sha256",
|
||||
"verifier_receipt_sha256",
|
||||
|
||||
Reference in New Issue
Block a user