diff --git a/scripts/pre1_harness_binding.py b/scripts/pre1_harness_binding.py index 9a3a0db..a00cde1 100644 --- a/scripts/pre1_harness_binding.py +++ b/scripts/pre1_harness_binding.py @@ -17,6 +17,10 @@ "scripts/test_pre1_qualification_case.py", "scripts/pre1_harness_binding.py", "scripts/pre1_environment_contract.py", + "scripts/pre1_package_execution.py", + "scripts/test_pre1_package_execution.py", + "scripts/run_php83_local_ci.py", + "scripts/test_php83_local_ci.py", }) diff --git a/scripts/pre1_package_execution.py b/scripts/pre1_package_execution.py new file mode 100644 index 0000000..44320e2 --- /dev/null +++ b/scripts/pre1_package_execution.py @@ -0,0 +1,881 @@ +"""Bind staged assertions to byte-verified installed frozen SDK payloads. + +Preparation never installs dependencies or grants qualification credit. The +caller owns network isolation, runtime selection and workspace lifetime. +""" +from __future__ import annotations + +import hashlib +import json +import os +import re +import stat +import subprocess +import tarfile +import tempfile +import zipfile +from pathlib import Path + +SCHEMA = "iicp.pre1-package-execution.v1" +BINDINGS = ( + "candidate_manifest_sha256", "artifact_materialization_sha256", + "runtime_map_sha256", "qualification_environment_sha256", +) +PYTHON_GUARD = '''import os, sys +from pathlib import Path +import pytest + +def check_origin(): + import iicp_client + root = Path(os.environ["IICP_PRE1_INSTALLED_PACKAGE"]).resolve() + for name, module in tuple(sys.modules.items()): + if name == "iicp_client" or name.startswith("iicp_client."): + origin = getattr(module, "__file__", None) + if origin is None or not Path(origin).resolve().is_relative_to(root): + raise RuntimeError("packaged Python import origin differs") + +def pytest_sessionstart(session): + check_origin() + +reports = [] + +def pytest_runtest_logreport(report): + reports.append(report) + +def pytest_sessionfinish(session, exitstatus): + calls = [report for report in reports if report.when == "call"] + if len(calls) != 1 or any(not r.passed or hasattr(r, "wasxfail") for r in reports): + session.exitstatus = 2 + check_origin() + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_call(item): + check_origin() + yield + check_origin() +''' + + +def digest(value: object) -> str: + return "sha256:" + hashlib.sha256(json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, + ).encode()).hexdigest() + + +def file_digest(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) + return "sha256:" + h.hexdigest() + + +def safe_path(path: Path) -> Path: + if not path.is_absolute() or not path.exists(): + raise ValueError("package execution path must exist and be absolute") + if any(unsafe_link(p) for p in (path, *path.parents)): + raise ValueError("package execution path contains a symlink") + return path.resolve() + + +def unsafe_link(path: Path) -> bool: + attributes = getattr(path.lstat(), "st_file_attributes", 0) + return path.is_symlink() or bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)) + + +def tree(path: Path) -> dict[str, str]: + safe_path(path) + result = {} + for item in sorted(path.rglob("*")): + if unsafe_link(item): + raise ValueError("package execution tree contains a symlink") + if item.is_file(): + if "__pycache__" in item.parts or item.suffix == ".pyc": + continue + result[item.relative_to(path).as_posix()] = file_digest(item) + elif not item.is_dir(): + raise ValueError("package execution tree contains a special file") + return result + + +def installed_payload(artifact: Path, installed: Path, component: str) -> dict[str, str]: + """Require exact package-file equality, not a self-reported install receipt.""" + safe_path(artifact) + readers = {"client-python": wheel_payload, "client-typescript": tarball_payload} + if component not in readers: + raise ValueError("no packaged adapter for this component") + expected = readers[component](artifact) + if not expected or any( + Path(name).is_absolute() or ".." in Path(name).parts for name in expected + ) or tree(installed) != expected: + raise ValueError("installed SDK differs from the frozen package payload") + return expected + + + +def wheel_payload(artifact: Path) -> dict[str, str]: + expected = {} + with zipfile.ZipFile(artifact, "r") as archive: + for row in archive.infolist(): + if row.filename.startswith("iicp_client/") and not row.is_dir(): + name = row.filename.removeprefix("iicp_client/") + expected[name] = "sha256:" + hashlib.sha256(archive.read(row)).hexdigest() + return expected + + +def tarball_payload(artifact: Path) -> dict[str, str]: + expected = {} + with tarfile.open(artifact, "r:gz") as archive: + for row in archive.getmembers(): + if row.isfile() and row.name.startswith("package/"): + name = row.name.removeprefix("package/") + handle = archive.extractfile(row) + if handle is None: + raise ValueError("package archive file is unavailable") + expected[name] = "sha256:" + hashlib.sha256(handle.read()).hexdigest() + elif row.issym() or row.islnk(): + raise ValueError("package archive contains a link") + return expected + + +def dependencies(workspace: Path, installed: Path, component: str) -> dict[str, str]: + base = installed.parent if component == "client-python" else workspace / "node_modules" + rows = {} + safe_path(base) + for item in sorted(base.rglob("*")): + name = item.relative_to(base).as_posix() + if "__pycache__" in item.parts or item.suffix == ".pyc": + continue + if unsafe_link(item): + if not item.is_symlink() or component != "client-typescript" or not name.startswith(".bin/") or not item.resolve().is_relative_to(base): + raise ValueError("test dependency contains an unsafe link") + rows[name] = digest({"link": os.readlink(item), "target_sha256": file_digest(item.resolve())}) + elif item.is_file(): + rows[name] = file_digest(item) + elif not item.is_dir(): + raise ValueError("test dependency contains a special file") + return rows + + +def rewrite_typescript(text: str) -> str: + """Redirect literal runtime/worker paths only; leave assertions unchanged.""" + def replace(match): + prefix, name = match.groups() + if ".." in Path(name).parts: + raise ValueError("unsafe TypeScript source reference") + name = re.sub(r"\.ts$", ".js", name) + if not Path(name).suffix: + name += ".js" + return f"{prefix}node_modules/@iicp/client/dist/{name}" + return re.sub(r"(\.{1,2}/)src/([A-Za-z0-9_./-]+)", replace, text) + + +def packaged_assertions(name: str, text: str) -> str: + """Strengthen three reviewed source fixtures without changing their vectors. + + Crypto cases use the same canonical runtime API already exercised by each + SDK's runtime-verifier suite. No test-local eligibility engine survives. + Version qualification observes the compiled CLI, not its source spelling. + These substitutions are fixture-digest and harness-commit bound. + """ + if name == "tests/test_dispatch_ticket_trust_crypto.py": + start = text.index("def _decision(vector: dict,") + end = text.index("def _assert_fixture_decision", start) + replacement = '''from iicp_client.dispatch_ticket_trust import ( + LocalReplayCache, TicketBindings, TrustBundle, verify_dispatch_ticket_v2, +) + +def _decision(vector: dict, keys: dict[str, dict], signature_valid: bool) -> str: + claims = vector["claims"] + bundle = TrustBundle.from_dict({ + "bundle_version": 4, + "keys": [keys[key_id] for key_id in vector["trust_bundle_key_ids"]], + }) + replay = LocalReplayCache() + if vector["jti_seen"]: + replay.remember(claims["jti"], claims["expires_at"]) + return verify_dispatch_ticket_v2( + claims, vector["signature_b64url"], bundle, + TicketBindings(claims["issuer"], claims["provider_id"], claims["intent"], claims["constraints_digest"]), + now=vector["now"], minimum_bundle_version=4, replay_cache=replay, + ).code + + +''' + return text[:start] + replacement + text[end:] + if name == "tests/dispatch_ticket_trust_crypto.test.ts": + start = text.index("function decision(vector: any,") + end = text.index("function assertFixtureDecision", start) + replacement = '''import { LocalDispatchReplayCache, verifyDispatchTicketV2 } from "../node_modules/@iicp/client/dist/dispatch_ticket_trust.js"; + +function decision(vector: any, keys: Map, signatureValid: boolean): string { + const replayCache = new LocalDispatchReplayCache(); + if (vector.jti_seen) replayCache.remember(vector.claims.jti, vector.claims.expires_at); + return verifyDispatchTicketV2( + vector.claims, vector.signature_b64url, + { bundle_version: 4, keys: vector.trust_bundle_key_ids.map((id: string) => keys.get(id)) }, + { issuer: vector.claims.issuer, provider_id: vector.claims.provider_id, + intent: vector.claims.intent, constraints_digest: vector.claims.constraints_digest }, + { now: vector.now, minimumBundleVersion: 4, replayCache }, + ).code; +} + +''' + return text[:start] + replacement + text[end:] + if name == "tests/test_service_lifecycle.py": + source_override = ' env = {**os.environ, "PYTHONPATH": str(Path(__file__).parents[1] / "src")}' + if text.count(source_override) != 1: + raise ValueError("reviewed lifecycle subprocess fixture shape differs") + return text.replace(source_override, " env = dict(os.environ)") + if name == "tests/pre1_release_boundaries.test.ts": + lines = text.splitlines() + source_check = [line for line in lines if "assert.match(cli," in line] + source_read = [line for line in lines if line.startswith("const cli = ")] + if len(source_check) != 1 or len(source_read) != 1: + raise ValueError("reviewed CLI source fixture shape differs") + text = text.replace(source_read[0], 'import { spawnSync } from "node:child_process";\nimport { fileURLToPath } from "node:url";') + return text.replace(source_check[0], ''' const version = spawnSync(process.execPath, [fileURLToPath(new URL("../node_modules/@iicp/client/dist/cli.js", import.meta.url)), "--version"], { encoding: "utf8" }); + assert.equal(version.status, 0); + assert.equal(version.stdout.trim(), `iicp-node ${pkg.version}`);''') + return text + + +def fixture_tree(workspace: Path) -> dict[str, str]: + rows = {} + for name in ("tests", "parity", "scripts", ".github"): + if (workspace / name).exists(): + rows.update({f"{name}/{p}": h for p, h in tree(workspace / name).items()}) + for name in ("package.json", "package-lock.json", "pyproject.toml", "uv.lock", "pre1_origin_guard.py"): + if (workspace / name).exists(): + safe_path(workspace / name) + rows[name] = file_digest(workspace / name) + if (workspace / "src").exists() or (workspace / "iicp_client").exists(): + raise ValueError("staged workspace contains checkout runtime source") + return rows + + +def assertion_files(root: Path, component: str) -> dict[str, bytes]: + metadata = {"pyproject.toml", "uv.lock", "package.json", "package-lock.json", + "scripts/run_sdk_quality.py", "scripts/run-sdk-quality.mjs", + ".github/workflows/release.yml"} + result = {} + files = subprocess.check_output(["git", "ls-files", "-z"], cwd=root).decode().split("\0") + for name in filter(None, files): + if not (name.startswith(("tests/", "parity/")) or name in metadata): + continue + source = safe_path(root / name) + if component == "client-typescript" and name.endswith(".ts"): + result[name] = packaged_assertions(name, rewrite_typescript(source.read_text())).encode() + elif component == "client-python" and name.endswith(".py"): + result[name] = packaged_assertions(name, source.read_text()).encode() + else: + result[name] = source.read_bytes() + if component == "client-python": + result["pre1_origin_guard.py"] = PYTHON_GUARD.encode() + return result + + +def stage(root: Path, workspace: Path, component: str) -> dict[str, str]: + """Copy only Git-bound fixtures/metadata; never copy SDK runtime sources.""" + root, workspace = safe_path(root), safe_path(workspace) + if workspace == root or workspace.is_relative_to(root): + raise ValueError("package workspace must be outside the checkout") + if fixture_tree(workspace): + raise ValueError("package assertion staging is not empty") + for name, data in assertion_files(root, component).items(): + dest = workspace / name + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data) + return fixture_tree(workspace) + + + +def validate_immutable_bindings(bindings: dict) -> None: + if set(bindings) != set(BINDINGS) or any( + re.fullmatch(r"sha256:[0-9a-f]{64}", str(v)) is None for v in bindings.values() + ): + raise ValueError("package execution immutable bindings differ") + +def create_binding(root: Path, workspace: Path, installed: Path, artifact: Path, + component: str, runtime: str, target: str, bindings: dict, + vendor_artifact: Path | None = None) -> dict: + if component == "management": + return create_management_binding(root, workspace, installed, artifact, runtime, target, bindings) + if component == "client-rust": + return create_rust_binding(root, workspace, installed, artifact, runtime, target, bindings, vendor_artifact) + validate_immutable_bindings(bindings) + if not safe_path(installed).is_relative_to(safe_path(workspace)): + raise ValueError("installed package must be in the run workspace") + payload = installed_payload(artifact, installed, component) + fixtures = fixture_tree(workspace) or stage(root, workspace, component) + expected_fixtures = {name: "sha256:" + hashlib.sha256(data).hexdigest() + for name, data in assertion_files(root, component).items()} + if fixtures != expected_fixtures: + raise ValueError("staged assertions differ from the reviewed source mapping") + value = { + "schema": SCHEMA, "component": component, "runtime": runtime, "target": target, + "bindings": bindings, "workspace": str(workspace), "installed_package": str(installed), + "artifact_sha256": file_digest(artifact), "installed_payload_sha256": digest(payload), + "fixtures_sha256": digest(fixtures), "binding_sha256": None, + "test_dependencies_sha256": digest(dependencies(workspace, installed, component)), + "assertion_adapter": "canonical-runtime-verifier-and-cli.v1", + "non_authorizing": True, "qualification_credit": False, + } + value["binding_sha256"] = digest(value) + return value + + + +def validate_binding_identity(value: dict) -> None: + copy = dict(value) + copy["binding_sha256"] = None + if value.get("schema") != SCHEMA or value.get("binding_sha256") != digest(copy): + raise ValueError("package execution binding digest differs") + if value.get("non_authorizing") is not True or value.get("qualification_credit") is not False: + raise ValueError("package execution binding cannot authorize or grant credit") + if value.get("assertion_adapter") != "canonical-runtime-verifier-and-cli.v1": + raise ValueError("package assertion adapter binding differs") + + +def validate_binding_context(value: dict, context: dict) -> None: + if any(value.get(k) != context[k] for k in ("component", "runtime", "target")) or value.get("bindings") != {k: context[k] for k in BINDINGS}: + raise ValueError("package execution candidate/environment/runtime binding differs") + +def validate_binding(value: dict, context: dict, artifact: Path, root: Path, + vendor_artifact: Path | None = None) -> Path: + if context["component"] == "management": + return validate_management_binding(value, context, artifact, root) + if context["component"] == "client-rust": + return validate_rust_binding(value, context, artifact, root, vendor_artifact) + validate_binding_identity(value) + validate_binding_context(value, context) + workspace = safe_path(Path(value["workspace"])) + home = safe_path(Path(os.environ["HOME"])) + installed = safe_path(Path(value["installed_package"])) + validate_workspace_boundary(workspace, home, installed, root) + if value["artifact_sha256"] != file_digest(artifact) or value["installed_payload_sha256"] != digest(installed_payload(artifact, installed, context["component"])): + raise ValueError("package execution installed artifact binding differs") + if value["fixtures_sha256"] != digest(fixture_tree(workspace)): + raise ValueError("package assertion fixtures changed") + expected = {name: "sha256:" + hashlib.sha256(data).hexdigest() + for name, data in assertion_files(root, context["component"]).items()} + if fixture_tree(workspace) != expected: + raise ValueError("package assertions differ from the reviewed source mapping") + if value["test_dependencies_sha256"] != digest(dependencies(workspace, installed, context["component"])): + raise ValueError("package test dependencies changed") + return workspace + + + +def validate_workspace_boundary(workspace: Path, home: Path, installed: Path, root: Path) -> None: + if not workspace.is_relative_to(home) or workspace == home or workspace.is_relative_to(root.resolve()) or not installed.is_relative_to(workspace): + raise ValueError("package execution workspace is not run-isolated") + +def package_command(root: Path, context: dict, component_manifest: dict, + artifact_root: Path, argv: list[str], env: dict) -> tuple[list[str], dict, Path, dict]: + path = safe_path(Path(os.environ["IICP_PRE1_PACKAGE_EXECUTION_BINDING"])) + value = json.loads(path.read_text()) + if value.get("binding_sha256") != os.environ.get("IICP_PRE1_PACKAGE_EXECUTION_SHA256"): + raise ValueError("package execution binding pin differs") + if context["component"] == "client-rust": + return rust_package_command(root, context, component_manifest, artifact_root, argv, env, value) + if context["component"] == "management": + rows = [r for r in component_manifest["artifacts"] if r["kind"] == "crate"] + if len(rows) != 1: + raise ValueError("Management candidate crate is ambiguous") + artifact = safe_path(artifact_root / "management" / rows[0]["name"]) + if file_digest(artifact) != rows[0]["sha256"]: + raise ValueError("Management candidate crate digest differs") + consumer = validate_management_binding(value, context, artifact, root) + home = safe_path(Path(env["HOME"])) + cargo_home = home / "rust-cargo-home" + cargo_home.mkdir(mode=0o700, exist_ok=True) + reject_inherited_cargo_config(home, safe_path(cargo_home)) + env = {**env, "CARGO_HOME": str(cargo_home), "CARGO_NET_OFFLINE": "true", "CARGO_INCREMENTAL": "0"} + argv = [*argv[:3], "--offline", *argv[3:]] + return argv, env, consumer, {"value": value, "artifact": artifact, "vendor_artifact": None} + kind = "wheel" if context["component"] == "client-python" else "npm-tarball" + artifacts = [r for r in component_manifest["artifacts"] if r["kind"] == kind] + if len(artifacts) != 1: + raise ValueError("candidate SDK install artifact is ambiguous") + artifact = artifact_root / context["component"] / artifacts[0]["name"] + workspace = validate_binding(value, context, artifact, root) + if context["component"] == "client-python": + argv = [*argv[:3], "-p", "pre1_origin_guard", *argv[3:]] + env["PYTHONPATH"] = os.pathsep.join((str(Path(value["installed_package"]).parent), str(workspace))) + env["IICP_PRE1_INSTALLED_PACKAGE"] = value["installed_package"] + env["PYTHONNOUSERSITE"] = "1" + env["PYTHONDONTWRITEBYTECODE"] = "1" + else: + expected = workspace / "node_modules/@iicp/client" + if Path(value["installed_package"]) != expected: + raise ValueError("TypeScript installed module path differs") + return argv, env, workspace, {"value": value, "artifact": artifact} + + +def execution_summary(value: dict) -> dict: + """Portable evidence; private execution paths never enter a receipt.""" + keys = ("component", "runtime", "target", "bindings", "artifact_sha256", + "installed_payload_sha256", "fixtures_sha256", "test_dependencies_sha256") + return {"schema": "iicp.pre1-package-execution-summary.v1", + **{key: value[key] for key in keys}, + "package_execution_sha256": value["binding_sha256"], + "non_authorizing": True} + + +def make_case_proof(value: dict, context: dict, assertion: str, exit_code: int, + run_id: str) -> dict: + if not isinstance(exit_code, int) or isinstance(exit_code, bool): + raise ValueError("case proof exit code is invalid") + if not isinstance(run_id, str) or not re.fullmatch(r"[a-zA-Z0-9._-]+", run_id): + raise ValueError("case proof run identifier is invalid") + if not isinstance(assertion, str) or not assertion: + raise ValueError("case proof assertion is missing") + result = {"schema": "iicp.pre1-packaged-case-proof.v2", "run_id": run_id, + "execution": execution_summary(value), "context": context, + "assertion": assertion, "exit_code": exit_code, + "non_authorizing": True, "proof_sha256": None} + result["proof_sha256"] = digest(result) + return result + + +def validate_case_proof(value: dict, context: dict, exit_code: int, + run_id: str, expected_execution: dict | None = None, + expected_assertion: str | None = None) -> None: + validate_case_proof_identity(value) + validate_proof_result(value, context, exit_code, run_id) + summary = value["execution"] + if expected_execution is not None and summary != expected_execution: + raise ValueError("case proof installed execution differs") + validate_execution_summary(summary, context) + validate_proof_assertion(value, expected_assertion) + + +def validate_case_proof_identity(value: dict) -> None: + fields = {"schema", "run_id", "execution", "context", "assertion", "exit_code", + "non_authorizing", "proof_sha256"} + if not isinstance(value, dict) or set(value) != fields: + raise ValueError("case proof fields differ") + copy = {**value, "proof_sha256": None} + if value["schema"] != "iicp.pre1-packaged-case-proof.v2" or value["proof_sha256"] != digest(copy): + raise ValueError("case proof schema or digest differs") + validate_proof_result_fields(value) + + + +def validate_proof_result(value: dict, context: dict, exit_code: int, run_id: str) -> None: + if value["context"] != context or value["exit_code"] != exit_code or value["run_id"] != run_id: + raise ValueError("case proof execution context or result differs") + + +def validate_proof_assertion(value: dict, expected_assertion: str | None) -> None: + if not isinstance(value["assertion"], str) or not value["assertion"]: + raise ValueError("case proof assertion is missing") + if expected_assertion is not None and value["assertion"] != expected_assertion: + raise ValueError("case proof assertion differs from the owned mapping") + + + +def validate_proof_result_fields(value: dict) -> None: + if not isinstance(value["exit_code"], int) or isinstance(value["exit_code"], bool) or value["non_authorizing"] is not True: + raise ValueError("case proof authority or exit code differs") + if not isinstance(value["run_id"], str) or not re.fullmatch(r"[a-zA-Z0-9._-]+", value["run_id"]): + raise ValueError("case proof run identifier is invalid") +def validate_execution_summary(summary: dict, context: dict) -> None: + validate_summary_identity(summary) + if any(summary[key] != context[key] for key in ("component", "runtime", "target")): + raise ValueError("package execution summary target differs") + if summary["bindings"] != {key: context[key] for key in BINDINGS}: + raise ValueError("package execution summary immutable bindings differ") + + +def validate_summary_identity(summary: dict) -> None: + fields = {"schema", "component", "runtime", "target", "bindings", + "artifact_sha256", "installed_payload_sha256", "fixtures_sha256", + "test_dependencies_sha256", "package_execution_sha256", "non_authorizing"} + if not isinstance(summary, dict) or set(summary) != fields: + raise ValueError("package execution summary fields differ") + if summary["schema"] != "iicp.pre1-package-execution-summary.v1" or summary["non_authorizing"] is not True: + raise ValueError("package execution summary schema or authority differs") + for key in fields - {"schema", "component", "runtime", "target", "bindings", "non_authorizing"}: + if re.fullmatch(r"sha256:[0-9a-f]{64}", str(summary[key])) is None: + raise ValueError("package execution summary digest is invalid") + + +def write_case_proof(value: dict) -> Path: + """Publish a complete sidecar atomically, without overwriting earlier evidence.""" + path = Path(os.environ["IICP_PRE1_CASE_PROOF_OUTPUT"]) + home = safe_path(Path(os.environ["HOME"])) + parent = safe_path(path.parent) + if not path.is_absolute() or not parent.is_relative_to(home) or path.exists() or path.is_symlink(): + raise ValueError("case proof output is unsafe or already exists") + descriptor, temporary = tempfile.mkstemp(prefix=".case-proof-", dir=parent) + try: + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + json.dump(value, handle, sort_keys=True) + handle.write("\n") + handle.flush() + os.fsync(handle.fileno()) + os.link(temporary, path) + finally: + Path(temporary).unlink(missing_ok=True) + return path + + +# Rust uses the unchanged crate payload, including its packaged assertions. +# The vendor bundle supplies only dependency/config files, never runtime source. +def rust_archive_files(artifact: Path, prefix: str) -> dict[str, str]: + safe_path(artifact) + result, seen, total = {}, set(), 0 + with tarfile.open(artifact, "r|gz") as archive: + for row in archive: + name = row.name.rstrip("/") + total = validate_rust_archive_member(row, name, seen, total) + if row.isfile() and name.startswith(prefix): + handle = archive.extractfile(row) + if handle is None: + raise ValueError("Rust archive member is unavailable") + h = hashlib.sha256() + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + h.update(chunk) + result[name.removeprefix(prefix)] = "sha256:" + h.hexdigest() + if not result: + raise ValueError("Rust archive payload is empty") + return result + + + + +def validate_rust_archive_name(name): + if not name or name.startswith("/") or "\\" in name or ":" in name: + raise ValueError("Rust archive contains an unsafe member name") + if any(part in {"", ".", ".."} for part in name.split("/")): + raise ValueError("Rust archive contains an unsafe member path") + + +def validate_rust_archive_member(row, name, seen, total): + validate_rust_archive_name(name) + if name in seen or not (row.isfile() or row.isdir()): + raise ValueError("Rust archive contains an unsafe or duplicate member") + seen.add(name) + total += row.size + if len(seen) > 20000 or total > 1024 * 1024 * 1024: + raise ValueError("Rust archive exceeds bounded extraction limits") + return total + + +def rust_dependencies(workspace: Path) -> dict[str, str]: + return {**{"vendor/" + k: v for k, v in tree(workspace / "vendor").items()}, + **{".cargo/" + k: v for k, v in tree(workspace / ".cargo").items()}} + + +def verify_rust_payload(workspace, installed, artifact, vendor_artifact): + if vendor_artifact is None: + raise ValueError("Rust vendor artifact must be candidate-bound") + crate = rust_archive_files(artifact, artifact.stem + "/") + if tree(installed) != crate: + raise ValueError("installed Rust SDK differs from the frozen crate") + source = rust_archive_files(vendor_artifact, "source/") + if source != crate: + raise ValueError("Rust vendor source differs from the frozen crate") + vendor = rust_archive_files(vendor_artifact, "vendor/") + config = rust_archive_files(vendor_artifact, ".cargo/") + expected = {**{"vendor/" + k: v for k, v in vendor.items()}, + **{".cargo/" + k: v for k, v in config.items()}} + if rust_dependencies(workspace) != expected: + raise ValueError("Rust vendor dependencies/config differ from the candidate") + return crate, expected + + +def rust_fixtures(root: Path, installed: Path) -> dict[str, str]: + mapping = "qualification/pre1-cases.json" + if (installed / mapping).read_bytes() != (root / mapping).read_bytes(): + raise ValueError("Rust packaged assertion mapping differs from reviewed source") + return {mapping: file_digest(installed / mapping), + **{"tests/" + k: v for k, v in tree(installed / "tests").items()}} + + +def create_rust_binding(root, workspace, installed, artifact, runtime, target, bindings, vendor_artifact): + validate_immutable_bindings(bindings) + validate_workspace_boundary(safe_path(workspace), safe_path(Path(os.environ["HOME"])), + safe_path(installed), root) + payload, deps = verify_rust_payload(workspace, installed, artifact, vendor_artifact) + value = {"schema": SCHEMA, "component": "client-rust", "runtime": runtime, + "target": target, "bindings": bindings, "workspace": str(workspace), + "installed_package": str(installed), "artifact_sha256": file_digest(artifact), + "vendor_artifact_sha256": file_digest(vendor_artifact), + "installed_payload_sha256": digest(payload), + "fixtures_sha256": digest(rust_fixtures(root, installed)), + "test_dependencies_sha256": digest(deps), "binding_sha256": None, + "assertion_adapter": "canonical-runtime-verifier-and-cli.v1", + "non_authorizing": True, "qualification_credit": False} + value["binding_sha256"] = digest(value) + return value + + +def validate_rust_binding(value, context, artifact, root, vendor_artifact): + validate_binding_identity(value) + validate_binding_context(value, context) + workspace = safe_path(Path(value["workspace"])) + installed = safe_path(Path(value["installed_package"])) + validate_workspace_boundary(workspace, safe_path(Path(os.environ["HOME"])), installed, root) + payload, deps = verify_rust_payload(workspace, installed, artifact, vendor_artifact) + expected = {"artifact_sha256": file_digest(artifact), + "vendor_artifact_sha256": file_digest(vendor_artifact), + "installed_payload_sha256": digest(payload), + "test_dependencies_sha256": digest(deps), + "fixtures_sha256": digest(rust_fixtures(root, installed))} + if any(value.get(k) != v for k, v in expected.items()): + raise ValueError("Rust package execution inputs changed") + return installed + + +def rust_artifacts(component, artifact_root): + paths = [] + for kind in ("crate", "vendored-offline-package"): + rows = [row for row in component["artifacts"] if row["kind"] == kind] + if len(rows) != 1: + raise ValueError("Rust candidate artifact is ambiguous") + path = safe_path(artifact_root / "client-rust" / rows[0]["name"]) + if file_digest(path) != rows[0]["sha256"]: + raise ValueError("Rust candidate artifact digest differs") + paths.append(path) + return paths + + +def rust_package_command(root, context, component, artifact_root, argv, env, value): + artifact, vendor = rust_artifacts(component, artifact_root) + installed = validate_binding(value, context, artifact, root, vendor) + workspace = safe_path(Path(value["workspace"])) + home = safe_path(Path(env["HOME"])) + cargo_home = home / "rust-cargo-home" + cargo_home.mkdir(mode=0o700, exist_ok=True) + safe_path(cargo_home) + target = home / "rust-build" / context["runtime"] + target.mkdir(mode=0o700, parents=True, exist_ok=True) + safe_path(target) + env = {**env, "CARGO_HOME": str(cargo_home), "CARGO_TARGET_DIR": str(target), + "CARGO_NET_OFFLINE": "true", "CARGO_INCREMENTAL": "0"} + # Cargo can otherwise discover unrelated configs above the run workspace. + if workspace.parent != home or installed.parent != workspace: + raise ValueError("Rust package workspace layout differs") + reject_inherited_cargo_config(home, cargo_home) + argv = [*argv[:3], "--offline", *argv[3:]] + return argv, env, installed, {"value": value, "artifact": artifact, "vendor_artifact": vendor} + + + +def extract_rust_packages(artifact: Path, vendor_artifact: Path, workspace: Path) -> Path: + """Extract only into an empty caller-owned workspace after validating both archives.""" + safe_path(workspace) + if any(workspace.iterdir()): + raise ValueError("Rust extraction workspace must be empty") + crate = rust_archive_files(artifact, artifact.stem + "/") + if crate != rust_archive_files(vendor_artifact, "source/"): + raise ValueError("Rust vendor source differs from the frozen crate") + rust_archive_files(vendor_artifact, "vendor/") + rust_archive_files(vendor_artifact, ".cargo/") + extract_rust_prefix(artifact, artifact.stem + "/", workspace / "source") + extract_rust_prefix(vendor_artifact, "vendor/", workspace / "vendor") + extract_rust_prefix(vendor_artifact, ".cargo/", workspace / ".cargo") + return safe_path(workspace / "source") + + +def extract_rust_prefix(artifact: Path, prefix: str, destination: Path) -> None: + destination.mkdir(mode=0o700) + with tarfile.open(artifact, "r|gz") as archive: + for row in archive: + if not row.isfile() or not row.name.startswith(prefix): + continue + extract_rust_file(archive, row, prefix, destination) + + +def extract_rust_file(archive, row, prefix, destination): + relative = row.name.removeprefix(prefix) + if Path(relative).is_absolute() or any(p in {"", ".", ".."} for p in relative.split("/")): + raise ValueError("Rust extraction path is unsafe") + path = destination / relative + path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + safe_path(path.parent) + handle = archive.extractfile(row) + if handle is None: + raise ValueError("Rust extraction member is unavailable") + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o700 if row.mode & 0o111 else 0o600) + with os.fdopen(fd, "wb") as output: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + output.write(chunk) + + +def reject_inherited_cargo_config(home, cargo_home): + for ancestor in (home, *home.parents): + for name in ("config", "config.toml"): + path = ancestor / ".cargo" / name + if path.exists() or path.is_symlink(): + raise ValueError("Rust inherited Cargo configuration is forbidden") + if (cargo_home / "config").exists() or (cargo_home / "config.toml").exists(): + raise ValueError("Rust inherited Cargo home configuration is forbidden") + + +def management_consumer_files(root: Path, payload: Path) -> dict[str, bytes]: + """Keep frozen runtime source separate from reviewed external assertions. + + Cargo disables automatic tests in the published Management crate. A + separate consumer manifest names only the existing mapped integration + fixtures and points its library/binaries at the untouched crate source. + No dependency, lockfile, assertion or runtime source is rewritten. + """ + root, payload = safe_path(root), safe_path(payload) + mapping = safe_path(root / "qualification/pre1-cases.json") + cases = json.loads(mapping.read_text()) + if cases.get("component") != "management" or cases.get("schema") != "iicp.pre1-component-case-map.v2": + raise ValueError("Management consumer case map differs") + names = set() + for case in [cases["support"], *cases["scenarios"].values()]: + command = case["command"] + if "--test" in command: + name = command[command.index("--test") + 1] + if not isinstance(name, str) or re.fullmatch(r"[a-z][a-z0-9_]*", name) is None: + raise ValueError("Management consumer test name is unsafe") + names.add(name) + tracked = set(subprocess.check_output(["git", "ls-files", "-z", "--", "tests"], cwd=root).decode().split("\0")) + files = {} + for name in sorted(names): + path = f"tests/{name}.rs" + if path not in tracked: + raise ValueError("Management consumer assertion is not Git-bound") + source = safe_path(root / path).read_text() + files[path] = management_assertions(path, source).encode() + for name in tree(payload): + if not name.startswith(("src/", "examples/")) and name != "Cargo.toml": + files[name] = (payload / name).read_bytes() + manifest = (payload / "Cargo.toml").read_text() + for path in re.findall(r'^path = "([^"\n]+)"$', manifest, flags=re.M): + validate_rust_archive_name(path) + if not path.startswith(("src/", "examples/")): + raise ValueError("Management consumer manifest source path differs") + manifest = re.sub(r'^path = "((?:src|examples)/[^"\n]+)"$', + lambda match: f'path = "../payload/{match[1]}"', manifest, flags=re.M) + for name in sorted(names): + manifest += f'\n[[test]]\nname = "{name}"\npath = "tests/{name}.rs"\n' + files["Cargo.toml"] = manifest.encode() + return files + + +def management_assertions(name, source): + """Validate pre-1 builder metadata; retain the older publisher assertions.""" + if name != "tests/release_manifest.rs": + return source + marker = ' let manifest: Value = serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap();' + optional = ' let Some(path) = env::var_os("IICP_RELEASE_MANIFEST") else {\n return;\n };' + if source.count(marker) != 1 or source.count(optional) != 1: + raise ValueError("Management release fixture shape differs") + branch = ''' + if manifest["schema"] == "iicp.pre1-management-release-manifest.v1" { + let candidate: Value = serde_json::from_str(&fs::read_to_string( + env::var_os("IICP_PRE1_CANDIDATE_MANIFEST").expect("candidate required") + ).unwrap()).unwrap(); + let component = candidate["components"].as_array().unwrap().iter() + .find(|row| row["id"] == "management").unwrap(); + assert_eq!(manifest["source_commit"], component["source_commit"]); + assert_eq!(manifest["version"], env!("CARGO_PKG_VERSION")); + assert_eq!(manifest["version"], component["source_version"]); + assert_eq!(manifest["product"], "iicp-management-core"); + assert_eq!(manifest["channel"], "developer-preview"); + assert_eq!(manifest["non_authorizing"], true); + for flag in ["publication_authorized", "deployment_authorized", "management_service", "directory_authority"] { + assert_eq!(manifest[flag], false); + } + assert_eq!(manifest["binaries"], json!(["iicp-management", "iicp-management-controller", "iicp-management-conformance"])); + assert_eq!(manifest.as_object().unwrap().len(), 11); + return; + } +''' + return source.replace(optional, ' let path = env::var_os("IICP_RELEASE_MANIFEST").expect("release manifest required");').replace(marker, marker + branch) + + +def stage_management_consumer(root: Path, artifact: Path, workspace: Path) -> dict: + """Prepare only; the caller owns dependency acquisition and isolation.""" + root, artifact, workspace = safe_path(root), safe_path(artifact), safe_path(workspace) + home = safe_path(Path(os.environ["HOME"])) + if workspace == home or not workspace.is_relative_to(home) or workspace.is_relative_to(root) or any(workspace.iterdir()): + raise ValueError("Management consumer workspace is not empty and run-isolated") + expected = rust_archive_files(artifact, artifact.stem + "/") + extract_rust_prefix(artifact, artifact.stem + "/", workspace / "payload") + payload = workspace / "payload" + if tree(payload) != expected: + raise ValueError("Management extracted crate differs") + files = management_consumer_files(root, payload) + consumer = workspace / "consumer" + consumer.mkdir(mode=0o700) + for name, data in files.items(): + path = consumer / name + path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + path.write_bytes(data) + return {"schema": "iicp.pre1-management-consumer.v1", + "artifact_sha256": file_digest(artifact), "payload_sha256": digest(expected), + "consumer_sha256": digest(tree(consumer)), + "non_authorizing": True, "qualification_credit": False} + + +def validate_management_consumer(root: Path, artifact: Path, workspace: Path, binding: dict) -> Path: + """Recheck artifact and reviewed assertions, even after a forged rehash.""" + workspace = safe_path(workspace) + home = safe_path(Path(os.environ["HOME"])) + if workspace == home or not workspace.is_relative_to(home) or workspace.is_relative_to(root.resolve()): + raise ValueError("Management consumer workspace is not run-isolated") + payload = safe_path(workspace / "payload") + consumer = safe_path(workspace / "consumer") + expected = rust_archive_files(artifact, artifact.stem + "/") + actual = tree(consumer) + fixtures = {name: "sha256:" + hashlib.sha256(data).hexdigest() + for name, data in management_consumer_files(root, payload).items()} + if binding != {"schema": "iicp.pre1-management-consumer.v1", + "artifact_sha256": file_digest(artifact), "payload_sha256": digest(expected), + "consumer_sha256": digest(actual), + "non_authorizing": True, "qualification_credit": False} or tree(payload) != expected or actual != fixtures: + raise ValueError("Management consumer artifact or reviewed fixtures changed") + return consumer + + +def management_consumer_binding(artifact, workspace): + return {"schema": "iicp.pre1-management-consumer.v1", + "artifact_sha256": file_digest(artifact), + "payload_sha256": digest(rust_archive_files(artifact, artifact.stem + "/")), + "consumer_sha256": digest(tree(workspace / "consumer")), + "non_authorizing": True, "qualification_credit": False} + + +def management_dependencies(workspace): + expected = ('[source.crates-io]\nreplace-with = "vendored-sources"\n\n' + '[source.vendored-sources]\ndirectory = ' + json.dumps(str(workspace / "vendor")) + '\n') + if tree(workspace / ".cargo") != {"config.toml": "sha256:" + hashlib.sha256(expected.encode()).hexdigest()}: + raise ValueError("Management offline Cargo configuration differs") + return rust_dependencies(workspace) + + +def create_management_binding(root, workspace, installed, artifact, runtime, target, bindings): + validate_immutable_bindings(bindings) + staged = management_consumer_binding(artifact, workspace) + if safe_path(installed) != validate_management_consumer(root, artifact, workspace, staged): + raise ValueError("Management consumer path differs") + value = {"schema": SCHEMA, "component": "management", "runtime": runtime, + "target": target, "bindings": bindings, "workspace": str(workspace), + "installed_package": str(installed), "artifact_sha256": staged["artifact_sha256"], + "installed_payload_sha256": staged["payload_sha256"], + "fixtures_sha256": staged["consumer_sha256"], + "test_dependencies_sha256": digest(management_dependencies(workspace)), + "binding_sha256": None, "assertion_adapter": "canonical-runtime-verifier-and-cli.v1", + "non_authorizing": True, "qualification_credit": False} + value["binding_sha256"] = digest(value) + return value + + +def validate_management_binding(value, context, artifact, root): + validate_binding_identity(value) + validate_binding_context(value, context) + workspace = safe_path(Path(value["workspace"])) + expected = create_management_binding(root, workspace, Path(value["installed_package"]), + artifact, context["runtime"], context["target"], {key: context[key] for key in BINDINGS}) + if value != expected: + raise ValueError("Management package execution inputs changed") + return safe_path(workspace / "consumer") diff --git a/scripts/run_pre1_qualification_case.py b/scripts/run_pre1_qualification_case.py index 865ce76..8e7c28d 100755 --- a/scripts/run_pre1_qualification_case.py +++ b/scripts/run_pre1_qualification_case.py @@ -15,8 +15,9 @@ sys.dont_write_bytecode = True sys.path.insert(0, str(Path(__file__).resolve().parent)) -from pre1_harness_binding import harness_identity, validate_harness_source -from pre1_environment_contract import validate_modern_environment +from pre1_environment_contract import validate_modern_environment # noqa: E402 +from pre1_harness_binding import harness_identity, validate_harness_source # noqa: E402 +from pre1_package_execution import package_command, validate_binding, make_case_proof, write_case_proof # noqa: E402 ROOT = Path(__file__).resolve().parents[1] COMPONENT = 'client-python' @@ -93,7 +94,9 @@ def description() -> dict: "scenarios": sorted(SCENARIO_COMMANDS), "network_policy": "isolated-fixtures-only", "evidence_policy": "digest-only", - "artifact_consumption": "verified-candidate-root", + "artifact_consumption": "verified-installed-package", + "package_execution_schema": "iicp.pre1-package-execution.v1", + "case_proof_schema": "iicp.pre1-packaged-case-proof.v2", "source_commit_binding": True, "supported_environment_schemas": [ "iicp.pre1-qualification-environment.v1", @@ -455,6 +458,8 @@ def command_environment(runtime_row: dict, runtime: str) -> dict[str, str]: if "IICP_PRE1_HARNESS_BINDING" in os.environ: env["IICP_PRE1_HARNESS_BINDING"] = os.environ["IICP_PRE1_HARNESS_BINDING"] env.update(runtime_row.get("env", {})) + primary = "python" if runtime.startswith("cpython-") else "node" + env["PATH"] = str(Path(runtime_row["programs"][primary]).parent) + os.pathsep + env.get("PATH", "") cache = Path(env["IICP_HOME"]) / "qualification-cache" / COMPONENT / runtime cache.mkdir(parents=True, exist_ok=True) env["CARGO_TARGET_DIR"] = str(cache / "cargo-target") @@ -509,13 +514,24 @@ def main() -> int: if args.scenario is not None and args.scenario not in SCENARIO_COMMANDS: parser.error("scenario is not owned by this component") try: - runtime, runtime_row, manifest, _context = validate_context( + runtime, runtime_row, manifest, context = validate_context( args.cell, args.scenario ) validate_runtime(runtime, runtime_row, manifest) template = SCENARIO_COMMANDS[args.scenario] if args.scenario else SUPPORT_COMMAND argv = expand_command(template, runtime_row) - result = subprocess.run(argv, cwd=ROOT, env=command_environment(runtime_row, runtime), check=False) + component = next(row for row in manifest["components"] if row["id"] == COMPONENT) + argv, env, workspace, proof = package_command( + ROOT, context, component, Path(os.environ["IICP_PRE1_ARTIFACT_ROOT"]), + argv, command_environment(runtime_row, runtime), + ) + result = subprocess.run(argv, cwd=workspace, env=env, check=False) + validate_binding(proof["value"], context, proof["artifact"], ROOT) + case = SCENARIO_CASES[args.scenario] if args.scenario else SUPPORT_CASE + sidecar = make_case_proof(proof["value"], context, case["assertion"], + result.returncode, os.environ["IICP_PRE1_RUN_ID"]) + write_case_proof(sidecar) + except (KeyError, OSError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as error: print(f"pre-1.0 {COMPONENT} case refused: {error}", file=sys.stderr) return 2 diff --git a/scripts/test_pre1_package_execution.py b/scripts/test_pre1_package_execution.py new file mode 100644 index 0000000..fa0d47e --- /dev/null +++ b/scripts/test_pre1_package_execution.py @@ -0,0 +1,477 @@ +"""Negative controls for installed-payload and staged-assertion provenance.""" +from __future__ import annotations + +import copy +import io +import json +import os +import subprocess +import tarfile +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +import pre1_package_execution as adapter + + +class PackageExecutionTests(unittest.TestCase): + def management_artifact(self): + self.workspace = self.home / "run/management" + self.workspace.mkdir(parents=True) + (self.root / "qualification").mkdir(exist_ok=True) + case = {"assertion": "test_fixture", "command": ["@cargo", "test", "--locked", "--test", "fixture", "test_fixture", "--", "--exact"]} + (self.root / "qualification/pre1-cases.json").write_text(json.dumps({ + "schema": "iicp.pre1-component-case-map.v2", "component": "management", + "support": case, "scenarios": {}})) + (self.root / "tests/fixture.rs").write_text("#[test]\nfn test_fixture() { assert!(true); }\n") + subprocess.run(["git", "add", "."], cwd=self.root, check=True) + artifact = self.home / "iicp-management-core-0.11.0.crate" + files = {"Cargo.toml": b'[package]\nname = "iicp-management-core"\nversion = "0.11.0"\nautotests = false\n[lib]\npath = "src/lib.rs"\n', + "Cargo.lock": b'unchanged lock', "src/lib.rs": b'pub fn frozen() {}', + "contracts/test.json": b'{"frozen":true}'} + with tarfile.open(artifact, "w:gz") as archive: + for name, data in files.items(): + row = tarfile.TarInfo(artifact.stem + "/" + name) + row.size = len(data) + archive.addfile(row, io.BytesIO(data)) + return artifact + + def test_management_manifest_bridge_requires_actual_candidate_and_retains_legacy(self): + source = ' let Some(path) = env::var_os("IICP_RELEASE_MANIFEST") else {\n return;\n };\n let manifest: Value = serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap();\nassert_eq!(manifest["authorizes_deployment"], false);' + staged = adapter.management_assertions("tests/release_manifest.rs", source) + self.assertIn('component["source_commit"]', staged) + self.assertIn('component["source_version"]', staged) + self.assertNotIn('component["version"]', staged) + self.assertIn('expect("release manifest required")', staged) + self.assertIn('assert_eq!(manifest["authorizes_deployment"], false)', staged) + self.assertIn('"management_service", "directory_authority"', staged) + with self.assertRaisesRegex(ValueError, "fixture shape differs"): + adapter.management_assertions("tests/release_manifest.rs", "unexpected") + + def test_management_consumer_keeps_frozen_source_and_lock(self): + artifact = self.management_artifact() + value = adapter.stage_management_consumer(self.root, artifact, self.workspace) + consumer = adapter.validate_management_consumer(self.root, artifact, self.workspace, value) + self.assertFalse(value["qualification_credit"]) + self.assertEqual((self.workspace / "payload/src/lib.rs").read_bytes(), b'pub fn frozen() {}') + self.assertFalse((consumer / "src").exists()) + self.assertEqual((consumer / "Cargo.lock").read_bytes(), b'unchanged lock') + self.assertIn('path = "../payload/src/lib.rs"', (consumer / "Cargo.toml").read_text()) + self.assertIn('name = "fixture"', (consumer / "Cargo.toml").read_text()) + self.assertEqual((consumer / "tests/fixture.rs").read_bytes(), (self.root / "tests/fixture.rs").read_bytes()) + + def test_management_rehashed_consumer_tamper_fails(self): + artifact = self.management_artifact() + value = adapter.stage_management_consumer(self.root, artifact, self.workspace) + (self.workspace / "consumer/tests/fixture.rs").write_text("weakened assertion") + value["consumer_sha256"] = adapter.digest(adapter.tree(self.workspace / "consumer")) + with self.assertRaisesRegex(ValueError, "reviewed fixtures changed"): + adapter.validate_management_consumer(self.root, artifact, self.workspace, value) + + def test_management_runtime_tamper_and_extra_files_fail(self): + artifact = self.management_artifact() + value = adapter.stage_management_consumer(self.root, artifact, self.workspace) + (self.workspace / "payload/src/lib.rs").write_text("checkout fallback") + value["payload_sha256"] = adapter.digest(adapter.tree(self.workspace / "payload")) + with self.assertRaises(ValueError): + adapter.validate_management_consumer(self.root, artifact, self.workspace, value) + + def test_management_untracked_or_unsafe_test_name_fails(self): + artifact = self.management_artifact() + mapping = self.root / "qualification/pre1-cases.json" + value = json.loads(mapping.read_text()) + for name in ["../escape", "untracked"]: + value["support"]["command"][4] = name + mapping.write_text(json.dumps(value)) + if (self.workspace / "payload").exists(): + import shutil + shutil.rmtree(self.workspace / "payload") + with self.subTest(name=name), self.assertRaises(ValueError): + adapter.stage_management_consumer(self.root, artifact, self.workspace) + + def test_management_staging_refuses_nonempty_or_outside_home(self): + artifact = self.management_artifact() + (self.workspace / "preserved").write_text("user work") + with self.assertRaises(ValueError): + adapter.stage_management_consumer(self.root, artifact, self.workspace) + self.assertEqual((self.workspace / "preserved").read_text(), "user work") + + def test_management_binding_is_context_bound_and_offline(self): + artifact = self.management_artifact() + adapter.stage_management_consumer(self.root, artifact, self.workspace) + (self.workspace / "vendor").mkdir() + (self.workspace / "vendor/dependency").write_text("locked dependency") + (self.workspace / ".cargo").mkdir() + config = ('[source.crates-io]\nreplace-with = "vendored-sources"\n\n' + '[source.vendored-sources]\ndirectory = ' + json.dumps(str(self.workspace / "vendor")) + '\n') + (self.workspace / ".cargo/config.toml").write_text(config) + installed = self.workspace / "consumer" + value = adapter.create_binding(self.root, self.workspace, installed, artifact, + "management", "rust-1.98.0", "macos-arm64", self.bindings) + context = {**self.context, "component": "management", "runtime": "rust-1.98.0"} + self.assertEqual(adapter.validate_binding(value, context, artifact, self.root), installed) + self.assertEqual(adapter.make_case_proof(value, context, "test_fixture", 0, "test-run")["schema"], "iicp.pre1-packaged-case-proof.v2") + with self.assertRaises(ValueError): + adapter.validate_binding(value, {**context, "target": "wrong"}, artifact, self.root) + (self.workspace / ".cargo/config.toml").write_text(config + '\n[build]\nrustc-wrapper = "fake"\n') + with self.assertRaisesRegex(ValueError, "offline Cargo configuration differs"): + adapter.validate_binding(value, context, artifact, self.root) + + def test_lifecycle_child_preserves_installed_import_environment(self): + source = ' env = {**os.environ, "PYTHONPATH": str(Path(__file__).parents[1] / "src")}' + bridged = adapter.packaged_assertions("tests/test_service_lifecycle.py", source) + self.assertEqual(bridged, " env = dict(os.environ)") + with self.assertRaisesRegex(ValueError, "fixture shape differs"): + adapter.packaged_assertions("tests/test_service_lifecycle.py", "changed") + + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="pre1-package-test-") + self.addCleanup(self.temp.cleanup) + self.home = Path(self.temp.name).resolve() + self.root = self.home / "checkout" + self.workspace = self.home / "run/workspace" + self.root.mkdir() + self.workspace.mkdir(parents=True) + (self.root / "tests").mkdir() + (self.root / "tests/test_fixture.py").write_text("def test_fixture(): pass\n") + (self.root / "src").mkdir() + (self.root / "src/runtime.py").write_text("must not be staged") + subprocess.run(["git", "init", "-q"], cwd=self.root, check=True) + subprocess.run(["git", "add", "."], cwd=self.root, check=True) + self.installed = self.workspace / "site/iicp_client" + self.installed.mkdir(parents=True) + (self.installed / "__init__.py").write_text("__version__ = '0.7.110'\n") + self.artifact = self.home / "sdk.whl" + with zipfile.ZipFile(self.artifact, "w") as archive: + archive.write(self.installed / "__init__.py", "iicp_client/__init__.py") + self.bindings = {key: "sha256:" + "a" * 64 for key in adapter.BINDINGS} + self.context = {"component": "client-python", "runtime": "cpython-3.13", + "target": "macos-arm64", **self.bindings} + self.env = patch.dict(os.environ, {"HOME": str(self.home)}) + self.env.start() + self.addCleanup(self.env.stop) + + def binding(self): + return adapter.create_binding(self.root, self.workspace, self.installed, + self.artifact, "client-python", "cpython-3.13", "macos-arm64", self.bindings) + + def validate(self, value): + return adapter.validate_binding(value, self.context, self.artifact, self.root) + + def test_exact_installed_payload_and_no_source_staging(self): + value = self.binding() + self.assertEqual(self.validate(value), self.workspace) + self.assertFalse((self.workspace / "src").exists()) + self.assertFalse(value["qualification_credit"]) + + def test_payload_missing_extra_and_modified_fail(self): + self.binding() + file = self.installed / "__init__.py" + original = file.read_bytes() + for action in (lambda: file.unlink(), lambda: file.write_text("modified")): + action() + with self.assertRaises(ValueError): + adapter.installed_payload(self.artifact, self.installed, "client-python") + file.write_bytes(original) + (self.installed / "extra.py").write_text("extra") + with self.assertRaises(ValueError): + adapter.installed_payload(self.artifact, self.installed, "client-python") + + def test_each_immutable_dimension_fails_even_when_rehashed(self): + value = self.binding() + for key in ("component", "target", "runtime"): + changed = copy.deepcopy(value) + changed[key] = "wrong" + changed["binding_sha256"] = None + changed["binding_sha256"] = adapter.digest(changed) + with self.subTest(key=key), self.assertRaises(ValueError): + self.validate(changed) + for key in adapter.BINDINGS: + changed = copy.deepcopy(value) + changed["bindings"][key] = "sha256:" + "b" * 64 + changed["binding_sha256"] = None + changed["binding_sha256"] = adapter.digest(changed) + with self.subTest(key=key), self.assertRaises(ValueError): + self.validate(changed) + + def test_fixture_tamper_and_checkout_source_fallback_fail(self): + value = self.binding() + (self.workspace / "tests/test_fixture.py").write_text("tamper") + with self.assertRaisesRegex(ValueError, "fixtures changed"): + self.validate(value) + (self.workspace / "src").mkdir() + with self.assertRaisesRegex(ValueError, "runtime source"): + self.validate(value) + + def test_staged_assertions_must_match_reviewed_mapping(self): + adapter.stage(self.root, self.workspace, "client-python") + (self.workspace / "tests/test_fixture.py").write_text("tamper") + with self.assertRaisesRegex(ValueError, "reviewed source mapping"): + self.binding() + + def test_symlinks_and_unsafe_ancestors_fail(self): + value = self.binding() + (self.installed / "link.py").symlink_to(self.root / "src/runtime.py") + with self.assertRaises(ValueError): + self.validate(value) + alias = self.home / "alias" + alias.symlink_to(self.workspace, target_is_directory=True) + with self.assertRaises(ValueError): + adapter.safe_path(alias / "tests") + + def test_dependency_tamper_fails(self): + value = self.binding() + (self.installed.parent / "pytest.py").write_text("injected") + with self.assertRaisesRegex(ValueError, "dependencies changed"): + self.validate(value) + + def test_rehashed_fixture_and_binding_cannot_replace_reviewed_assertions(self): + value = self.binding() + (self.workspace / "tests/test_fixture.py").write_text("def test_fixture(): assert True\n") + value["fixtures_sha256"] = adapter.digest(adapter.fixture_tree(self.workspace)) + value["binding_sha256"] = None + value["binding_sha256"] = adapter.digest(value) + with self.assertRaisesRegex(ValueError, "reviewed source mapping"): + self.validate(value) + + def test_cli_adapter_decodes_file_urls(self): + fixture = 'const cli = readFileSync("src/cli.ts");\n assert.match(cli, /version/);\n' + staged = adapter.packaged_assertions("tests/pre1_release_boundaries.test.ts", fixture) + self.assertIn('import { fileURLToPath } from "node:url"', staged) + self.assertIn("fileURLToPath(new URL(", staged) + self.assertNotIn(".pathname", staged) + + def test_cli_subprocess_with_spaces_and_unicode_path(self): + import shutil + node = shutil.which("node") + if not node: + self.fail("Node is required for the CLI portability regression") + directory = self.home / "IICP space ΓΌ" + directory.mkdir() + cli = directory / "cli.mjs" + cli.write_text('console.log("iicp-node 0.7.110");') + script = '''import {fileURLToPath} from 'node:url'; +import {spawnSync} from 'node:child_process'; +const result=spawnSync(process.execPath,[fileURLToPath(new URL(process.argv[1])),'--version'],{encoding:'utf8'}); +process.stdout.write(result.stdout); process.exit(result.status ?? 1);''' + result = subprocess.run([node, "--input-type=module", "-e", script, cli.as_uri()], + capture_output=True, text=True, check=False) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "iicp-node 0.7.110") + + def test_case_proof_is_portable_and_bound_to_result(self): + value = adapter.make_case_proof(self.binding(), self.context, "test_fixture", 0, "pre1-test") + adapter.validate_case_proof(value, self.context, 0, "pre1-test") + self.assertNotIn(str(self.home), json.dumps(value)) + for field, replacement in (("run_id", "different"), ("exit_code", 1), + ("context", {**self.context, "target": "windows-x86_64"})): + changed = copy.deepcopy(value) + changed[field] = replacement + changed["proof_sha256"] = None + changed["proof_sha256"] = adapter.digest(changed) + with self.subTest(field=field), self.assertRaises(ValueError): + adapter.validate_case_proof(changed, self.context, 0, "pre1-test") + + def test_case_proof_atomic_non_overwriting_and_safe(self): + proof = adapter.make_case_proof(self.binding(), self.context, "test_fixture", 0, "pre1-test") + output = self.home / "proof.json" + with patch.dict(os.environ, {"IICP_PRE1_CASE_PROOF_OUTPUT": str(output)}): + adapter.write_case_proof(proof) + self.assertEqual(json.loads(output.read_text()), proof) + with self.assertRaises(ValueError): + adapter.write_case_proof(proof) + self.assertEqual(list(self.home.glob(".case-proof-*")), []) + link = self.home / "linked-proof" + link.symlink_to(output) + for unsafe in (link, self.home.parent / "outside-proof.json"): + with patch.dict(os.environ, {"IICP_PRE1_CASE_PROOF_OUTPUT": str(unsafe)}), self.assertRaises(ValueError): + adapter.write_case_proof(proof) + + def test_case_proof_refuses_unknown_fields_and_tampered_summary(self): + value = adapter.make_case_proof(self.binding(), self.context, "test_fixture", 0, "pre1-test") + for change in (lambda v: v.update(output="secret"), + lambda v: v["execution"].update(installed_package=str(self.installed)), + lambda v: v["execution"]["bindings"].update(runtime_map_sha256="wrong")): + changed = copy.deepcopy(value) + change(changed) + changed["proof_sha256"] = None + changed["proof_sha256"] = adapter.digest(changed) + with self.assertRaises(ValueError): + adapter.validate_case_proof(changed, self.context, 0, "pre1-test") + + def test_no_binding_cannot_run_source_case(self): + with patch.dict(os.environ, {}, clear=True), self.assertRaises(KeyError): + adapter.package_command(self.root, self.context, {}, self.home, [], {}) + + def test_crypto_bridge_uses_runtime_verifier_not_test_local_policy(self): + text = "def _decision(vector: dict, keys: dict, signature_valid: bool):\n return 'fake'\n\ndef _assert_fixture_decision(): pass\n" + bridged = adapter.packaged_assertions("tests/test_dispatch_ticket_trust_crypto.py", text) + self.assertIn("verify_dispatch_ticket_v2(", bridged) + self.assertNotIn("return 'fake'", bridged) + text = "function decision(vector: any, keys: Map, signatureValid: boolean): string { return 'fake'; }\nfunction assertFixtureDecision(): void {}" + bridged = adapter.packaged_assertions("tests/dispatch_ticket_trust_crypto.test.ts", text) + self.assertIn("verifyDispatchTicketV2(", bridged) + self.assertNotIn("return 'fake'", bridged) + + def test_cli_fixture_requires_reviewed_shape(self): + with self.assertRaisesRegex(ValueError, "fixture shape differs"): + adapter.packaged_assertions("tests/pre1_release_boundaries.test.ts", "changed fixture") + + def test_python_command_uses_only_installed_path_and_guard(self): + value = self.binding() + path = self.home / "binding.json" + path.write_text(json.dumps(value)) + component = {"artifacts": [{"kind": "wheel", "name": "sdk.whl"}]} + artifact_root = self.home / "artifacts" + (artifact_root / "client-python").mkdir(parents=True) + (artifact_root / "client-python/sdk.whl").write_bytes(self.artifact.read_bytes()) + with patch.dict(os.environ, {"IICP_PRE1_PACKAGE_EXECUTION_BINDING": str(path), + "IICP_PRE1_PACKAGE_EXECUTION_SHA256": value["binding_sha256"]}): + argv, env, cwd, _ = adapter.package_command(self.root, self.context, + component, artifact_root, ["python", "-m", "pytest", "-q", "tests/test_fixture.py::test_fixture"], {}) + self.assertEqual(argv[3:5], ["-p", "pre1_origin_guard"]) + self.assertEqual(cwd, self.workspace) + self.assertNotIn(str(self.root), env["PYTHONPATH"]) + + def test_typescript_literals_and_child_worker_bind_to_dist(self): + text = 'import x from "../src/client.js"; require("../src/trust"); import y from "./src/service_lifecycle.ts";' + rewritten = adapter.rewrite_typescript(text) + self.assertNotIn("src/", rewritten) + self.assertIn('"./node_modules/@iicp/client/dist/service_lifecycle.js"', rewritten) + with self.assertRaises(ValueError): + adapter.rewrite_typescript('import "../src/../../escape.js"') + + def test_typescript_archive_and_missing_compiled_file(self): + artifact = self.home / "sdk.tgz" + installed = self.workspace / "node_modules/@iicp/client" + (installed / "dist").mkdir(parents=True) + (installed / "dist/client.js").write_bytes(b"compiled") + with tarfile.open(artifact, "w:gz") as archive: + info = tarfile.TarInfo("package/dist/client.js") + info.size = len(b"compiled") + archive.addfile(info, io.BytesIO(b"compiled")) + adapter.installed_payload(artifact, installed, "client-typescript") + (installed / "dist/client.js").unlink() + with self.assertRaises(ValueError): + adapter.installed_payload(artifact, installed, "client-typescript") + + + +class RustPackageExecutionTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="pre1-rust-package-test-") + self.addCleanup(self.temp.cleanup) + self.home = Path(self.temp.name).resolve() + self.root = self.home / "checkout" + self.workspace = self.home / "workspace" + self.root.mkdir() + self.workspace.mkdir() + self.mapping = b'{"support":{},"scenarios":{}}' + (self.root / "qualification").mkdir() + (self.root / "qualification/pre1-cases.json").write_bytes(self.mapping) + self.files = {"Cargo.toml": b'[package]\nname="iicp-client"\nversion="0.7.110"\n', + "Cargo.lock": b"# locked\n", "src/lib.rs": b"pub fn run() {}\n", + "tests/exact.rs": b"#[test] fn exact() {}\n", + "qualification/pre1-cases.json": self.mapping} + self.artifact = self.home / "iicp-client-0.7.110.crate" + self.vendor = self.home / "vendor.tar.gz" + self.archive(self.artifact, {"iicp-client-0.7.110/" + k: v for k,v in self.files.items()}) + self.archive(self.vendor, {**{"source/"+k:v for k,v in self.files.items()}, + "vendor/dependency/src/lib.rs": b"pub fn dep() {}\n", + ".cargo/config.toml": b'[source.crates-io]\nreplace-with="vendored-sources"\n'}) + self.env = patch.dict(os.environ, {"HOME": str(self.home)}) + self.env.start() + self.addCleanup(self.env.stop) + self.installed = adapter.extract_rust_packages(self.artifact, self.vendor, self.workspace) + self.bindings = {key: "sha256:"+"a"*64 for key in adapter.BINDINGS} + self.context = {"component":"client-rust", "runtime":"msrv-1.86", "target":"macos-arm64", **self.bindings} + + def archive(self, path, files): + with tarfile.open(path, "w:gz") as archive: + for name,data in files.items(): + row=tarfile.TarInfo(name); row.size=len(data) + archive.addfile(row, io.BytesIO(data)) + + def binding(self): + return adapter.create_binding(self.root, self.workspace, self.installed, self.artifact, + "client-rust", "msrv-1.86", "macos-arm64", self.bindings, self.vendor) + + def validate(self, value): + return adapter.validate_binding(value, self.context, self.artifact, self.root, self.vendor) + + def test_frozen_crate_and_vendor_source_are_identical(self): + value=self.binding() + self.assertEqual(self.validate(value), self.installed) + self.assertFalse(value["qualification_credit"]) + proof=adapter.make_case_proof(value,self.context,"exact",0,"rust-test") + adapter.validate_case_proof(proof,self.context,0,"rust-test") + self.assertNotIn(str(self.home),json.dumps(proof)) + + def test_modified_missing_extra_source_and_vendor_fail(self): + value=self.binding() + for file in (self.installed/"src/lib.rs",self.workspace/"vendor/dependency/src/lib.rs",self.workspace/".cargo/config.toml"): + original=file.read_bytes(); file.write_bytes(b"tamper") + with self.assertRaises(ValueError): self.validate(value) + file.unlink() + with self.assertRaises(ValueError): self.validate(value) + file.write_bytes(original) + (self.installed/"extra.rs").write_text("extra") + with self.assertRaises(ValueError): self.validate(value) + + def test_forged_binding_cannot_replace_packaged_assertions(self): + value=self.binding(); (self.installed/"tests/exact.rs").write_text("#[test] fn exact() {} // forged") + value["installed_payload_sha256"]=adapter.digest(adapter.tree(self.installed)) + value["binding_sha256"]=None; value["binding_sha256"]=adapter.digest(value) + with self.assertRaises(ValueError):self.validate(value) + + def test_all_semantic_dimensions_fail_even_after_rehash(self): + value=self.binding() + for key in ("component","runtime","target"): + changed=copy.deepcopy(value); changed[key]="wrong" + changed["binding_sha256"]=None; changed["binding_sha256"]=adapter.digest(changed) + with self.assertRaises(ValueError):self.validate(changed) + for key in adapter.BINDINGS: + changed=copy.deepcopy(value); changed["bindings"][key]="sha256:"+"b"*64 + changed["binding_sha256"]=None; changed["binding_sha256"]=adapter.digest(changed) + with self.assertRaises(ValueError):self.validate(changed) + + def test_vendor_artifact_must_be_bound_and_source_must_match(self): + with self.assertRaises(ValueError):adapter.validate_binding(self.binding(),self.context,self.artifact,self.root) + self.archive(self.vendor,{"source/forged.rs":b"forged"}) + with self.assertRaisesRegex(ValueError,"vendor source differs"):self.binding() + + def test_reviewed_mapping_and_symlink_checks(self): + (self.root/"qualification/pre1-cases.json").write_text("changed") + with self.assertRaisesRegex(ValueError,"mapping differs"):self.binding() + (self.root/"qualification/pre1-cases.json").write_bytes(self.mapping) + (self.workspace/"vendor/link").symlink_to(self.root) + with self.assertRaises(ValueError):self.binding() + + def test_archive_rejects_traversal_links_and_duplicates(self): + for name in ("../escape", "/absolute", "vendor/../escape", "vendor/a:b", "vendor/evil\\file"): + self.archive(self.vendor,{name:b"unsafe"}) + with self.assertRaises(ValueError):adapter.rust_archive_files(self.vendor,"vendor/") + for kind in (tarfile.SYMTYPE,tarfile.LNKTYPE,tarfile.FIFOTYPE): + with tarfile.open(self.vendor,"w:gz") as archive: + row=tarfile.TarInfo("vendor/link");row.type=kind;row.linkname="escape";archive.addfile(row) + with self.assertRaises(ValueError):adapter.rust_archive_files(self.vendor,"vendor/") + with tarfile.open(self.vendor,"w:gz") as archive: + for _ in range(2): + row=tarfile.TarInfo("vendor/same");row.size=1;archive.addfile(row,io.BytesIO(b"x")) + with self.assertRaises(ValueError):adapter.rust_archive_files(self.vendor,"vendor/") + + def test_extraction_refuses_nonempty_workspace_and_has_restrictive_permissions(self): + with self.assertRaisesRegex(ValueError,"empty"):adapter.extract_rust_packages(self.artifact,self.vendor,self.workspace) + self.assertEqual((self.installed/"src/lib.rs").stat().st_mode & 0o777,0o600) + + def test_rust_archive_bytes_are_bounded_before_allocation(self): + row=tarfile.TarInfo("vendor/large");row.size=1024*1024*1024+1 + from unittest.mock import MagicMock + opened=MagicMock();opened.__enter__.return_value.__iter__.return_value=iter([row]) + with patch.object(adapter.tarfile,"open",return_value=opened): + with self.assertRaisesRegex(ValueError,"bounded"):adapter.rust_archive_files(self.vendor,"vendor/") + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_pre1_qualification_case.py b/scripts/test_pre1_qualification_case.py index ac4e9bd..f25c134 100755 --- a/scripts/test_pre1_qualification_case.py +++ b/scripts/test_pre1_qualification_case.py @@ -6,6 +6,8 @@ import unittest from pathlib import Path +from test_pre1_package_execution import PackageExecutionTests # noqa: F401 + ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location( "pre1_driver", ROOT / "scripts/run_pre1_qualification_case.py"