5.0 KiB
PRD prd-new: smolmachines sidecar VM
- Status: Active
- Author: codex
- Created: 2026-07-09
- Issue: #332
Summary
Run the smolmachines backend's trusted sidecar bundle as its own smolVM instead of a Docker container. A bottle then consists of an agent VM plus a sidecar VM, with the agent VM's TSI allowlist limited to the per-bottle agent-facing sidecar surface.
Problem
The smolmachines backend currently runs the agent in smolVM but keeps egress/git-gate/supervise in a Docker sidecar bundle. That hybrid launch path keeps Docker in the trusted runtime path and leaves smolmachines with a different isolation boundary than its agent VM design suggests.
The existing Docker sidecar path also uses Docker port publishing to bind only agent-facing services to the per-bottle host address. TSI is IP-only, so the smolVM replacement must preserve that scoping: publishing sidecar services on generic host localhost would let the agent reach unrelated host services or other bottle sidecars if those services share the allowed address.
Goals / Success Criteria
BOT_BOTTLE_BACKEND=smolmachines ./cli.py start <agent>runs with both the agent and sidecar as smolVMs.- The smolmachines launch path no longer uses
docker runfor the sidecar bundle. - Agent-facing egress, git-gate, and supervise endpoints are exposed only through the bottle's own published sidecar surface.
- Internal sidecar-only ports are not reachable from the agent VM.
- The agent VM TSI allowlist remains fail-closed and limited to the per-bottle address.
- Sidecar config, secrets, and state are delivered to the sidecar VM without exposing provider or forge credentials to the agent VM.
- Teardown removes both VMs and any host-side published port state.
Non-goals
- Rewriting the egress, git-gate, or supervise daemons.
- Changing the Docker backend's sidecar bundle behavior.
- Replacing the existing agent image build and pack pipeline except where shared helper extraction is needed for sidecar image packing.
- Weakening the current smolmachines TSI allowlist checks.
Design
The sidecar VM continues to use the existing sidecar bundle image and
/app/sidecar_init.py supervisor. The launch flow builds the sidecar image,
packs it into a .smolmachine artifact, creates a per-bottle sidecar VM from
that artifact, passes the same daemon-selection environment the Docker bundle
uses today, mounts or copies the same daemon-private config/state inputs, starts
the sidecar VM, wraps the sidecar VM's raw smolVM-published ports with a local
per-bottle forwarder, then starts the agent VM with
--allow-cidr <per-bottle-address>/32.
smolVM currently exposes guest ports as HOST:GUEST port pairs bound on host
loopback. It does not expose an address-scoped bind like
127.0.0.16:<port>:9099. Because TSI is IP-only, bot-bottle must not advertise
those raw loopback ports directly to the agent. Instead, bot-bottle runs one
small stdlib Python TCP forwarder process per bottle:
agent VM
-> TSI allows only <bottle-address>/32
-> bot-bottle forwarder bound to <bottle-address>:<random-port>
-> raw smolVM-published sidecar port on 127.0.0.1:<raw-port>
-> sidecar VM service
Agent-facing URLs are stamped from the forwarder ports, never the raw smolVM ports:
HTTP_PROXY/HTTPS_PROXYpoint at the published egress proxy port.GIT_GATE_URLpoints at the published git HTTP port when git-gate is enabled.MCP_SUPERVISE_URLpoints at the published supervise port when supervise is enabled.
Internal sidecar-only services stay bound inside the sidecar VM and are not published.
Forwarder constraints
- Reject wildcard listener binds (
0.0.0.0,::) and generic localhost listener binds (127.0.0.1,::1) for agent-facing forwards. - Accept only fixed loopback targets selected by launch; the client cannot choose or influence the forwarding target.
- Forward bytes transparently without parsing HTTP, CONNECT, TLS, Git, or MCP.
- Log lifecycle and endpoint metadata only; never log payload bytes.
- Create forwards only for egress, git HTTP when enabled, and supervise when enabled.
- Fail closed if any listener cannot bind exactly the requested per-bottle address.
Implementation chunks
- Extract the existing image-to-smolmachine pack helper so agent and sidecar artifacts share the same cache and registry path.
- Add a strict stdlib host TCP forwarder for per-bottle address-bound forwarding to raw smolVM-published sidecar ports.
- Add a sidecar VM launch spec and lifecycle helpers: pack, create, start, publish/discover ports, stop, delete.
- Switch smolmachines launch from Docker sidecar bundle lifecycle to sidecar VM plus forwarder lifecycle.
- Update egress apply / reload paths for the sidecar VM supervisor.
- Add unit tests for argv shape, URL stamping, teardown, and fail-closed behavior.
- Add or update integration coverage for agent-to-sidecar reachability, host localhost denial, other-bottle alias denial, internal port denial, and teardown cleanup.