feat(smolmachines): backend skeleton + Smolfile/gvproxy renderers (PRD 0023 chunk 1) #62
Reference in New Issue
Block a user
Delete Branch "prd-0023-chunk-1-skeleton"
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
PRD 0023 chunk 1: the smolmachines backend's prepare side. Subpackage layout,
_BACKENDSregistration under"smolmachines",smolvm+gvproxypreflight check, and the two config-file renderers (Smolfile TOML + gvproxy YAML). Launch / provisioning raiseNotImplementedErroruntil chunks 2 + 4.CLAUDE_BOTTLE_BACKEND=smolmachines python3 cli.py start <agent>will now run throughprepare, write the Smolfile + gvproxy.yaml to the stage dir, render the y/N preflight, and fail cleanly at launch with a chunk-2 pointer.Module layout (mirrors backend/docker/)
What
resolve_plandoessmolvm,gvproxyon PATH ordiewith install pointers).BottleMetadata(same schema as docker).sha256(slug) % 254, skipping octet 17 (avoids the historic 192.168.17.x collision with docker's default bridge).<stage>/gvproxy.yaml(subnet + DNS rule that resolves onlyproxy.internal+ oneport_forwardsentry per active sidecar) and<stage>/smolfile.toml(guest command/env + virtio-net device wired to the gvproxy unixgram socket; no TSI flags).SmolmachinesBottlePlancarrying the slug, paths, subnet/gateway, and host port map.Tests
29 new unit tests across three files:
test_smolfile.py— TOML renderer correctness, round-trip through stdlibtomllib, special-char escapes, and explicit assertion that no TSI primitives leak (--allow-cidr,--allow-host,--outbound-localhost-only).test_smolmachines_gvproxy_config.py— subnet/gateway pass-through, DNS rule resolves ONLYproxy.internal(the PRD 0022 DNS-exfil regression check), port forwards render correctly, no TSI leakage.test_smolmachines_util.py— subnet derivation stable for same slug + differs across slugs + avoids docker-default octet, loopback port allocation, preflight error paths (missing one binary / both / install pointers in message).Full unit suite: 532 passing.
Out of scope for chunk 1
smolvm.pysubprocess wrapper, OCI archive build, launch + teardown.port_forwardswired to the published host ports.smolvm machine exec.Ships the smolmachines backend's prepare side: subpackage layout, `_BACKENDS` registration under "smolmachines", preflight check for `smolvm` + `gvproxy` on PATH, and the two config-file renderers (Smolfile TOML + gvproxy YAML). Launch raises NotImplementedError until chunk 2. New module layout (mirrors backend/docker/): claude_bottle/backend/smolmachines/ __init__.py re-exports SmolmachinesBottleBackend backend.py SmolmachinesBottleBackend façade bottle.py SmolmachinesBottle stub (NotImpl until ch2) bottle_plan.py SmolmachinesBottlePlan + .print() bottle_cleanup_plan.py SmolmachinesBottleCleanupPlan stub prepare.py resolve_plan: writes both config files smolfile.py TOML renderer (stdlib, no tomli_w dep) gvproxy_config.py YAML renderer (same shape as pipelock_yaml) util.py preflight + per-slug subnet + loopback port The renderers are pure functions. `resolve_plan` runs the preflight, allocates one host-side loopback port per active sidecar (pipelock always; git-gate / supervise conditional), derives a per-slug gvproxy subnet (hash-mod-254, skipping the docker-default 17), and writes: - <stage>/gvproxy.yaml: subnet + DNS rule resolving only `proxy.internal` + port_forwards (one per active sidecar). - <stage>/smolfile.toml: guest command/env + virtio-net device backed by gvproxy's unixgram socket. No TSI flags — see PRD 0023 "Why gvproxy, not TSI". The agent's HTTPS_PROXY etc. point at `proxy.internal:<gateway- port>` so the guest dials through gvproxy. gvproxy resolves only `proxy.internal` → the gateway IP, and forwards exactly the listed ports to the host-side sidecar bundle (PRD 0024); every other destination — host LAN, host loopback, public internet directly — is unreachable by construction. 29 new unit tests covering renderer correctness, subnet derivation stability + collision-avoidance, loopback port allocation, and preflight error paths. Full unit suite: 532 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Respond to the changes I specified
@@ -0,0 +46,4 @@info(f"agent: {spec.agent_name}")info(f"bottle: {agent.bottle}")info(f"slug: {self.slug}")info(f"gvproxy: {self.gvproxy_gateway} on {self.gvproxy_subnet}")remove this
@@ -0,0 +52,4 @@skills = list(agent.skills)upstreams = [g.Name for g in bottle.git]routes = [r.host for r in bottle.egress.routes]info(f"env: {', '.join(env_names) if env_names else '(none)'}")can you print this multiline like for docker? use a shared util method
@@ -0,0 +55,4 @@info(f"env: {', '.join(env_names) if env_names else '(none)'}")info(f"skills: {', '.join(skills) if skills else '(none)'}")info(f"git: {', '.join(upstreams) if upstreams else '(none)'}")info(f"routes: {', '.join(routes) if routes else '(none)'}")Actually use the shared util method for all of these
@@ -0,0 +56,4 @@info(f"skills: {', '.join(skills) if skills else '(none)'}")info(f"git: {', '.join(upstreams) if upstreams else '(none)'}")info(f"routes: {', '.join(routes) if routes else '(none)'}")info(f"smolfile: {self.smolfile_path}")Remove this
@@ -0,0 +57,4 @@info(f"git: {', '.join(upstreams) if upstreams else '(none)'}")info(f"routes: {', '.join(routes) if routes else '(none)'}")info(f"smolfile: {self.smolfile_path}")info(f"gvproxy config: {self.gvproxy_config_path}")Don't need this either
@@ -0,0 +57,4 @@agent = manifest.agents[spec.agent_name]bottle = manifest.bottle_for(spec.agent_name)slug = spec.identity or bottle_identity(spec.agent_name)why not just use the bottle identity?
print_multifor plan preflightsGood
Addressed in commit
2aca9e6: dropped the three internal-detail lines (gvproxy subnet, smolfile path, gvproxy-config path) and lifted the multi-value printer into claude_bottle/backend/print_util.py:print_multi — both backends share it now.On the slug = spec.identity or bottle_identity(...) comment (#432): the docker backend uses the same pattern to preserve identity across
resume(PRD 0016: cli.py resume sets spec.identity to the recorded slug so prepare reuses it instead of minting a fresh one). Dropping theorbranch would silently break resume once smolmachines launch lands. Want me to:(a) keep the pattern (matches docker, resume-ready), or
(b) drop the fallback for chunk 1 since launch/resume don't exist yet, and add it back when needed?
Leaning toward (a). Let me know if I should switch.
keep it