Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
10 KiB
Bwrap runtime and private state
ext.bwrap can mount a standalone Python runtime selected by the owner's
extension configuration. A create/exec caller cannot select host paths or
replace the sanitized child environment.
config:
runtime:
path: /srv/sandboxer/runtimes/rein-aharness-example
sha256: <reviewed-complete-artifact-digest>
The path must be a canonical absolute directory with pyvenv.cfg and
bin/python3. Its complete tree digest includes file contents, modes, directory
entries, and internal symlinks. Absolute/escaping symlinks and special files
are refused. Missing files, added files, changed permissions, or changed
content invalidate the pin. The runtime must not overlap the source checkout
or writable sandbox workspace.
The owner mounts the artifact read-only at /opt/sandboxer/runtime and prepends
its bin directory to the child PATH. Console entrypoints must use that fixed
mount prefix. Artifacts remain an owner trust boundary: publish them in a
protected location and do not mutate them while sandboxes use them. Digest
validation occurs at creation; it is not a mechanism for making a mutable host
directory immutable to its owner.
Building and testing the Python rein bundle
From this repository, with committed rein/llm-connect source checkouts:
uv run python scripts/build-rein-runtime.py \
--output /tmp/rein-runtime-candidate \
--rein-source ../rein-aharness \
--llm-source ../llm-connect
The builder creates a standalone system-Python venv, installs both packages
non-editably with their dependencies, rewrites Python console entrypoint
shebangs to the fixed mount path, and records source revisions plus resolved
package versions in build-info.json. It prints the resulting path/digest.
Dependency versions are resolved during this candidate build; the digest pins
the produced artifact, not a promise that another build will be bit-identical.
No interactive home, credentials, or provider login is copied into the bundle.
Prove the artifact using the returned values:
uv run python scripts/smoke-bwrap-runtime.py \
--runtime-path /tmp/rein-runtime-candidate \
--runtime-sha256 <returned-digest>
The smoke uses an isolated owner extension configuration and the real broker.
It imports the rein's Claude adapter and llm-connect, runs rein-aharness --help, proves the runtime rejects writes, writes private state outside the
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 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
The broker creates mode-0700 directories in the namespace's temporary root:
| Environment variable | Location |
|---|---|
HOME |
/run/sandboxer/state/home |
XDG_CONFIG_HOME |
/run/sandboxer/state/config |
XDG_CACHE_HOME |
/run/sandboxer/state/cache |
XDG_STATE_HOME |
/run/sandboxer/state/data |
TMPDIR |
/run/sandboxer/state/tmp |
These directories persist between exec requests in the same sandbox, remain outside the Git worktree, and disappear with namespace teardown. Python user site packages and bytecode writes are disabled. The owner's child environment continues to carry exact consumer identity and value-free route references.
Network and credentials remain separate gates
Bwrap refuses network.default: allow and setup secret references. Network
egress is opt-in through the owner allowlist described in bwrap-egress.md;
empty-egress profiles retain loopback-only networking. Credential exec delivery is now opt-in through the owner route contract
in bwrap-credentials.md; no production route is active. Returning a
declared egress list as evidence would not make that list enforced or usable.
Exec credential route references contain no values. Nonempty references now
require an owner-configured, consumer-bound provider; references alone grant nothing.
On 2026-09-05, warden route find anthropic --json and warden route find claude-code --json returned no matching workload routes. The generic OpenBao
entry is explicitly a routing template, not an executable credential lane.
The OpenRouter AppRole route belongs to the other rein and is not a substitute
for Claude authentication.
SAND-WP-0015-T04 / SAND-WP-0014-T05 and GLAS-WP-0012-T02 retain the
production requirements: a concrete owner-approved Claude credential route,
delivery and revocation semantics, a pinned Claude runtime, and explicitly
enforced provider egress with negative tests. Glas local profiles remain
blocked until those requirements and the real-model acceptance pass.
2026-09-05 candidate evidence
Runtime SHA-256:
4c316737ec2715936a12c4f49621a5e4be3d1f4fe4739130393f28cdda66fbd9.
Candidate path: /tmp/sandboxer-rein-aharness-runtime-20260905-v2.
Source revisions: rein-aharness 1429db5ad4c83331b6375349ffde1eb13af9575b,
llm-connect 00560945f81ba6ff1f5cacd9fe99c7fe756cc4b1; Python 3.12.3.
Sandbox f333fb66 passed the real CLI/import/read-only/private-state smoke.
sys.prefix was /opt/sandboxer/runtime, HOME was outside the worktree with
mode 0700, Git stayed clean, source was absent, only lo existed, credential
references were empty, and the workspace was removed. No model request or
credential acquisition occurred.
Final validation: make check passed lint and 132 tests. Authenticated owner
API smoke 223db65b returned HTTP 200 with exact identity/stdin and complete
teardown. Follow-up runtime smoke d4de9531 repeated the real CLI/startup
checks and proved private state survives a second exec in the same namespace.
The default unconfigured profile.bwrap-local still has no selected rein
bundle; candidate startup does not constitute production deployment.
2026-09-06 combined-runtime return to Glas (GLAS-WP-0015 handoff)
Returned for GLAS-WP-0012 review of revision 1.1.0. Nothing here is activated.
| Field | Value |
|---|---|
| Profile | profile.claude-agent-dev-proof, version 1.1.0, ext.bwrap |
| Host scope | localhost only (placement.prefer: [localhost], no fallback) |
| Consumer tuple | actor: agt, project: glas-local-proof, nonempty run_id |
| Credential route | glas-claude-agent-dev-anthropic, exec-env, ANTHROPIC_API_KEY |
| Declared egress | api.anthropic.com:443, under network.default: deny |
| Python runtime digest | 4c316737ec2715936a12c4f49621a5e4be3d1f4fe4739130393f28cdda66fbd9 |
| Runtime mount | read-only /opt/sandboxer/runtime, bin prepended to child PATH |
| Runtime sources | rein-aharness 1429db5, llm-connect 0056094, Python 3.12.3 |
| Claude executable | not pinned — no path, no digest, no startup proof |
The project example is reconciled to the acceptance runner's glas-local-proof;
the route is not broadened to other Glas projects. See bwrap-credentials.md.
Denial and cleanup evidence already recorded, all value-free: wrong-project
credential denial and next-exec key absence in sandbox 0e5fb35a; undeclared
example.com and direct 1.1.1.1:443 denial with proxy/workspace teardown in
sandbox e290e788; read-only runtime, private 0700 HOME, clean worktree, absent
source and loopback-only networking in sandboxes f333fb66 / d4de9531. The
committed profile declares egress but grants none: ext.bwrap refuses it unless
the owner extension config independently allowlists the destination, and a
regression test asserts that fail-closed default.
Still operator-blocked, so this profile stays unready: the concrete Claude
credential lane and its delivery/revocation contract (SECRETS-WP-0009 upstream
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.