feat(smolmachines): thread inner Plans + bundle daemons run (PRD 0023 chunk 4b) #70
Reference in New Issue
Block a user
Delete Branch "prd-0023-chunk-4b-inner-plans"
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?
Summary
Chunk 4b: bundle daemons now actually start with their config files. Chunks 2d + 3 ran with
daemons_csv=""so the bundle's init supervisor idled; chunk 4b wires up the real path. From inside the smolvm guest,wget http://<bundle-ip>:8888/now reaches a live pipelock.The remaining gap to full end-to-end is the agent-image-conversion (chunk 4c) — the chunk-2d alpine placeholder doesn't have claude-code, git, curl, or a writable trust store, so
provision_ca/provision_git/provision_supervisestill wait (chunk 4d).Mechanism
bottle_plan.py— new fieldsproxy_plan/git_gate_plan/egress_plan/supervise_plan. Same shape the docker backend's plan uses; docker-network-only fields stay at dataclass defaults.prepare.py— instantiatesDockerPipelockProxy/DockerEgress/DockerGitGate/DockerSuperviseand calls their.prepare()methods to write per-bottle config files (pipelock.yaml, routes.yaml, git-gate entrypoint/hooks, supervise queue dir) under per-bottle state dirs. The.prepare()methods live on the platform-neutral ABCs — the "Docker" class names are misleading but it's a one-line cleanup for a future PR.launch.py— pipelock_tls_init at launch (always); egress_tls_init only when routes declared. Inner Plans updated in place with the CA paths + EGRESS_UPSTREAM_PROXY=http://127.0.0.1:8888(egress's upstream is pipelock on the bundle's own loopback; same container's network namespace).BundleLaunchSpecenv + volumes built from the inner Plans.daemons_csv=["egress", "pipelock"]+["git-gate"](if upstreams) +["supervise"](if enabled). Token env values resolved from host env and threaded through the docker-run subprocess env.Tests
GITEA_ACTIONS):test_smoke_exec_echo— still works.test_localhost_reach_probe— host loopback still refused.test_egress_port_bypass_probe—<bundle-ip>:9099still refused, NOW WITH EGRESS ACTUALLY RUNNING. Chunk 3's127.0.0.1bind-address is doing its job.test_prompt_file_lands_in_guest— still works.test_pipelock_answers_on_bundle_ip— NEW.wgetto<bundle-ip>:8888gets an HTTP response (not "connection refused"). Proves pipelock is listening with its config + CA bind-mounts intact.Roadmap for the rest of chunk 4
smolvm pack create --from-vmafter manual VM setup, or push the docker image to a smolvm-reachable registry.provision_ca+provision_git+provision_superviseonce 4c lands.Bundle daemons (pipelock, egress, optionally git-gate + supervise) now actually start with their config files bind-mounted from the inner Plans the docker backend already produces. Chunks 2d + 3 ran with daemons_csv="" so the bundle's init supervisor idled; chunk 4b wires up the real path: agent → pipelock → egress → internet (when routes declared) is now functional, modulo agent- image gaps (claude-code / TLS-trust-store / git in the guest) that chunk 4c addresses. bottle_plan.py — added the four inner Plan fields: proxy_plan: PipelockProxyPlan git_gate_plan: GitGatePlan egress_plan: EgressPlan supervise_plan: SupervisePlan | None Same shape the docker backend's plan uses. Docker-network-only fields (internal_network, egress_network) stay at dataclass defaults — the smolmachines bundle is on a per-bottle bridge with a pinned IP, not docker's --internal + egress topology. prepare.py — instantiates DockerPipelockProxy / DockerEgress / DockerGitGate / DockerSupervise and calls their .prepare() methods to write the per-bottle config files (pipelock.yaml, routes.yaml, git-gate entrypoint/hooks, supervise queue dir) under the per-bottle state dir. (The "Docker" prefix on the class names is a misnomer here — .prepare() is platform-neutral, inherited from each sidecar's ABC. A future cleanup could factor the prepare logic out of the docker subpackage.) launch.py — major rewrite: - pipelock_tls_init at launch (always); egress_tls_init only when the bottle declares routes (otherwise the CA files aren't bind-mounted and openssl runs would be wasted). - Inner Plans updated in place with launch-time CA paths + EGRESS_UPSTREAM_PROXY = http://127.0.0.1:8888 (egress's upstream is pipelock on the bundle's own loopback; same container's network namespace). - BundleLaunchSpec env + volumes built from the inner Plans: pipelock.yaml + CA + key (always); egress routes + CAs + upstream env + token-slot bare names (when routes); git-gate entrypoint + hooks + per-upstream identity files (when upstreams); supervise queue dir + env (when enabled). - daemons_csv = ["egress", "pipelock"] + ["git-gate"] (if upstreams) + ["supervise"] (if enabled). - Token env values resolved from host env via `egress_resolve_token_values` and threaded into the docker-run subprocess env (bare-name -e entries in spec inherit from there — values never land on argv). Tests: - 552 unit passing (no new unit cases; fixture updated to populate the new plan fields). - 5 integration cases passing locally (Darwin + smolvm + docker + not GITEA_ACTIONS): * test_smoke_exec_echo — still works. * test_localhost_reach_probe — host loopback still refused. * test_egress_port_bypass_probe — <bundle-ip>:9099 still refused, NOW WITH EGRESS ACTUALLY RUNNING (chunk 3's 127.0.0.1 bind-address is doing its job). * test_prompt_file_lands_in_guest — still works. * test_pipelock_answers_on_bundle_ip — NEW. From inside the guest, wget to <bundle-ip>:8888 gets an HTTP response (not "connection refused") — proves pipelock is actually listening and the bind-mount + CA generation path works. What's left in chunk 4: - 4c: agent-image-conversion (claude-code + git + curl + ca-certificates in the guest). Chunk 2d's alpine placeholder stays for now. - 4d: provision_ca + provision_git + provision_supervise once the agent image has the required tools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>@@ -89,0 +98,4 @@# backend's concrete subclasses — the `.prepare()` method# they inherit is platform-neutral (writes config files +# returns a Plan dataclass); the docker-specific subclasses# exist only to satisfy ABC instantiation. Future: factorDo this refactor now
@@ -89,0 +98,4 @@# platform-neutral — `.prepare()` writes config files + returns# a Plan dataclass with no backend-specific assumptions. State# dirs are still keyed by slug under the docker backend's# bottle_state layout (shared on-host convention; not a dockerDone in
73dc0d4— moved CA in-container path constants up toclaude_bottle/pipelock.py, madePipelockProxya regular class, and deleted the four emptyDocker*sidecar subclasses. Both backends now instantiate the platform-neutral ABCs directly.@@ -5,1 +1,3 @@the container-name helper the renderer's network alias targets.""""""Docker-side supervise helpers: container naming for the legacyper-sidecar topology (kept so the bundle's docker-network aliasresolves the old name to the bundle IP). The prepare-timeWe don't care about preserving the legacy topology
@@ -1,3 +1,3 @@"""Docker-side supervise helpers: container naming for the legacyper-sidecar topology (kept so the bundle's docker-network aliasresolves the old name to the bundle IP). The prepare-timeDropped in
8ecba2b— deletedclaude_bottle/backend/docker/supervise.pyentirely (only legacy-name function left after the refactor), removed the network alias incompose.py, removed the orphan probe inprepare.py, and stopped asserting the long-form alias intest_compose.py. The bundle keeps the shortsupervisealias sinceSUPERVISE_HOSTNAMEis what consumers actually use.The other container name functions can probably go too, right?
@@ -1,3 +1,3 @@"""Docker-side supervise helpers: container naming for the legacyper-sidecar topology (kept so the bundle's docker-network aliasresolves the old name to the bundle IP). The prepare-timeYep — done in
727f30d. Killedpipelock_container_name,egress_container_name,git_gate_container_name, pluspipelock_proxy_urlandgit_gate_hostsince they only existed to build URLs with the legacy long-form names. AddedPIPELOCK_HOSTNAME = "pipelock"andGIT_GATE_HOSTNAME = "git-gate"(mirroringEGRESS_HOSTNAME = "egress"), so the agent's HTTPS_PROXY is nowhttp://pipelock:8888and the gitconfig insteadOf rewrites aregit://git-gate/<repo>.git. The bundle-internal egress→pipelock hop drops the DNS alias entirely and uses loopback (BUNDLE_LOCAL_PIPELOCK_URL = http://127.0.0.1:8888) — same pattern smolmachines was already using.The prepare-time orphan probe collapses to one check on the bundle container name (
claude-bottle-sidecars-<slug>) instead of the four legacy per-sidecar names nothing creates anymore.One thing I left alone:
capability_apply._per_bottle_container_namesstill lists the four legacy names in its teardown helper. Harmless (missing containers are silently skipped) but technically also dead. Easy to wipe in a follow-up if you want.wipe that out too
@@ -1,3 +1,3 @@"""Docker-side supervise helpers: container naming for the legacyper-sidecar topology (kept so the bundle's docker-network aliasresolves the old name to the bundle IP). The prepare-timeDone in
519a71f._per_bottle_container_namesis now just[agent, sidecar-bundle]; the four legacy per-sidecar literals are gone. Integration test follows — the fake supervise sidecar (which existed to give teardown an extra container to nuke) switches to a fake sidecar bundle.