feat(smolmachines): backend skeleton + Smolfile/gvproxy renderers (PRD 0023 chunk 1)
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>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""SmolmachinesBottlePlan — concrete BottlePlan for the smolmachines
|
||||
backend (PRD 0023).
|
||||
|
||||
Chunk 1 fields: slug, smolfile_path, gvproxy_config_path, gvproxy
|
||||
subnet + socket, and the per-bottle port map. VM lifecycle fields
|
||||
(machine name, OCI archive path, etc.) land in later chunks as the
|
||||
launch flow grows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ...log import info
|
||||
from .. import BottlePlan
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SmolmachinesBottlePlan(BottlePlan):
|
||||
"""Resolved fields the launch step needs to bring up the bottle.
|
||||
|
||||
Inherits `spec` and `stage_dir` from BottlePlan."""
|
||||
|
||||
slug: str
|
||||
smolfile_path: Path
|
||||
gvproxy_config_path: Path
|
||||
gvproxy_socket: Path
|
||||
gvproxy_subnet: str
|
||||
gvproxy_gateway: str
|
||||
# Daemon name → host-side loopback port the bundle binds.
|
||||
# Always includes "pipelock"; "git-gate" and "supervise"
|
||||
# conditional on the bottle's manifest.
|
||||
host_port_map: dict[str, int]
|
||||
|
||||
def print(self, *, remote_control: bool) -> None:
|
||||
"""Compact y/N preflight for the smolmachines path. Mirrors
|
||||
the docker preflight's layout so operators don't have to
|
||||
learn two formats."""
|
||||
del remote_control # not surfaced in the compact summary
|
||||
spec = self.spec
|
||||
manifest = spec.manifest
|
||||
agent = manifest.agents[spec.agent_name]
|
||||
bottle = manifest.bottle_for(spec.agent_name)
|
||||
|
||||
info(f"backend: smolmachines")
|
||||
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}")
|
||||
|
||||
env_names = sorted(bottle.env.keys())
|
||||
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)'}")
|
||||
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}")
|
||||
info(f"gvproxy config: {self.gvproxy_config_path}")
|
||||
info(
|
||||
"(chunk 1 of PRD 0023: prepare-only — launch is "
|
||||
"not yet implemented)"
|
||||
)
|
||||
Reference in New Issue
Block a user