feat(smolmachines): end-to-end launch + Bottle.exec + smoke + probes (PRD 0023 chunk 2d)
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 41s
test / unit (push) Successful in 22s
test / integration (push) Successful in 41s

End-to-end launch flow for the smolmachines backend. Brings up
the per-bottle docker bridge + sidecar bundle, creates and
starts the smolvm guest pointed at the bundle's pinned IP via
TSI's `--allow-cidr <bundle-ip>/32`, yields a SmolmachinesBottle
handle that routes exec/cp through `smolvm machine exec / cp`,
tears everything down on context exit.

launch.py:
- ExitStack-managed: create_bundle_network → start_bundle →
  machine_create → machine_start (each registered for reverse
  teardown).
- daemons_csv="" for chunk 2d — bundle init logs "no daemons
  selected" and idles. Real daemon bringup with inner-Plan-driven
  env + volumes lands in chunk 4.

bottle.py:
- SmolmachinesBottle.exec → smolvm.machine_exec (captured).
- SmolmachinesBottle.exec_claude → direct subprocess.run with
  inherited TTY for interactive sessions.
- SmolmachinesBottle.cp_in → smolvm.machine_cp.

Architecture pivots forced by smolvm 0.8.0's CLI shape:
1. `--from <smolmachine>` and `--smolfile <toml>` are MUTUALLY
   EXCLUSIVE in smolvm 0.8.0. We need --from to avoid the
   registry-pull race that bit us on machine_start (libkrun
   agent's network attempt got refused by macOS with
   "connect: permission denied" on IPv6). So Smolfile is dropped
   entirely; per-bottle env + allow_cidrs flow as CLI flags
   (`--allow-cidr CIDR`, `-e K=V`) directly to machine_create.
2. `smolvm pack create --image` doesn't pull from the local
   docker daemon — only OCI registries via crane. The real
   claude-bottle:latest image lives in the local docker daemon
   and isn't reachable that way. Chunk 2d ships with an alpine
   placeholder; the agent-image-conversion gap belongs to
   chunk 4 (push the image to a registry, or smolvm grows a
   docker-daemon transport).

Other changes:
- machine_create grew `image=` / `from_path=` / `allow_cidrs=`
  / `env=` kwargs; smolfile= dropped.
- bottle_plan: smolfile_path → agent_from_path + guest_env.
- prepare: pack_create against `alpine:latest`, cached under
  ~/.cache/claude-bottle/smolmachines/ keyed by image ref.
- Deleted smolfile.py + test_smolfile.py (dead code now).

Tests:
- Unit: 540 passing (smolvm wrapper grew 4 new flag forms; one
  test renamed to reflect --from + --allow-cidr + -e combo).
- Integration: 3 new cases in tests/integration/
  test_smolmachines_launch.py, gated on Darwin + smolvm on PATH
  + docker + not GITEA_ACTIONS:
    * smoke: bottle.exec("echo hello-from-vm") round-trips with
      the correct stdout + returncode.
    * localhost-reach probe: agent dials 127.0.0.1:9 → connect
      refused (TSI's <bundle-ip>/32 allowlist doesn't include
      loopback). The regression test for the gap the PRD design
      pivot was about.
    * egress-port-bypass probe: agent dials <bundle-ip>:9099
      (egress's port) → connect refused. Chunk 2d has no
      daemons running so nothing's listening anyway; chunk 3
      will preserve this property once egress is up but bound
      to 127.0.0.1 inside the bundle.

End-to-end smoke + both probes green locally on macOS with
smolvm 0.8.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #67.
This commit is contained in:
2026-05-27 04:39:52 -04:00
parent 6b861a1418
commit 9f65b137b9
10 changed files with 386 additions and 265 deletions
+25 -6
View File
@@ -98,17 +98,36 @@ def pack_create(image: str, output: Path) -> None:
def machine_create(
name: str,
*,
image: str | None = None,
from_path: Path | None = None,
smolfile: Path | None = None,
allow_cidrs: Sequence[str] = (),
env: Mapping[str, str] | None = None,
) -> None:
"""`smolvm machine create NAME [--from PATH] [--smolfile PATH]`.
NAME is positional (the CLI's exception to the `--name`
pattern other subcommands use)."""
"""`smolvm machine create NAME [--image IMG | --from PATH]
[--allow-cidr CIDR ...] [-e K=V ...]`. NAME is positional
(the CLI's exception to the `--name` pattern other
subcommands use).
`image` (registry ref like `alpine:latest`) and `from_path`
(a `.smolmachine` artifact) are mutually exclusive — one or
the other tells smolvm what to boot. The wrapper doesn't
enforce exclusivity; smolvm errors clearly enough.
`allow_cidrs` and `env` are passed as CLI flags instead of a
Smolfile because `--from` and `--smolfile` are themselves
mutually exclusive in smolvm 0.8.0 — and we want `--from`'s
no-pull-at-start property. The flag form gives the same
result without the Smolfile complication."""
args: list[str] = ["machine", "create"]
if image is not None:
args += ["--image", image]
if from_path is not None:
args += ["--from", str(from_path)]
if smolfile is not None:
args += ["--smolfile", str(smolfile)]
for cidr in allow_cidrs:
args += ["--allow-cidr", cidr]
if env:
for k, v in env.items():
args += ["-e", f"{k}={v}"]
args.append(name)
_smolvm(*args)