"""Prepare step for the Docker bottle backend. `resolve_plan` does all host-side resolution (image and container names, env-file, prompt-file, proxy plan, runtime detection) and returns a frozen DockerBottlePlan. No Docker resources are created; the only side effects are scratch files under `stage_dir` and a probe of `docker info`. Cross-backend host-side validation has already run via the base class's `prepare` template before this is called. """ from __future__ import annotations import os from pathlib import Path 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 .. import BottleSpec from ..resolve_common import ( merge_provision_env_vars, mint_slug, prepare_agent_state_dir, prepare_egress, prepare_git_gate, prepare_supervise, resolve_manifest_dockerfile, write_launch_metadata, ) 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 .sidecar_bundle import sidecar_bundle_container_name def preflight(): docker_mod.require_docker() def resolve_plan( spec: BottleSpec, *, stage_dir: Path, ) -> DockerBottlePlan: """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.""" preflight() manifest = spec.manifest 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") 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" 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() return DockerBottlePlan( 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, env_file=env_file, forwarded_env=forwarded_env, prompt_file=prompt_file, git_gate_plan=git_gate_plan, egress_plan=egress_plan, supervise_plan=supervise_plan, use_runsc=use_runsc, agent_provision=agent_provision, # workspace_plan=workspace_plan, ) def _write_env_file(resolved: ResolvedEnv, env_file: Path) -> None: """Serialize the literal portion of a ResolvedEnv into docker's `--env-file` syntax (NAME=VALUE per line, mode 600 since the file may carry verbatim values from the manifest). Forwarded names ride on the plan as a structured tuple instead.""" env_lines: list[str] = [] for name, value in resolved.literals.items(): if "\n" in value: die( f"env entry {name} (literal) contains a newline; " f"docker --env-file cannot represent multi-line values." ) env_lines.append(f"{name}={value}") env_file.write_text("\n".join(env_lines) + ("\n" if env_lines else "")) env_file.chmod(0o600)