From df5e941814bbcc5417d750c61f944c5380a15fb7 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 8 Sep 2026 21:08:18 +0200 Subject: [PATCH] feat(runtime): install pinned artifacts in a private owner store Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc --- docs/bwrap-runtime.md | 26 ++++ ...15-protected-local-install-2026-09-08.json | 40 +++++++ scripts/install-bwrap-runtime.py | 23 ++++ src/sandboxer/extensions/runtime_store.py | 89 ++++++++++++++ tests/test_runtime_store.py | 111 ++++++++++++++++++ ...WP-0015-bwrap-runtime-and-private-state.md | 45 ++++++- 6 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json create mode 100644 scripts/install-bwrap-runtime.py create mode 100644 src/sandboxer/extensions/runtime_store.py create mode 100644 tests/test_runtime_store.py diff --git a/docs/bwrap-runtime.md b/docs/bwrap-runtime.md index 63da92d..bf3a4cc 100644 --- a/docs/bwrap-runtime.md +++ b/docs/bwrap-runtime.md @@ -194,3 +194,29 @@ Limits: `--version` proves startup with the proxy environment, not that Claude has sent a provider request through it. No credential acquisition or model run. The /tmp candidate is not production placement; protected artifact installation, owner configuration, credential adoption and real-model proof remain T04 gates. + + +## Install a reviewed artifact in the local owner's store + +Use `scripts/install-bwrap-runtime.py --source --sha256 +` from the owner checkout with its Python environment. +The default store is `~/.local/share/sandboxer/runtimes`, with mode 0700. The +command accepts only a canonical store beneath that owner's home, checks owner +and ancestor write permissions, verifies the source, copies into a private +staging directory, verifies the copy and atomically publishes under its digest. +It refuses unsafe modes, store aliases and altered existing destinations. +Reinstalling the same unchanged artifact is idempotent; it never overwrites one. + +This is owner-controlled local storage. The owner remains able to change its +files, as in the artifact trust contract above; it is not root-owned storage or +Railiance placement. Sandboxes still recheck the pin on creation and receive a +read-only mount. Installation does not select a profile, configure credentials, +activate network access or enable a schedule. Rollback selects a separately +reviewed retained digest through owner configuration; no in-place replacement +or automatic deletion is part of this command. + +The 2026-09-08 bnt-lap001 installation used the existing combined Claude 2.1.263 +candidate unchanged. Its installed-path startup/lifecycle proof is +[the local installation receipt](evidence/SAND-WP-0015-protected-local-install-2026-09-08.json). +SAND-WP-0015-T06 is complete. T04 still owns configured execution, native +credential adoption, real-model acceptance and production placement. diff --git a/docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json b/docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json new file mode 100644 index 0000000..10f80c5 --- /dev/null +++ b/docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json @@ -0,0 +1,40 @@ +{ + "date": "2026-09-08", + "scope": "local owner artifact installation; not Railiance admission", + "host": "bnt-lap001", + "owner_uid": 1000, + "store_mode": "0700", + "runtime_path": "/home/worsch/.local/share/sandboxer/runtimes/5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922", + "runtime_sha256": "5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922", + "binary_version": "2.1.263", + "binary_sha256": "26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba", + "candidate_rebuilt": false, + "installed_entries": 358, + "installed_regular_bytes": 245176062, + "root_owned": false, + "smoke": { + "ok": true, + "sandbox_id": "51b59587", + "rein_cli_started": true, + "adapter_imported": true, + "claude_version": "2.1.263 (Claude Code)", + "runtime_readonly": true, + "source_absent": true, + "home_outside_workspace": true, + "home_mode": "0700", + "worktree_clean": true, + "interfaces": [ + "lo" + ], + "python_prefix": "/opt/sandboxer/runtime", + "https_proxy_present": true, + "credential_refs": [], + "private_state_persisted": true, + "workspace_removed": true, + "proxy_removed": true, + "exit_code": 0 + }, + "model_run_proven": false, + "profile_activated": false, + "credential_delivery_configured": false +} diff --git a/scripts/install-bwrap-runtime.py b/scripts/install-bwrap-runtime.py new file mode 100644 index 0000000..bab4ca8 --- /dev/null +++ b/scripts/install-bwrap-runtime.py @@ -0,0 +1,23 @@ +"""Install an explicitly pinned artifact; enables no execution or credential lane.""" + +import argparse +import json +from pathlib import Path + +from sandboxer.extensions.runtime_store import install_runtime + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--sha256", required=True) + parser.add_argument("--store", type=Path, + default=Path.home() / ".local/share/sandboxer/runtimes") + args = parser.parse_args() + result = install_runtime(args.source, args.store, args.sha256) + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/sandboxer/extensions/runtime_store.py b/src/sandboxer/extensions/runtime_store.py new file mode 100644 index 0000000..44cbe9f --- /dev/null +++ b/src/sandboxer/extensions/runtime_store.py @@ -0,0 +1,89 @@ +"""Publish a reviewed runtime in the local owner's private artifact store.""" + +from __future__ import annotations + +import fcntl +import os +import re +import shutil +import stat +import tempfile +from pathlib import Path + +from sandboxer.extensions.runtime import verified_runtime + + +def _owned_directory(path: Path, *, private: bool = False) -> None: + info = path.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid(): + raise ValueError("runtime store requires owner-controlled directories") + mode = stat.S_IMODE(info.st_mode) + if mode & 0o022 or (private and mode != 0o700): + raise ValueError("runtime store permissions are not protected") + + +def _prepare_store(store: Path) -> None: + home = Path.home().resolve(strict=True) + if not store.is_absolute() or store.resolve() != store or store == home: + raise ValueError("store must be a canonical absolute directory below the owner home") + if not store.is_relative_to(home): + raise ValueError("store must be below the owner home") + _owned_directory(home) + current = home + for part in store.relative_to(home).parts: + current /= part + current.mkdir(mode=0o700, exist_ok=True) + _owned_directory(current, private=current == store) + + +def _protected_tree(root: Path) -> None: + for path in [root, *root.rglob("*")]: + info = path.lstat() + if info.st_uid != os.getuid(): + raise ValueError("runtime artifact is not owned by the installing owner") + if not stat.S_ISLNK(info.st_mode) and stat.S_IMODE(info.st_mode) & 0o7022: + raise ValueError("runtime artifact has unsafe permission bits") + + +def install_runtime(source: Path, store: Path, sha256: str) -> dict: + """Verify, copy, reverify and atomically publish; never replace an artifact. + + The local owner remains trusted and can alter its own files. This store + protects against other local users; the broker separately mounts the + artifact read-only and verifies its digest for every sandbox creation. + This function enables no profile, credentials, egress or schedule. + """ + if not re.fullmatch(r"[0-9a-f]{64}", sha256): + raise ValueError("expected runtime digest must be a lowercase SHA-256") + config = {"runtime": {"path": str(source), "sha256": sha256}} + verified_runtime(config) + if source.is_relative_to(store) or store.is_relative_to(source): + raise ValueError("source and runtime store must not overlap") + _prepare_store(store) + destination = store / sha256 + fd = os.open(store / ".install.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, "rb") as lock: + info = os.fstat(lock.fileno()) + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or stat.S_IMODE(info.st_mode) != 0o600): + raise ValueError("runtime install lock is not private and owner-controlled") + fcntl.flock(lock, fcntl.LOCK_EX) + reused = destination.exists() or destination.is_symlink() + if reused: + verified_runtime({"runtime": {"path": str(destination), "sha256": sha256}}) + _protected_tree(destination) + else: + staging = Path(tempfile.mkdtemp(prefix=".install-", dir=store)) + try: + shutil.copytree(source, staging, symlinks=True, dirs_exist_ok=True) + verified_runtime({"runtime": {"path": str(staging), "sha256": sha256}}) + _protected_tree(staging) + staging.rename(destination) + finally: + if staging.exists(): + shutil.rmtree(staging) + return { + "runtime": {"path": str(destination), "sha256": sha256}, + "owner_uid": os.getuid(), "store_mode": "0700", "reused": reused, + "profile_activated": False, "credential_delivery_configured": False, + } diff --git a/tests/test_runtime_store.py b/tests/test_runtime_store.py new file mode 100644 index 0000000..c4906ca --- /dev/null +++ b/tests/test_runtime_store.py @@ -0,0 +1,111 @@ +from pathlib import Path + +import pytest + +from sandboxer.extensions import runtime_store +from sandboxer.extensions.runtime import runtime_digest, verified_runtime + + +@pytest.fixture +def inputs(tmp_path, monkeypatch): + home = tmp_path / "owner-home" + home.mkdir(mode=0o700) + monkeypatch.setenv("HOME", str(home)) + source = tmp_path / "candidate" + (source / "bin").mkdir(parents=True) + (source / "pyvenv.cfg").write_text("home = /usr/bin\n") + (source / "bin/python3").write_bytes(b"fixture executable") + (source / "bin/python3").chmod(0o755) + (source / "bin/python").symlink_to("python3") + return source, home / "runtimes", runtime_digest(source) + + +def test_install_preserves_pin_and_does_not_share_mutable_source(inputs): + source, store, digest = inputs + result = runtime_store.install_runtime(source, store, digest) + installed = verified_runtime(result) + assert installed == store / digest + assert store.stat().st_mode & 0o777 == 0o700 + assert not result["profile_activated"] + assert not result["credential_delivery_configured"] + assert (installed / "bin/python").is_symlink() + (source / "bin/python3").write_bytes(b"changed after installation") + assert verified_runtime(result) == installed + + +def test_repeat_is_idempotent_but_never_repairs_or_overwrites_existing_artifact(inputs): + source, store, digest = inputs + first = runtime_store.install_runtime(source, store, digest) + assert runtime_store.install_runtime(source, store, digest)["reused"] + installed = Path(first["runtime"]["path"]) + target = installed / "bin/python3" + target.write_bytes(b"tampered installed artifact") + with pytest.raises(ValueError, match="digest"): + runtime_store.install_runtime(source, store, digest) + assert target.read_bytes() == b"tampered installed artifact" + + +def test_wrong_digest_refuses_before_creating_store(inputs): + source, store, _ = inputs + with pytest.raises(ValueError, match="digest"): + runtime_store.install_runtime(source, store, "0" * 64) + assert not store.exists() + + +def test_copy_tampering_never_publishes(inputs, monkeypatch): + source, store, digest = inputs + copytree = runtime_store.shutil.copytree + + def corrupt(src, dst, *args, **kwargs): + result = copytree(src, dst, *args, **kwargs) + if Path(src) == source: + (Path(dst) / "bin/python3").write_bytes(b"corrupted during copy") + return result + + monkeypatch.setattr(runtime_store.shutil, "copytree", corrupt) + with pytest.raises(ValueError, match="digest"): + runtime_store.install_runtime(source, store, digest) + assert not (store / digest).exists() + assert not list(store.glob(".install-*")) + + +@pytest.mark.parametrize("mode", [0o777, 0o775, 0o755]) +def test_store_must_be_private(inputs, mode): + source, store, digest = inputs + store.mkdir(mode=mode) + store.chmod(mode) + with pytest.raises(ValueError, match="permissions"): + runtime_store.install_runtime(source, store, digest) + assert not (store / digest).exists() + + +def test_store_symlink_is_refused(inputs): + source, store, digest = inputs + actual = store.parent / "actual" + actual.mkdir(mode=0o700) + store.symlink_to(actual) + with pytest.raises(ValueError, match="canonical"): + runtime_store.install_runtime(source, store, digest) + + +@pytest.mark.parametrize("mode", [0o777, 0o4755, 0o2755]) +def test_even_a_matching_pin_cannot_admit_unsafe_artifact_modes(inputs, mode): + source, store, _ = inputs + (source / "bin/python3").chmod(mode) + digest = runtime_digest(source) + with pytest.raises(ValueError, match="unsafe permission"): + runtime_store.install_runtime(source, store, digest) + assert not (store / digest).exists() + + +def test_world_writable_parent_is_refused(inputs): + source, store, digest = inputs + store.parent.chmod(0o777) + with pytest.raises(ValueError, match="permissions"): + runtime_store.install_runtime(source, store, digest) + + +def test_store_outside_owner_home_is_refused(inputs): + source, store, digest = inputs + with pytest.raises(ValueError, match="below the owner home"): + runtime_store.install_runtime(source, store.parent.parent / "other", digest) 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 43955cb..f592397 100644 --- a/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md +++ b/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md @@ -8,7 +8,7 @@ status: blocked owner: codex topic_slug: bwrap-runtime-and-private-state created: "2026-09-05" -updated: "2026-09-06" +updated: "2026-09-08" state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d" --- @@ -204,3 +204,46 @@ Limits: `--version` proves startup with the proxy environment, not that Claude has sent a provider request through it. No credential acquisition or model run. The /tmp candidate is not production placement; protected artifact installation, owner configuration, credential adoption and real-model proof remain T04 gates. + + +## Install the pinned runtime in the local owner's protected store + +```task +id: SAND-WP-0015-T06 +status: done +priority: high +assignee: the-custodian +``` + +HFACT-WP-0001-T04 consumes this independent installation return. Reuse the +already verified combined candidate with digest +`5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922`; +do not rebuild resolved dependencies. Publish it under the local owner's +private artifact store, verify exact copied content/modes and refusal of unsafe +or corrupted destinations, and prove startup/read-only access/teardown through +the installed path. The owner remains trusted; this is not root-owned storage. +T04 retains real credential adoption, owner configuration, model acceptance +and production placement. No credential route or profile is activated here. + +`scripts/install-bwrap-runtime.py` installs below the owner's home into a +mode-0700 store, verifies before and after copying, preserves artifact modes +and internal symlinks, serializes publishers and never overwrites a published +artifact. Existing modified artifacts are refused rather than repaired silently. +Group/other-writable ancestors, unsafe permission bits and store aliases refuse. +Unit tests cover integrity, idempotence, copy-time corruption and access modes. + + +T06 completed 2026-09-08 on bnt-lap001, local owner UID 1000. Installed the +unchanged 358-entry / 245176062-byte candidate at +`/home/worsch/.local/share/sandboxer/runtimes/5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922`. +The private store is mode 0700; the installing owner remains trusted. A root-owned +system installation was not claimed or required for this local owner contract. +The installed-path sandbox smoke `51b59587` passed real Claude 2.1.263 and rein +startup, read-only mount, private state persistence, clean worktree, absent source, +loopback-only interfaces and complete workspace/proxy teardown. No model call +or credential acquisition occurred. `make check`: lint clean, 188 tests passed. +Two existing tests require the checkout directory name `sand-boxer`; the final +full check used that canonical basename. Evidence: +`docs/evidence/SAND-WP-0015-protected-local-install-2026-09-08.json`. +T04 retains owner execution configuration, native credential/egress and real-model +acceptance; Railiance installation needs its own target-specific return.