sand-boxer/docs/bwrap-runtime.md
tegwick 23d0c2b34d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat: return combined bwrap runtime candidate to Glas
Answer the GLAS-WP-0015 handoff for SAND-WP-0015-T04 without activating any
production path.

Add candidate profile profile.claude-agent-dev-proof v1.1.0 (ext.bwrap,
localhost-only, default: deny, declared api.anthropic.com:443) for GLAS-WP-0012
review. The committed profile grants no egress by itself — ext.bwrap refuses it
unless owner extension config independently allowlists the destination — and a
regression test asserts that fail-closed default.

Reconcile the differing project examples in favour of the acceptance runner's
actor agt / project glas-local-proof, keeping the documented credential route
bound to that single project rather than broadening it.

Record the return contract (profile revision, host scope, consumer tuple,
runtime digest and mount paths, declared egress, value-free denial/cleanup
receipts) in docs/bwrap-runtime.md.

T04 stays wait: Claude credential lane, owner machine authentication, pinned
Claude executable and real-model acceptance remain operator gated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjyScPKb8MV8y2VZHGFSSV

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 716401@bnt-lap001
Assistant-Session: 0d02392b-d4a8-4fed-98e3-32333f768169
2026-09-06 20:34:51 +02:00

8 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 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 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.