Commit Graph

3 Commits

Author SHA1 Message Date
didericis-claude 2edc1abb9a feat(smolmachines): per-bottle loopback alias scopes TSI to single /32
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 41s
PR #74's Docker-Desktop fix routed the agent through
`127.0.0.1:<random>` loopback forwards, but TSI filters by IP
only — so the allowlist `127.0.0.1/32` let the agent VM reach
**any** host service on macOS loopback (postgres, dev servers,
other bottles' published ports, mDNSResponder, ...). Real
downgrade vs the docker backend's `--internal` network.

Resolution: per-bottle loopback alias.

- New `loopback_alias` module manages a pool of
  `127.0.0.16` .. `127.0.0.31` on `lo0`. macOS only routes
  `127.0.0.1` by default; the extras need `sudo ifconfig lo0
  alias`. `ensure_pool()` lazily adds the missing entries via
  one sudo prompt on first launch per reboot — aliases persist
  on `lo0` until reboot, so subsequent launches skip the
  prompt entirely.
- `allocate(slug)` picks the lowest-numbered unused alias by
  inspecting running bundle containers' port-binding HostIps.
  No on-disk reservation — docker is the source of truth.
- Bundle bringup binds published ports to the allocated alias
  (`docker run -p <alias>::<port>`) instead of `127.0.0.1`.
- TSI allowlist becomes the alias's /32 — narrows reachability
  to this bottle's bundle only.
- Linux native daemons share the host's network namespace;
  `127.0.0.0/8` works without aliases, so the module no-ops on
  non-Darwin and returns `127.0.0.1` from `allocate`.

Tracking issue closed: gitea/issues/75.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:23:17 -04:00
didericis-claude 4f136a9932 fix(smolmachines): agent dials bundle via host loopback ports, not docker bridge IP
test / unit (pull_request) Successful in 26s
test / integration (pull_request) Successful in 39s
Claude hung on outbound network calls under
CLAUDE_BOTTLE_BACKEND=smolmachines:

  Unable to connect to API (FailedToOpenSocket)

Root cause: the PRD-0023 design pinned the bundle at a docker
bridge IP (192.168.X.2) and set the smolvm guest's TSI allowlist
to `<bundle-ip>/32`. On native Linux this works — host shares
the docker bridge's network namespace, TSI's syscall
impersonation reaches the bridge IP directly. On Docker Desktop
(macOS), the daemon runs in its own Linux VM and docker bridge
IPs aren't reachable from macOS networking, so the smolvm
guest's TSI requests die "Network is unreachable" before they
hit pipelock.

Fix: publish each agent-facing bundle daemon's port on host
loopback (-p 127.0.0.1::PORT), discover the random host-side
ports after start, and route the agent through
`127.0.0.1:<host port>` instead of the bridge IP. macOS loopback
is the surface Docker Desktop's gvproxy forwards into the
daemon's VM, so the chain (guest TSI -> macOS loopback ->
daemon VM port-forward -> bundle container) works on both
Docker Desktop and native Linux.

Concrete changes:
- BundleLaunchSpec: add `ports_to_publish` so start_bundle adds
  `-p 127.0.0.1::PORT` for the agent-facing ports (pipelock
  always; git-gate when upstreams declared; supervise when
  enabled). Egress's port stays bundle-internal.
- sidecar_bundle.bundle_host_port(): wrap `docker port <bundle>
  <container_port>/tcp` so launch can look up the random
  host-side mapping after start.
- launch.py: discover the host ports, build URLs of the form
  `http://127.0.0.1:<host port>` / `git://127.0.0.1:<host port>`,
  stamp onto guest_env + new agent_*_url fields on the plan.
- launch.py: TSI allow_cidrs flips to `["127.0.0.1/32"]`. The
  bundle IP is no longer the agent's target.
- prepare.py: stop synthesizing HTTPS_PROXY / GIT_GATE_URL /
  MCP_SUPERVISE_URL at prepare time — launch owns those now
  (the values depend on a port docker hasn't assigned yet).
- provision_git: gate_host from plan.agent_git_gate_host.
- provision_supervise: URL from plan.agent_supervise_url.

End-to-end verified on Docker Desktop / macOS: guest dials
pipelock through TSI, pipelock forwards to api.anthropic.com,
the API responds with 401 (i.e. it received the request).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:31:44 -04:00
didericis-claude 495be7f9c0 feat(smolmachines): bundle bringup on per-bottle docker bridge (PRD 0023 chunk 2c)
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 43s
claude_bottle/backend/smolmachines/sidecar_bundle.py — primitives
for the per-bottle bridge + bundle container with pinned IP:

  - bundle_network_name(slug) / bundle_container_name(slug)
  - create_bundle_network(name, subnet, gateway)
  - remove_bundle_network(name)
  - start_bundle(BundleLaunchSpec, env=)
  - stop_bundle(slug)

`BundleLaunchSpec` carries the launch-time fields (network +
subnet + gateway + bundle_ip + daemons_csv + environment +
volumes). Wiring it up from the inner Plans (PipelockProxyPlan,
EgressPlan, GitGatePlan, SupervisePlan) is chunk 2d's job; this
module is the docker-argv surface only.

Pinning the bundle IP via `docker run --ip <bundle-ip>` is what
makes smolvm's TSI allowlist (`<bundle-ip>/32`) safe to compute
at prepare time — without pinning, we'd have to inspect the
assigned IP after start and feed it back into the Smolfile.

Idempotent semantics where it matters: `create_bundle_network`
treats "already exists" as success, `remove_bundle_network` +
`stop_bundle` treat "no such ..." as success. Other failures
die / warn depending on whether the launch flow can recover.

Tests:
- 15 unit cases (mocked subprocess.run): argv shape for create
  / remove / start / stop, idempotent paths, host-env
  inheritance to docker run subprocess.
- 1 integration case (real docker daemon, gated on docker
  available + not GITEA_ACTIONS): end-to-end bringup of an
  empty-daemons bundle on a 192.168.211.0/24 bridge, confirms
  the container lands at the pinned IP. Skipped if the
  claude-bottle-sidecars:latest image isn't built (operator
  hasn't run a docker bottle yet).

546 unit tests passing. Real-docker bundle bringup green
locally.

Launch wiring + provisioning + PRD 0022 acceptance probes
land in chunk 2d.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 04:19:31 -04:00