From 2aca9e609a5e8cc19bafa38749fc4c24fbaff374 Mon Sep 17 00:00:00 2001 From: didericis Date: Wed, 27 May 2026 02:36:03 -0400 Subject: [PATCH] refactor(backend): extract shared `print_multi` for plan preflights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- claude_bottle/backend/docker/bottle_plan.py | 21 +++------- claude_bottle/backend/print_util.py | 28 +++++++++++++ .../backend/smolmachines/bottle_plan.py | 39 +++++++++---------- 3 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 claude_bottle/backend/print_util.py diff --git a/claude_bottle/backend/docker/bottle_plan.py b/claude_bottle/backend/docker/bottle_plan.py index 1805dd6..1efb14e 100644 --- a/claude_bottle/backend/docker/bottle_plan.py +++ b/claude_bottle/backend/docker/bottle_plan.py @@ -17,6 +17,7 @@ from ...log import info from ...pipelock import PipelockProxyPlan from ...supervise import SupervisePlan from .. import BottlePlan +from ..print_util import print_multi @dataclass(frozen=True) @@ -70,22 +71,10 @@ class DockerBottlePlan(BottlePlan): # from the agent to the proxy is needed. 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) info(f"agent : {spec.agent_name}") - _multi("env ", env_names) - _multi("skills ", list(agent.skills)) + print_multi("env ", env_names) + print_multi("skills ", list(agent.skills)) info(f"bottle : {agent.bottle}") git_lines = [ @@ -93,13 +82,13 @@ class DockerBottlePlan(BottlePlan): for u in self.git_gate_plan.upstreams ] if git_lines: - _multi(" git gate ", git_lines) + print_multi(" git gate ", git_lines) if self.egress_plan.routes: egress_lines = [] for r in self.egress_plan.routes: auth = f" [auth:{r.auth_scheme}]" if r.auth_scheme else "" egress_lines.append(f"{r.host}{auth}") - _multi(" egress ", egress_lines) + print_multi(" egress ", egress_lines) print(file=sys.stderr) diff --git a/claude_bottle/backend/print_util.py b/claude_bottle/backend/print_util.py new file mode 100644 index 0000000..f37e22c --- /dev/null +++ b/claude_bottle/backend/print_util.py @@ -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: ` 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}") diff --git a/claude_bottle/backend/smolmachines/bottle_plan.py b/claude_bottle/backend/smolmachines/bottle_plan.py index 81c728d..166da28 100644 --- a/claude_bottle/backend/smolmachines/bottle_plan.py +++ b/claude_bottle/backend/smolmachines/bottle_plan.py @@ -8,11 +8,13 @@ launch flow grows.""" from __future__ import annotations +import sys from dataclasses import dataclass from pathlib import Path from ...log import info from .. import BottlePlan +from ..print_util import print_multi @dataclass(frozen=True) @@ -33,32 +35,27 @@ class SmolmachinesBottlePlan(BottlePlan): host_port_map: dict[str, int] def print(self, *, remote_control: bool) -> None: - """Compact y/N preflight for the smolmachines path. Mirrors - the docker preflight's layout so operators don't have to - learn two formats.""" + """Compact y/N preflight. Same shape as the Docker + backend's so operators see one format across backends.""" del remote_control # not surfaced in the compact summary spec = self.spec manifest = spec.manifest agent = manifest.agents[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()) - skills = list(agent.skills) - upstreams = [g.Name for g in bottle.git] + upstreams = [ + f"{g.Name} → {g.Upstream}" for g in bottle.git + ] 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)'}") - info(f"git: {', '.join(upstreams) if upstreams else '(none)'}") - info(f"routes: {', '.join(routes) if routes else '(none)'}") - info(f"smolfile: {self.smolfile_path}") - info(f"gvproxy config: {self.gvproxy_config_path}") - info( - "(chunk 1 of PRD 0023: prepare-only — launch is " - "not yet implemented)" - ) + + print(file=sys.stderr) + info(f"agent : {spec.agent_name}") + print_multi("env ", env_names) + print_multi("skills ", list(agent.skills)) + info(f"bottle : {agent.bottle}") + if upstreams: + print_multi(" git gate ", upstreams) + if routes: + print_multi(" egress ", routes) + print(file=sys.stderr)