refactor: make AgentProvisionPlan the source of truth for instance_name, prompt_file, image, dockerfile, guest_home
Drop the parallel fields passed through prepare() → _resolve_plan and read everything from agent_provision instead. The provider plugin now declares its own guest_home (so the backend stops hardcoding "/home/node") and the wrapper that builds the provision plan accepts instance_name and prompt_file, which providers store on the plan. DockerBottlePlan and SmolmachinesBottlePlan expose container_name / machine_name, image / agent_image, dockerfile_path / agent_dockerfile_path, and prompt_file as properties that delegate to agent_provision so existing call sites keep working unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,8 @@ class AgentProvisionPlan:
|
||||
image: str
|
||||
dockerfile: str
|
||||
guest_home: str
|
||||
instance_name: str
|
||||
prompt_file: Path
|
||||
guest_env: dict[str, str]
|
||||
env_vars: dict[str, str] = field(default_factory=dict)
|
||||
dirs: tuple[AgentProvisionDir, ...] = ()
|
||||
@@ -128,6 +130,14 @@ class AgentProvider(ABC):
|
||||
"""The static command / image / prompt-mode table for this
|
||||
template."""
|
||||
|
||||
@property
|
||||
def guest_home(self) -> str:
|
||||
"""In-guest home directory for the agent user. Defaults to
|
||||
`/home/node` to match the Debian-based bot-bottle-* images
|
||||
(USER node). Override for plugins whose image runs as a
|
||||
different user."""
|
||||
return "/home/node"
|
||||
|
||||
@property
|
||||
def dockerfile(self) -> Path:
|
||||
"""Path to the provider's Dockerfile.
|
||||
@@ -143,7 +153,8 @@ class AgentProvider(ABC):
|
||||
*,
|
||||
dockerfile: str,
|
||||
state_dir: Path,
|
||||
guest_home: str,
|
||||
instance_name: str,
|
||||
prompt_file: Path,
|
||||
guest_env: dict[str, str] | None = None,
|
||||
auth_token: str = "",
|
||||
forward_host_credentials: bool = False,
|
||||
@@ -333,7 +344,8 @@ def build_agent_provision_plan(
|
||||
template: str,
|
||||
dockerfile: str,
|
||||
state_dir: Path,
|
||||
guest_home: str,
|
||||
instance_name: str,
|
||||
prompt_file: Path,
|
||||
guest_env: dict[str, str] | None = None,
|
||||
auth_token: str = "",
|
||||
forward_host_credentials: bool = False,
|
||||
@@ -347,7 +359,8 @@ def build_agent_provision_plan(
|
||||
return get_provider(template).provision_plan(
|
||||
dockerfile=dockerfile,
|
||||
state_dir=state_dir,
|
||||
guest_home=guest_home,
|
||||
instance_name=instance_name,
|
||||
prompt_file=prompt_file,
|
||||
guest_env=guest_env,
|
||||
auth_token=auth_token,
|
||||
forward_host_credentials=forward_host_credentials,
|
||||
|
||||
@@ -292,7 +292,6 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
manifest_bottle = manifest.bottle_for(spec.agent_name)
|
||||
manfiest_agent_provider = manifest_bottle.agent_provider
|
||||
agent_provider = get_provider(manfiest_agent_provider.template)
|
||||
agent_image = agent_provider.runtime.image
|
||||
resolved_env = resolve_env(manifest, spec.agent_name)
|
||||
|
||||
slug = mint_slug(spec)
|
||||
@@ -307,7 +306,6 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
)
|
||||
else:
|
||||
agent_dockerfile_path = str(agent_provider.dockerfile)
|
||||
instance_name = f"bot-bottle-{slug}"
|
||||
|
||||
agent_dir, prompt_file = prepare_agent_state_dir(slug, spec)
|
||||
|
||||
@@ -315,7 +313,8 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
template=manfiest_agent_provider.template,
|
||||
dockerfile=agent_dockerfile_path,
|
||||
state_dir=agent_dir,
|
||||
guest_home="/home/node", # FIXME: should be coming from the agent plan
|
||||
instance_name=f"bot-bottle-{slug}",
|
||||
prompt_file=prompt_file,
|
||||
guest_env=self._build_guest_env(resolved_env),
|
||||
forward_host_credentials=manfiest_agent_provider.forward_host_credentials,
|
||||
auth_token=manfiest_agent_provider.auth_token,
|
||||
@@ -333,10 +332,6 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
spec,
|
||||
slug=slug,
|
||||
resolved_env=resolved_env,
|
||||
instance_name=instance_name, # FIXME: move to agent provision plan
|
||||
agent_image=agent_image, # FIXME: move to agent provision plan
|
||||
prompt_file=prompt_file, # FIXME: move to agent provision plan
|
||||
agent_dockerfile_path=agent_dockerfile_path, # FIXME: move to agent provision plan
|
||||
agent_provision_plan=agent_provision_plan,
|
||||
egress_plan=egress_plan,
|
||||
supervise_plan=supervise_plan,
|
||||
@@ -408,18 +403,16 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
*,
|
||||
slug: str,
|
||||
resolved_env: ResolvedEnv,
|
||||
instance_name: str,
|
||||
agent_image: str,
|
||||
prompt_file: Path,
|
||||
agent_provision_plan: AgentProvisionPlan,
|
||||
agent_dockerfile_path: str,
|
||||
egress_plan: EgressPlan,
|
||||
git_gate_plan: GitGatePlan,
|
||||
supervise_plan: SupervisePlan | None,
|
||||
stage_dir: Path) -> PlanT:
|
||||
"""Backend-specific plan resolution: image/container names,
|
||||
env-file, prompt-file, proxy plan, runtime detection. Called by
|
||||
`prepare` after `_validate` succeeds."""
|
||||
`prepare` after `_validate` succeeds. Instance name, image,
|
||||
prompt file, Dockerfile path, and guest home all live on
|
||||
`agent_provision_plan` — the source of truth."""
|
||||
|
||||
@abstractmethod
|
||||
def launch(self, plan: PlanT) -> AbstractContextManager[Bottle]:
|
||||
|
||||
@@ -54,11 +54,7 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
|
||||
*,
|
||||
slug: str,
|
||||
resolved_env,
|
||||
instance_name: str,
|
||||
agent_image: str,
|
||||
prompt_file: Path,
|
||||
agent_provision_plan,
|
||||
agent_dockerfile_path: str,
|
||||
egress_plan,
|
||||
git_gate_plan,
|
||||
supervise_plan,
|
||||
@@ -68,10 +64,6 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
|
||||
spec,
|
||||
slug=slug,
|
||||
resolved_env=resolved_env,
|
||||
instance_name=instance_name,
|
||||
agent_image=agent_image,
|
||||
agent_dockerfile_path=agent_dockerfile_path,
|
||||
prompt_file=prompt_file,
|
||||
agent_provision_plan=agent_provision_plan,
|
||||
egress_plan=egress_plan,
|
||||
supervise_plan=supervise_plan,
|
||||
|
||||
@@ -22,21 +22,32 @@ class DockerBottlePlan(BottlePlan):
|
||||
`agent_provision` from BottlePlan."""
|
||||
|
||||
slug: str
|
||||
container_name: str
|
||||
image: str
|
||||
# Absolute path to the Dockerfile that builds `image`. Empty means
|
||||
# use the repo's default Dockerfile. Populated to a per-bottle
|
||||
# state file (~/.bot-bottle/state/<slug>/Dockerfile) after a
|
||||
# capability-block remediation (PRD 0016).
|
||||
dockerfile_path: str
|
||||
# name -> value for vars forwarded into the docker-run child process
|
||||
# via subprocess env (so values never land on argv or in a file).
|
||||
# repr=False keeps secret/interpolated/OAuth values out of any
|
||||
# accidental log of the plan dataclass.
|
||||
forwarded_env: dict[str, str] = field(repr=False)
|
||||
prompt_file: Path
|
||||
use_runsc: bool
|
||||
|
||||
@property
|
||||
def container_name(self) -> str:
|
||||
return self.agent_provision.instance_name
|
||||
|
||||
@property
|
||||
def image(self) -> str:
|
||||
return self.agent_provision.image
|
||||
|
||||
@property
|
||||
def dockerfile_path(self) -> str:
|
||||
"""Absolute path to the Dockerfile that builds `image`. Sourced
|
||||
from the agent provision plan — the manifest may override per
|
||||
bottle; otherwise the provider plugin's bundled Dockerfile."""
|
||||
return self.agent_provision.dockerfile
|
||||
|
||||
@property
|
||||
def prompt_file(self) -> Path:
|
||||
return self.agent_provision.prompt_file
|
||||
|
||||
@property
|
||||
def agent_command(self) -> str:
|
||||
return self.agent_provision.command
|
||||
|
||||
@@ -34,10 +34,6 @@ def resolve_plan(
|
||||
spec: BottleSpec,
|
||||
slug: str,
|
||||
resolved_env: ResolvedEnv,
|
||||
instance_name: str,
|
||||
agent_image: str,
|
||||
agent_dockerfile_path: str,
|
||||
prompt_file: Path,
|
||||
agent_provision_plan: AgentProvisionPlan,
|
||||
egress_plan: EgressPlan,
|
||||
supervise_plan: SupervisePlan,
|
||||
@@ -55,12 +51,7 @@ def resolve_plan(
|
||||
spec=spec,
|
||||
stage_dir=stage_dir,
|
||||
slug=slug,
|
||||
container_name=instance_name,
|
||||
# container_name_pinned=container_name_pinned,
|
||||
image=agent_image,
|
||||
dockerfile_path=agent_dockerfile_path,
|
||||
forwarded_env=dict(resolved_env.forwarded),
|
||||
prompt_file=prompt_file,
|
||||
git_gate_plan=git_gate_plan,
|
||||
egress_plan=egress_plan,
|
||||
supervise_plan=supervise_plan,
|
||||
|
||||
@@ -47,11 +47,7 @@ class SmolmachinesBottleBackend(
|
||||
*,
|
||||
slug: str,
|
||||
resolved_env,
|
||||
instance_name: str,
|
||||
agent_image: str,
|
||||
prompt_file: Path,
|
||||
agent_provision_plan,
|
||||
agent_dockerfile_path: str,
|
||||
egress_plan,
|
||||
git_gate_plan,
|
||||
supervise_plan,
|
||||
@@ -61,10 +57,6 @@ class SmolmachinesBottleBackend(
|
||||
spec,
|
||||
slug=slug,
|
||||
resolved_env=resolved_env,
|
||||
instance_name=instance_name,
|
||||
agent_image=agent_image,
|
||||
agent_dockerfile_path=agent_dockerfile_path,
|
||||
prompt_file=prompt_file,
|
||||
agent_provision_plan=agent_provision_plan,
|
||||
egress_plan=egress_plan,
|
||||
supervise_plan=supervise_plan,
|
||||
|
||||
@@ -29,27 +29,6 @@ class SmolmachinesBottlePlan(BottlePlan):
|
||||
bundle_subnet: str
|
||||
bundle_gateway: str
|
||||
bundle_ip: str
|
||||
# smolvm machine name + agent image source. machine_create
|
||||
# boots from a packed `.smolmachine` artifact (pre-baked at
|
||||
# prepare time via `smolvm pack create`); using `--from`
|
||||
# instead of `--image` avoids the registry-pull race we hit
|
||||
# when machine_start tried to fetch on-demand and the libkrun
|
||||
# agent's network attempt got refused by macOS.
|
||||
#
|
||||
# Chunk 2d ships with a public placeholder image (alpine)
|
||||
# since bot-bottle-claude:latest lives in the operator's local
|
||||
# docker daemon and smolvm's crane backend can't read from
|
||||
# there; chunk 4 resolves the agent-image-conversion gap
|
||||
# (push to a registry first, or smolvm grows a docker-daemon
|
||||
# transport).
|
||||
machine_name: str
|
||||
# Agent image ref (docker tag). `launch` runs the
|
||||
# build → save → registry push → smolvm pack pipeline against
|
||||
# this and feeds the resulting `.smolmachine` artifact to
|
||||
# `machine_create --from`. The pipeline runs at launch time
|
||||
# (not prepare time) so the docker build output doesn't garble
|
||||
# the dashboard's preflight modal.
|
||||
agent_image: str
|
||||
# In-guest env vars (HTTPS_PROXY etc) — IP-literal URLs since
|
||||
# the guest has no DNS resolver inside the TSI allowlist.
|
||||
# Passed to `smolvm machine create` as `-e K=V` flags.
|
||||
@@ -57,11 +36,6 @@ class SmolmachinesBottlePlan(BottlePlan):
|
||||
# `--smolfile` is mutually exclusive with `--from`, and
|
||||
# `--from` is the path that avoids the registry-pull race).
|
||||
guest_env: dict[str, str]
|
||||
# Path to the agent's prompt file on the host. Always written
|
||||
# (mode 0o600) so the in-VM path always exists; the file is
|
||||
# empty when the agent has no prompt — claude-code reads it
|
||||
# via --append-system-prompt-file only when non-empty.
|
||||
prompt_file: Path
|
||||
# Inner Plans for the sidecar bundle daemons. The same shape the
|
||||
# docker backend uses — same `.prepare()` calls produced
|
||||
# them — but our launch step doesn't populate the
|
||||
@@ -82,6 +56,34 @@ class SmolmachinesBottlePlan(BottlePlan):
|
||||
agent_git_gate_host: str = ""
|
||||
agent_supervise_url: str = ""
|
||||
|
||||
@property
|
||||
def machine_name(self) -> str:
|
||||
"""smolvm machine name. `machine_create` boots from a packed
|
||||
`.smolmachine` artifact (pre-baked at prepare time via
|
||||
`smolvm pack create`); using `--from` instead of `--image`
|
||||
avoids the registry-pull race we hit when machine_start tried
|
||||
to fetch on-demand and the libkrun agent's network attempt
|
||||
got refused by macOS."""
|
||||
return self.agent_provision.instance_name
|
||||
|
||||
@property
|
||||
def agent_image(self) -> str:
|
||||
"""Agent image ref (docker tag). `launch` runs the
|
||||
build → save → registry push → smolvm pack pipeline against
|
||||
this and feeds the resulting `.smolmachine` artifact to
|
||||
`machine_create --from`. The pipeline runs at launch time
|
||||
(not prepare time) so the docker build output doesn't garble
|
||||
the dashboard's preflight modal."""
|
||||
return self.agent_provision.image
|
||||
|
||||
@property
|
||||
def prompt_file(self) -> Path:
|
||||
"""Path to the agent's prompt file on the host. Always written
|
||||
(mode 0o600) so the in-VM path always exists; the file is
|
||||
empty when the agent has no prompt — claude-code reads it
|
||||
via --append-system-prompt-file only when non-empty."""
|
||||
return self.agent_provision.prompt_file
|
||||
|
||||
@property
|
||||
def git_gate_insteadof_host(self) -> str:
|
||||
return self.agent_git_gate_host
|
||||
|
||||
@@ -51,10 +51,6 @@ def resolve_plan(
|
||||
spec: BottleSpec,
|
||||
slug: str,
|
||||
resolved_env: ResolvedEnv,
|
||||
instance_name: str,
|
||||
agent_image: str,
|
||||
agent_dockerfile_path: str,
|
||||
prompt_file: Path,
|
||||
agent_provision_plan: AgentProvisionPlan,
|
||||
egress_plan: EgressPlan,
|
||||
supervise_plan: SupervisePlan,
|
||||
@@ -79,10 +75,7 @@ def resolve_plan(
|
||||
bundle_subnet=subnet,
|
||||
bundle_gateway=gateway,
|
||||
bundle_ip=bundle_ip,
|
||||
machine_name=instance_name,
|
||||
agent_image=agent_image,
|
||||
guest_env=agent_provision_plan.guest_env,
|
||||
prompt_file=prompt_file,
|
||||
git_gate_plan=git_gate_plan,
|
||||
egress_plan=egress_plan,
|
||||
supervise_plan=supervise_plan,
|
||||
|
||||
@@ -59,7 +59,8 @@ class ClaudeAgentProvider(AgentProvider):
|
||||
*,
|
||||
dockerfile: str,
|
||||
state_dir: Path,
|
||||
guest_home: str,
|
||||
instance_name: str,
|
||||
prompt_file: Path,
|
||||
guest_env: dict[str, str] | None = None,
|
||||
auth_token: str = "",
|
||||
forward_host_credentials: bool = False,
|
||||
@@ -70,6 +71,7 @@ class ClaudeAgentProvider(AgentProvider):
|
||||
) -> AgentProvisionPlan:
|
||||
del forward_host_credentials, host_env # Codex-only knobs
|
||||
resolved_guest_env = dict(guest_env or {})
|
||||
guest_home = self.guest_home
|
||||
trusted_path = trusted_project_path or guest_home
|
||||
|
||||
env_vars: dict[str, str] = {
|
||||
@@ -111,6 +113,8 @@ class ClaudeAgentProvider(AgentProvider):
|
||||
image=_RUNTIME.image,
|
||||
dockerfile=dockerfile,
|
||||
guest_home=guest_home,
|
||||
instance_name=instance_name,
|
||||
prompt_file=prompt_file,
|
||||
env_vars=env_vars,
|
||||
guest_env=resolved_guest_env,
|
||||
files=files,
|
||||
|
||||
@@ -67,7 +67,8 @@ class CodexAgentProvider(AgentProvider):
|
||||
*,
|
||||
dockerfile: str,
|
||||
state_dir: Path,
|
||||
guest_home: str,
|
||||
instance_name: str,
|
||||
prompt_file: Path,
|
||||
guest_env: dict[str, str] | None = None,
|
||||
auth_token: str = "",
|
||||
forward_host_credentials: bool = False,
|
||||
@@ -78,6 +79,7 @@ class CodexAgentProvider(AgentProvider):
|
||||
) -> AgentProvisionPlan:
|
||||
del auth_token, label, color # Claude-only knobs
|
||||
resolved_guest_env = dict(guest_env or {})
|
||||
guest_home = self.guest_home
|
||||
trusted_path = trusted_project_path or guest_home
|
||||
|
||||
env_vars: dict[str, str] = {
|
||||
@@ -148,6 +150,8 @@ class CodexAgentProvider(AgentProvider):
|
||||
image=_RUNTIME.image,
|
||||
dockerfile=dockerfile,
|
||||
guest_home=guest_home,
|
||||
instance_name=instance_name,
|
||||
prompt_file=prompt_file,
|
||||
env_vars=env_vars,
|
||||
guest_env=resolved_guest_env,
|
||||
dirs=tuple(dirs),
|
||||
|
||||
Reference in New Issue
Block a user