feat(smolmachines): end-to-end launch + Bottle.exec + smoke + probes (PRD 0023 chunk 2d) #67

Merged
didericis merged 1 commits from prd-0023-chunk-2d-launch into main 2026-05-27 04:44:53 -04:00
Collaborator

Summary

Final sub-PR of chunk 2. End-to-end launch flow for the smolmachines backend: per-bottle docker bridge → sidecar bundle → smolvm guest → exec round trip → teardown. PRD 0023's two acceptance probes (localhost-reach + egress-port-bypass) included.

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

Two architecture pivots forced by empirical smolvm 0.8.0

  1. --from and --smolfile are mutually exclusive. We need --from to avoid the registry-pull race that hit on machine_start (libkrun agent's network attempt got refused by macOS with connect: permission denied on IPv6). So the Smolfile is dropped entirely; per-bottle env + allow_cidrs flow as CLI flags (--allow-cidr CIDR, -e K=V) directly to machine_create. The smolfile.py renderer + its tests are deleted as dead code.

  2. smolvm pack create --image doesn't pull from local docker. Only OCI registries via crane. claude-bottle:latest lives in the local docker daemon and isn't reachable that way. Chunk 2d uses an alpine:latest placeholder; the agent-image-conversion gap is a chunk-4 concern.

API

# Now wired end-to-end
backend = get_bottle_backend()
plan = backend.prepare(spec, stage_dir=...)
with backend.launch(plan) as bottle:
    r = bottle.exec("echo hi")
    bottle.cp_in("/host/file", "/dest/file")
    rc = bottle.exec_claude([...], tty=True)

launch uses an ExitStack so partial-bringup failures unwind cleanly. The lifecycle is: create_bundle_network → start_bundle → machine_create --from → machine_start, each registered for reverse teardown on context exit.

Tests

  • 540 unit passing (smolvm wrapper grew --allow-cidr / -e flag support; one test renamed).
  • 3 new integration cases in tests/integration/test_smolmachines_launch.py, gated on macOS + smolvm + docker + not GITEA_ACTIONS:
    • test_smoke_exec_echobottle.exec("echo hello-from-vm") round-trips with the right stdout/returncode.
    • test_localhost_reach_probe — agent dials 127.0.0.1:9 → connect refused. The regression test for the gap the PRD design pivot was about.
    • test_egress_port_bypass_probe — agent dials <bundle-ip>:9099 → connect refused. Chunk 2d's bundle isn't running any daemons (daemons_csv=""), so nothing listens on :9099 anyway; chunk 3 preserves this once egress is up but bound to 127.0.0.1 inside the bundle.

What's left

PRD 0023 chunks 3-5:

  • 3 — egress binds 127.0.0.1:9099 inside the bundle (one-line change to PRD 0024's bundle entrypoint; chunk 2d's daemons_csv="" defers this).
  • 4 — provisioning parity (CA install, prompt, skills, .git, supervise) + agent-image-conversion (the chunk-2d alpine placeholder → real claude-bottle image).
  • 5 — PRD 0022 sandbox-escape suite green under CLAUDE_BOTTLE_BACKEND=smolmachines.

Files

  • New: launch.py, tests/integration/test_smolmachines_launch.py.
  • Deleted: smolfile.py, tests/unit/test_smolfile.py.
  • Modified: backend.py (real launch wired), bottle.py (exec/cp/exec_claude implementations), bottle_plan.py (agent_from_path + guest_env), prepare.py (pack_create + cache), smolvm.py (--from/--allow-cidr/-e flag support).
## Summary Final sub-PR of chunk 2. End-to-end launch flow for the smolmachines backend: per-bottle docker bridge → sidecar bundle → smolvm guest → exec round trip → teardown. PRD 0023's two acceptance probes (localhost-reach + egress-port-bypass) included. **End-to-end smoke + both probes green locally** on macOS with smolvm 0.8.0 + docker. ## Two architecture pivots forced by empirical smolvm 0.8.0 1. **`--from` and `--smolfile` are mutually exclusive.** We need `--from` to avoid the registry-pull race that hit on `machine_start` (libkrun agent's network attempt got refused by macOS with `connect: permission denied` on IPv6). So the Smolfile is dropped entirely; per-bottle env + allow_cidrs flow as CLI flags (`--allow-cidr CIDR`, `-e K=V`) directly to `machine_create`. The `smolfile.py` renderer + its tests are deleted as dead code. 2. **`smolvm pack create --image` doesn't pull from local docker.** Only OCI registries via crane. `claude-bottle:latest` lives in the local docker daemon and isn't reachable that way. Chunk 2d uses an `alpine:latest` placeholder; the agent-image-conversion gap is a chunk-4 concern. ## API ```python # Now wired end-to-end backend = get_bottle_backend() plan = backend.prepare(spec, stage_dir=...) with backend.launch(plan) as bottle: r = bottle.exec("echo hi") bottle.cp_in("/host/file", "/dest/file") rc = bottle.exec_claude([...], tty=True) ``` `launch` uses an `ExitStack` so partial-bringup failures unwind cleanly. The lifecycle is: `create_bundle_network → start_bundle → machine_create --from → machine_start`, each registered for reverse teardown on context exit. ## Tests - **540 unit passing** (smolvm wrapper grew `--allow-cidr` / `-e` flag support; one test renamed). - **3 new integration cases** in `tests/integration/test_smolmachines_launch.py`, gated on macOS + smolvm + docker + not `GITEA_ACTIONS`: * `test_smoke_exec_echo` — `bottle.exec("echo hello-from-vm")` round-trips with the right stdout/returncode. * `test_localhost_reach_probe` — agent dials `127.0.0.1:9` → connect refused. The regression test for the gap the PRD design pivot was about. * `test_egress_port_bypass_probe` — agent dials `<bundle-ip>:9099` → connect refused. Chunk 2d's bundle isn't running any daemons (daemons_csv=""), so nothing listens on :9099 anyway; chunk 3 preserves this once egress is up but bound to `127.0.0.1` inside the bundle. ## What's left PRD 0023 chunks 3-5: - **3** — egress binds `127.0.0.1:9099` inside the bundle (one-line change to PRD 0024's bundle entrypoint; chunk 2d's daemons_csv="" defers this). - **4** — provisioning parity (CA install, prompt, skills, .git, supervise) + agent-image-conversion (the chunk-2d alpine placeholder → real claude-bottle image). - **5** — PRD 0022 sandbox-escape suite green under `CLAUDE_BOTTLE_BACKEND=smolmachines`. ## Files - New: `launch.py`, `tests/integration/test_smolmachines_launch.py`. - Deleted: `smolfile.py`, `tests/unit/test_smolfile.py`. - Modified: `backend.py` (real launch wired), `bottle.py` (exec/cp/exec_claude implementations), `bottle_plan.py` (agent_from_path + guest_env), `prepare.py` (pack_create + cache), `smolvm.py` (`--from`/`--allow-cidr`/`-e` flag support).
didericis-claude added 1 commit 2026-05-27 04:40:24 -04:00
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
9f65b137b9
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>
didericis approved these changes 2026-05-27 04:44:34 -04:00
didericis merged commit 9f65b137b9 into main 2026-05-27 04:44:53 -04:00
didericis deleted branch prd-0023-chunk-2d-launch 2026-05-27 04:44:53 -04:00
Sign in to join this conversation.