133 lines
4.7 KiB
Python
133 lines
4.7 KiB
Python
"""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)
|
|
|
|
|