Repository navigation
feat(sandbox): attach Docker Compose networks and fail closed on sandbox config errors - #334
Merged
Merged
Conversation
…ve posture surfaces
The execution-environment block listed generated container names (app-redis-1) instead of the names other containers answer to on the network. Read each attached container's per-network aliases with one docker inspect, drop its own name and short ID, and fall back to the container name when inspect fails or no alias exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… none With sandbox_networks set, the environment block, the startup line and the batch-card note still said the network was "none" although the attached networks replace it. Report "attached networks only" or "bridge plus attached networks" (startup line: attached / bridge+attached), and tell the model whether the internet is reachable: bridge or any non-internal attached network reaches it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The approval prompt and container setup each resolved the project's networks, so a network that joined the Compose project (or was recreated under an approved name) between the two was attached without being shown. Record the approved IDs per project directory at every approval path and refuse, at setup, any project-contributed network whose ID was not approved. The approval key now hashes network IDs in sorted order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and the pinned image docs/CLI.md gains --sandbox-networks, --sandbox-compose and --sandbox-pids-limit, the fail-closed --sandbox-network values and a Compose example. SANDBOXING.md shows the execution-environment block the model receives. Help text and comments no longer name alpine:latest as the default image. The weak network-mode test is replaced by the exact table test added with the label fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er Desktop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d name docker network inspect resolves its argument by full ID, then name, then ID prefix, so a requested name such as "ca064c" (or a Compose network named after bridge's ID) came back as the predefined bridge network and was attached under the requested name, bypassing the reserved-mode refusal. Require the inspected network's own name to equal the request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs when Docker answers In implicit mode an invalid image, memory, cpus or user value, or any docker run failure, still fell back to unsandboxed host execution because those errors were not marked as configuration errors. When the Docker daemon answers, such a failure comes from the configuration, so it is now fatal; only an unreachable daemon keeps the loud fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sted data Network names and Compose service aliases can be chosen by the project's compose file, and names that pass the charset check can still read as instructions. They were appended to the system prompt after the security rules as plain text; they now sit inside a nonce'd untrusted-content wrapper, while the fixed facts (network, internet, localhost) stay plain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DefaultImage duplicated the FROM line of default.Dockerfile, so every Dependabot bump of that file would have failed the consistency test until someone edited the constant by hand. DefaultImage is now parsed from the embedded file, and an embedded file without a FROM line is an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sandboxed child only had the block in its shell tool description; its system prompt never said where commands run or what is reachable. Append the block for the child's own container after its sandbox starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classifying every docker run failure as a configuration error also made an uncached image that cannot be pulled (offline machine, registry outage) fatal in implicit mode. Pull the image before docker run and report a failed pull unmarked, so it keeps the loud fallback; a run failure with the image present still comes from the configuration. Also keep short service names that happen to be a hex prefix of the container ID, and document that serve refuses new sandboxes after the approved project networks change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ingest The engine-label wrapper is reserved for engine-injected context. Use the external-content wrapper (with the guard scan) and record no ingest, so listing network and service names never taints the session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
odek | c7f83a4 | Commit Preview URL Branch Preview URL |
Oct 25 2026, 12:22 PM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets the Docker sandbox reach services started by Docker Compose (Redis, Postgres, Mongo, …) by name, and makes every sandbox decision fail closed and visible.
docker compose up -d odek run --sandbox --sandbox-compose myapp "run the integration tests against redis and postgres"What changes
Attaching existing Docker networks
sandbox_networks(ODEK_SANDBOX_NETWORKS,--sandbox-networks a,b) attaches existing user-defined networks.sandbox_compose_project(ODEK_SANDBOX_COMPOSE_PROJECT,--sandbox-compose <project>) attaches every network carrying the project'scom.docker.compose.projectlabel. It never guesses<project>_default.bridge-driver networks, at most 8. A network is accepted only whendocker network inspectreturns the requested name, and it is passed to Docker by full ID. Reserved modes,container:/ns:forms and ID-prefix lookups are refused. No alias is added for the sandbox.docker run, the rest are connected after start, and the attachment is verified by inspect. Any failure removes the container. Networks are never deleted.Internal, or whenbridgeis combined with attached networks../odek.jsonasking for networks goes through the existing sandbox-override approval. The prompt shows each network's name, full ID,Internalflag and Compose project. The approval key binds those IDs and is byte-identical to today's key for configs without networks. At container creation, the networks are checked again against the approved IDs: a network that joined the project, or was recreated, after the prompt is refused.What the agent is told
run,continue,repl,serveand sandboxed sub-agents, and to theshelltool description everywhere. It lists:/workspace;none,bridge,attached networks only,bridge plus attached networks) and whether the internet is reachable;redis,postgres);localhostis the container itself.Fail-closed sandbox setup
Configuration errors no longer fall back to the host. Before, a sandbox the user didn't explicitly request fell back to unsandboxed host execution on any setup error. Now any of these stops the run, whether or not
--sandboxwas passed:sandbox_network;docker runfailure while the daemon answers.Only an unreachable Docker daemon keeps the existing loud fallback (
ODEK_REQUIRE_SANDBOX=1still makes that fatal).sandbox_networkaccepts onlynoneandbridge.host,container:…or a network name used to be silently rewritten tonone; now it's an error.ODEK_SANDBOX_NETWORK_LENIENT=1restores the downgrade.Container defaults
--pids-limit 4096, configurable assandbox_pids_limit/ODEK_SANDBOX_PIDS_LIMIT/--sandbox-pids-limit(a project may only lower it), plus--init, andnosuidon the/tmptmpfs.alpine:3.22@sha256:2e0b81d6…b55finstead of the floatingalpine:latest.internal/sandbox/default.Dockerfile, which Dependabot (dockerecosystem,/internal/sandboxonly) keeps current.tests/supplychainasserts the pin.docker buildx imagetools inspect alpine:3.22.odek: sandbox network=… pids=… image=… networks=…) andconfig_view(CLI tool and serve/api/config) show the settings actually applied.Docs
SANDBOXING.md:bridgeis isolated from host services: it reaches the Docker gateway, host-bound services and cloud metadata.CLI.md,CONFIG.md,SECURITY.md,DEVELOPMENT.md: new flags, env vars and config keys; the fail-closed rules.AGENTS.md: running the Docker-gated tests on Docker Desktop.Behaviour changes to know about
sandbox_networkvalue other thannone/bridgenow stops the run. SetODEK_SANDBOX_NETWORK_LENIENT=1for the old downgrade.Dockerfile.odekbuild, or a memory, CPU or user value Docker rejects now stops implicit (default-on) runs instead of falling back to the host. An unreachable daemon or an image that cannot be pulled still falls back loudly.Dockerfile.odek, the sandbox runs the pinnedalpine:3.22digest instead ofalpine:latest.Testing
Unit and regression tests:
TestRED_*regression tests cover service-name aliases, the network label, the approval/setup ID re-check, exact-name resolution, start-failure classification, untrusted wrapping of names, the default-image parse and the sub-agent environment block.security_report_validation_test.gogains the sandbox claims.Real Docker (Docker Desktop 29.6.2, scratch
HOME, scripted mock model). A Compose project ran Redis on the normal network plus aninternal: truebackend network, and Postgres on the backend only.--sandbox-compose:redisandpostgresresolve by service name, and Redis answersPONG;Network: attached networks only,Internet: reachableandpostgres, redis;pids.maxis 4096, PID 1 isdocker-init, and/tmpisnosuid,noexec;--sandbox-networks <internal net>: services reachable, internet blocked.--sandbox-pids-limit 64gives 64.--sandbox);--sandbox-network host;bridgeorcontainer:xas a network name;ca064c);--sandbox-memory lotsin implicit mode.alpine:9.99-nonexistent) in implicit mode keeps the loud fallback.odek.jsonrequesting networks is refused without approval and attached once approved. The interactive prompt lists name, ID, internal flag and Compose project.Commands:
go test -count=1for./internal/sandbox ./internal/config ./internal/loop ./tests/supplychain ./cmd/odek: pass, with the Docker daemon both reachable and unreachable.go test -raceon the same packages: pass.DOCKER_HOST=… ODEK_E2E=true go test ./cmd/odek -run 'TestSandbox|TestE2E_Sandbox': pass, nothing skipped.golangci-lint runon the changed packages: 0 issues.govulncheck ./...: no vulnerabilities.gofmt -l: clean.Coverage:
internal/sandbox93.0% → 93.8%,internal/config87.0% → 87.2%. Changed executable lines incmd/odek: 84.7% (459/542).Review: an adversarial full-branch review (Opus) found three major issues, all fixed here:
Also fixed: two of its minor findings (the duplicated image pin that would break Dependabot bumps, the missing sub-agent environment block) and the wording of a third (how sub-agents narrow networks). A follow-up Sonnet pass on the fix commits found that an image that cannot be pulled had become fatal in implicit mode (now pulled separately and kept on the loud fallback) and that short service names that look like hex prefixes were dropped; both are fixed.
🤖 Generated with Claude Code