refactor(backend): extract shared print_multi for plan preflights
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 42s

Addresses PR #62 review comments on
claude_bottle/backend/smolmachines/bottle_plan.py:

- Lift the multi-value label printer (was a nested helper inside
  DockerBottlePlan.print) into a new module
  claude_bottle/backend/print_util.py:print_multi. Both backends
  use it for env / skills / git / egress lines.

- Strip the three smolmachines-preflight lines the review flagged:
  the gvproxy subnet line, the smolfile path line, and the
  gvproxy-config path line. Internal detail — operators see the
  agent / env / skills / bottle / git / egress that already
  matter on the docker side, and nothing else.

- Add `git → upstream` to the smolmachines git output to match
  what's useful at preflight time (the docker version shows
  upstream_host:port; this is similar shape).

Leaves the slug=spec.identity-or-mint pattern alone pending a
reply on PR comment #432 — the docker backend uses the same
pattern to preserve identity across `resume`, so dropping it
would silently break the resume path once smolmachines launch
lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 02:36:03 -04:00
parent 20f411b22e
commit 2aca9e609a
3 changed files with 51 additions and 37 deletions
+5 -16
View File
@@ -17,6 +17,7 @@ from ...log import info
from ...pipelock import PipelockProxyPlan from ...pipelock import PipelockProxyPlan
from ...supervise import SupervisePlan from ...supervise import SupervisePlan
from .. import BottlePlan from .. import BottlePlan
from ..print_util import print_multi
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -70,22 +71,10 @@ class DockerBottlePlan(BottlePlan):
# from the agent to the proxy is needed. # from the agent to the proxy is needed.
env_names = sorted(set(bottle.env.keys()) | set(self.forwarded_env.keys())) env_names = sorted(set(bottle.env.keys()) | set(self.forwarded_env.keys()))
def _multi(label: str, values: list[str]) -> None:
"""Print a label with N continuation-indented values. Used
for env / skills / git-gate / egress where one item
per line keeps the summary scannable."""
if not values:
info(f"{label}: (none)")
return
info(f"{label}: {values[0]}")
indent = " " * (len(label) + 2)
for v_ in values[1:]:
info(f"{indent}{v_}")
print(file=sys.stderr) print(file=sys.stderr)
info(f"agent : {spec.agent_name}") info(f"agent : {spec.agent_name}")
_multi("env ", env_names) print_multi("env ", env_names)
_multi("skills ", list(agent.skills)) print_multi("skills ", list(agent.skills))
info(f"bottle : {agent.bottle}") info(f"bottle : {agent.bottle}")
git_lines = [ git_lines = [
@@ -93,13 +82,13 @@ class DockerBottlePlan(BottlePlan):
for u in self.git_gate_plan.upstreams for u in self.git_gate_plan.upstreams
] ]
if git_lines: if git_lines:
_multi(" git gate ", git_lines) print_multi(" git gate ", git_lines)
if self.egress_plan.routes: if self.egress_plan.routes:
egress_lines = [] egress_lines = []
for r in self.egress_plan.routes: for r in self.egress_plan.routes:
auth = f" [auth:{r.auth_scheme}]" if r.auth_scheme else "" auth = f" [auth:{r.auth_scheme}]" if r.auth_scheme else ""
egress_lines.append(f"{r.host}{auth}") egress_lines.append(f"{r.host}{auth}")
_multi(" egress ", egress_lines) print_multi(" egress ", egress_lines)
print(file=sys.stderr) print(file=sys.stderr)
+28
View File
@@ -0,0 +1,28 @@
"""Shared print helpers for BottlePlan.print implementations.
Lifts the multi-value label printer out of DockerBottlePlan so the
smolmachines backend (and any future backend) renders the same
two-column scannable preflight without duplicating the indent
math."""
from __future__ import annotations
from typing import Sequence
from ..log import info
def print_multi(label: str, values: Sequence[str]) -> None:
"""Print `label: <value>` with continuation lines indented to
align under the first value. Empty `values` renders `(none)`.
Used by every backend's `BottlePlan.print` for env / skills /
git / egress — one item per line keeps the preflight summary
scannable when an agent has many of any of these."""
if not values:
info(f"{label}: (none)")
return
info(f"{label}: {values[0]}")
indent = " " * (len(label) + 2)
for v in values[1:]:
info(f"{indent}{v}")
@@ -8,11 +8,13 @@ launch flow grows."""
from __future__ import annotations from __future__ import annotations
import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from ...log import info from ...log import info
from .. import BottlePlan from .. import BottlePlan
from ..print_util import print_multi
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -33,32 +35,27 @@ class SmolmachinesBottlePlan(BottlePlan):
host_port_map: dict[str, int] host_port_map: dict[str, int]
def print(self, *, remote_control: bool) -> None: def print(self, *, remote_control: bool) -> None:
"""Compact y/N preflight for the smolmachines path. Mirrors """Compact y/N preflight. Same shape as the Docker
the docker preflight's layout so operators don't have to backend's so operators see one format across backends."""
learn two formats."""
del remote_control # not surfaced in the compact summary del remote_control # not surfaced in the compact summary
spec = self.spec spec = self.spec
manifest = spec.manifest manifest = spec.manifest
agent = manifest.agents[spec.agent_name] agent = manifest.agents[spec.agent_name]
bottle = manifest.bottle_for(spec.agent_name) bottle = manifest.bottle_for(spec.agent_name)
info(f"backend: smolmachines")
info(f"agent: {spec.agent_name}")
info(f"bottle: {agent.bottle}")
info(f"slug: {self.slug}")
info(f"gvproxy: {self.gvproxy_gateway} on {self.gvproxy_subnet}")
env_names = sorted(bottle.env.keys()) env_names = sorted(bottle.env.keys())
skills = list(agent.skills) upstreams = [
upstreams = [g.Name for g in bottle.git] f"{g.Name}{g.Upstream}" for g in bottle.git
]
routes = [r.host for r in bottle.egress.routes] routes = [r.host for r in bottle.egress.routes]
info(f"env: {', '.join(env_names) if env_names else '(none)'}")
info(f"skills: {', '.join(skills) if skills else '(none)'}") print(file=sys.stderr)
info(f"git: {', '.join(upstreams) if upstreams else '(none)'}") info(f"agent : {spec.agent_name}")
info(f"routes: {', '.join(routes) if routes else '(none)'}") print_multi("env ", env_names)
info(f"smolfile: {self.smolfile_path}") print_multi("skills ", list(agent.skills))
info(f"gvproxy config: {self.gvproxy_config_path}") info(f"bottle : {agent.bottle}")
info( if upstreams:
"(chunk 1 of PRD 0023: prepare-only — launch is " print_multi(" git gate ", upstreams)
"not yet implemented)" if routes:
) print_multi(" egress ", routes)
print(file=sys.stderr)