(draft design by Claude, not validated) ## Summary Many agent workflows need a working Docker engine inside the sandbox — Testcontainers, building images, running `docker compose` integration stacks. Today that isn't possible in a sandcat container, and the obvious workaround (bind-mounting the host `docker.sock`) would punch a hole straight through sandcat's whole reason for existing. This is a request to support **nested, isolated Docker** in a way that preserves the network-policy and secret-substitution guarantees — **and that keeps working through "Reopen in Container" in VS Code**, since that IDE workflow is the whole point of sandcat over a CLI tool like Matchlock. (Scoping this to VS Code for now; JetBrains parity is a possible follow-up but shouldn't gate the design.) ## Motivation In a JVM/Scala-heavy workflow the agent routinely hits tasks that require Docker: - **Testcontainers** for integration tests (Postgres, Kafka, Localstack, etc.). - Building and smoke-testing OCI images the agent just produced. - Bringing up a `docker compose` stack the project ships for local/dev. When the agent can't do these inside the sandbox, the choice becomes "run the agent unsandboxed for these steps" — which is exactly the situation sandcat is meant to remove. ## Why the easy fix is unacceptable Mounting `/var/run/docker.sock` into the sandbox (DooD) defeats the threat model: 1. **Host-daemon access ≈ host root.** The agent can launch privileged containers, mount arbitrary host paths, and escape the sandbox entirely. 2. **Network policy bypass.** Containers the agent launches via the host daemon are *not* attached to sandcat's WireGuard/mitmproxy path, so their egress skips the allowlist and policy engine. 3. **Secret-substitution bypass.** The placeholder→real-value swap happens at the proxy; traffic that never transits the proxy never gets that protection, and worse, gives the agent a channel that the proxy can't see at all. So the requirement isn't "make Docker work" — it's "make Docker work *without* opening an egress hole or a host-escape path." ## The central design constraint Whatever the mechanism, **traffic from containers the agent launches must still transit the transparent mitmproxy** (allowlist + placeholder substitution applied), or be denied. A nested daemon naively configured will create its own bridge network and its own DNS, and the inner containers won't trust the mitmproxy CA — so even if egress happens to NAT out through the sandbox's WireGuard interface, TLS bumping breaks and placeholders aren't injected. Any solution needs to address, explicitly: - inner-container egress is forced through the same proxied route (no direct path); - the mitmproxy CA is present and trusted inside inner containers; - `SANDCAT_PLACEHOLDER_*` propagation into nested containers is defined (propagate, or deliberately withhold); - DNS for inner containers points at the sandbox resolver, not a daemon default. ## Second constraint: it must survive "Reopen in Container" in VS Code The solution still can't assume a hand-run start step — the full sandcat topology (app + wg-client + mitmproxy + any Docker daemon) has to come up as the compose project the IDE launches via Reopen in Container. The good news with a VS-Code-only scope is that this is permissive: - VS Code Dev Containers drives the Docker CLI / a Docker **context**, so it works against **Colima** as well as Docker Desktop. That matters because a custom OCI runtime (Sysbox) can't be registered in Docker Desktop on macOS, but *can* be in a Colima VM — and VS Code is happy to target that context. - Compose, Features, lifecycle scripts, and per-service `runtime:` are all honored through the normal flow. So a runtime swap is back on the table: the only real setup cost on macOS is "use Colima instead of Docker Desktop," not an IDE limitation. (If JetBrains parity is ever wanted, note its Dev Containers support can't use a Colima backend today, which would push that scenario back toward the privileged-sidecar option below — but that's explicitly out of scope here.) ## Proposed approaches (in rough order of fit) **1. Sysbox runtime swap on the app service (preferred).** Run the app service under `sysbox-runc` so it hosts an *unprivileged* `dockerd` via user-namespace remapping — no `--privileged`, no socket mount, inner daemon fully isolated from the host daemon. This is the same mechanism GitHub Codespaces uses for in-environment Docker, so it's a proven dev-container pattern. Needs `sysbox-ce` on the Docker host and the runtime set on the app's compose service; it ships an arm64 build. On macOS the only wrinkle is that it requires a Colima VM (Docker Desktop can't add runtimes) — but with VS-Code-only scope that's just a backend choice, not a blocker. Best isolation-per-cost of the options here. **2. Docker-daemon sidecar as a compose service.** Add a dedicated `dockerd` service to sandcat's compose project, attached to the *same* WireGuard/mitmproxy network, exposing its socket to the app service only over an internal compose network (`DOCKER_HOST` pointed at it). Containers it spawns inherit the controlled egress path by construction, and it works on **stock Docker Desktop with no custom runtime** — so it's the lower-setup option for anyone who doesn't want to move to Colima. The cost is that the daemon service needs `privileged: true`, mitigated by scoping it to an internal network only the app service can reach and leaning on the disposable-VM boundary beneath. Also the most explicit option for the network-routing story (no reliance on emergent NAT behavior). **3. Rootless `dockerd` inside the app container.** No custom runtime and no privileged sidecar, all within a single compose service — but the cgroup-delegation / `fuse-overlayfs` setup is fiddly and the isolation story is weaker than Sysbox. Reasonable fallback if both a runtime swap and a privileged sidecar are off the table. ## Out of scope / noted alternative True per-task VM isolation (Kata / Firecracker, à la Matchlock) gives a separate kernel and a real escape boundary, but that's a different product shape than sandcat's dev-container model and needs hardware virtualization (nested virt on Apple Silicon is M3+ only). Flagging it as the "if you need kernel-level isolation, this is the other tool" path rather than something to fold into sandcat. ## Suggested acceptance criteria - A documented opt-in (e.g. a profile or template flag) that enables Docker inside the sandbox. - **"Reopen in Container" works in VS Code**, with the IDE building and launching the compose project itself — no manual pre-start step. - `docker run` / `docker build` / `docker compose` work for the agent. - A Testcontainers-based JVM test suite runs to completion inside the sandbox. - Egress from agent-launched containers is provably routed through the mitmproxy (verified: allowlisted host returns 200 with trusted CA; non-allowlisted host is blocked; a placeholder leaks nowhere it shouldn't). - No `docker.sock` from the host is mounted, and no inner container can reach a host path or the host daemon. - macOS + arm64 path documented, including the Colima requirement for the Sysbox path vs. the Docker-Desktop-friendly sidecar path. ## Open questions - Should the baseline be the Sysbox runtime swap (unprivileged, best isolation, requires Colima on macOS), with the privileged sidecar offered as the lower-setup option for Docker Desktop users — or the reverse? - How should placeholder secrets behave for nested containers — propagated by default, or withheld unless explicitly opted in per container? - Sandcat already notes that VS Code's "Rebuild Container" rebuilds only the agent service, not mitmproxy/wg-client. For the sidecar variant, how should the Docker-daemon service behave across rebuilds so the agent doesn't end up pointed at a dead daemon? - Is there appetite for an integration test in the repo that exercises the egress-still-proxied property, so this doesn't silently regress?