Skip to content

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.

  • off (default) — no sccache binds, no RUSTC_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/sccache as an RW bind and forwards RUSTC_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 as manual, plus seal-daemon owns the host sccache daemon’s lifecycle. The first session that has sccache = "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.

When seal-daemon receives a session-open or manifest-reload that resolves to grants containing a cargo bundle with sccache = "on":

  1. 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).
  2. Binary check. seal-daemon shells out sccache --version to confirm the binary is installed. If not, session open / reload fails with a clear error message naming the three valid sccache values. Silent fallback would mask the user’s explicit on intent.
  3. 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 as External ownership and never tries to manage it.
  4. Owned spawn. Otherwise, seal-daemon spawns sccache --start-server (with SCCACHE_NO_DAEMON removed from the child env so the daemon actually starts) and waits up to 3 seconds for the port to bind.
  5. Liveness poll. A background task probes 127.0.0.1:4226 every 30 seconds. If the daemon goes silent, the supervisor respawns (only for Owned daemons; externally-managed daemons are someone else’s problem).

pgrep -af sccache after the first on session should show:

$ pgrep -af sccache
631234 sccache --start-server

What 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) or sandbox-exec with a deny-all network rule (macOS) — 127.0.0.1 inside 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:4226 inside the sandbox.
  • The bridge tunnels each connection through a per-port UDS to the host’s 127.0.0.1:4226 daemon.
  • 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.

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 mysql

The < 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 dials unix:///path/to/socket directly. 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) — use host_localhost_ports. The bridge tunnels TCP over a UDS internally; sandboxed code sees a normal connect("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.

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:

Terminal window
# 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 units

For other services, a host TCP listener confirms the bridge end-to-end:

Terminal window
# Host shell — pre-bind a port the user controls:
python3 -c "
import socket
s = 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.

After sccache = "on" is set in a manifest and a session opens:

Terminal window
# 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 -20

If sccache isn’t installed:

$ seal
[sandbox.os] cargo bundle has sccache = "on" but seal-daemon
couldn't start the sccache server: sccache binary not found on
PATH; install sccache, or change [sandbox.os] cargo bundle's
sccache knob to "off" or "manual"

The pre-SEA-501 bool form (sccache = true / false) is rejected at parse time. Migration mapping:

OldNew
sccache = falsesccache = "off"
sccache = truesccache = "manual" (the bool’s previous behavior — binds + env but no daemon lifecycle management)
newsccache = "on" (binds + env + seal-daemon-managed daemon)