Run containers inside a bottle (nested_containers) #456
Open
didericis-claude
wants to merge 13 commits from
prd-nested-containers into main
pull from: prd-nested-containers
merge into: didericis:main
didericis:main
didericis:prd-secret-provider-encrypted-env
didericis:feat/encrypted-egress-secrets
didericis:spike/rootless-docker-macos
didericis:fix/ci-coverage-artifact-paths
didericis:claude-forward-host-credentials-rebased
didericis:fix-gateway-gitleaks-arch
didericis:fix/websocket-response-dlp-multitenant
didericis:orchestrator-agent-compose
didericis:orchestrator-gateway-ca
didericis:orchestrator-consolidated-launch
didericis:orchestrator-gateway-provision
didericis:orchestrator-gateway-network
didericis:orchestrator-client
didericis:orchestrator-gateway-net
didericis:orchestrator-gitgate-provision
didericis:orchestrator-registration
didericis:orchestrator-lifecycle
didericis:orchestrator-supervise-writers
didericis:orchestrator-supervise-multitenant
didericis:orchestrator-gitgate-multitenant
didericis:orchestrator-rename-gateway
didericis:orchestrator-slice8
didericis:orchestrator-slice7
didericis:orchestrator-slice6
didericis:prd-orchestrator
didericis:orchestrator-slice5
didericis:orchestrator-slice4
didericis:orchestrator-slice3
didericis:orchestrator-slice2
didericis:firecracker-backend
didericis:forge-native-integration
didericis:prd-smolmachines-linux
didericis:prd-egress-control-plane
didericis:manifest-break-import-cycle
didericis:dlp-supervise-quality-fixes
didericis:table-drive-dlp-tests
didericis:fix-integration-test-failures
didericis:fix/macos-container-relative-dockerfile
didericis:prd-0054-install-script
didericis:commit-bottle-state
didericis:pr-211
didericis:move-codex-auth-to-contrib
didericis:feat/pipelock-skip-scan-extensions
didericis:prd-0049-named-labelled-agents
didericis:harden-git-gate-shell-rendering
Labels
Clear labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Status/Needs Triage
Breaking change that won't be backward compatible
Something is not working
Documentation changes
Improve existing functionality
New functionality
This is security issue
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Low
4
The priority is low
Priority
Medium
3
The priority is medium
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
3
This issue won't be fixed
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
Awaiting initial classification
No Label
Milestone
No items
No Milestone
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: didericis/bot-bottle#456
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "prd-nested-containers"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #392.
PRD: https://gitea.dideric.is/didericis/bot-bottle/src/branch/prd-nested-containers/docs/prds/prd-new-nested-containers.md
Summary
Adds the
nested_containersbottle flag. On themacos-containerbackend it starts a guest-local rootless podman service inside the bottle and exposes its Docker-compatible API socket, so the agent keeps typingdockeranddocker compose. No host Docker socket is mounted and the guest gains no capabilities.This ports
spike/rootless-docker-macosonto main and resolves the issue's open questions:/dev/fuseand/dev/net/tun.docker_accesswas the wrong name — it implies Docker and grants access to nothing on the host.dockerandfirecrackerrejectnested_containers: truein the sharedpreparetemplate rather than silently ignoring it. Mounting the host socket stays off the table on every backend.fuse-overlayfs,slirp4netns, anduidmap.Nested containers are not an isolation layer: the single-UID mapping means
rootinside one is the agent user outside it. Documented in the README, the PRD, and the module docstring.Schema
preserve_auth(#453) is required for Docker Hub and GHCR: both use a client-fetched per-scope bearer token the proxy would otherwise strip. DLP off on registry routes sidesteps #455 (large-pull OOM) and costs nothing — the scanned bodies are compressed layer blobs.Merge rule
nested_containersfollowssupervise: later bottle replaces earlier in bothextends:chains and runtime multi-bottle composition.Verification
Unit: 2106 pass, pylint 9.83, pyright clean.
Live acceptance criteria from the issue thread run on the macOS host and posted as a follow-up comment before merge.
Live verification on the macOS host — mechanism works, acceptance criteria do not pass
Ran on macOS 26 / Apple Container 1.0.0 with
nested_containers: trueand the registry routes from the issue comment.What passed
bot-bottle-claude:latest-nested-containersbuilds, including thedocker:28-clilayer.guest-local container engine is ready— the rootless podman bootstrap succeeds inside the bottle with zero added capabilities, on two independent runs.What failed
docker composeintegration testsRegistry reach is the blocker for 2/3/4.
quay.io/podman/hello:latest→egress: host 'cdn01.quay.io' is not in the bottle's egress.routes allowlist. The manifest host is routed; its blob CDN is not. Fixable by routingcdn0*.quay.io.alpine:3.20(Docker Hub) →unauthorized: authentication requiredghcr.io/linuxcontainers/alpine:latest→unauthorizedThe last two are the important finding:
preserve_auth: trueonregistry-1.docker.io+auth.docker.io+ghcr.iowas not sufficient. The registry-auth strip called out in #392's "Blocked on" list is still blocking, and #453 alone does not close it. Needs a look at the gateway log (the run hadlog: 2) to see whether the route flag applied and where the bearer went.Submodule clone (check 5) is an unrelated pre-existing gap.
bot-bottle-platform's.gitmodulesrecordsssh://git@gitea.dideric.is:30009/..., while the git-gateinsteadOfrules written into~/.gitconfigonly cover thessh://git@100.78.141.42:30009/...IP form. No rewrite fires, git tries a direct SSH connection, and DNS forgitea.dideric.isfails inside the bottle — correctly, since it isn't a routed host. Check 6 then fails downstream of it (emptyconsolesubmodule → nothing to build).Reading
The
nested_containersmechanism in this PR is verified working. Everything that failed is in the surrounding plumbing — egress routes for registry CDNs, the registry-auth strip, and git-gate submodule URL rewrites — none of which this diff touches.So: merging this is safe and it is genuinely useful for locally-built images, but it does not close #392 on its own. The issue's own acceptance list needs the registry-auth decision resolved first.
Live verification: green, on macOS 26 / Apple Container 1.0.0 / podman 5.4.2
Every check below ran with plain
docker, no extra flags (no--add-host,-e, or-v).docker pullfrom Docker Hubdocker pullfrom GHCRdocker pullfrom quay.iodocker runexit-code propagationexit=7)docker composeup / logs / downcurl https://quay.iofrom inside a nested containercurl https://pypi.org/simple/from inside a nested containercurl https://example.comfrom inside a nested containerbot-bottle-platformwith submodulesThe egress boundary applies to nested containers, not just the bottle — that 403 comes from the proxy, and the gateway log shows the request arriving there.
What the verification turned up
It took twelve live runs, because the spike was written against bookworm's podman 4.3.1 and #451's trixie bump moved it to podman 5 — a different networking stack. Each missing piece failed at a different layer while image pulls kept working, which made every one of them read as a compat-API bug:
passtmissing — podman 5 defaults to pasta; nothing started at all.nft/aardvark-dnsmissing — containers created but never started; then DNS inside them failed.--no-install-recommendson the base image's podman means none of the networking stack arrives with it. This is the likeliest explanation for the "DNS inside nested containers fails entirely" note in the issue — a missing package, not a design gap.preserve_authnever serialized._route_to_yaml_fieldsparsed the flag off the manifest and the addon honored it, but the renderer in between dropped it. Docker Hub fetched a valid bearer, the proxy stripped it, and the registry answered the anonymous request with a fresh challenge — indistinguishable from a credential problem. This means #453's flag has never worked end to end; it is not specific to nested containers.production.cloudfront.docker.com/pkg-containers.githubusercontent.com/cdn0*.quay.io.containers.confenv,http_proxy=false, and the service environment all looked right on disk and changed nothing inside a container. The decider is~/.docker/config.json's proxies block — client-side, copied in by the Docker CLI, beating everything the service does.containers.confhosts_filehas the same blind spot: nativepodman runonly.Not fixed here
wgetdrops the connection after TLS interception ("error getting response"). The proxy logs the decrypted request andcurlon the same host returns 200, so this is a BusyBox client limitation. Documented in the README.DNS open question, resolved
Public DNS inside a nested container fails by design — everything egresses through the proxy. The real breakage was reaching the proxy: it is named
bot-bottle-gateway, which resolves only through the bottle's/etc/hosts, and a nested container gets its own. The name is now resolved in the bottle and the address substituted into the proxy URL.93cf0c8dd9to913bcab6a0@@ -0,0 +23,4 @@Nested containers run *within* the bottle boundary, not inside a new one: thesingle-UID mapping means `root` in a nested container is the agent useroutside it. This is for build and test workloads, not for sandboxinguntrusted code.@didericis-claude This is kind of a silly thing to emphasize, since we're already in a safe environment. I think you emphasized this in the PRD and the related issues as well. Please remove this.
@@ -0,0 +74,4 @@with tempfile.TemporaryDirectory(prefix="bot-bottle-nested-containers.") as tmp:context = Path(tmp)shutil.copy2(init_script, context / "nested-containers-init.sh")(context / "Dockerfile").write_text(@didericis-claude Add a comment calling out an intended refactor here: we'll want to use a docker layer abstraction we build as a part of #394 for this in the near future.
Also, since we're already modifying the agent dockerfile here, should move the default podman dependencies we added to the build in agent images out of the agent images and into this.
ef9f81ba83tocc094765fdView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.