refactor: replace runtime.dockerfile with AgentProvider.dockerfile property

Drop the `dockerfile` field from `AgentProviderRuntime` and replace it
with a convention-based `dockerfile` property on `AgentProvider`: the
base class looks for a `Dockerfile` file next to the provider's own
`agent_provider.py` module (via `inspect.getfile`), returning its path
or None. Built-in providers inherit the default automatically; custom
user providers work the same way by dropping a Dockerfile next to their
plugin file; any provider needing a non-standard path can override.

All callers (`docker/prepare.py`, `smolmachines/prepare.py`,
`capability_apply.py`) now resolve the provider object once and call
`.dockerfile` directly instead of reading `runtime.dockerfile`.
This commit is contained in:
2026-06-08 03:56:04 +00:00
committed by didericis (codex)
parent 007133bfac
commit 11935ed842
9 changed files with 44 additions and 39 deletions
@@ -34,6 +34,7 @@ import shutil
import subprocess
from pathlib import Path
from ...agent_provider import get_provider
from ...log import info, warn
from .bottle_state import (
mark_preserved,
@@ -93,11 +94,11 @@ def fetch_current_dockerfile(slug: str) -> str:
override = per_bottle_dockerfile(slug)
if override is not None:
return override
repo_dockerfile = _repo_dockerfile_path()
if repo_dockerfile.is_file():
repo_dockerfile = get_provider("claude").dockerfile
if repo_dockerfile is not None and repo_dockerfile.is_file():
return repo_dockerfile.read_text()
raise CapabilityApplyError(
f"no per-bottle Dockerfile for {slug} and no repo Dockerfile at "
f"no per-bottle Dockerfile for {slug} and no provider Dockerfile at "
f"{repo_dockerfile}"
)
@@ -125,12 +126,6 @@ def apply_capability_change(slug: str, new_dockerfile: str) -> tuple[str, str]:
# --- Internals -------------------------------------------------------------
def _repo_dockerfile_path() -> Path:
"""Path to the Claude provider Dockerfile. Resolved at call time so
the path is correct regardless of where this module is imported from."""
# bot_bottle/backend/docker/ -> bot_bottle/ -> contrib/claude/Dockerfile
return Path(__file__).resolve().parent.parent.parent / "contrib" / "claude" / "Dockerfile"
def snapshot_transcript(slug: str) -> None:
"""`docker cp` /home/node/.claude out of the agent container into
+13 -18
View File
@@ -15,7 +15,7 @@ from datetime import datetime, timezone
from dataclasses import replace
from pathlib import Path
from ...agent_provider import PROVIDER_TEMPLATES, agent_provision_plan, runtime_for
from ...agent_provider import PROVIDER_TEMPLATES, agent_provision_plan, get_provider
from ...egress import Egress
from ...env import ResolvedEnv, resolve_env
from ...git_gate import GitGate
@@ -59,7 +59,8 @@ def resolve_plan(
agent = manifest.agents[spec.agent_name]
bottle = manifest.bottle_for(spec.agent_name)
provider = bottle.agent_provider
provider_runtime = runtime_for(provider.template)
provider_obj = get_provider(provider.template)
provider_runtime = provider_obj.runtime
guest_home = "/home/node"
workspace_plan = resolve_workspace_plan(spec, guest_home=guest_home)
@@ -99,20 +100,16 @@ def resolve_plan(
elif provider.dockerfile:
image_default = f"bot-bottle-{provider.template}:{slug}"
dockerfile_path = _resolve_manifest_dockerfile(provider.dockerfile, spec)
elif provider_runtime.dockerfile:
image_default = provider_runtime.image
dockerfile_path = provider_runtime.dockerfile
elif provider.template not in PROVIDER_TEMPLATES:
user_dockerfile = (
Path.home() / ".bot-bottle" / "contrib" / provider.template / "Dockerfile"
)
if user_dockerfile.is_file():
image_default = f"bot-bottle-{provider.template}:{slug}"
dockerfile_path = str(user_dockerfile)
else:
p_dockerfile = provider_obj.dockerfile
if p_dockerfile is not None:
if provider.template in PROVIDER_TEMPLATES:
image_default = provider_runtime.image
else:
image_default = f"bot-bottle-{provider.template}:{slug}"
dockerfile_path = str(p_dockerfile)
else:
image_default = provider_runtime.image
else:
image_default = provider_runtime.image
image = os.environ.get("BOT_BOTTLE_IMAGE", image_default)
derived_image = ""
runtime_image = image
@@ -216,13 +213,11 @@ def resolve_plan(
# moved it behind the `list-egress-routes` MCP tool so the
# agent gets live state rather than a launch-time snapshot.)
supervise_dockerfile_path = (
Path(dockerfile_path)
if dockerfile_path
else Path(__file__).resolve().parent.parent.parent / "contrib" / "claude" / "Dockerfile"
Path(dockerfile_path) if dockerfile_path else provider_obj.dockerfile
)
dockerfile_content = (
supervise_dockerfile_path.read_text(encoding="utf-8")
if supervise_dockerfile_path.is_file()
if supervise_dockerfile_path is not None and supervise_dockerfile_path.is_file()
else ""
)
supervise_dir = supervise_state_dir(slug)