feat(v1): Podman and Apptainer runtimes, private container networking - #2509
feat(v1): Podman and Apptainer runtimes, private container networking#2509xeophon wants to merge 3 commits into
Conversation
…rivate container networking Extract the engine-independent half of the Docker runtime into runtimes/container.py, built on one `_exec` hook. Podman becomes a two-line subclass of DockerRuntime; Apptainer provides `_exec`, `start` and `cleanup`. Docker/Podman containers now always use the engine's bridge network, fixing the host-port collisions of #2319. Host loopback stays reachable at its own port: on Linux through listeners planted on the container's loopback and relayed by the host ("doors"), on macOS through host.docker.internal. The restricted-mode proxy and cut are unchanged in spirit but the proxy is now a plain host service and the vf.host.internal alias is gone. The service port is published to host loopback and Runtime.expose defaults to host loopback, so MCP placement has one rule for every runtime. Fixes #2319. Resolves #2359 (Apptainer). Supersedes #2469, #2470, #2473.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 309046be9e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds two production runtime modes and materially changes existing Docker networking, egress enforcement, process execution, service exposure, and MCP routing. The cross-cutting runtime and network-boundary changes are too substantial for automatic approval. You can add or adjust custom eligibility rules. Learn more. |
- macOS restricted mode sends framework traffic through the proxy again (the proxy dials host loopback for host.docker.internal), so the cut opens only the proxy port: no per-route host openings, and a reused box keeps working after a second prepare_execution. - Create the workdir as root, as `run --workdir` did, so non-root images start. - Scheme-aware default port for loopback URLs without an explicit port. - open_process polls the pidfile once more after the target exits, so a short-lived process is not mistaken for a failed start.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1e43eaa. Configure here.
… doors bind privileged ports Resolving the control argv (which may open doors) before the target is spawned means a failure there cannot leak the attached process. The door helper gains NET_BIND_SERVICE so a host service on a default port (80/443) can be reached from inside too.

Superseded by #2528.
What this does
Adds two new local runtimes,
type = "podman"andtype = "apptainer", and moves all local containers onto a private network so a task's ports can never collide with the host's.--network host, so a task binding port 8000 could silently talk to an unrelated host process and get graded wrong).How it is built
One shared base for every local container.
runtimes/container.pyholds what used to be the engine-independent half of the Docker runtime: running a command, opening a live process, background processes, reading and writing files, and sending signals. All of it goes through a single method,_exec(env), that returns "the host command that runs something inside this container". A runtime only has to provide_exec,startandcleanup.Podman is Docker with a different binary.
PodmanRuntimeis a two-line subclass ofDockerRuntime. Podman accepts the same commands and even aliaseshost.docker.internal, so there is nothing else to write. Two small differences are handled inside the Docker code: Podman refuses a--workdirthat does not exist in the image (Docker creates it), so the workdir is now created withmkdir -pright after start; and Podman's--gpustakesallwhere Docker takes a count.Apptainer is a small subclass too. It starts an unprivileged instance, binds a host directory as the workdir, and backs the contained
/tmpand$HOMEwith disk instead of a tiny tmpfs. Images given as Docker references are pulled to a SIF once and cached under~/.cache/verifiers, the same way an engine keeps a pulled image. A local.sifpath is used as is. Apptainer always shares the host network, so it has no egress fields; a task that asks for a network policy on it gets a clear error.How the networking works now
Every Docker/Podman container runs on the engine's bridge network. That is the fix for #2319. Two things then have to keep working:
The container must reach the interception server on the host. The interception server (and any host-side tool server) listens on the host's loopback. On Linux,
host_urlleaves the URL unchanged and remembers its port. Before the next command runs in the container, one small helper container that shares the network namespace binds a listener at that port on the container's loopback and hands the socket back to us; we relay every connection to the host's loopback. Inside the container,http://127.0.0.1:PORTsimply works, with no proxy settings needed, so harnesses that ignoreHTTP_PROXY(Node based ones) keep working. On macOS,host_urlrewrites loopback tohost.docker.internal, as before.Egress must be cuttable. Restricted runtimes keep the existing HTTP(S) policy proxy and the existing route cut. The proxy is now an ordinary host-loopback service reached like any other. On Linux the cut leaves loopback alone, so the doors keep working. On macOS, restricted framework traffic goes through the proxy as before (the proxy dials the host's loopback for
host.docker.internal), so the cut opens only the proxy port. The proxy-inside-the-container listener and the made-upvf.host.internalname are gone; the engine's own alias does that job.The host must reach a tool server placed inside a container. The fixed service port is published to a host loopback port, and
Runtime.exposereturns it.Runtime.exposenow defaults to host loopback, so the MCP launch code has one rule for every runtime instead of three cases.Behavior changes for Docker
run --workdirdid) instead of viarun --workdir.docker.io/library/...) and the socket directory is mounted with--volume ...:Z, so Podman and SELinux hosts work.run_backgroundstarts the process in the background inside the container instead ofexec --detach, which Apptainer does not have.How I tested it
Automated:
uv run pytest tests/v1 -m "not e2e"(82 tests),uv run pre-commit run --all-files, andty check verifiersall pass. The e2e suite needsPRIME_API_KEY, which I do not have here.Live, with two throwaway scripts (below) run against each engine. The first covers the process side: run, environment values containing commas, stdin closed for plain runs, binary write/read, a live process fed on stdin and then terminated with its child reaped, a background server, and installing and running a uv script inside. The second covers networking: a host HTTP server reached from inside through
host_url, proof that the namespace is private (a port busy on the host is free inside), a server inside reached from the host throughexpose, two doors opened in one batch, and the restricted flow: host reachable before and after the cut, a blocked host gets 403, an allowed host works through the proxy, raw TCP egress is dead, and live processes see the proxy environment.Not tested by hand: Podman on macOS through
podman machineand GPU flags. Docker on Linux is covered by CI's live E2E suite, which passed on this branch. After the review fixes, both macOS Docker runs were repeated; Linux is re-verified by CI on the final commit.Two bugs the live tests caught that unit tests would not have: rootless Podman defaults to pasta, which has no
eth0, so--network bridgeis now explicit for both engines; and the listener-planting helper dropped its first socket to garbage collection when two ports were requested at once.Smoke script 1: processes and files (
smoke_runtime.py <docker|podman|apptainer> [--skip-restricted])Smoke script 2: networking (
smoke_net.py <docker|podman|apptainer> [--skip-restricted])Known limits
--gpus all, which needs Podman 5. Docker keeps its device count.--nv(all NVIDIA GPUs); the count is advisory.allow/blockare rejected for it.Note
High Risk
Changes sandbox networking, egress restriction, and service URL exposure across Docker/Podman/MCP—mistakes could break grading, interception reachability, or leak/block traffic incorrectly.
Overview
Adds
podmanandapptaineras v1 runtimes (wired throughRuntimeConfig, public exports, and architecture docs) and refactors local containers around a sharedContainerRuntimeincontainer.pyso Docker/Podman only implement_exec,start, andcleanup.Docker/Podman now run on the engine bridge (not host networking), publish the fixed service port to host loopback, and implement
exposefor that URL. On Linux,host_urlschedules loopback “doors” (listeners in the container netns relayed to the host) so harnesses can reach interception on127.0.0.1without relying onHTTP_PROXY; restricted egress keeps the policy proxy on host loopback and updates the network cut /HOST_ALIAS(host.docker.internal) accordingly. Podman is a thin subclass; Apptainer runs unprivileged instances on the host network with cached SIF pulls.Runtime.exposenow always returns a concrete URL (defaulthttp://127.0.0.1:{port}), which unifies MCPreachable_url(alwaysexpose, Prime Tunnel only when a local runtime serves a remote consumer) and gatesMCP_HOSTon bothexposedandpublished_port.Reviewed by Cursor Bugbot for commit dc14ff9. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add
PodmanRuntimeandApptainerRuntimeand refactor container networkingContainerRuntimeandContainerConfigbase classes so Docker and Podman share container lifecycle and execution logic.PodmanRuntimereuses the Docker implementation with thepodmanCLI.ApptainerRuntimefor unprivileged local instances using the host network, with local image file support and a digest-keyed SIF cache.host.docker.internalfor host resolution.Runtime.expose()now returns a concrete loopback HTTP URL instead ofNone.EgressProxy.startno longer accepts an externally supplied listener socket. The MCP launcher omitsMCP_HOSTwhenserve_in_runtimeis called withexposed=False.Macroscope summarized dc14ff9.