From 4dbe44e7ccd65aefabe42e875b12b0248cd95532 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 8 Jun 2026 12:28:08 -0400 Subject: [PATCH] chore: SAVEPOINT --- bot_bottle/backend/__init__.py | 4 +- bot_bottle/backend/docker/bottle_plan.py | 3 - bot_bottle/backend/docker/compose.py | 2 +- bot_bottle/backend/docker/launch.py | 4 - bot_bottle/backend/docker/resolve_plan.py | 159 +++++------------- .../backend/smolmachines/resolve_plan.py | 52 +++--- bot_bottle/supervise.py | 5 - tests/unit/test_compose.py | 4 +- tests/unit/test_contrib_claude_provider.py | 2 +- tests/unit/test_contrib_codex_provider.py | 2 +- tests/unit/test_docker_launch_teardown.py | 2 +- tests/unit/test_docker_provision_git_user.py | 2 +- tests/unit/test_plan_print_parity.py | 2 +- 13 files changed, 75 insertions(+), 168 deletions(-) diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index 1abde4d..35c72ea 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -46,7 +46,7 @@ from ..log import die, info from ..manifest import ManifestGitEntry, Manifest from ..supervise import SupervisePlan from ..util import expand_tilde -from ..workspace import WorkspacePlan +# from ..workspace import WorkspacePlan from .print_util import print_multi, visible_agent_env_names from .util import host_skill_dir @@ -100,7 +100,7 @@ class BottlePlan(ABC): egress_plan: EgressPlan supervise_plan: SupervisePlan | None agent_provision: AgentProvisionPlan - workspace_plan: WorkspacePlan + # workspace_plan: WorkspacePlan def print(self, *, remote_control: bool) -> None: """Render the y/N preflight summary to stderr.""" diff --git a/bot_bottle/backend/docker/bottle_plan.py b/bot_bottle/backend/docker/bottle_plan.py index 29a8100..871f53d 100644 --- a/bot_bottle/backend/docker/bottle_plan.py +++ b/bot_bottle/backend/docker/bottle_plan.py @@ -23,10 +23,7 @@ class DockerBottlePlan(BottlePlan): slug: str container_name: str - container_name_pinned: bool image: str - derived_image: str # "" -> no derived image - runtime_image: str # image actually launched (derived or base) # 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//Dockerfile) after a diff --git a/bot_bottle/backend/docker/compose.py b/bot_bottle/backend/docker/compose.py index 4ba695d..8cda870 100644 --- a/bot_bottle/backend/docker/compose.py +++ b/bot_bottle/backend/docker/compose.py @@ -222,7 +222,7 @@ def _agent_service(plan: DockerBottlePlan) -> dict[str, Any]: env.append(name) service: dict[str, Any] = { - "image": plan.runtime_image, + "image": plan.image, "container_name": plan.container_name, "command": ["sleep", "infinity"], "networks": {"internal": None}, diff --git a/bot_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py index 7171237..d90edf7 100644 --- a/bot_bottle/backend/docker/launch.py +++ b/bot_bottle/backend/docker/launch.py @@ -97,10 +97,6 @@ def launch( plan.image, _REPO_DIR, dockerfile=plan.dockerfile_path, ) - if plan.derived_image: - docker_mod.build_image_with_cwd( - plan.derived_image, plan.image, plan.workspace_plan - ) internal_network = network_mod.network_name_for_slug(plan.slug) egress_network = network_mod.network_egress_name_for_slug(plan.slug) diff --git a/bot_bottle/backend/docker/resolve_plan.py b/bot_bottle/backend/docker/resolve_plan.py index d702933..5ed0ac4 100644 --- a/bot_bottle/backend/docker/resolve_plan.py +++ b/bot_bottle/backend/docker/resolve_plan.py @@ -13,10 +13,10 @@ from __future__ import annotations import os from pathlib import Path -from ...agent_provider import PROVIDER_TEMPLATES, agent_provision_plan, get_provider +from ...agent_provider import agent_provision_plan, get_provider from ...env import ResolvedEnv, resolve_env from ...log import die -from ...workspace import workspace_plan as resolve_workspace_plan +# from ...workspace import workspace_plan as resolve_workspace_plan from .. import BottleSpec from ..resolve_common import ( merge_provision_env_vars, @@ -30,14 +30,17 @@ from ..resolve_common import ( ) from . import util as docker_mod from .bottle_plan import DockerBottlePlan -from ...bottle_state import ( - clear_preserve_marker, - per_bottle_dockerfile, - per_bottle_dockerfile_path, - per_bottle_image_tag, -) +# from ...bottle_state import ( +# # clear_preserve_marker, +# per_bottle_dockerfile, +# per_bottle_dockerfile_path, +# per_bottle_image_tag, +# ) from .sidecar_bundle import sidecar_bundle_container_name +def preflight(): + docker_mod.require_docker() + def resolve_plan( spec: BottleSpec, @@ -47,135 +50,57 @@ def resolve_plan( """Resolve Docker-specific names and write scratch files. Trusts that the agent and its skills/git-gate keys are present — validation already ran in the base class.""" - docker_mod.require_docker() + preflight() manifest = spec.manifest - bottle = manifest.bottle_for(spec.agent_name) - provider = bottle.agent_provider - 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) + manifest_bottle = manifest.bottle_for(spec.agent_name) + manfiest_agent_provider = manifest_bottle.agent_provider + agent_provider = get_provider(manfiest_agent_provider.template) slug = mint_slug(spec) + # FIXME: don't thin the compose project should be directly written to metadata like this, + # should probably be a backend specific metadata field for details like this write_launch_metadata(slug, spec, compose_project=f"bot-bottle-{slug}", backend="docker") - # Clear any leftover preserve marker from a prior capability-block - # so this fresh launch can be cleaned up at session-end unless - # the agent triggers another capability-block. - clear_preserve_marker(slug) - # PRD 0016 capability-block: if a per-bottle Dockerfile has been - # written (via apply_capability_change), the base image becomes - # per_bottle_image_tag(slug) built from that file. --cwd still - # layers a derived image on top. - if provider.template in PROVIDER_TEMPLATES: - image = provider_runtime.image - else: - image = f"bot-bottle-{provider.template}:{slug}" - dockerfile_path = str(provider_obj.dockerfile) - if per_bottle_dockerfile(slug) is not None: - image = per_bottle_image_tag(slug) - dockerfile_path = str(per_bottle_dockerfile_path(slug)) - elif provider.dockerfile: - image = f"bot-bottle-{provider.template}:{slug}" - dockerfile_path = resolve_manifest_dockerfile(provider.dockerfile, spec) - derived_image = "" - runtime_image = image - if spec.copy_cwd: - derived_image = os.environ.get( - "BOT_BOTTLE_DERIVED_IMAGE", f"bot-bottle-cwd:{slug}" - ) - runtime_image = derived_image - - default_container = f"bot-bottle-{slug}" - pinned_container = os.environ.get("BOT_BOTTLE_CONTAINER", "") - container_name_pinned = bool(pinned_container) - if container_name_pinned: - container_name = pinned_container - if docker_mod.container_exists(container_name): - die( - f"container '{container_name}' already exists " - f"(pinned via BOT_BOTTLE_CONTAINER). " - f"Remove it with 'docker rm -f {container_name}' or unset the override." - ) - else: - container_name = "" - for candidate in docker_mod.container_name_candidates(default_container): - if not docker_mod.container_exists(candidate): - container_name = candidate - break - if not container_name: - die( - f"could not find a free container name after " - f"{default_container}-{docker_mod.MAX_CONTAINER_SUFFIX}; " - f"clean up old containers with 'docker rm -f '" - ) - - # Probe the sidecar-bundle container name for an orphan from a - # previous run. Otherwise a stale bundle surfaces as a - # docker-create conflict deep inside launch() with no actionable - # hint; failing fast here points at the cleanup command. - bundle_name = sidecar_bundle_container_name(slug) - if docker_mod.container_exists(bundle_name): - die( - f"sidecar bundle container '{bundle_name}' already exists. " - f"This is an orphan from a previous run; clean it up with " - f"'./cli.py cleanup' (or 'docker rm -f {bundle_name}') and " - f"retry." - ) + agent_image = agent_provider.runtime.image + agent_dockerfile_path = resolve_manifest_dockerfile(manfiest_agent_provider.dockerfile, spec) + instance_name = f"bot-bottle-{slug}" agent_dir, prompt_file = prepare_agent_state_dir(slug, spec) env_file = agent_dir / "agent.env" - git_gate_plan = prepare_git_gate(bottle, slug) + agent_provision = agent_provision_plan( + 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 + forward_host_credentials=manfiest_agent_provider.forward_host_credentials, + auth_token=manfiest_agent_provider.auth_token, + host_env=dict(os.environ), + # trusted_project_path=workspace_plan.workdir, + label=spec.label, + color=spec.color, + ) + agent_provision = merge_provision_env_vars(agent_provision) + egress_plan = prepare_egress(manifest_bottle, slug, agent_provision) + supervise_plan = prepare_supervise(manifest_bottle, slug) + git_gate_plan = prepare_git_gate(manifest_bottle, slug) resolved = resolve_env(manifest, spec.agent_name) forwarded_env: dict[str, str] = dict(resolved.forwarded) _write_env_file(resolved, env_file) + # ==== docker specific setup ==== use_runsc = docker_mod.runsc_available() - agent_provision = agent_provision_plan( - template=provider.template, - dockerfile=dockerfile_path, - state_dir=agent_dir, - guest_home=guest_home, - forward_host_credentials=provider.forward_host_credentials, - auth_token=provider.auth_token, - host_env=dict(os.environ), - trusted_project_path=workspace_plan.workdir, - label=spec.label, - color=spec.color, - ) - agent_provision = merge_provision_env_vars(agent_provision) - - egress_plan = prepare_egress(bottle, slug, agent_provision) - - # Current Dockerfile for the agent image. For `--cwd` derived - # images the base Dockerfile is what the agent should propose - # changes against (the derived layer is just a workspace copy). - # (routes.yaml used to land here too but PRD 0017 chunk 3 - # 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 provider_obj.dockerfile - ) - dockerfile_content = ( - supervise_dockerfile_path.read_text(encoding="utf-8") - if supervise_dockerfile_path.is_file() - else "" - ) - supervise_plan = prepare_supervise(bottle, slug, dockerfile_content=dockerfile_content) return DockerBottlePlan( spec=spec, stage_dir=stage_dir, slug=slug, - container_name=container_name, - container_name_pinned=container_name_pinned, - image=image, - derived_image=derived_image, - runtime_image=runtime_image, - dockerfile_path=dockerfile_path, + container_name=instance_name, + # container_name_pinned=container_name_pinned, + image=agent_image, + dockerfile_path=agent_dockerfile_path, env_file=env_file, forwarded_env=forwarded_env, prompt_file=prompt_file, @@ -184,7 +109,7 @@ def resolve_plan( supervise_plan=supervise_plan, use_runsc=use_runsc, agent_provision=agent_provision, - workspace_plan=workspace_plan, + # workspace_plan=workspace_plan, ) diff --git a/bot_bottle/backend/smolmachines/resolve_plan.py b/bot_bottle/backend/smolmachines/resolve_plan.py index 474607f..5935fad 100644 --- a/bot_bottle/backend/smolmachines/resolve_plan.py +++ b/bot_bottle/backend/smolmachines/resolve_plan.py @@ -16,7 +16,7 @@ from pathlib import Path from ...agent_provider import PROVIDER_TEMPLATES, agent_provision_plan, get_provider from ...backend import BottleSpec from ...env import resolve_env -from ...workspace import workspace_plan as resolve_workspace_plan +# from ...workspace import workspace_plan as resolve_workspace_plan from ..resolve_common import ( merge_provision_env_vars, mint_slug, @@ -30,6 +30,8 @@ from ..resolve_common import ( from .bottle_plan import SmolmachinesBottlePlan from .util import smolmachines_bundle_subnet, smolmachines_preflight +def preflight(): + smolmachines_preflight() def resolve_plan( spec: BottleSpec, *, stage_dir: Path @@ -41,21 +43,19 @@ def resolve_plan( pull. Per-bottle guest env + the TSI allow_cidrs land on the plan for launch to pass straight through to `machine create` flags.""" - smolmachines_preflight() + preflight() + manifest = spec.manifest - bottle = manifest.bottle_for(spec.agent_name) - provider = bottle.agent_provider - 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) + manifest_bottle = manifest.bottle_for(spec.agent_name) + manfiest_agent_provider = manifest_bottle.agent_provider + agent_provider = get_provider(manfiest_agent_provider.template) slug = mint_slug(spec) write_launch_metadata(slug, spec, compose_project="", backend="smolmachines") + # ==== smolmachines specific setup ==== subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug) - # Agent's env: resolve through resolve_env() so ?prompt entries # are prompted and ${HOST_VAR} entries are interpolated — matching # the Docker backend's contract. Forwarded (secret/interpolated) @@ -73,36 +73,30 @@ def resolve_plan( "SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt", "REQUESTS_CA_BUNDLE": "/etc/ssl/certs/ca-certificates.crt", } + # ============== + + agent_dockerfile_path = resolve_manifest_dockerfile(manfiest_agent_provider.dockerfile, spec) + instance_name = f"bot-bottle-{slug}" - git_gate_plan = prepare_git_gate(bottle, slug) agent_dir, prompt_file = prepare_agent_state_dir(slug, spec) - machine_name = f"bot-bottle-{slug}" - if provider.template in PROVIDER_TEMPLATES: - agent_image_ref = provider_runtime.image - else: - agent_image_ref = f"bot-bottle-{provider.template}:{slug}" - agent_dockerfile_path = str(provider_obj.dockerfile) - if provider.dockerfile: - agent_image_ref = f"bot-bottle-{provider.template}:{slug}" - agent_dockerfile_path = resolve_manifest_dockerfile(provider.dockerfile, spec) agent_provision = agent_provision_plan( - template=provider.template, + template=manfiest_agent_provider.template, dockerfile=agent_dockerfile_path, state_dir=agent_dir, - guest_home=guest_home, + guest_home="/home/node", # FIXME: should be coming from the agent plan guest_env=guest_env, - forward_host_credentials=provider.forward_host_credentials, - auth_token=provider.auth_token, + forward_host_credentials=manfiest_agent_provider.forward_host_credentials, + auth_token=manfiest_agent_provider.auth_token, host_env=dict(os.environ), - trusted_project_path=workspace_plan.workdir, + # trusted_project_path=workspace_plan.workdir, label=spec.label, color=spec.color, ) agent_provision = merge_provision_env_vars(agent_provision) - - egress_plan = prepare_egress(bottle, slug, agent_provision) - supervise_plan = prepare_supervise(bottle, slug) + egress_plan = prepare_egress(manifest_bottle, slug, agent_provision) + supervise_plan = prepare_supervise(manifest_bottle, slug) + git_gate_plan = prepare_git_gate(manifest_bottle, slug) return SmolmachinesBottlePlan( spec=spec, @@ -111,7 +105,7 @@ def resolve_plan( bundle_subnet=subnet, bundle_gateway=gateway, bundle_ip=bundle_ip, - machine_name=machine_name, + machine_name=instance_name, agent_image_ref=agent_image_ref, guest_env=agent_provision.guest_env, prompt_file=prompt_file, @@ -119,5 +113,5 @@ def resolve_plan( egress_plan=egress_plan, supervise_plan=supervise_plan, agent_provision=agent_provision, - workspace_plan=workspace_plan, + # workspace_plan=workspace_plan, ) diff --git a/bot_bottle/supervise.py b/bot_bottle/supervise.py index 68c1bf1..df01a67 100644 --- a/bot_bottle/supervise.py +++ b/bot_bottle/supervise.py @@ -465,8 +465,6 @@ class Supervise(ABC): self, slug: str, stage_dir: Path, - *, - dockerfile_content: str = "", ) -> SupervisePlan: """Stage the per-bottle queue dir on the host and the current-config dir under `stage_dir`. Returns the plan; @@ -476,9 +474,6 @@ class Supervise(ABC): queue_dir.mkdir(parents=True, exist_ok=True) current_config_dir = stage_dir / "current-config" current_config_dir.mkdir(parents=True, exist_ok=True) - dockerfile_path = current_config_dir / CURRENT_CONFIG_DOCKERFILE - dockerfile_path.write_text(dockerfile_content) - dockerfile_path.chmod(0o644) return SupervisePlan( slug=slug, queue_dir=queue_dir, diff --git a/tests/unit/test_compose.py b/tests/unit/test_compose.py index 8dd6472..28e3367 100644 --- a/tests/unit/test_compose.py +++ b/tests/unit/test_compose.py @@ -156,7 +156,7 @@ def _plan( container_name_pinned=False, image="bot-bottle-claude:latest", derived_image="", - runtime_image="bot-bottle-claude:latest", + agent_image="bot-bottle-claude:latest", dockerfile_path="", env_file=Path("/dev/null"), # exists, size 0 → renderer skips env_file forwarded_env={"CLAUDE_CODE_OAUTH_TOKEN": "x"}, @@ -210,7 +210,7 @@ class TestAgentAlwaysPresent(unittest.TestCase): def test_agent_image_uses_runtime_image(self): plan = _plan() s = bottle_plan_to_compose(plan)["services"]["agent"] - self.assertEqual(plan.runtime_image, s["image"]) + self.assertEqual(plan.agent_image, s["image"]) def test_agent_only_on_internal_network(self): s = bottle_plan_to_compose(_plan())["services"]["agent"] diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index fd665ae..2426d2b 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -83,7 +83,7 @@ def _plan( container_name_pinned=False, image="bot-bottle-claude:latest", derived_image="", - runtime_image="bot-bottle-claude:latest", + agent_image="bot-bottle-claude:latest", dockerfile_path="", env_file=Path("/tmp/agent.env"), forwarded_env={}, diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index 1400241..6b3d518 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -84,7 +84,7 @@ def _plan( container_name_pinned=False, image="bot-bottle-codex:latest", derived_image="", - runtime_image="bot-bottle-codex:latest", + agent_image="bot-bottle-codex:latest", dockerfile_path="", env_file=Path("/tmp/agent.env"), forwarded_env={}, diff --git a/tests/unit/test_docker_launch_teardown.py b/tests/unit/test_docker_launch_teardown.py index 0bfeae5..77129f4 100644 --- a/tests/unit/test_docker_launch_teardown.py +++ b/tests/unit/test_docker_launch_teardown.py @@ -74,7 +74,7 @@ def _plan(tmp: str) -> DockerBottlePlan: container_name_pinned=False, image="bot-bottle-claude:latest", derived_image="", - runtime_image="bot-bottle-claude:latest", + agent_image="bot-bottle-claude:latest", dockerfile_path="", env_file=stage / "env", forwarded_env={}, diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index 046cde8..7b85a04 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -68,7 +68,7 @@ def _plan(*, git_user: dict | None = None, # type: ignore container_name_pinned=False, image="bot-bottle-claude:latest", derived_image="", - runtime_image="bot-bottle-claude:latest", + agent_image="bot-bottle-claude:latest", dockerfile_path="", env_file=Path("/tmp/agent.env"), forwarded_env={}, diff --git a/tests/unit/test_plan_print_parity.py b/tests/unit/test_plan_print_parity.py index 22c915d..f355546 100644 --- a/tests/unit/test_plan_print_parity.py +++ b/tests/unit/test_plan_print_parity.py @@ -106,7 +106,7 @@ def _docker_plan(spec: BottleSpec, tmp: str) -> DockerBottlePlan: container_name_pinned=False, image="bot-bottle-claude:latest", derived_image="", - runtime_image="bot-bottle-claude:latest", + agent_image="bot-bottle-claude:latest", dockerfile_path="", env_file=stage / "env", forwarded_env={},