refactor: hoist plan fields and print to BottlePlan base class (PRD 0044)
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.
This commit is contained in:
@@ -32,15 +32,21 @@ manifest does not carry a backend field; the host picks.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from contextlib import AbstractContextManager
|
from contextlib import AbstractContextManager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Generic, Sequence, TypeVar
|
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 ..manifest import GitEntry, Manifest
|
||||||
|
from ..supervise import SupervisePlan
|
||||||
from ..util import expand_tilde
|
from ..util import expand_tilde
|
||||||
|
from .print_util import print_multi, visible_agent_env_names
|
||||||
from .util import host_skill_dir
|
from .util import host_skill_dir
|
||||||
|
|
||||||
|
|
||||||
@@ -65,15 +71,56 @@ class BottleSpec:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BottlePlan(ABC):
|
class BottlePlan(ABC):
|
||||||
"""Base output of a backend's prepare step. Concrete subclasses
|
"""Base output of a backend's prepare step. Concrete subclasses
|
||||||
(e.g. DockerBottlePlan) add backend-specific resolved fields and
|
(e.g. DockerBottlePlan) add backend-specific resolved fields."""
|
||||||
implement `print`."""
|
|
||||||
|
|
||||||
spec: BottleSpec
|
spec: BottleSpec
|
||||||
stage_dir: Path
|
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:
|
def print(self, *, remote_control: bool) -> None:
|
||||||
"""Render the y/N preflight summary to stderr."""
|
"""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)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -2,30 +2,25 @@
|
|||||||
|
|
||||||
Carries the Docker-specific resolved fields produced by
|
Carries the Docker-specific resolved fields produced by
|
||||||
DockerBottleBackend.prepare. The launch step consumes it without
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ...agent_provider import AgentProvisionPlan, PromptMode
|
from ...agent_provider import PromptMode
|
||||||
from ...egress import EgressPlan
|
|
||||||
from ...git_gate import GitGatePlan
|
|
||||||
from ...log import info
|
|
||||||
from ...pipelock import PipelockProxyPlan
|
from ...pipelock import PipelockProxyPlan
|
||||||
from ...supervise import SupervisePlan
|
|
||||||
from .. import BottlePlan
|
from .. import BottlePlan
|
||||||
from ..print_util import print_multi, visible_agent_env_names
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DockerBottlePlan(BottlePlan):
|
class DockerBottlePlan(BottlePlan):
|
||||||
"""Docker-specific resolved fields produced by
|
"""Docker-specific resolved fields produced by
|
||||||
DockerBottleBackend.prepare. Inherits `spec` and `stage_dir` from
|
DockerBottleBackend.prepare. Inherits `spec`, `stage_dir`,
|
||||||
BottlePlan."""
|
`git_gate_plan`, `egress_plan`, `supervise_plan`, and
|
||||||
|
`agent_provision` from BottlePlan."""
|
||||||
|
|
||||||
slug: str
|
slug: str
|
||||||
container_name: str
|
container_name: str
|
||||||
@@ -46,13 +41,7 @@ class DockerBottlePlan(BottlePlan):
|
|||||||
forwarded_env: dict[str, str] = field(repr=False)
|
forwarded_env: dict[str, str] = field(repr=False)
|
||||||
prompt_file: Path
|
prompt_file: Path
|
||||||
proxy_plan: PipelockProxyPlan
|
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
|
use_runsc: bool
|
||||||
agent_provision: AgentProvisionPlan
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agent_command(self) -> str:
|
def agent_command(self) -> str:
|
||||||
@@ -65,55 +54,3 @@ class DockerBottlePlan(BottlePlan):
|
|||||||
@property
|
@property
|
||||||
def agent_provider_template(self) -> str:
|
def agent_provider_template(self) -> str:
|
||||||
return self.agent_provision.template
|
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)
|
|
||||||
|
|||||||
@@ -8,25 +8,20 @@ in chunk 4."""
|
|||||||
|
|
||||||
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 ...agent_provider import AgentProvisionPlan, PromptMode
|
from ...agent_provider import PromptMode
|
||||||
from ...egress import EgressPlan
|
|
||||||
from ...git_gate import GitGatePlan
|
|
||||||
from ...log import info
|
|
||||||
from ...pipelock import PipelockProxyPlan
|
from ...pipelock import PipelockProxyPlan
|
||||||
from ...supervise import SupervisePlan
|
|
||||||
from .. import BottlePlan
|
from .. import BottlePlan
|
||||||
from ..print_util import print_multi, visible_agent_env_names
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SmolmachinesBottlePlan(BottlePlan):
|
class SmolmachinesBottlePlan(BottlePlan):
|
||||||
"""Resolved fields the launch step needs to bring up the bottle.
|
"""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
|
slug: str
|
||||||
# Per-bottle docker subnet for the sidecar bundle container.
|
# 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
|
# per-bottle bridge with a pinned IP. The unused fields stay
|
||||||
# at their dataclass defaults.
|
# at their dataclass defaults.
|
||||||
proxy_plan: PipelockProxyPlan
|
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
|
# Agent-side endpoints. On Docker Desktop the docker bridge
|
||||||
# IPs aren't reachable from the smolvm guest (TSI uses macOS
|
# IPs aren't reachable from the smolvm guest (TSI uses macOS
|
||||||
# networking; docker container IPs live in the daemon's VM),
|
# networking; docker container IPs live in the daemon's VM),
|
||||||
@@ -110,42 +99,3 @@ class SmolmachinesBottlePlan(BottlePlan):
|
|||||||
@property
|
@property
|
||||||
def agent_dockerfile_path(self) -> str:
|
def agent_dockerfile_path(self) -> str:
|
||||||
return self.agent_provision.dockerfile
|
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user