20f411b22e
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>
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""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)"
|
|
)
|