feat(smolmachines): rewrite Smolfile to smolvm 0.8.0 schema + drop gvproxy (PRD 0023 chunk 2a)
First sub-PR of chunk 2: rewrite the renderer chunk 1 shipped to match smolvm 0.8.0's actual Smolfile shape, delete the dead gvproxy renderer + its tests, simplify the prepare flow now that there's no gvproxy socket + no loopback-port allocation. Smolfile renderer: - Old shape (under the abandoned gvproxy design): name = ..., command = [...], [[net]] attachment = "unixgram", socket = "...". - New shape (smolvm 0.8.0): env = [...] (sorted K=V pairs), [network] allow_cidrs = ["<bundle-ip>/32"]. Nothing else. image / entrypoint / cmd come from the .smolmachine artifact built in chunk 2b; cpus / memory left at smolvm defaults. - Tests assert no leakage of TSI's --outbound-localhost-only or the old gvproxy/unixgram keys. util.py: - smolmachines_gvproxy_subnet → smolmachines_bundle_subnet, returning (subnet, gateway, bundle_ip). bundle_ip is always at .2 (gateway .1); subnet is /24, third octet derived from the slug hash, skipping the docker-default 17 to avoid the common 192.168.17.x collision. - allocate_loopback_port: deleted. The bundle gets a pinned docker IP now; the agent dials that IP directly through TSI. - smolmachines_preflight: dropped the gvproxy check; only smolvm is required. prepare.py: - Drops the gvproxy.yaml render + the loopback port allocation + the gvproxy_socket field on the plan. - Derives subnet / gateway / bundle_ip from the slug and populates the new SmolmachinesBottlePlan fields. - Agent env now uses IP-literal URLs (http://<bundle-ip>:8888 etc) since the guest will have no DNS resolver inside TSI's allowlist. bottle_plan.py: - Old fields: gvproxy_config_path, gvproxy_socket, gvproxy_subnet, gvproxy_gateway, host_port_map. - New fields: bundle_subnet, bundle_gateway, bundle_ip, smolfile_path. (smolmachine artifact path lands in chunk 2b.) Net: -410 lines. Full unit suite: 516 passing. The VM lifecycle + bundle bringup + launch wiring + smoke tests land in chunk 2b. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
"""smolmachines `_resolve_plan` (PRD 0023 chunk 1).
|
||||
"""smolmachines `_resolve_plan` (PRD 0023 chunk 2a).
|
||||
|
||||
Lays down the two config files the launch step will consume —
|
||||
Smolfile (TOML) and gvproxy YAML — under the bottle's stage dir.
|
||||
No VM bringup. The plan it returns is enough for the y/N
|
||||
preflight to render."""
|
||||
Resolves the per-bottle docker subnet + bundle IP and writes the
|
||||
Smolfile to the stage dir. No VM bringup. The plan it returns is
|
||||
enough for the y/N preflight to render."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,151 +15,77 @@ from ...backend.docker.bottle_state import (
|
||||
bottle_identity,
|
||||
write_metadata,
|
||||
)
|
||||
from ...backend.docker.sidecar_bundle import sidecar_bundle_container_name
|
||||
from .bottle_plan import SmolmachinesBottlePlan
|
||||
from .gvproxy_config import (
|
||||
PortForward,
|
||||
gvproxy_config_build,
|
||||
gvproxy_config_write,
|
||||
)
|
||||
from .smolfile import (
|
||||
GVPROXY_GIT_GATE_GATEWAY_PORT,
|
||||
GVPROXY_PIPELOCK_GATEWAY_PORT,
|
||||
GVPROXY_SUPERVISE_GATEWAY_PORT,
|
||||
smolfile_build,
|
||||
smolfile_write,
|
||||
)
|
||||
from .util import (
|
||||
allocate_loopback_port,
|
||||
smolmachines_gvproxy_subnet,
|
||||
smolmachines_preflight,
|
||||
)
|
||||
from .smolfile import smolfile_build, smolfile_write
|
||||
from .util import smolmachines_bundle_subnet, smolmachines_preflight
|
||||
|
||||
|
||||
# Gateway ports the bundle exposes inside its container — pipelock
|
||||
# HTTPS proxy, git-gate's git-daemon, supervise's MCP. The agent
|
||||
# inside the smolvm guest dials these on the bundle's pinned IP.
|
||||
_BUNDLE_PIPELOCK_PORT = 8888
|
||||
_BUNDLE_GIT_GATE_PORT = 9418
|
||||
_BUNDLE_SUPERVISE_PORT = 9100
|
||||
|
||||
|
||||
def resolve_plan(
|
||||
spec: BottleSpec, *, stage_dir: Path
|
||||
) -> SmolmachinesBottlePlan:
|
||||
"""Materialize the smolmachines plan. Three things land on disk
|
||||
under stage_dir:
|
||||
|
||||
- `gvproxy.sock` path (created at launch time by gvproxy,
|
||||
not by prepare — prepare only records where it'll go).
|
||||
- `gvproxy.yaml` — subnet + DNS rule + port_forwards.
|
||||
- `smolfile.toml` — guest command/env + virtio-net device
|
||||
wired to the gvproxy unixgram socket.
|
||||
|
||||
The y/N preflight reads from the returned plan; chunk-2 launch
|
||||
consumes the file paths it points at."""
|
||||
"""Materialize the smolmachines plan. The Smolfile lands at
|
||||
`<stage>/smolfile.toml`; the bundle's docker subnet + pinned
|
||||
IP are derived from the slug and carried on the plan for
|
||||
launch to consume."""
|
||||
smolmachines_preflight()
|
||||
|
||||
manifest = spec.manifest
|
||||
agent = manifest.agents[spec.agent_name]
|
||||
bottle = manifest.bottle_for(spec.agent_name)
|
||||
|
||||
slug = spec.identity or bottle_identity(spec.agent_name)
|
||||
|
||||
# Record minimal metadata. `resume` (PRD 0016) reuses the slug
|
||||
# via spec.identity, so the metadata file is the recoverability
|
||||
# contract — keep it in lockstep with the docker backend's
|
||||
# bottle_state schema.
|
||||
# Record minimal metadata so `cli.py resume` can recover the
|
||||
# slug. Same schema as the docker backend.
|
||||
write_metadata(BottleMetadata(
|
||||
identity=slug,
|
||||
agent_name=spec.agent_name,
|
||||
cwd=spec.user_cwd if spec.copy_cwd else "",
|
||||
copy_cwd=spec.copy_cwd,
|
||||
started_at=datetime.now(timezone.utc).isoformat(),
|
||||
# No compose project for smolmachines bottles. Empty string
|
||||
# so the dashboard's compose-project-based discovery skips
|
||||
# these entries cleanly until chunk 4 teaches it about
|
||||
# `smolvm machine list`.
|
||||
# No compose project for smolmachines bottles; chunk 4
|
||||
# will give dashboard discovery a backend-specific path.
|
||||
compose_project="",
|
||||
))
|
||||
|
||||
# Per-bottle gvproxy subnet + gateway. Deterministic from slug;
|
||||
# collisions surface at launch time (chunk 2).
|
||||
subnet, gateway = smolmachines_gvproxy_subnet(slug)
|
||||
subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug)
|
||||
|
||||
# Allocate one host-side loopback port per active sidecar
|
||||
# daemon. The bundle (PRD 0024) binds these; gvproxy
|
||||
# port-forwards from a fixed gateway port -> host port.
|
||||
host_port_map: dict[str, int] = {
|
||||
"pipelock": allocate_loopback_port(),
|
||||
}
|
||||
port_forwards: list[PortForward] = [PortForward(
|
||||
gateway_port=GVPROXY_PIPELOCK_GATEWAY_PORT,
|
||||
host_port=host_port_map["pipelock"],
|
||||
)]
|
||||
if bottle.git:
|
||||
host_port_map["git-gate"] = allocate_loopback_port()
|
||||
port_forwards.append(PortForward(
|
||||
gateway_port=GVPROXY_GIT_GATE_GATEWAY_PORT,
|
||||
host_port=host_port_map["git-gate"],
|
||||
))
|
||||
if bottle.supervise:
|
||||
host_port_map["supervise"] = allocate_loopback_port()
|
||||
port_forwards.append(PortForward(
|
||||
gateway_port=GVPROXY_SUPERVISE_GATEWAY_PORT,
|
||||
host_port=host_port_map["supervise"],
|
||||
))
|
||||
|
||||
# Render + write the two config files.
|
||||
gvproxy_socket = stage_dir / "gvproxy.sock"
|
||||
gvproxy_yaml = stage_dir / "gvproxy.yaml"
|
||||
smolfile = stage_dir / "smolfile.toml"
|
||||
|
||||
gvproxy_config_write(
|
||||
gvproxy_config_build(
|
||||
subnet=subnet,
|
||||
gateway=gateway,
|
||||
port_forwards=tuple(port_forwards),
|
||||
),
|
||||
gvproxy_yaml,
|
||||
)
|
||||
|
||||
# Build the guest env. proxy_url points at the gvproxy gateway;
|
||||
# gvproxy resolves `proxy.internal` to the gateway IP via its
|
||||
# DNS rule, then port-forwards to the host sidecar bundle.
|
||||
# Agent's env. IP literals; no DNS resolution inside the guest
|
||||
# (TSI allowlist contains only `<bundle_ip>/32` — no resolver).
|
||||
guest_env: dict[str, str] = {
|
||||
**bottle.env,
|
||||
"HTTPS_PROXY": f"http://proxy.internal:{GVPROXY_PIPELOCK_GATEWAY_PORT}",
|
||||
"HTTP_PROXY": f"http://proxy.internal:{GVPROXY_PIPELOCK_GATEWAY_PORT}",
|
||||
"HTTPS_PROXY": f"http://{bundle_ip}:{_BUNDLE_PIPELOCK_PORT}",
|
||||
"HTTP_PROXY": f"http://{bundle_ip}:{_BUNDLE_PIPELOCK_PORT}",
|
||||
"NO_PROXY": "localhost,127.0.0.1",
|
||||
}
|
||||
if bottle.git:
|
||||
guest_env["GIT_GATE_URL"] = (
|
||||
f"git://proxy.internal:{GVPROXY_GIT_GATE_GATEWAY_PORT}"
|
||||
f"git://{bundle_ip}:{_BUNDLE_GIT_GATE_PORT}"
|
||||
)
|
||||
if bottle.supervise:
|
||||
guest_env["MCP_SUPERVISE_URL"] = (
|
||||
f"http://proxy.internal:{GVPROXY_SUPERVISE_GATEWAY_PORT}"
|
||||
f"http://{bundle_ip}:{_BUNDLE_SUPERVISE_PORT}"
|
||||
)
|
||||
|
||||
smolfile_path = stage_dir / "smolfile.toml"
|
||||
smolfile_write(
|
||||
smolfile_build(
|
||||
slug=slug,
|
||||
gvproxy_socket=gvproxy_socket,
|
||||
env=guest_env,
|
||||
),
|
||||
smolfile,
|
||||
smolfile_build(env=guest_env, bundle_ip=bundle_ip),
|
||||
smolfile_path,
|
||||
)
|
||||
|
||||
return SmolmachinesBottlePlan(
|
||||
spec=spec,
|
||||
stage_dir=stage_dir,
|
||||
slug=slug,
|
||||
smolfile_path=smolfile,
|
||||
gvproxy_config_path=gvproxy_yaml,
|
||||
gvproxy_socket=gvproxy_socket,
|
||||
gvproxy_subnet=subnet,
|
||||
gvproxy_gateway=gateway,
|
||||
host_port_map=host_port_map,
|
||||
smolfile_path=smolfile_path,
|
||||
bundle_subnet=subnet,
|
||||
bundle_gateway=gateway,
|
||||
bundle_ip=bundle_ip,
|
||||
)
|
||||
|
||||
|
||||
# Used by future cleanup logic — the bundle container that runs on
|
||||
# the host carries this name even when its host is a smolmachines
|
||||
# microVM. Re-exported here so chunk 3's sidecar-bringup path has
|
||||
# a single import target.
|
||||
__all__ = [
|
||||
"resolve_plan",
|
||||
"sidecar_bundle_container_name",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user