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
This commit is contained in:
parent
174dba17b6
commit
df5e941814
6 changed files with 333 additions and 1 deletions
|
|
@ -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 <absolute-candidate> --sha256
|
||||
<reviewed-complete-digest>` 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
23
scripts/install-bwrap-runtime.py
Normal file
23
scripts/install-bwrap-runtime.py
Normal file
|
|
@ -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())
|
||||
89
src/sandboxer/extensions/runtime_store.py
Normal file
89
src/sandboxer/extensions/runtime_store.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
111
tests/test_runtime_store.py
Normal file
111
tests/test_runtime_store.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue