feat(supervise): Docker lifecycle + bottle integration (PRD 0013)
Phase 3 of PRD 0013. Wires the supervise sidecar into bottle launch: - Manifest: bottle.supervise (bool, default False). Opt-in for v1 so existing bottles are unchanged. - supervise.py: adds SupervisePlan + abstract Supervise(ABC) with a prepare template that stages the per-bottle queue dir on the host and the current-config dir under stage_dir (routes.json + allowlist + Dockerfile). Stdlib-only so it still runs as the in-container shared helper. - backend/docker/supervise.py: DockerSupervise concrete start/stop. No egress network (the sidecar doesn't make outbound calls); just the bottle's internal network with network-alias "supervise" and a bind-mount of the host queue dir at /run/supervise/queue. - Prepare wires supervise.prepare into the DockerBottlePlan, derives routes_content from cred_proxy_plan, allowlist_content from pipelock_effective_allowlist, and dockerfile_content from the repo's Dockerfile. supervise sidecar added to the orphan probe. - Launch starts the supervise sidecar after pipelock + cred-proxy but before the agent (so DNS resolution for `supervise` is up on the agent's first tool call). - Agent container gets a read-only bind-mount of the current-config dir at /etc/claude-bottle/current-config when supervise is enabled. - bottle_plan print + to_dict surface the supervise state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ from typing import Callable, Generator
|
||||
|
||||
from ...log import die, info
|
||||
from ...pipelock import pipelock_build_config, pipelock_render_yaml
|
||||
from ...supervise import CURRENT_CONFIG_DIR_IN_AGENT
|
||||
from . import network as network_mod
|
||||
from . import util as docker_mod
|
||||
from .bottle import DockerBottle
|
||||
@@ -33,6 +34,7 @@ from .pipelock import (
|
||||
pipelock_tls_init,
|
||||
)
|
||||
from .provision.ca import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
||||
from .supervise import DockerSupervise
|
||||
|
||||
|
||||
# Where the repo root lives, for `docker build` context. Computed once.
|
||||
@@ -46,6 +48,7 @@ def launch(
|
||||
proxy: DockerPipelockProxy,
|
||||
git_gate: DockerGitGate,
|
||||
cred_proxy: DockerCredProxy,
|
||||
supervise: DockerSupervise,
|
||||
provision: Callable[[DockerBottlePlan, str], str | None],
|
||||
) -> Generator[DockerBottle, None, None]:
|
||||
"""Build, launch, and provision a Docker bottle. Teardown on exit.
|
||||
@@ -156,6 +159,19 @@ def launch(
|
||||
cred_proxy_name = cred_proxy.start(plan.cred_proxy_plan)
|
||||
stack.callback(cred_proxy.stop, cred_proxy_name)
|
||||
|
||||
# Supervise sidecar (PRD 0013). Opt-in via bottle.supervise.
|
||||
# Internal-network only — the sidecar makes no outbound calls.
|
||||
# Must come up BEFORE the agent so DNS resolution for
|
||||
# `supervise` succeeds on the agent's first tool call.
|
||||
if plan.supervise_plan is not None:
|
||||
supervise_plan = dataclasses.replace(
|
||||
plan.supervise_plan,
|
||||
internal_network=internal_network,
|
||||
)
|
||||
plan = dataclasses.replace(plan, supervise_plan=supervise_plan)
|
||||
supervise_name = supervise.start(plan.supervise_plan)
|
||||
stack.callback(supervise.stop, supervise_name)
|
||||
|
||||
container = _run_agent_container(plan, internal_network)
|
||||
stack.callback(docker_mod.force_remove_container, container)
|
||||
|
||||
@@ -196,6 +212,16 @@ def _run_agent_container(plan: DockerBottlePlan, internal_network: str) -> str:
|
||||
for name in plan.forwarded_env:
|
||||
docker_args.extend(["-e", name])
|
||||
|
||||
# PRD 0013: read-only current-config mount so the agent can read
|
||||
# routes.json / allowlist / Dockerfile before composing a
|
||||
# supervise tool-call proposal. Mounted from the per-bottle
|
||||
# stage_dir/current-config/ populated at prepare time.
|
||||
if plan.supervise_plan is not None:
|
||||
docker_args.extend([
|
||||
"-v",
|
||||
f"{plan.supervise_plan.current_config_dir}:{CURRENT_CONFIG_DIR_IN_AGENT}:ro",
|
||||
])
|
||||
|
||||
docker_args.extend([plan.runtime_image, "sleep", "infinity"])
|
||||
|
||||
info(f"starting container {plan.container_name} from {plan.runtime_image}")
|
||||
|
||||
Reference in New Issue
Block a user