Package sandbox definitions and build a pinned owner runtime
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 22:54:39 +02:00
parent bfe0e4c4c8
commit be42de7caf
9 changed files with 127 additions and 8 deletions

View file

@ -220,3 +220,27 @@ candidate unchanged. Its installed-path startup/lifecycle proof is
[the local installation receipt](evidence/SAND-WP-0015-protected-local-install-2026-09-08.json). [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 SAND-WP-0015-T06 is complete. T04 still owns configured execution, native
credential adoption, real-model acceptance and production placement. credential adoption, real-model acceptance and production placement.
## Standalone owner candidate
`build-rein-runtime.py --owner-runtime` verifies rein's runtime-contract lock,
then installs non-editable rein/llm/Glas/sandboxer packages from rein's frozen
uv.lock. The artifact records source pins, package versions and the uv.lock digest.
Use the existing explicit `--claude-binary/--claude-sha256/--claude-version` pin;
a rolling workstation alias is not a candidate input. Console shebangs remain
fixed to the namespace mount; use the candidate interpreter with `-I -B -m` for
host owner commands. No credential, provider call or installation is part of build.
Sandboxer wheels now include profile and extension definitions with packaged-first
lookup and source-checkout fallback. This closes a concrete local-versus-installed
owner discrepancy for bwrap. It is not a claim that every remote backend's external
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
`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.

View file

@ -23,6 +23,10 @@ sandboxer = "sandboxer.cli:app"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/sandboxer"] packages = ["src/sandboxer"]
[tool.hatch.build.targets.wheel.force-include]
"profiles" = "sandboxer/data/profiles"
"extensions" = "sandboxer/data/extensions"
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = ["src"] pythonpath = ["src"]

View file

@ -9,6 +9,7 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import json import json
import os
import re import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@ -16,8 +17,8 @@ from pathlib import Path
from sandboxer.extensions.runtime import RUNTIME_MOUNT, runtime_digest from sandboxer.extensions.runtime import RUNTIME_MOUNT, runtime_digest
def checked(command: list[str]) -> str: def checked(command: list[str], *, env: dict[str, str] | None = None) -> str:
result = subprocess.run(command, capture_output=True, text=True, timeout=300) result = subprocess.run(command, capture_output=True, text=True, timeout=300, env=env)
if result.returncode: if result.returncode:
raise RuntimeError(f"runtime build command failed: {Path(command[0]).name}") raise RuntimeError(f"runtime build command failed: {Path(command[0]).name}")
return result.stdout.strip() return result.stdout.strip()
@ -44,7 +45,7 @@ def install_claude(output: Path, source: Path, sha256: str, version: str) -> dic
def build(output: Path, rein_source: Path, llm_source: Path, def build(output: Path, rein_source: Path, llm_source: Path,
claude_binary: Path | None = None, claude_sha256: str | None = None, claude_binary: Path | None = None, claude_sha256: str | None = None,
claude_version: str | None = None) -> dict: claude_version: str | None = None, *, owner_runtime: bool = False) -> dict:
supplied = (claude_binary, claude_sha256, claude_version) supplied = (claude_binary, claude_sha256, claude_version)
if any(x is not None for x in supplied) and not all(x is not None for x in supplied): if any(x is not None for x in supplied) and not all(x is not None for x in supplied):
raise ValueError("Claude binary, SHA-256 and expected version must be supplied together") raise ValueError("Claude binary, SHA-256 and expected version must be supplied together")
@ -53,10 +54,26 @@ def build(output: Path, rein_source: Path, llm_source: Path,
if checked(["git", "-C", str(source), "status", "--porcelain"]): if checked(["git", "-C", str(source), "status", "--porcelain"]):
raise ValueError(f"{name} source must be committed before building") raise ValueError(f"{name} source must be committed before building")
revisions[name] = checked(["git", "-C", str(source), "rev-parse", "HEAD"]) revisions[name] = checked(["git", "-C", str(source), "rev-parse", "HEAD"])
lock_report = None
if owner_runtime:
lock_report = json.loads(checked([
"/usr/bin/python3", str(rein_source / "scripts/verify_runtime_lock.py")
]))
if not lock_report.get("ok"):
raise ValueError("owner runtime requires the verified sibling lock")
if next(x["commit"] for x in lock_report["dependencies"]
if x["distribution"] == "llm-connect") != revisions["llm-connect"]:
raise ValueError("owner runtime llm source does not match sibling lock")
revisions.update({x["distribution"]: x["commit"] for x in lock_report["dependencies"]})
output.mkdir(parents=True, exist_ok=False) output.mkdir(parents=True, exist_ok=False)
checked(["/usr/bin/python3", "-m", "venv", "--copies", "--without-pip", str(output)]) checked(["/usr/bin/python3", "-m", "venv", "--copies", "--without-pip", str(output)])
checked(["uv", "pip", "install", "--python", str(output / "bin/python3"), if owner_runtime:
str(rein_source), str(llm_source)]) checked(["uv", "sync", "--project", str(rein_source), "--frozen", "--no-editable",
"--no-dev", "--extra", "glas", "--extra", "llm", "--python", "/usr/bin/python3"],
env={**os.environ, "UV_PROJECT_ENVIRONMENT": str(output)})
else:
checked(["uv", "pip", "install", "--python", str(output / "bin/python3"),
str(rein_source), str(llm_source)])
# Console entrypoints must reference the in-sandbox mount, not the build host. # Console entrypoints must reference the in-sandbox mount, not the build host.
for path in (output / "bin").iterdir(): for path in (output / "bin").iterdir():
if not path.is_file() or path.is_symlink(): if not path.is_file() or path.is_symlink():
@ -75,6 +92,12 @@ def build(output: Path, rein_source: Path, llm_source: Path,
if claude_binary is not None: if claude_binary is not None:
metadata["claude"] = install_claude(output, claude_binary, claude_sha256, claude_version) metadata["claude"] = install_claude(output, claude_binary, claude_sha256, claude_version)
metadata["source_revisions"] = revisions metadata["source_revisions"] = revisions
if owner_runtime:
metadata["owner_runtime"] = True
metadata["runtime_contract"] = lock_report
metadata["uv_lock_sha256"] = hashlib.sha256(
(rein_source / "uv.lock").read_bytes()
).hexdigest()
(output / "build-info.json").write_text(json.dumps(metadata, indent=2) + "\n") (output / "build-info.json").write_text(json.dumps(metadata, indent=2) + "\n")
return {"runtime": {"path": str(output), "sha256": runtime_digest(output)}, return {"runtime": {"path": str(output), "sha256": runtime_digest(output)},
"build": metadata} "build": metadata}
@ -88,9 +111,12 @@ def main() -> int:
parser.add_argument("--claude-binary", type=Path) parser.add_argument("--claude-binary", type=Path)
parser.add_argument("--claude-sha256") parser.add_argument("--claude-sha256")
parser.add_argument("--claude-version") parser.add_argument("--claude-version")
parser.add_argument("--owner-runtime", action="store_true",
help="Include the pinned owner packages from rein uv.lock")
args = parser.parse_args() args = parser.parse_args()
result = build(args.output.resolve(), args.rein_source.resolve(), args.llm_source.resolve(), result = build(args.output.resolve(), args.rein_source.resolve(), args.llm_source.resolve(),
args.claude_binary, args.claude_sha256, args.claude_version) args.claude_binary, args.claude_sha256, args.claude_version,
owner_runtime=args.owner_runtime)
print(json.dumps(result, indent=2)) print(json.dumps(result, indent=2))
return 0 return 0

View file

@ -19,6 +19,9 @@ class OwnerMessagesRoute:
run_id: str run_id: str
private_paths: tuple[Path, ...] = field(default=(), repr=False) private_paths: tuple[Path, ...] = field(default=(), repr=False)
runtime_path: Path | None = None
runtime_sha256: str | None = None
def validate(self, profile, consumer, backend, inputs) -> None: def validate(self, profile, consumer, backend, inputs) -> None:
from sandboxer.extensions.bwrap import BwrapExtension from sandboxer.extensions.bwrap import BwrapExtension
@ -32,6 +35,15 @@ class OwnerMessagesRoute:
or not self.run_id or not self.run_id
): ):
raise ValueError("metered route consumer mismatch") raise ValueError("metered route consumer mismatch")
if self.runtime_path is not None or self.runtime_sha256 is not None:
if (self.runtime_path is None or not self.runtime_path.is_absolute()
or not isinstance(self.runtime_sha256, str)
or not re.fullmatch(r"[0-9a-f]{64}", self.runtime_sha256)):
raise ValueError("metered runtime requires exact path and digest")
# Trusted, in-memory bootstrap selection; never copied into public status.
backend.config = {**backend.config, "runtime": {
"path": str(self.runtime_path), "sha256": self.runtime_sha256,
}}
if profile.setup.secret_refs: if profile.setup.secret_refs:
raise ValueError("metered route refuses setup credential acquisition") raise ValueError("metered route refuses setup credential acquisition")
if profile.network.default != "deny" or profile.network.egress: if profile.network.default != "deny" or profile.network.egress:

View file

@ -11,7 +11,8 @@ import yaml
from sandboxer.models import Extension, Profile from sandboxer.models import Extension, Profile
_REPO_ROOT = Path(__file__).resolve().parents[3] _REPO_ROOT = Path(__file__).resolve().parents[3]
_EXTENSIONS_DIR = _REPO_ROOT / "extensions" _PACKAGED_DIR = Path(__file__).resolve().parents[1] / "data" / "extensions"
_EXTENSIONS_DIR = _PACKAGED_DIR if _PACKAGED_DIR.is_dir() else _REPO_ROOT / "extensions"
_REQUIRED_CAPABILITY_FIELDS = ("isolation_levels", "pricing_model") _REQUIRED_CAPABILITY_FIELDS = ("isolation_levels", "pricing_model")

View file

@ -9,7 +9,8 @@ import yaml
from sandboxer.models import Profile from sandboxer.models import Profile
_REPO_ROOT = Path(__file__).resolve().parents[3] _REPO_ROOT = Path(__file__).resolve().parents[3]
_PROFILES_DIR = _REPO_ROOT / "profiles" _PACKAGED_DIR = Path(__file__).resolve().parents[1] / "data" / "profiles"
_PROFILES_DIR = _PACKAGED_DIR if _PACKAGED_DIR.is_dir() else _REPO_ROOT / "profiles"
def profiles_dir() -> Path: def profiles_dir() -> Path:

View file

@ -62,3 +62,19 @@ def test_route_refuses_expansion(binding, tmp_path, change):
backend = SimpleNamespace() backend = SimpleNamespace()
with pytest.raises(ValueError): with pytest.raises(ValueError):
binding.validate(profile, consumer, backend, {}) binding.validate(profile, consumer, backend, {})
def test_trusted_runtime_pin_is_injected_and_overlap_refused(binding, tmp_path):
runtime = tmp_path / "runtime"
binding = replace(binding, runtime_path=runtime, runtime_sha256="b" * 64)
backend = BwrapExtension({"base_dir": str(tmp_path / "workspaces")})
profile = load_profile("profile.bwrap-local")
consumer = Consumer(actor="agt", project="fixture", run_id="run-1")
binding.validate(profile, consumer, backend, {})
assert backend.config["runtime"] == {"path": str(runtime), "sha256": "b" * 64}
with pytest.raises(ValueError, match="overlaps"):
replace(binding, runtime_path=binding.socket_path.parent).validate(
profile, consumer, backend, {}
)
with pytest.raises(ValueError, match="exact path"):
replace(binding, runtime_sha256=None).validate(profile, consumer, backend, {})

View file

@ -50,3 +50,22 @@ def test_incomplete_claude_pin_refuses_before_build(tmp_path):
with pytest.raises(ValueError, match="supplied together"): with pytest.raises(ValueError, match="supplied together"):
builder.build(tmp_path / "out", tmp_path, tmp_path, claude_binary=tmp_path / "claude") builder.build(tmp_path / "out", tmp_path, tmp_path, claude_binary=tmp_path / "claude")
assert not (tmp_path / "out").exists() assert not (tmp_path / "out").exists()
def test_owner_build_requires_matching_sibling_lock_before_output(tmp_path, monkeypatch):
import json
rein = tmp_path / "rein"
llm = tmp_path / "llm"
output = tmp_path / "output"
def checked(argv, **kwargs):
if "--porcelain" in argv:
return ""
if "rev-parse" in argv:
return "a" * 40
return json.dumps({"ok": True, "dependencies": [
{"distribution": "llm-connect", "commit": "b" * 40}
]})
monkeypatch.setattr(builder, "checked", checked)
with pytest.raises(ValueError, match="does not match"):
builder.build(output, rein, llm, owner_runtime=True)
assert not output.exists()

View file

@ -260,3 +260,19 @@ admitted provider-to-owner bootstrap, updated protected runtime/profile, Railian
placement, live compatibility and G0. Existing child-provider-key/direct-CONNECT placement, live compatibility and G0. Existing child-provider-key/direct-CONNECT
proofs do not admit this different credential holder or metered profile. No CCR, proofs do not admit this different credential holder or metered profile. No CCR,
secret read, deployment or paid request was performed. secret read, deployment or paid request was performed.
## 2026-09-09 standalone owner packaging and runtime selection
Added frozen-lock owner build mode for the matched rein/llm/Glas/sandboxer set,
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
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
build and isolated installed-interpreter/CLI proof. T04 remains wait for accepted
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.