From 53219a55e16923c7d00951c09ab1a77de5a2a761 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 16:08:20 +0000 Subject: [PATCH] refactor: hoist plan fields and print to BottlePlan base class (PRD 0044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move git_gate_plan, egress_plan, supervise_plan, and agent_provision from DockerBottlePlan and SmolmachinesBottlePlan into BottlePlan. Replace the abstract print method with a single concrete implementation that renders git gate entries as "name → upstream_host:upstream_port" and egress routes with conditional "[auth:scheme]" annotations. --- bot_bottle/backend/__init__.py | 55 +++++++++++++- bot_bottle/backend/docker/bottle_plan.py | 73 ++----------------- .../backend/smolmachines/bottle_plan.py | 56 +------------- 3 files changed, 59 insertions(+), 125 deletions(-) diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index c55faac..6fc9aa9 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -32,15 +32,21 @@ manifest does not carry a backend field; the host picks. from __future__ import annotations import os +import sys from abc import ABC, abstractmethod from contextlib import AbstractContextManager from dataclasses import dataclass from pathlib import Path from typing import Any, Generic, Sequence, TypeVar -from ..log import die +from ..agent_provider import AgentProvisionPlan +from ..egress import EgressPlan +from ..git_gate import GitGatePlan +from ..log import die, info from ..manifest import GitEntry, Manifest +from ..supervise import SupervisePlan from ..util import expand_tilde +from .print_util import print_multi, visible_agent_env_names from .util import host_skill_dir @@ -65,15 +71,56 @@ class BottleSpec: @dataclass(frozen=True) class BottlePlan(ABC): """Base output of a backend's prepare step. Concrete subclasses - (e.g. DockerBottlePlan) add backend-specific resolved fields and - implement `print`.""" + (e.g. DockerBottlePlan) add backend-specific resolved fields.""" spec: BottleSpec stage_dir: Path + git_gate_plan: GitGatePlan + egress_plan: EgressPlan + supervise_plan: SupervisePlan | None + agent_provision: AgentProvisionPlan - @abstractmethod def print(self, *, remote_control: bool) -> None: """Render the y/N preflight summary to stderr.""" + del remote_control + spec = self.spec + manifest = spec.manifest + agent = manifest.agents[spec.agent_name] + bottle = manifest.bottle_for(spec.agent_name) + + env_names = visible_agent_env_names( + sorted( + set(bottle.env.keys()) + | set(self.agent_provision.guest_env.keys()) + ), + hidden_env_names=self.agent_provision.hidden_env_names, + ) + + print(file=sys.stderr) + info(f"agent : {spec.agent_name}") + info(f"provider : {self.agent_provision.template}") + print_multi("env ", env_names) + print_multi("skills ", list(agent.skills)) + info(f"bottle : {agent.bottle}") + + identity = manifest.git_identity_summary(spec.agent_name) + if identity: + info(f" git identity : {identity}") + + git_lines = [ + f"{u.name} → {u.upstream_host}:{u.upstream_port}" + for u in self.git_gate_plan.upstreams + ] + if 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}") + print_multi(" egress ", egress_lines) + print(file=sys.stderr) @dataclass(frozen=True) diff --git a/bot_bottle/backend/docker/bottle_plan.py b/bot_bottle/backend/docker/bottle_plan.py index e6240f1..6fd4a56 100644 --- a/bot_bottle/backend/docker/bottle_plan.py +++ b/bot_bottle/backend/docker/bottle_plan.py @@ -2,30 +2,25 @@ Carries the Docker-specific resolved fields produced by DockerBottleBackend.prepare. The launch step consumes it without -further resolution; show_plan-style rendering is the `print` method. +further resolution; preflight rendering is inherited from BottlePlan. """ from __future__ import annotations -import sys from dataclasses import dataclass, field from pathlib import Path -from ...agent_provider import AgentProvisionPlan, PromptMode -from ...egress import EgressPlan -from ...git_gate import GitGatePlan -from ...log import info +from ...agent_provider import PromptMode from ...pipelock import PipelockProxyPlan -from ...supervise import SupervisePlan from .. import BottlePlan -from ..print_util import print_multi, visible_agent_env_names @dataclass(frozen=True) class DockerBottlePlan(BottlePlan): """Docker-specific resolved fields produced by - DockerBottleBackend.prepare. Inherits `spec` and `stage_dir` from - BottlePlan.""" + DockerBottleBackend.prepare. Inherits `spec`, `stage_dir`, + `git_gate_plan`, `egress_plan`, `supervise_plan`, and + `agent_provision` from BottlePlan.""" slug: str container_name: str @@ -46,13 +41,7 @@ class DockerBottlePlan(BottlePlan): forwarded_env: dict[str, str] = field(repr=False) prompt_file: Path proxy_plan: PipelockProxyPlan - git_gate_plan: GitGatePlan - egress_plan: EgressPlan - # None when bottle.supervise is False. PRD 0013 supervise sidecar - # is opt-in via the manifest's bottle.supervise field. - supervise_plan: SupervisePlan | None use_runsc: bool - agent_provision: AgentProvisionPlan @property def agent_command(self) -> str: @@ -65,55 +54,3 @@ class DockerBottlePlan(BottlePlan): @property def agent_provider_template(self) -> str: return self.agent_provision.template - - def print(self, *, remote_control: bool) -> None: - """Render the y/N preflight summary to stderr — compact form - intended to fit on screen without scrolling. The full - structured shape (image, container, runtime, etc.) lives on - this dataclass for tooling that wants to introspect it.""" - 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) - # The agent sees the union of literal env names (rendered into - # --env-file) and forwarded env names (`-e NAME` with the - # value arriving via subprocess env). The forwarded set holds - # the OAuth token (CLAUDE_CODE_OAUTH_TOKEN) and any host-env - # interpolations from the manifest; egress holds - # upstream tokens in its own environ, so no token forwarding - # from the agent to the proxy is needed. - env_names = visible_agent_env_names( - sorted( - set(bottle.env.keys()) - | set(self.forwarded_env.keys()) - | set(self.agent_provision.guest_env.keys()) - ), - hidden_env_names=self.agent_provision.hidden_env_names, - ) - - print(file=sys.stderr) - info(f"agent : {spec.agent_name}") - info(f"provider : {self.agent_provider_template}") - print_multi("env ", env_names) - print_multi("skills ", list(agent.skills)) - info(f"bottle : {agent.bottle}") - - identity = manifest.git_identity_summary(spec.agent_name) - if identity: - info(f" git identity : {identity}") - - git_lines = [ - f"{u.upstream_host}:{u.upstream_port}" - for u in self.git_gate_plan.upstreams - ] - if 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}") - print_multi(" egress ", egress_lines) - print(file=sys.stderr) diff --git a/bot_bottle/backend/smolmachines/bottle_plan.py b/bot_bottle/backend/smolmachines/bottle_plan.py index 84ef5d4..e3d3cb2 100644 --- a/bot_bottle/backend/smolmachines/bottle_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_plan.py @@ -8,25 +8,20 @@ in chunk 4.""" from __future__ import annotations -import sys from dataclasses import dataclass from pathlib import Path -from ...agent_provider import AgentProvisionPlan, PromptMode -from ...egress import EgressPlan -from ...git_gate import GitGatePlan -from ...log import info +from ...agent_provider import PromptMode from ...pipelock import PipelockProxyPlan -from ...supervise import SupervisePlan from .. import BottlePlan -from ..print_util import print_multi, visible_agent_env_names @dataclass(frozen=True) class SmolmachinesBottlePlan(BottlePlan): """Resolved fields the launch step needs to bring up the bottle. - Inherits `spec` and `stage_dir` from BottlePlan.""" + Inherits `spec`, `stage_dir`, `git_gate_plan`, `egress_plan`, + `supervise_plan`, and `agent_provision` from BottlePlan.""" slug: str # Per-bottle docker subnet for the sidecar bundle container. @@ -77,12 +72,6 @@ class SmolmachinesBottlePlan(BottlePlan): # per-bottle bridge with a pinned IP. The unused fields stay # at their dataclass defaults. proxy_plan: PipelockProxyPlan - git_gate_plan: GitGatePlan - egress_plan: EgressPlan - # None when bottle.supervise is False, matching the docker - # backend's convention. - supervise_plan: SupervisePlan | None - agent_provision: AgentProvisionPlan # Agent-side endpoints. On Docker Desktop the docker bridge # IPs aren't reachable from the smolvm guest (TSI uses macOS # networking; docker container IPs live in the daemon's VM), @@ -110,42 +99,3 @@ class SmolmachinesBottlePlan(BottlePlan): @property def agent_dockerfile_path(self) -> str: return self.agent_provision.dockerfile - - def print(self, *, remote_control: bool) -> None: - """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) - - env_names = visible_agent_env_names( - sorted( - set(bottle.env.keys()) - | set(self.agent_provision.guest_env.keys()) - ), - hidden_env_names=self.agent_provision.hidden_env_names, - ) - upstreams = [ - f"{g.Name} → {g.Upstream}" for g in bottle.git - ] - # Use the resolved egress_plan (lowercase `host` on the - # plan-level EgressRoute) rather than `bottle.egress.routes`, - # which is the manifest's capitalized-attr form. - routes = [r.host for r in self.egress_plan.routes] - - print(file=sys.stderr) - info(f"agent : {spec.agent_name}") - info(f"provider : {self.agent_provider_template}") - print_multi("env ", env_names) - print_multi("skills ", list(agent.skills)) - info(f"bottle : {agent.bottle}") - identity = manifest.git_identity_summary(spec.agent_name) - if identity: - info(f" git identity : {identity}") - if upstreams: - print_multi(" git gate ", upstreams) - if routes: - print_multi(" egress ", routes) - print(file=sys.stderr)