feat(smolmachines): rewrite Smolfile to smolvm 0.8.0 schema + drop gvproxy (PRD 0023 chunk 2a)
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 39s

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:
2026-05-27 04:01:07 -04:00
parent b57256789f
commit c73d717f71
8 changed files with 203 additions and 613 deletions
@@ -1,10 +1,10 @@
"""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."""
Chunk 1 + 2a fields: slug, smolfile_path, bundle docker subnet /
gateway / pinned IP. VM lifecycle + provisioning fields (machine
name, `.smolmachine` artifact path, etc.) land in later chunks
as the launch flow grows."""
from __future__ import annotations
@@ -25,14 +25,12 @@ class SmolmachinesBottlePlan(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]
# Per-bottle docker subnet for the sidecar bundle container.
# The bundle runs at `bundle_ip` (always `.2`); the gateway is
# at `.1`. smolvm's TSI allowlist is set to `bundle_ip/32`.
bundle_subnet: str
bundle_gateway: str
bundle_ip: str
def print(self, *, remote_control: bool) -> None:
"""Compact y/N preflight. Same shape as the Docker
@@ -1,101 +0,0 @@
"""gvproxy YAML config renderer (PRD 0023).
The gvproxy config defines:
- The per-bottle subnet + gateway IP — derived from the slug so
parallel bottles don't collide on 192.168.X.0/24.
- A DNS rule that resolves only `proxy.internal` to the gateway.
Every other hostname returns NXDOMAIN. This is the load-bearing
rule for PRD 0022's DNS-exfil attack: the guest can't `dig`
arbitrary names.
- `port_forwards` — one entry per sidecar daemon the bottle uses.
Only what's listed here is reachable from the guest. The
host-side sidecar bundle listens on the resolved `host_port`s;
gvproxy port-forwards `gateway_port` (what the guest dials) →
host `host_port`.
This is the file the PRD 0023 design names as the network
primitive. TSI is explicitly NOT used — see PRD 0023 "Why gvproxy,
not TSI".
The renderer is pure; disk writes happen in prepare.py."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class PortForward:
"""One gvproxy port forward. The guest dials `gateway_port`; the
host receives the connection at `127.0.0.1:host_port`."""
gateway_port: int
host_port: int
def gvproxy_config_build(
*,
subnet: str,
gateway: str,
port_forwards: tuple[PortForward, ...],
) -> dict[str, Any]:
"""Build the gvproxy YAML config dict. The shape matches the
recipe in `agent-vm-isolation.md` § "Full Setup": top-level
`subnet`, `gateway`, `dns:` with the `proxy.internal` carve-out,
and `port_forwards`."""
return {
"subnet": subnet,
"gateway": gateway,
"dns": [
{
"zone": ".",
"records": [
{"name": "proxy.internal", "ip": gateway},
],
},
],
"port_forwards": [
{
"gateway_port": p.gateway_port,
"host": "127.0.0.1",
"host_port": p.host_port,
}
for p in port_forwards
],
}
def gvproxy_config_render(cfg: dict[str, Any]) -> str:
"""Render the gvproxy config as a small YAML subset (the same
shape `pipelock_render_yaml` uses). Stdlib has no YAML writer
and the config shape is narrow — strings + ints + lists of dicts
— so rendering by hand is straightforward and keeps the project
stdlib-only."""
lines: list[str] = []
lines.append(f'subnet: "{cfg["subnet"]}"')
lines.append(f'gateway: "{cfg["gateway"]}"')
lines.append("dns:")
for zone in cfg["dns"]:
lines.append(f' - zone: "{zone["zone"]}"')
lines.append(" records:")
for record in zone["records"]:
lines.append(f' - name: "{record["name"]}"')
lines.append(f' ip: "{record["ip"]}"')
if cfg["port_forwards"]:
lines.append("port_forwards:")
for pf in cfg["port_forwards"]:
lines.append(f' - gateway_port: {pf["gateway_port"]}')
lines.append(f' host: "{pf["host"]}"')
lines.append(f' host_port: {pf["host_port"]}')
else:
lines.append("port_forwards: []")
return "\n".join(lines) + "\n"
def gvproxy_config_write(cfg: dict[str, Any], path: Path) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(gvproxy_config_render(cfg))
path.chmod(0o600)
return path
+37 -112
View File
@@ -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",
]
+37 -67
View File
@@ -1,90 +1,65 @@
"""Smolfile (TOML) renderer for the smolmachines backend (PRD 0023).
The Smolfile pins the per-bottle microVM's command + env + virtio-net
device. Three fields drive what we emit:
The Smolfile shape comes from `smolvm machine create --smolfile`
in smolvm 0.8.0. The renderer emits only the runtime overrides
that vary per bottle:
- `command` — the entrypoint claude-bottle runs inside the guest.
Chunk 1 ships a placeholder (`sleep infinity`); chunk 4 wires
the real `claude` entrypoint once provisioning is in place.
- `env = ["K=V", ...]` — agent's HTTPS_PROXY / NO_PROXY /
NODE_EXTRA_CA_CERTS, all using IP literals pointing at the
per-bottle sidecar bundle's pinned docker IP.
- `env` — the agent's HTTP_PROXY / NO_PROXY / CA paths, pointing
at `proxy.internal:<gateway-port>`. gvproxy resolves
`proxy.internal` to the gateway IP and port-forwards to the
host-side sidecar bundle.
- `[network] allow_cidrs = ["<bundle-ip>/32"]` — TSI's single-IP
allowlist. With this and no other `allow_*` or
`outbound-localhost-only`, the agent can dial exactly one IP:
the bundle. Host loopback, LAN, and the public internet
directly are all refused at the VMM layer.
- `[[net]]` — a virtio-net device backed by gvproxy's unixgram
socket via the VFKT handshake. This is the line that rejects
libkrun's TSI mode: TSI's CIDR allowlist permits the entire
127.0.0.0/8 of host loopback, which exposes every host-side
service; gvproxy's explicit port-forward list is the only thing
the guest can reach.
What the renderer does NOT emit:
The renderer is a pure function. Disk writes happen in
`prepare.py` via `smolfile_write`."""
- `image` / `entrypoint` / `cmd` — those come from the
`.smolmachine` artifact (produced by `smolvm pack create
--image claude-bottle:latest`) and don't vary across bottles
of the same agent image.
- `cpus` / `memory` — left at smolvm defaults until the
operator surfaces a need to override per bottle (the manifest
has no such field today).
Pure function; disk writes happen via `smolfile_write`."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Mapping
# Default port assignments INSIDE the gvproxy network — what the
# guest dials. The agent's HTTPS_PROXY etc. resolve to
# `proxy.internal:<one of these>`. Host-side mapping is dynamic
# (chunk 3 allocates loopback ports per bottle).
GVPROXY_PIPELOCK_GATEWAY_PORT = 8888
GVPROXY_GIT_GATE_GATEWAY_PORT = 8889
GVPROXY_SUPERVISE_GATEWAY_PORT = 8890
def smolfile_build(
*,
slug: str,
gvproxy_socket: Path,
env: Mapping[str, str],
command: tuple[str, ...] = ("sleep", "infinity"),
bundle_ip: str,
) -> dict[str, Any]:
"""Build the Smolfile config dict.
`gvproxy_socket` is the unixgram socket gvproxy listens on; the
guest's virtio-net device handshakes (VFKT magic) with it on
start. `env` is `{NAME: VALUE}` for the guest's process env.
`command` is the entrypoint argv inside the guest (placeholder
until chunk 4 — see module docstring).
Returns a TOML-shaped dict; render with `smolfile_render`."""
"""Build the Smolfile config dict. `env` is `{NAME: VALUE}` for
the guest's process env (IP-literal HTTPS_PROXY etc.).
`bundle_ip` is the pinned docker IP of the per-bottle sidecar
bundle; it lands in `[network] allow_cidrs` as a /32."""
return {
"name": f"claude-bottle-{slug}",
"command": list(command),
"env": [f"{k}={v}" for k, v in sorted(env.items())],
"net": [
{
"type": "virtio-net",
"attachment": "unixgram",
"socket": str(gvproxy_socket),
},
],
"network": {
"allow_cidrs": [f"{bundle_ip}/32"],
},
}
def smolfile_render(cfg: dict[str, Any]) -> str:
"""Render the Smolfile dict as TOML. Stdlib has `tomllib` for
reading TOML but no writer; the smolmachines schema we emit is
narrow enough (string scalars + string lists + one inline table
per net device) to render by hand. Avoids a `tomli_w` runtime
dep and keeps the project stdlib-only."""
"""Render the Smolfile dict as TOML. Schema is narrow (string
list + one table with a string list) so we render by hand and
stay stdlib-only."""
lines: list[str] = []
lines.append(f'name = {_toml_str(cfg["name"])}')
lines.append(f'command = {_toml_array(cfg["command"])}')
lines.append(f'env = {_toml_array(cfg["env"])}')
lines.append("")
for net in cfg.get("net", ()):
lines.append("[[net]]")
for key, value in net.items():
lines.append(f'{key} = {_toml_str(value)}')
lines.append("")
return "\n".join(lines).rstrip("\n") + "\n"
lines.append("[network]")
lines.append(f'allow_cidrs = {_toml_array(cfg["network"]["allow_cidrs"])}')
return "\n".join(lines) + "\n"
def smolfile_write(cfg: dict[str, Any], path: Path) -> Path:
@@ -96,16 +71,11 @@ def smolfile_write(cfg: dict[str, Any], path: Path) -> Path:
def _toml_str(value: Any) -> str:
"""TOML basic string: double-quoted with backslash + double-quote
escapes. The smolmachines fields we emit (slugs, paths, env
pairs) are ASCII-safe; the escape table covers what's reachable."""
escapes."""
s = str(value)
s = s.replace("\\", "\\\\").replace('"', '\\"')
return f'"{s}"'
def _toml_array(values: list[Any]) -> str:
"""TOML inline array. Uses `_toml_str` so quoting is consistent;
the alternative would be `json.dumps(values)` which renders
identical text for ASCII-only lists, but going through the same
quoter is one less surprise on future inputs."""
return "[" + ", ".join(_toml_str(v) for v in values) + "]"
+28 -64
View File
@@ -1,84 +1,48 @@
"""Slug / preflight helpers for the smolmachines backend (PRD 0023).
Backend-specific utilities the prepare/launch flow reaches for.
Kept in its own module so the renderers can be unit-tested without
importing the docker subprocess paths."""
"""Slug / preflight / subnet helpers for the smolmachines backend
(PRD 0023). Kept in its own module so the renderers can be
unit-tested without importing the docker subprocess paths."""
from __future__ import annotations
import hashlib
import os
import shutil
import socket
from contextlib import closing
from ...log import die
def smolmachines_preflight() -> None:
"""Ensure the binaries the smolmachines backend shells out to are
on PATH. Called from the backend's `_resolve_plan` before any
file writes; gives the operator a clear pointer rather than a
cryptic FileNotFoundError later.
`smolvm` drives the libkrun microVM lifecycle. `gvproxy`
(gvisor-tap-vsock) is the userspace TCP/IP stack the guest's
virtio-net device hooks into."""
missing: list[tuple[str, str]] = []
if shutil.which("smolvm") is None:
missing.append((
"smolvm",
"brew install smolmachines-dev/smolmachines/smolmachines "
"# or build from https://smolmachines.com/",
))
if shutil.which("gvproxy") is None:
missing.append((
"gvproxy",
"go install "
"github.com/containers/gvisor-tap-vsock/cmd/gvproxy@latest "
"# requires Go + ensure $GOPATH/bin on PATH",
))
if not missing:
"""Ensure `smolvm` is on PATH before the launch flow runs.
Called from `_resolve_plan`; gives the operator a clear
install pointer rather than a cryptic FileNotFoundError
later. `gvproxy` is no longer required — see the PRD's design
pivot section."""
if shutil.which("smolvm") is not None:
return
lines = [
f"CLAUDE_BOTTLE_BACKEND=smolmachines requires the following "
f"binar{'y' if len(missing) == 1 else 'ies'} on PATH:",
]
for name, install in missing:
lines.append(f" - {name} install: {install}")
die("\n".join(lines))
die(
"CLAUDE_BOTTLE_BACKEND=smolmachines requires `smolvm` on "
"PATH. Install with: "
"curl -sSL https://smolmachines.com/install.sh | sh"
)
def smolmachines_gvproxy_subnet(slug: str) -> tuple[str, str]:
"""Derive a per-bottle subnet + gateway IP from the slug.
def smolmachines_bundle_subnet(slug: str) -> tuple[str, str, str]:
"""Derive a per-bottle docker subnet + gateway IP + bundle IP
from the slug.
Returns `(subnet_cidr, gateway_ip)`. The third octet comes from
SHA-256 of the slug mod 254 (1..254, skipping 0 and 255 + the
127.x.x.x range) — collision-free for any reasonable concurrent-
bottle count and stable across `start` / `resume` of the same
bottle. Bottles that DO collide would fail at gvproxy bringup;
`launch.py` (chunk 2) detects the conflict and surfaces a clear
error rather than silently reusing another bottle's gateway."""
Returns `(subnet_cidr, gateway_ip, bundle_ip)`. The third
octet comes from SHA-256 of the slug mod 254 (skipping 17 to
avoid the docker-default bridge), so parallel bottles get
distinct /24s and `resume` reuses the same /24. The bundle
container always lands at `.2`; gateway is `.1`; the smolvm
Smolfile's `allow_cidrs` is `<bundle_ip>/32`."""
digest = hashlib.sha256(slug.encode("utf-8")).digest()
octet = (digest[0] % 254) + 1
# Skip docker-default 17 to dodge the usual second-bridge
# collision. Operators with docker's default bridge at
# 172.17.x.x — common Linux setup — would otherwise see a
# collision the moment they run a parallel docker bottle.
# Skip the docker-default bridge to dodge the most common
# collision (operators with `docker0` at 172.17.x.x or a
# 192.168.17.x VPN client).
if octet == 17:
octet = 18
subnet = f"192.168.{octet}.0/24"
gateway = f"192.168.{octet}.1"
return subnet, gateway
def allocate_loopback_port() -> int:
"""Bind / release dance to grab a free TCP port on 127.0.0.1.
Race window: a parallel allocator could grab the same port
between our close and the bundle's bind. The window is small
enough that chunk 1 doesn't address it; chunk 3 will add a
retry loop if it shows up in practice."""
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(("127.0.0.1", 0))
return int(s.getsockname()[1])
bundle_ip = f"192.168.{octet}.2"
return subnet, gateway, bundle_ip