Package pinned native Claude and prove isolated startup
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
tegwick 2026-09-06 23:51:50 +02:00
parent cfc79d51af
commit 174dba17b6
7 changed files with 299 additions and 8 deletions

View file

@ -122,3 +122,4 @@
| task | SAND-WP-0015-T02 | done | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
| task | SAND-WP-0015-T03 | done | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
| task | SAND-WP-0015-T04 | wait | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |
| task | SAND-WP-0015-T05 | done | — | workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md |

View file

@ -58,9 +58,9 @@ It imports the rein's Claude adapter and llm-connect, runs `rein-aharness
worktree, verifies source absence and a clean Git tree, checks loopback-only
networking, and tears down. This is real CLI startup, not a model task.
The Python bundle does not package or pin the separate Claude executable.
That executable's release, startup behavior, and authentication still require
review in the eventual production runtime. No committed profile selects a
The default Python-only build does not package Claude. The optional explicit
Claude pin and isolated startup proof are documented in the later candidate
section below; production authentication remains a separate gate. No committed profile selects a
temporary `/tmp` build as its production runtime.
## Private writable state
@ -157,3 +157,40 @@ of CCR-2026-0016), owner machine authentication, the pinned Claude executable
with its own HTTPS_PROXY startup proof, and the real-model acceptance run.
Provider workspace scope and spend limit remain operator inputs; a model
`budget_tokens` setting is not a provider spend limit.
## Pinned Claude candidate and isolated startup — 2026-09-06
The builder now accepts `--claude-binary`, `--claude-sha256` and
`--claude-version` together. It requires a regular native ELF file, verifies the
exact copied bytes, refuses symlinks/wrappers/mismatches and existing destination
files, and copies only the executable as bin/claude mode 0755. The expected
version is metadata until the sandbox smoke checks the actual output. No
interactive HOME, provider key or login state is copied.
Candidate provenance: the explicitly selected installed native release
`/home/worsch/.local/share/claude/versions/2.1.263`, SHA-256
`26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba`.
This is a digest pin of the selected local artifact, not an upstream signature
verification claim. Complete runtime candidate:
`/tmp/sandboxer-claude-runtime-20260906`, SHA-256
`5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922`.
Mount remains `/opt/sandboxer/runtime`; Claude is `/opt/sandboxer/runtime/bin/claude`.
Rein sources remain 1429db5 / llm-connect 0056094; resolved dependency versions
are recorded in the evidence. Rebuilding may resolve different dependencies;
the complete digest pins this artifact, not future builds.
Sandbox f8821ec2 ran real `claude --version` -> `2.1.263 (Claude Code)` plus rein
CLI/import checks, with owner allowlisted provider egress and HTTPS_PROXY set
by the broker. Runtime read-only, private 0700 HOME/state persistence, clean Git,
absent source and loopback-only interfaces passed; workspace and proxy removed.
`make check`: lint passed, 175 tests passed.
Evidence: docs/evidence/SAND-WP-0015-claude-startup-2026-09-06.json.
Reproduce with the builder options above and `scripts/smoke-bwrap-runtime.py
--runtime-path <candidate> --runtime-sha256 <digest> --claude-version 2.1.263`.
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.

View file

@ -0,0 +1,94 @@
{
"build": {
"runtime": {
"path": "/tmp/sandboxer-claude-runtime-20260906",
"sha256": "5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922"
},
"build": {
"python": "3.12.3",
"packages": {
"idna": "3.19",
"h11": "0.16.0",
"httpcore": "1.0.9",
"llm-connect": "0.1.0",
"certifi": "2026.7.22",
"PyYAML": "6.0.3",
"toml": "0.10.2",
"typing_extensions": "4.16.0",
"httpx": "0.28.1",
"rein-aharness": "0.1.0",
"anyio": "4.15.1"
},
"claude": {
"path": "/opt/sandboxer/runtime/bin/claude",
"sha256": "26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba",
"expected_version": "2.1.263",
"source": "/home/worsch/.local/share/claude/versions/2.1.263"
},
"source_revisions": {
"rein-aharness": "1429db5ad4c83331b6375349ffde1eb13af9575b",
"llm-connect": "00560945f81ba6ff1f5cacd9fe99c7fe756cc4b1"
}
}
},
"smoke": {
"ok": true,
"sandbox_id": "f8821ec2",
"runtime_sha256": "5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922",
"proof": {
"rein_cli_started": true,
"adapter_imported": true,
"runtime_readonly": true,
"source_absent": true,
"home_outside_workspace": true,
"home_mode": "0700",
"worktree_clean": true,
"interfaces": [
"lo"
],
"python_prefix": "/opt/sandboxer/runtime",
"claude_version": "2.1.263 (Claude Code)",
"https_proxy_present": true,
"claude_provider_request_proven": false,
"credential_refs": []
},
"workspace_removed": true,
"exit_code": 0,
"model_run_proven": false,
"private_state_persisted": true,
"proxy_removed": true
},
"verification": {
"lint": "passed",
"tests_passed": 175
},
"production_deployed": false,
"provenance": "Explicitly selected installed native executable; no interactive HOME copied.",
"absolute_path_smoke": {
"ok": true,
"sandbox_id": "979c0208",
"runtime_sha256": "5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922",
"proof": {
"rein_cli_started": true,
"adapter_imported": true,
"runtime_readonly": true,
"source_absent": true,
"home_outside_workspace": true,
"home_mode": "0700",
"worktree_clean": true,
"interfaces": [
"lo"
],
"python_prefix": "/opt/sandboxer/runtime",
"claude_version": "2.1.263 (Claude Code)",
"https_proxy_present": true,
"claude_provider_request_proven": false,
"credential_refs": []
},
"workspace_removed": true,
"exit_code": 0,
"model_run_proven": false,
"private_state_persisted": true,
"proxy_removed": true
}
}

View file

@ -7,7 +7,9 @@ Run with the sand-boxer Python environment; uv must be available on PATH.
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from pathlib import Path
@ -21,7 +23,31 @@ def checked(command: list[str]) -> str:
return result.stdout.strip()
def build(output: Path, rein_source: Path, llm_source: Path) -> dict:
def install_claude(output: Path, source: Path, sha256: str, version: str) -> dict:
"""Copy only an explicitly pinned native executable, never an interactive HOME."""
if not re.fullmatch(r"[0-9a-f]{64}", sha256) or not version.strip():
raise ValueError("Claude requires an exact SHA-256 and expected version")
if source.is_symlink() or not source.is_file():
raise ValueError("Claude source must be a regular file, not a symlink")
content = source.read_bytes()
if hashlib.sha256(content).hexdigest() != sha256:
raise ValueError("Claude executable digest mismatch")
if not content.startswith(b"\x7fELF"):
raise ValueError("Claude executable must be native ELF, not a host wrapper")
destination = output / "bin/claude"
with destination.open("xb") as stream:
stream.write(content)
destination.chmod(0o755)
return {"path": f"{RUNTIME_MOUNT}/bin/claude", "sha256": sha256,
"expected_version": version, "source": str(source)}
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) -> dict:
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):
raise ValueError("Claude binary, SHA-256 and expected version must be supplied together")
revisions = {}
for name, source in (("rein-aharness", rein_source), ("llm-connect", llm_source)):
if checked(["git", "-C", str(source), "status", "--porcelain"]):
@ -46,6 +72,8 @@ def build(output: Path, rein_source: Path, llm_source: Path) -> dict:
"print(json.dumps({'python':platform.python_version(),'packages':"
"{d.metadata['Name']:d.version for d in importlib.metadata.distributions()}}))",
]))
if claude_binary is not None:
metadata["claude"] = install_claude(output, claude_binary, claude_sha256, claude_version)
metadata["source_revisions"] = revisions
(output / "build-info.json").write_text(json.dumps(metadata, indent=2) + "\n")
return {"runtime": {"path": str(output), "sha256": runtime_digest(output)},
@ -57,8 +85,12 @@ def main() -> int:
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--rein-source", type=Path, required=True)
parser.add_argument("--llm-source", type=Path, required=True)
parser.add_argument("--claude-binary", type=Path)
parser.add_argument("--claude-sha256")
parser.add_argument("--claude-version")
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)
print(json.dumps(result, indent=2))
return 0

View file

@ -38,6 +38,17 @@ assert not source.exists()
help_result = subprocess.run(['rein-aharness', '--help'], capture_output=True, timeout=15)
assert help_result.returncode == 0
assert b'usage:' in help_result.stdout
claude_version = None
proxy_present = False
if sys.argv[2]:
assert 'ANTHROPIC_API_KEY' not in os.environ
proxy_present = os.environ['HTTPS_PROXY'].startswith('http://127.0.0.1:')
assert proxy_present
cli = subprocess.run([str(runtime / 'bin/claude'), '--version'],
capture_output=True, text=True, timeout=20)
assert cli.returncode == 0, cli.stderr
claude_version = cli.stdout.strip()
assert claude_version == sys.argv[2] + ' (Claude Code)', claude_version
assert subprocess.check_output(['git', 'status', '--porcelain', '--ignored=matching']) == b''
interfaces = [line.split(':', 1)[0].strip()
for line in Path('/proc/net/dev').read_text().splitlines()[2:]]
@ -47,6 +58,8 @@ print(json.dumps({'rein_cli_started': True, 'adapter_imported': True,
'home_outside_workspace': True, 'home_mode': '0700',
'worktree_clean': True, 'interfaces': interfaces,
'python_prefix': sys.prefix,
'claude_version': claude_version, 'https_proxy_present': proxy_present,
'claude_provider_request_proven': False,
'credential_refs': json.loads(os.environ['SANDBOXER_CREDENTIAL_ROUTE_REFS'])}))
'''
@ -55,22 +68,26 @@ def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--runtime-path", required=True)
parser.add_argument("--runtime-sha256", required=True)
parser.add_argument("--claude-version", default="")
args = parser.parse_args()
with tempfile.TemporaryDirectory(prefix="sandboxer-runtime-proof-") as temporary:
root = Path(temporary)
source = root / "source"
source.mkdir()
subprocess.run(["git", "init", "-q", str(source)], check=True)
egress = ["api.anthropic.com:443"] if args.claude_version else []
extension = BwrapExtension({
"allowed_egress": egress,
"base_dir": str(root / "sandboxes"),
"runtime": {"path": args.runtime_path, "sha256": args.runtime_sha256},
})
profile = Profile(id="profile.runtime-proof", version="1", extension="ext.bwrap")
profile = Profile(id="profile.runtime-proof", version="1", extension="ext.bwrap",
network={"default": "deny", "egress": egress})
handle = extension.provision(profile, {"repo": str(source)}, "localhost")
try:
extension.wait_ready(handle)
result = extension.execute(
handle, ["python3", "-c", PROBE, str(source)],
handle, ["python3", "-c", PROBE, str(source), args.claude_version],
credential_route_refs=[],
execution_context={"actor": "agt", "project": "sand-boxer-runtime-proof",
"run_id": "sand-wp-0015-proof"},
@ -91,12 +108,17 @@ def main() -> int:
facts = json.loads(result["stdout"]) if passed else {}
private_state_persisted = persistence["exit_code"] == 0
passed = passed and private_state_persisted and teardown["workspace_removed"] == "True"
proxy_removed = not egress or (
not Path(handle["egress_dir"]).exists()
and not extension._pid_alive(int(handle["egress_pid"]))
)
passed = passed and proxy_removed
print(json.dumps({
"ok": passed, "sandbox_id": handle["sandbox_id"],
"runtime_sha256": args.runtime_sha256, "proof": facts,
"workspace_removed": teardown["workspace_removed"] == "True",
"exit_code": result["exit_code"], "model_run_proven": False,
"private_state_persisted": private_state_persisted,
"private_state_persisted": private_state_persisted, "proxy_removed": proxy_removed,
}, indent=2))
return 0 if passed else 1

View file

@ -0,0 +1,52 @@
import hashlib
import importlib.util
from pathlib import Path
import pytest
spec = importlib.util.spec_from_file_location(
"runtime_builder", Path(__file__).parents[1] / "scripts/build-rein-runtime.py"
)
builder = importlib.util.module_from_spec(spec)
spec.loader.exec_module(builder)
def test_claude_copy_is_pinned_and_excludes_home(tmp_path):
source = tmp_path / "native"
source.write_bytes(b"\x7fELFtest artifact, never executed")
(tmp_path / "credentials.json").write_text("must not enter artifact")
output = tmp_path / "runtime"
(output / "bin").mkdir(parents=True)
digest = hashlib.sha256(source.read_bytes()).hexdigest()
metadata = builder.install_claude(output, source, digest, "test-version")
assert metadata["sha256"] == digest
assert metadata["path"] == "/opt/sandboxer/runtime/bin/claude"
assert list((output / "bin").iterdir()) == [output / "bin/claude"]
assert (output / "bin/claude").read_bytes() == source.read_bytes()
assert (output / "bin/claude").stat().st_mode & 0o777 == 0o755
assert not (output / "credentials.json").exists()
with pytest.raises(FileExistsError):
builder.install_claude(output, source, digest, "test-version")
def test_changed_binary_and_host_wrapper_refuse(tmp_path):
source = tmp_path / "binary"
source.write_bytes(b"#!/usr/bin/env node\n")
output = tmp_path / "runtime"
(output / "bin").mkdir(parents=True)
with pytest.raises(ValueError, match="digest mismatch"):
builder.install_claude(output, source, "0" * 64, "test-version")
digest = hashlib.sha256(source.read_bytes()).hexdigest()
with pytest.raises(ValueError, match="native ELF"):
builder.install_claude(output, source, digest, "test-version")
link = tmp_path / "link"
link.symlink_to(source)
with pytest.raises(ValueError, match="symlink"):
builder.install_claude(output, link, digest, "test-version")
assert not (output / "bin/claude").exists()
def test_incomplete_claude_pin_refuses_before_build(tmp_path):
with pytest.raises(ValueError, match="supplied together"):
builder.build(tmp_path / "out", tmp_path, tmp_path, claude_binary=tmp_path / "claude")
assert not (tmp_path / "out").exists()

View file

@ -151,3 +151,56 @@ contract, owner machine authentication, the pinned Claude executable with its
own proxy startup proof, and the real-model acceptance are still operator and
upstream-owner gated. Provider workspace scope and spend limit remain operator
inputs. `SAND-WP-0014-T05` remains open on the same gates.
## Package and prove the pinned Claude executable independently of custody
```task
id: SAND-WP-0015-T05
status: done
priority: high
state_hub_task_id: "f0163cda-b581-56a0-a0dd-9c8a436f5bbe"
```
Split the independent executable packaging/startup portion from T04 so credential
custody does not hide completed local runtime work. Implement explicit binary
pinning, reject mismatches/host wrappers, and prove actual startup with the
broker proxy environment and complete cleanup. T04 retains protected deployment,
credential adoption and real-provider acceptance.
## Pinned Claude candidate and isolated startup — 2026-09-06
The builder now accepts `--claude-binary`, `--claude-sha256` and
`--claude-version` together. It requires a regular native ELF file, verifies the
exact copied bytes, refuses symlinks/wrappers/mismatches and existing destination
files, and copies only the executable as bin/claude mode 0755. The expected
version is metadata until the sandbox smoke checks the actual output. No
interactive HOME, provider key or login state is copied.
Candidate provenance: the explicitly selected installed native release
`/home/worsch/.local/share/claude/versions/2.1.263`, SHA-256
`26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba`.
This is a digest pin of the selected local artifact, not an upstream signature
verification claim. Complete runtime candidate:
`/tmp/sandboxer-claude-runtime-20260906`, SHA-256
`5cf9a16c5d77a16bdb2cb5b3df06ea655356bc2d44741791e3fedfee20d7e922`.
Mount remains `/opt/sandboxer/runtime`; Claude is `/opt/sandboxer/runtime/bin/claude`.
Rein sources remain 1429db5 / llm-connect 0056094; resolved dependency versions
are recorded in the evidence. Rebuilding may resolve different dependencies;
the complete digest pins this artifact, not future builds.
Sandbox f8821ec2 ran real `claude --version` -> `2.1.263 (Claude Code)` plus rein
CLI/import checks, with owner allowlisted provider egress and HTTPS_PROXY set
by the broker. Runtime read-only, private 0700 HOME/state persistence, clean Git,
absent source and loopback-only interfaces passed; workspace and proxy removed.
`make check`: lint passed, 175 tests passed.
Evidence: docs/evidence/SAND-WP-0015-claude-startup-2026-09-06.json.
Reproduce with the builder options above and `scripts/smoke-bwrap-runtime.py
--runtime-path <candidate> --runtime-sha256 <digest> --claude-version 2.1.263`.
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.