# 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 1. `BOT_BOTTLE_BACKEND=smolmachines ./cli.py start ` runs with both the agent and sidecar as smolVMs. 2. The smolmachines launch path no longer uses `docker run` for the sidecar bundle. 3. Agent-facing egress, git-gate, and supervise endpoints are exposed only through the bottle's own published sidecar surface. 4. Internal sidecar-only ports are not reachable from the agent VM. 5. The agent VM TSI allowlist remains fail-closed and limited to the per-bottle address. 6. Sidecar config, secrets, and state are delivered to the sidecar VM without exposing provider or forge credentials to the agent VM. 7. 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 /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::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: ```text agent VM -> TSI allows only /32 -> bot-bottle forwarder bound to : -> raw smolVM-published sidecar port on 127.0.0.1: -> sidecar VM service ``` Agent-facing URLs are stamped from the forwarder ports, never the raw smolVM ports: - `HTTP_PROXY` / `HTTPS_PROXY` point at the published egress proxy port. - `GIT_GATE_URL` points at the published git HTTP port when git-gate is enabled. - `MCP_SUPERVISE_URL` points 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 1. Extract the existing image-to-smolmachine pack helper so agent and sidecar artifacts share the same cache and registry path. 2. Add a strict stdlib host TCP forwarder for per-bottle address-bound forwarding to raw smolVM-published sidecar ports. 3. Add a sidecar VM launch spec and lifecycle helpers: pack, create, start, publish/discover ports, stop, delete. 4. Switch smolmachines launch from Docker sidecar bundle lifecycle to sidecar VM plus forwarder lifecycle. 5. Update egress apply / reload paths for the sidecar VM supervisor. 6. Add unit tests for argv shape, URL stamping, teardown, and fail-closed behavior. 7. Add or update integration coverage for agent-to-sidecar reachability, host localhost denial, other-bottle alias denial, internal port denial, and teardown cleanup.