sccache support
The cargo bundle has an sccache knob with three modes:
[sandbox.os]command_tools = [ { tool = "cargo", sccache = "off" }, # default { tool = "cargo", sccache = "manual" }, { tool = "cargo", sccache = "on" },]All three opt-in your project to the cargo bundle’s other binds + env passthroughs (the ~/.cargo/ reads/writes, the crates.io domain allowlist, etc.). The sccache knob is purely about the rustc-wrapper layer.
Which one to pick
Section titled “Which one to pick”off(default) — no sccache binds, noRUSTC_WRAPPER/SCCACHE_*env passthrough, no daemon supervision. Use this if sccache isn’t installed or you don’t want compile caching.manual— bundle adds~/.cache/sccacheas an RW bind and forwardsRUSTC_WRAPPER+SCCACHE_*envs into sandboxed cargo invocations. You are responsible for starting / managing the host sccache daemon (sccache --start-server). Use this if you already run sccache via systemd, launchd, or just have it running long-lived from your shell.on— same bundle surface asmanual, plus seal-daemon owns the host sccache daemon’s lifecycle. The first session that hassccache = "on"triggers seal-daemon to start the daemon (or adopt an externally-running one); the daemon stays alive until seal-daemon shuts down. Use this if you want set-and-forget caching with no daemon-lifecycle management on your end.
What on does, in detail
Section titled “What on does, in detail”When seal-daemon receives a session-open or manifest-reload that resolves to grants containing a cargo bundle with sccache = "on":
- Latch flips on, once per daemon process. Subsequent on-sessions are idempotent — they don’t spawn a second daemon. The latch is monotonic; once on, it stays on until seal-daemon shuts down (we don’t want to bounce the daemon every time the user opens and closes sessions).
- Binary check. seal-daemon shells out
sccache --versionto confirm the binary is installed. If not, session open / reload fails with a clear error message naming the three validsccachevalues. Silent fallback would mask the user’s explicitonintent. - External-daemon probe. seal-daemon checks
127.0.0.1:4226(sccache’s default IPC port). If something’s already listening — the user pre-started a daemon, or a previous seal-daemon instance leaked one — seal-daemon adopts it asExternalownership and never tries to manage it. - Owned spawn. Otherwise, seal-daemon spawns
sccache --start-server(withSCCACHE_NO_DAEMONremoved from the child env so the daemon actually starts) and waits up to 3 seconds for the port to bind. - Liveness poll. A background task probes
127.0.0.1:4226every 30 seconds. If the daemon goes silent, the supervisor respawns (only forOwneddaemons; externally-managed daemons are someone else’s problem).
pgrep -af sccache after the first on session should show:
$ pgrep -af sccache631234 sccache --start-serverWhat sandboxed sccache calls do, before vs after the bridge
Section titled “What sandboxed sccache calls do, before vs after the bridge”Before the sandbox→host TCP-localhost bridge shipped, sandboxed cargo invocations could NOT reach the host sccache daemon. The reason:
- sccache’s client speaks to the daemon over TCP at
127.0.0.1:4226. - The OS sandbox uses
bwrap --unshare-net(Linux) orsandbox-execwith a deny-all network rule (macOS) —127.0.0.1inside the sandbox is a different loopback than the host’s.
To prevent sandboxed cargo from silently forking its own daemon (which it’d then be unable to reach across the netns), seal-daemon sets SCCACHE_NO_DAEMON=1 in its own process env at startup. The bundle’s SCCACHE_* glob env passthrough delivers SCCACHE_NO_DAEMON=1 into sandboxed cargo invocations.
SEA-683: when the cargo bundle has sccache = "manual" or "on", seal-policy auto-adds port 4226 to host_localhost_ports so the sandbox→host bridge wires up without the user having to opt the port in manually. The supervisor (in on mode) or the user’s manually-managed sccache daemon (in manual mode) is bridged transparently.
Override the no-daemon behavior: if you want to test sandbox sccache without the bridge or supervisor support, export SCCACHE_NO_DAEMON=0 in your shell. seal-daemon inherits from your shell’s env, so your value wins.
Sandbox→host bridge (host_localhost_ports)
Section titled “Sandbox→host bridge (host_localhost_ports)”The TCP-localhost bridge has shipped. With port 4226 auto-added when the cargo bundle has sccache = "manual" or "on" (SEA-683), the wiring is:
- Sandboxed cargo invocations connect to
127.0.0.1:4226inside the sandbox. - The bridge tunnels each connection through a per-port UDS to the host’s
127.0.0.1:4226daemon. - Host’s supervised sccache daemon services the request.
- Cache populates from both host shell builds AND agent CI runs.
No explicit host_localhost_ports = [4226] entry needed — the bundle drives the grant. The sccache = "on" daemon-supervision piece exists specifically so that when the bridge grant lands, the daemon’s already running and ready.
Manifest example for sccache over the bridge
Section titled “Manifest example for sccache over the bridge”[sandbox.os]command_tools = [ { tool = "cargo", sccache = "on" },]# No `[sandbox.os.network] host_localhost_ports = [4226]` needed —# the bundle auto-grants 4226. You can still add other ports# (postgres, redis, etc.) under `host_localhost_ports` — those# coexist with the bundle's auto-add.That’s the full set-and-forget setup: sccache = "on" brings up the host daemon as part of session open AND auto-grants the sandbox→host port bridge; sandboxed cargo’s connect(127.0.0.1:4226) reaches the host daemon transparently.
Binding other host services
Section titled “Binding other host services”The same host_localhost_ports knob covers every host TCP-localhost service the agent needs to reach from inside the sandbox. The bridge is service-agnostic — it just forwards bytes between an in-sandbox listener and a host loopback target. Common cases:
[sandbox.os.network]host_localhost_ports = [ 4226, # sccache (TCP IPC) 5432, # postgres 6379, # redis (if not using SEA-501's UDS pattern) 11434, # ollama / llama.cpp / lmstudio 8080, # mitmproxy / charles / dev proxy]prompt = true on a per-entry table forces a runtime prompt every call, even though the port is listed — useful for high-blast-radius services where you want one-tap-per-invocation confirmation:
host_localhost_ports = [ 4226, # silent for sccache { port = 5432, prompt = true }, # prompt for postgres every call]host_localhost_ports_deny takes precedence over host_localhost_ports and is the way to revoke a prior Allow always or pre-empt accidental exposure of a sensitive port:
host_localhost_ports_deny = [22, 3306] # never allow ssh or mysqlThe < 1024 privileged range is rejected at manifest parse — sshd and friends already need root to bind, and listing them here would only invite confusion.
How the bridge differs from the SEA-501 Redis-over-UDS pattern
Section titled “How the bridge differs from the SEA-501 Redis-over-UDS pattern”Both patterns let a sandboxed process reach a host service. Pick based on what the service speaks:
- Service speaks UDS natively (Redis, postgres with
unix_socket_directories, etc.) — use the SEA-501 Redis-over-UDS shape. The bundle’s--bind <socket>mounts the UDS into the sandbox, sandboxed code dialsunix:///path/to/socketdirectly. No bridge process needed, minimal overhead. - Service is TCP-only (sccache’s default IPC, ollama, mitmproxy, the user’s already-running postgres on
127.0.0.1:5432) — usehost_localhost_ports. The bridge tunnels TCP over a UDS internally; sandboxed code sees a normalconnect("127.0.0.1:<port>").
The bridge has slight per-connection overhead (one extra accept() + connect() + copy_bidirectional task per sandbox connection) but the cost is invisible in practice — cargo build against a warm sccache cache sees the same wall-clock time with the bridge as without it, because the dominant cost is sccache’s own work, not the bridge’s byte-copying.
Verifying the bridge
Section titled “Verifying the bridge”After host_localhost_ports = [4226] is granted and a session opens, ask the agent to run a build and watch the sccache stats from a host shell:
# Before the agent build:sccache --show-stats | grep 'Cache hits' | head -1# Cache hits 1234
# After the agent build:sccache --show-stats | grep 'Cache hits' | head -1# Cache hits 5678 ← went up by the build's compile unitsFor other services, a host TCP listener confirms the bridge end-to-end:
# Host shell — pre-bind a port the user controls:python3 -c "import sockets = socket.socket()s.bind(('127.0.0.1', 9999))s.listen(1)print('listening')c, _ = s.accept()data = c.recv(64)print(f'got: {data!r}')c.sendall(b'HELLO')c.close()"With host_localhost_ports = [9999] granted, ask the agent to python3 -c 'import socket; s=socket.socket(); s.connect(("127.0.0.1", 9999)); s.sendall(b"hi from sandbox"); print(s.recv(64))' and watch both sides see the exchange.
Verifying
Section titled “Verifying”After sccache = "on" is set in a manifest and a session opens:
# Host shell — daemon should be running.pgrep -af sccache# 631234 sccache --start-server
# Host shell — confirm Redis-or-disk backend is configured if# you've set SCCACHE_REDIS_ENDPOINT / etc. Otherwise local disk:sccache --show-stats | head -20If sccache isn’t installed:
$ seal[sandbox.os] cargo bundle has sccache = "on" but seal-daemoncouldn't start the sccache server: sccache binary not found onPATH; install sccache, or change [sandbox.os] cargo bundle'ssccache knob to "off" or "manual"Migration from the old sccache = true
Section titled “Migration from the old sccache = true”The pre-SEA-501 bool form (sccache = true / false) is rejected at parse time. Migration mapping:
| Old | New |
|---|---|
sccache = false | sccache = "off" |
sccache = true | sccache = "manual" (the bool’s previous behavior — binds + env but no daemon lifecycle management) |
| new | sccache = "on" (binds + env + seal-daemon-managed daemon) |
See also
Section titled “See also”- Manifest reference:
[sandbox.os.command_tools]— full per-bundle reference. - OS sandbox — overall sandbox model.