diff --git a/docs/bwrap-runtime.md b/docs/bwrap-runtime.md index 2cef21f..8561eaa 100644 --- a/docs/bwrap-runtime.md +++ b/docs/bwrap-runtime.md @@ -237,10 +237,15 @@ owner discrepancy for bwrap. It is not a claim that every remote backend's exter hosts/templates/tools are packaged or admitted. The ephemeral Messages binding may select an exact runtime path/digest; usual overlap and default-deny checks apply. -Required `make check`: lint clean, 201 tests passed. Rein's +Required `make check`: lint clean, 205 tests passed. Rein's `scripts/prove-metered-runtime.py` consumes the candidate to test installed imports, packaged definitions, explicit one-cycle bootstrap, actual pinned CLI/metered stream, insufficient-capacity refusal and teardown against fixtures. The resulting receipt belongs to `prj-helixforge-factory/evidence/2026-09-09-owner-bootstrap.json`. Production installation and credential-holder/empty-egress profile/placement admission remain SAND-WP-0015-T04 and HFACT-WP-0001-T03/T04; no prior profile is promoted. + +The first standalone candidate exposed a stale cached local wheel despite current +Git metadata. Owner builds now refresh all local distributions and compare every +installed package source/definition file with its tracked source; stale, missing or +extra files refuse the build. Four content-conformance regressions cover that gap. diff --git a/scripts/build-rein-runtime.py b/scripts/build-rein-runtime.py index bbcd071..615f222 100644 --- a/scripts/build-rein-runtime.py +++ b/scripts/build-rein-runtime.py @@ -43,6 +43,33 @@ def install_claude(output: Path, source: Path, sha256: str, version: str) -> dic "expected_version": version, "source": str(source)} +def verify_source_files(site: Path, mappings: list[tuple[Path, str, str]]) -> dict: + """Refuse stale/missing/extra package contents even when versions match.""" + expected = {} + for source, prefix, destination in mappings: + tracked = checked(["git", "-C", str(source), "ls-files", prefix]).splitlines() + if not tracked: + raise ValueError("owner package source mapping is empty") + for name in tracked: + relative = Path(destination) / Path(name).relative_to(prefix) + wanted = hashlib.sha256((source / name).read_bytes()).hexdigest() + installed = site / relative + if (not installed.is_file() or installed.is_symlink() + or hashlib.sha256(installed.read_bytes()).hexdigest() != wanted): + raise ValueError(f"owner package content mismatch: {relative}") + expected[relative.as_posix()] = wanted + actual = set() + for package in {Path(name).parts[0] for name in expected}: + for path in (site / package).rglob("*"): + if path.is_file() and "__pycache__" not in path.parts: + actual.add(path.relative_to(site).as_posix()) + if actual != set(expected): + raise ValueError("owner package has unexpected installed files") + return {"files_verified": len(expected), "manifest_sha256": hashlib.sha256( + json.dumps(expected, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest()} + + def build(output: Path, rein_source: Path, llm_source: Path, claude_binary: Path | None = None, claude_sha256: str | None = None, claude_version: str | None = None, *, owner_runtime: bool = False) -> dict: @@ -68,8 +95,10 @@ def build(output: Path, rein_source: Path, llm_source: Path, output.mkdir(parents=True, exist_ok=False) checked(["/usr/bin/python3", "-m", "venv", "--copies", "--without-pip", str(output)]) if owner_runtime: + refresh = [arg for name in revisions for arg in ("--refresh-package", name)] checked(["uv", "sync", "--project", str(rein_source), "--frozen", "--no-editable", - "--no-dev", "--extra", "glas", "--extra", "llm", "--python", "/usr/bin/python3"], + "--no-dev", "--extra", "glas", "--extra", "llm", "--python", "/usr/bin/python3", + *refresh], env={**os.environ, "UV_PROJECT_ENVIRONMENT": str(output)}) else: checked(["uv", "pip", "install", "--python", str(output / "bin/python3"), @@ -93,6 +122,23 @@ def build(output: Path, rein_source: Path, llm_source: Path, metadata["claude"] = install_claude(output, claude_binary, claude_sha256, claude_version) metadata["source_revisions"] = revisions if owner_runtime: + lock = json.loads((rein_source / "deploy/runtime-contract-lock.json").read_text()) + sources = {x["distribution"]: (rein_source / x["source"]).resolve() + for x in lock["dependencies"]} + site = Path(checked([str(output / "bin/python3"), "-I", "-B", "-c", + "import sysconfig; print(sysconfig.get_path('purelib'))"])) + if not site.resolve().is_relative_to(output.resolve()): + raise ValueError("owner site-packages is outside artifact") + metadata["source_contents"] = verify_source_files(site, [ + (rein_source, "rein_aharness", "rein_aharness"), + (sources["llm-connect"], "llm_connect", "llm_connect"), + (sources["glas-harness"], "src/glas_harness", "glas_harness"), + (sources["glas-harness"], "profiles", "glas_harness/data/profiles"), + (sources["glas-harness"], "registry/reins", "glas_harness/data/reins"), + (sources["sandboxer"], "src/sandboxer", "sandboxer"), + (sources["sandboxer"], "profiles", "sandboxer/data/profiles"), + (sources["sandboxer"], "extensions", "sandboxer/data/extensions"), + ]) metadata["owner_runtime"] = True metadata["runtime_contract"] = lock_report metadata["uv_lock_sha256"] = hashlib.sha256( diff --git a/tests/test_runtime_builder.py b/tests/test_runtime_builder.py index 15717d0..c4bb5a0 100644 --- a/tests/test_runtime_builder.py +++ b/tests/test_runtime_builder.py @@ -69,3 +69,30 @@ def test_owner_build_requires_matching_sibling_lock_before_output(tmp_path, monk with pytest.raises(ValueError, match="does not match"): builder.build(output, rein, llm, owner_runtime=True) assert not output.exists() + + +@pytest.mark.parametrize("change", [None, "stale", "missing", "extra"]) +def test_installed_package_contents_must_match_committed_source(tmp_path, change): + import subprocess + source = tmp_path / "source" + package = source / "fixture" + package.mkdir(parents=True) + (package / "__init__.py").write_text("VERSION = 2\n") + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run(["git", "-C", str(source), "add", "fixture"], check=True) + site = tmp_path / "site" + installed = site / "fixture" + installed.mkdir(parents=True) + (installed / "__init__.py").write_text("VERSION = 2\n") + if change == "stale": + (installed / "__init__.py").write_text("VERSION = 1\n") + elif change == "missing": + (installed / "__init__.py").unlink() + elif change == "extra": + (installed / "retired.py").write_text("old = True\n") + if change: + with pytest.raises(ValueError, match="owner package"): + builder.verify_source_files(site, [(source, "fixture", "fixture")]) + else: + result = builder.verify_source_files(site, [(source, "fixture", "fixture")]) + assert result["files_verified"] == 1 diff --git a/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md b/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md index 1751c5a..24ec0b6 100644 --- a/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md +++ b/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md @@ -268,7 +268,7 @@ with non-editable installation and recorded lock/source/package pins. Sand-boxer wheels now carry their profile and extension definitions, fixing a bwrap owner failure that source-checkout tests could hide. The trusted Messages binding can select the digest-pinned runtime without adding an API or profile override. -`make check`: lint clean, 201 passed. Existing standalone workload builds remain +`make check`: lint clean, 205 passed. Existing standalone workload builds remain supported. See docs/bwrap-runtime.md and rein's docs/owner-bootstrap.md. The project's `evidence/2026-09-09-owner-bootstrap.json` records the actual candidate @@ -276,3 +276,8 @@ build and isolated installed-interpreter/CLI proof. T04 remains wait for accepte credential-to-owner delivery, current protected installation and Railiance placement, provider compatibility and G0. Source packaging does not reopen completed T01-T03, T05-T06, broaden the old CCRs or activate the previously installed 2.1.263 artifact. + +The first standalone candidate exposed a stale cached local wheel despite current +Git metadata. Owner builds now refresh all local distributions and compare every +installed package source/definition file with its tracked source; stale, missing or +extra files refuse the build. Four content-conformance regressions cover that gap.