refactor(contrib): inline provision steps per-provider, drop shared apply module

Each AgentProvider now owns its skills / prompt / provision /
supervise_mcp end-to-end. The base ABC declares all four as
abstract; ClaudeAgentProvider and CodexAgentProvider each carry
their own copy loop.

Per PR review feedback (review #128): the shared
_provision_apply.py abstraction was weak — Claude and Codex
harnesses already diverge (codex's dummy-auth + login-status
verify has no claude analogue) and forcing both onto one helper
just postpones the split. Duplication is intentional.

Deletes bot_bottle/_provision_apply.py and consolidates testing
under tests/unit/test_contrib_{claude,codex}_provider.py (one
file per provider, covering all four methods).
This commit is contained in:
2026-06-04 01:00:13 +00:00
committed by didericis
parent f44751c4b8
commit bcdffc8400
8 changed files with 611 additions and 410 deletions
+302
View File
@@ -0,0 +1,302 @@
"""Unit: ClaudeAgentProvider provisioning (PRD 0050, contrib/claude).
Each provider owns its own in-guest provisioning end-to-end —
skills copy, prompt copy, declarative dirs/files/pre_copy/verify
apply, and supervise MCP registration. The Claude / Codex paths
intentionally don't share a helper module: harness changes on
either side are expected to diverge the implementations."""
from __future__ import annotations
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from bot_bottle.agent_provider import (
AgentProvisionCommand,
AgentProvisionDir,
AgentProvisionFile,
AgentProvisionPlan,
)
from bot_bottle.backend import Bottle, BottleSpec, ExecResult
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
from bot_bottle.contrib.claude.agent_provider import ClaudeAgentProvider
from bot_bottle.egress import EgressPlan
from bot_bottle.git_gate import GitGatePlan
from bot_bottle.manifest import Manifest
from bot_bottle.pipelock import PipelockProxyPlan
from bot_bottle.supervise import SupervisePlan
from bot_bottle.workspace import workspace_plan
_URL = "http://supervise:9100/"
def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock:
bottle = MagicMock(spec=Bottle)
bottle.name = "bot-bottle-demo-abc12"
bottle.exec.return_value = (
exec_result if exec_result is not None
else ExecResult(returncode=0, stdout="", stderr="")
)
return bottle
def _exec_scripts(bottle: MagicMock) -> list[str]:
return [c.args[0] for c in bottle.exec.call_args_list]
def _plan(
*,
agent_prompt: str = "",
skills: list[str] | None = None,
agent_provision: AgentProvisionPlan | None = None,
supervise: bool = False,
) -> DockerBottlePlan:
bottle_json: dict = {"agent_provider": {"template": "claude"}}
if supervise:
bottle_json["supervise"] = True
manifest = Manifest.from_json_obj({
"bottles": {"dev": bottle_json},
"agents": {
"demo": {
"skills": list(skills or []),
"prompt": agent_prompt,
"bottle": "dev",
},
},
})
spec = BottleSpec(
manifest=manifest, agent_name="demo",
copy_cwd=False, user_cwd="/tmp/x",
)
supervise_plan = None
if supervise:
supervise_plan = SupervisePlan(
slug="demo-abc12",
queue_dir=Path("/tmp/queue"),
current_config_dir=Path("/tmp/current-config"),
)
return DockerBottlePlan(
spec=spec,
stage_dir=Path("/tmp/stage"),
slug="demo-abc12",
container_name="bot-bottle-demo-abc12",
container_name_pinned=False,
image="bot-bottle-claude:latest",
derived_image="",
runtime_image="bot-bottle-claude:latest",
dockerfile_path="",
env_file=Path("/tmp/agent.env"),
forwarded_env={},
prompt_file=Path("/tmp/state/demo-abc12/agent/prompt.txt"),
proxy_plan=PipelockProxyPlan(
yaml_path=Path("/tmp/pipelock.yaml"), slug="demo-abc12",
),
git_gate_plan=GitGatePlan(
slug="demo-abc12",
entrypoint_script=Path("/tmp/git-gate-entrypoint.sh"),
hook_script=Path("/tmp/git-gate-hook"),
access_hook_script=Path("/tmp/git-gate-access-hook"),
upstreams=(),
),
egress_plan=EgressPlan(
slug="demo-abc12",
routes_path=Path("/tmp/routes.yaml"),
routes=(),
token_env_map={},
),
supervise_plan=supervise_plan,
use_runsc=False,
agent_provision=agent_provision or AgentProvisionPlan(
template="claude", command="claude", prompt_mode="append_file",
image="", dockerfile="", guest_env={},
),
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
)
class TestClaudeProvisionPrompt(unittest.TestCase):
def test_cp_uses_bottle_cp_in(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_prompt(_plan(), bottle)
bottle.cp_in.assert_called_once_with(
"/tmp/state/demo-abc12/agent/prompt.txt",
"/home/node/.bot-bottle-prompt.txt",
)
def test_returns_path_when_agent_has_prompt(self):
bottle = _make_bottle()
r = ClaudeAgentProvider().provision_prompt(
_plan(agent_prompt="You are helpful."), bottle,
)
self.assertEqual("/home/node/.bot-bottle-prompt.txt", r)
def test_returns_none_when_agent_has_no_prompt(self):
bottle = _make_bottle()
r = ClaudeAgentProvider().provision_prompt(_plan(agent_prompt=""), bottle)
self.assertIsNone(r)
bottle.cp_in.assert_called_once()
def test_chowns_to_node_after_copy(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_prompt(_plan(), bottle)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("chown node:node" in s
and "/home/node/.bot-bottle-prompt.txt" in s
for s in scripts)
)
self.assertTrue(
any("chmod 600" in s
and "/home/node/.bot-bottle-prompt.txt" in s
for s in scripts)
)
class TestClaudeProvisionSkills(unittest.TestCase):
def test_noop_when_agent_has_no_skills(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_skills(_plan(skills=[]), bottle)
bottle.cp_in.assert_not_called()
bottle.exec.assert_not_called()
def test_mkdir_plus_cp_per_skill(self):
bottle = _make_bottle()
with patch(
"bot_bottle.backend.util.host_skill_dir",
side_effect=lambda n: f"/host/skills/{n}",
), patch(
"bot_bottle.contrib.claude.agent_provider.os.path.isdir",
return_value=True,
):
ClaudeAgentProvider().provision_skills(
_plan(skills=["init-prd", "verify"]), bottle,
)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("mkdir -p" in s and "/home/node/.claude/skills" in s
for s in scripts)
)
cp_targets = {c.args[1] for c in bottle.cp_in.call_args_list}
self.assertEqual({
"/home/node/.claude/skills/init-prd/",
"/home/node/.claude/skills/verify/",
}, cp_targets)
self.assertEqual(
2, sum(1 for s in scripts if "chown -R node:node" in s),
)
def test_missing_skill_dies(self):
bottle = _make_bottle()
with patch(
"bot_bottle.backend.util.host_skill_dir",
side_effect=lambda n: f"/host/skills/{n}",
), patch(
"bot_bottle.contrib.claude.agent_provider.os.path.isdir",
return_value=False,
):
with self.assertRaises(SystemExit):
ClaudeAgentProvider().provision_skills(
_plan(skills=["init-prd"]), bottle,
)
class TestClaudeProvision(unittest.TestCase):
"""The declarative dirs/files/pre_copy/verify apply loop for
the claude.json trust marker."""
def test_noop_on_empty_provision_plan(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision(_plan(), bottle)
bottle.cp_in.assert_not_called()
bottle.exec.assert_not_called()
def test_copies_files_and_chowns(self):
provision = AgentProvisionPlan(
template="claude", command="claude", prompt_mode="append_file",
image="", dockerfile="", guest_env={},
files=(AgentProvisionFile(
Path("/tmp/claude.json"), "/home/node/.claude.json",
),),
)
bottle = _make_bottle()
ClaudeAgentProvider().provision(
_plan(agent_provision=provision), bottle,
)
bottle.cp_in.assert_called_once_with(
"/tmp/claude.json", "/home/node/.claude.json",
)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("chown" in s and "/home/node/.claude.json" in s for s in scripts)
)
self.assertTrue(
any("chmod" in s and "/home/node/.claude.json" in s for s in scripts)
)
def test_dies_when_file_chown_fails(self):
provision = AgentProvisionPlan(
template="claude", command="claude", prompt_mode="append_file",
image="", dockerfile="", guest_env={},
files=(AgentProvisionFile(
Path("/tmp/claude.json"), "/home/node/.claude.json",
),),
)
bottle = _make_bottle(
exec_result=ExecResult(1, "", "chown: no such file\n"),
)
with self.assertRaises(SystemExit):
ClaudeAgentProvider().provision(
_plan(agent_provision=provision), bottle,
)
def test_runs_verify_commands(self):
provision = AgentProvisionPlan(
template="claude", command="claude", prompt_mode="append_file",
image="", dockerfile="", guest_env={},
verify=(AgentProvisionCommand(
("/usr/bin/true",), "verify failed",
),),
)
bottle = _make_bottle()
ClaudeAgentProvider().provision(
_plan(agent_provision=provision), bottle,
)
scripts = _exec_scripts(bottle)
self.assertTrue(any("/usr/bin/true" in s for s in scripts))
class TestClaudeSuperviseMcp(unittest.TestCase):
def test_noop_when_supervise_disabled(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_supervise_mcp(
_plan(supervise=False), bottle, _URL,
)
bottle.exec.assert_not_called()
def test_runs_claude_mcp_add_as_node(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_supervise_mcp(
_plan(supervise=True), bottle, _URL,
)
bottle.exec.assert_called_once()
script = bottle.exec.call_args.args[0]
self.assertEqual("node", bottle.exec.call_args.kwargs.get("user"))
self.assertIn("claude mcp add", script)
self.assertIn("--scope user", script)
self.assertIn("--transport http", script)
self.assertIn("supervise", script)
self.assertIn(_URL, script)
def test_logs_warning_on_failure_but_does_not_raise(self):
bottle = _make_bottle(
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
)
ClaudeAgentProvider().provision_supervise_mcp(
_plan(supervise=True), bottle, _URL,
)
if __name__ == "__main__":
unittest.main()
@@ -1,11 +1,10 @@
"""Unit: shared provision-apply helpers (PRD 0050).
"""Unit: CodexAgentProvider provisioning (PRD 0050, contrib/codex).
Covers `bot_bottle._provision_apply.apply_skills` /
`apply_prompt` / `apply_provision` the backend-agnostic helpers
that AgentProvider's default `provision_skills` / `provision_prompt`
/ `provision` dispatch through. The same suite covered the
docker / smolmachines `provision/{skills,prompt,provider_auth}.py`
modules before they were deleted."""
The Codex provider owns its own skills / prompt / provision /
supervise-mcp end-to-end symmetric with the claude provider but
not sharing a helper module, since codex's apply steps include
the dummy-auth dance and a `codex login status` verify that have
no claude equivalent."""
from __future__ import annotations
@@ -13,14 +12,6 @@ import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from bot_bottle import _provision_apply
from bot_bottle._provision_apply import (
PROMPT_PATH,
SKILLS_DIR,
apply_prompt,
apply_provision,
apply_skills,
)
from bot_bottle.agent_provider import (
AgentProvisionCommand,
AgentProvisionDir,
@@ -29,13 +20,18 @@ from bot_bottle.agent_provider import (
)
from bot_bottle.backend import Bottle, BottleSpec, ExecResult
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
from bot_bottle.contrib.codex.agent_provider import CodexAgentProvider
from bot_bottle.egress import EgressPlan
from bot_bottle.git_gate import GitGatePlan
from bot_bottle.manifest import Manifest
from bot_bottle.pipelock import PipelockProxyPlan
from bot_bottle.supervise import SupervisePlan
from bot_bottle.workspace import workspace_plan
_URL = "http://supervise:9100/"
def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock:
bottle = MagicMock(spec=Bottle)
bottle.name = "bot-bottle-demo-abc12"
@@ -55,9 +51,13 @@ def _plan(
agent_prompt: str = "",
skills: list[str] | None = None,
agent_provision: AgentProvisionPlan | None = None,
supervise: bool = False,
) -> DockerBottlePlan:
bottle_json: dict = {"agent_provider": {"template": "codex"}}
if supervise:
bottle_json["supervise"] = True
manifest = Manifest.from_json_obj({
"bottles": {"dev": {}},
"bottles": {"dev": bottle_json},
"agents": {
"demo": {
"skills": list(skills or []),
@@ -67,27 +67,31 @@ def _plan(
},
})
spec = BottleSpec(
manifest=manifest,
agent_name="demo",
copy_cwd=False,
user_cwd="/tmp/x",
manifest=manifest, agent_name="demo",
copy_cwd=False, user_cwd="/tmp/x",
)
supervise_plan = None
if supervise:
supervise_plan = SupervisePlan(
slug="demo-abc12",
queue_dir=Path("/tmp/queue"),
current_config_dir=Path("/tmp/current-config"),
)
return DockerBottlePlan(
spec=spec,
stage_dir=Path("/tmp/stage"),
slug="demo-abc12",
container_name="bot-bottle-demo-abc12",
container_name_pinned=False,
image="bot-bottle-claude:latest",
image="bot-bottle-codex:latest",
derived_image="",
runtime_image="bot-bottle-claude:latest",
runtime_image="bot-bottle-codex:latest",
dockerfile_path="",
env_file=Path("/tmp/agent.env"),
forwarded_env={},
prompt_file=Path("/tmp/state/demo-abc12/agent/prompt.txt"),
proxy_plan=PipelockProxyPlan(
yaml_path=Path("/tmp/pipelock.yaml"),
slug="demo-abc12",
yaml_path=Path("/tmp/pipelock.yaml"), slug="demo-abc12",
),
git_gate_plan=GitGatePlan(
slug="demo-abc12",
@@ -102,105 +106,81 @@ def _plan(
routes=(),
token_env_map={},
),
supervise_plan=None,
supervise_plan=supervise_plan,
use_runsc=False,
agent_provision=agent_provision or AgentProvisionPlan(
template="claude",
command="claude",
prompt_mode="append_file",
image="",
dockerfile="",
guest_env={},
template="codex", command="codex", prompt_mode="read_prompt_file",
image="", dockerfile="", guest_env={},
),
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
)
class TestApplyPrompt(unittest.TestCase):
def test_cp_uses_bottle_cp_in(self):
class TestCodexProvisionPrompt(unittest.TestCase):
def test_cp_uses_bottle_cp_in_and_chowns(self):
bottle = _make_bottle()
apply_prompt(_plan(), bottle)
r = CodexAgentProvider().provision_prompt(
_plan(agent_prompt="hello"), bottle,
)
self.assertEqual("/home/node/.bot-bottle-prompt.txt", r)
bottle.cp_in.assert_called_once_with(
"/tmp/state/demo-abc12/agent/prompt.txt",
PROMPT_PATH,
"/home/node/.bot-bottle-prompt.txt",
)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("chown node:node" in s
and "/home/node/.bot-bottle-prompt.txt" in s
for s in scripts)
)
def test_returns_path_when_agent_has_prompt(self):
bottle = _make_bottle()
r = apply_prompt(_plan(agent_prompt="You are a helpful assistant."), bottle)
self.assertEqual(PROMPT_PATH, r)
def test_returns_none_when_agent_has_no_prompt(self):
bottle = _make_bottle()
r = apply_prompt(_plan(agent_prompt=""), bottle)
r = CodexAgentProvider().provision_prompt(_plan(agent_prompt=""), bottle)
self.assertIsNone(r)
bottle.cp_in.assert_called_once()
def test_chowns_to_node_after_copy(self):
bottle = _make_bottle()
apply_prompt(_plan(), bottle)
scripts = _exec_scripts(bottle)
self.assertTrue(any("chown node:node" in s and PROMPT_PATH in s for s in scripts))
self.assertTrue(any("chmod 600" in s and PROMPT_PATH in s for s in scripts))
class TestApplySkills(unittest.TestCase):
class TestCodexProvisionSkills(unittest.TestCase):
def test_noop_when_agent_has_no_skills(self):
bottle = _make_bottle()
apply_skills(_plan(skills=[]), bottle)
CodexAgentProvider().provision_skills(_plan(skills=[]), bottle)
bottle.cp_in.assert_not_called()
bottle.exec.assert_not_called()
def test_mkdir_plus_cp_per_skill(self):
bottle = _make_bottle()
with patch.object(
_provision_apply, "host_skill_dir", create=True,
side_effect=lambda n: f"/host/skills/{n}",
) if False else patch(
"bot_bottle.backend.util.host_skill_dir",
side_effect=lambda n: f"/host/skills/{n}",
), patch("bot_bottle._provision_apply.os.path.isdir", return_value=True):
apply_skills(_plan(skills=["init-prd", "verify"]), bottle)
scripts = _exec_scripts(bottle)
self.assertTrue(any("mkdir -p" in s and SKILLS_DIR in s for s in scripts))
cp_targets = {c.args[1] for c in bottle.cp_in.call_args_list}
self.assertEqual({
f"{SKILLS_DIR}/init-prd/",
f"{SKILLS_DIR}/verify/",
}, cp_targets)
self.assertEqual(
2,
sum(1 for s in scripts if "chown -R node:node" in s),
)
def test_missing_skill_dies(self):
bottle = _make_bottle()
with patch(
"bot_bottle.backend.util.host_skill_dir",
side_effect=lambda n: f"/host/skills/{n}",
), patch("bot_bottle._provision_apply.os.path.isdir", return_value=False):
with self.assertRaises(SystemExit):
apply_skills(_plan(skills=["init-prd"]), bottle)
), patch(
"bot_bottle.contrib.codex.agent_provider.os.path.isdir",
return_value=True,
):
CodexAgentProvider().provision_skills(
_plan(skills=["init-prd"]), bottle,
)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("mkdir -p" in s and "/home/node/.claude/skills" in s
for s in scripts)
)
bottle.cp_in.assert_called_once()
self.assertEqual(
"/home/node/.claude/skills/init-prd/",
bottle.cp_in.call_args.args[1],
)
class TestApplyProvision(unittest.TestCase):
"""The `dirs` / `pre_copy` / `files` / `verify` apply loop that
used to live in `provision_provider_auth`."""
class TestCodexProvision(unittest.TestCase):
"""Codex's declarative provision step: ~/.codex/ dir + config.toml
+ (optional) dummy-auth.json + `codex login status` verify."""
def test_noop_on_empty_provision_plan(self):
bottle = _make_bottle()
apply_provision(_plan(), bottle)
bottle.cp_in.assert_not_called()
bottle.exec.assert_not_called()
def test_codex_provision_creates_dir_and_copies_config(self):
def test_creates_dir_and_copies_config(self):
provision = AgentProvisionPlan(
template="codex",
command="codex",
template="codex", command="codex",
prompt_mode="read_prompt_file",
image="bot-bottle-codex:latest",
dockerfile="",
guest_env={},
image="", dockerfile="", guest_env={},
dirs=(AgentProvisionDir("/home/node/.codex"),),
files=(AgentProvisionFile(
Path("/tmp/codex-config.toml"),
@@ -208,7 +188,9 @@ class TestApplyProvision(unittest.TestCase):
),),
)
bottle = _make_bottle()
apply_provision(_plan(agent_provision=provision), bottle)
CodexAgentProvider().provision(
_plan(agent_provision=provision), bottle,
)
bottle.cp_in.assert_called_once_with(
"/tmp/codex-config.toml",
"/home/node/.codex/config.toml",
@@ -220,12 +202,9 @@ class TestApplyProvision(unittest.TestCase):
def test_runs_pre_copy_then_verify(self):
provision = AgentProvisionPlan(
template="codex",
command="codex",
template="codex", command="codex",
prompt_mode="read_prompt_file",
image="",
dockerfile="",
guest_env={},
image="", dockerfile="", guest_env={},
pre_copy=(AgentProvisionCommand(
("find", "/home/node/.codex", "-name", "*.sqlite", "-delete"),
"could not reset runtime db files",
@@ -236,24 +215,55 @@ class TestApplyProvision(unittest.TestCase):
),),
)
bottle = _make_bottle()
apply_provision(_plan(agent_provision=provision), bottle)
CodexAgentProvider().provision(
_plan(agent_provision=provision), bottle,
)
scripts = _exec_scripts(bottle)
self.assertTrue(any("find" in s and "-delete" in s for s in scripts))
self.assertTrue(any("runuser" in s and "codex login status" in s for s in scripts))
def test_dies_when_dir_creation_fails(self):
provision = AgentProvisionPlan(
template="codex",
command="codex",
template="codex", command="codex",
prompt_mode="read_prompt_file",
image="",
dockerfile="",
guest_env={},
image="", dockerfile="", guest_env={},
dirs=(AgentProvisionDir("/home/node/.codex"),),
)
bottle = _make_bottle(exec_result=ExecResult(1, "", "mkdir: nope\n"))
with self.assertRaises(SystemExit):
apply_provision(_plan(agent_provision=provision), bottle)
CodexAgentProvider().provision(
_plan(agent_provision=provision), bottle,
)
class TestCodexSuperviseMcp(unittest.TestCase):
def test_noop_when_supervise_disabled(self):
bottle = _make_bottle()
CodexAgentProvider().provision_supervise_mcp(
_plan(supervise=False), bottle, _URL,
)
bottle.exec.assert_not_called()
def test_runs_codex_mcp_add_as_node(self):
bottle = _make_bottle()
CodexAgentProvider().provision_supervise_mcp(
_plan(supervise=True), bottle, _URL,
)
bottle.exec.assert_called_once()
script = bottle.exec.call_args.args[0]
self.assertEqual("node", bottle.exec.call_args.kwargs.get("user"))
self.assertIn("codex mcp add", script)
self.assertIn("--transport http", script)
self.assertIn("supervise", script)
self.assertIn(_URL, script)
def test_logs_warning_on_failure_but_does_not_raise(self):
bottle = _make_bottle(
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
)
CodexAgentProvider().provision_supervise_mcp(
_plan(supervise=True), bottle, _URL,
)
if __name__ == "__main__":
-163
View File
@@ -1,163 +0,0 @@
"""Unit: contrib supervise MCP registration (PRD 0050).
Each provider plugin's `provision_supervise_mcp` runs the
provider's own CLI (`claude mcp add` / `codex mcp add`) inside the
agent guest to register the per-bottle supervise sidecar in the
provider's user config. The previous claude-only `provision_supervise`
modules under backend/{docker,smolmachines}/provision/supervise.py
covered this behavior pre-PRD-0050."""
from __future__ import annotations
import unittest
from pathlib import Path
from unittest.mock import MagicMock
from bot_bottle.agent_provider import AgentProvisionPlan
from bot_bottle.backend import Bottle, BottleSpec, ExecResult
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
from bot_bottle.contrib.claude.agent_provider import ClaudeAgentProvider
from bot_bottle.contrib.codex.agent_provider import CodexAgentProvider
from bot_bottle.egress import EgressPlan
from bot_bottle.git_gate import GitGatePlan
from bot_bottle.manifest import Manifest
from bot_bottle.pipelock import PipelockProxyPlan
from bot_bottle.supervise import SupervisePlan
from bot_bottle.workspace import workspace_plan
def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock:
bottle = MagicMock(spec=Bottle)
bottle.name = "bot-bottle-demo-abc12"
bottle.exec.return_value = (
exec_result if exec_result is not None
else ExecResult(returncode=0, stdout="", stderr="")
)
return bottle
def _plan(*, supervise: bool, template: str = "claude") -> DockerBottlePlan:
manifest = Manifest.from_json_obj({
"bottles": {
"dev": {"agent_provider": {"template": template},
**({"supervise": True} if supervise else {})},
},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
spec = BottleSpec(
manifest=manifest, agent_name="demo",
copy_cwd=False, user_cwd="/tmp/x",
)
supervise_plan = None
if supervise:
supervise_plan = SupervisePlan(
slug="demo-abc12",
queue_dir=Path("/tmp/queue"),
current_config_dir=Path("/tmp/current-config"),
)
return DockerBottlePlan(
spec=spec,
stage_dir=Path("/tmp/stage"),
slug="demo-abc12",
container_name="bot-bottle-demo-abc12",
container_name_pinned=False,
image="bot-bottle-claude:latest",
derived_image="",
runtime_image="bot-bottle-claude:latest",
dockerfile_path="",
env_file=Path("/tmp/agent.env"),
forwarded_env={},
prompt_file=Path("/tmp/prompt.txt"),
proxy_plan=PipelockProxyPlan(
yaml_path=Path("/tmp/pipelock.yaml"), slug="demo-abc12",
),
git_gate_plan=GitGatePlan(
slug="demo-abc12",
entrypoint_script=Path("/tmp/git-gate-entrypoint.sh"),
hook_script=Path("/tmp/git-gate-hook"),
access_hook_script=Path("/tmp/git-gate-access-hook"),
upstreams=(),
),
egress_plan=EgressPlan(
slug="demo-abc12",
routes_path=Path("/tmp/routes.yaml"),
routes=(),
token_env_map={},
),
supervise_plan=supervise_plan,
use_runsc=False,
agent_provision=AgentProvisionPlan(
template=template, command=template,
prompt_mode="append_file" if template == "claude" else "read_prompt_file",
image="", dockerfile="", guest_env={},
),
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
)
_URL = "http://supervise:9100/"
class TestClaudeSuperviseMcp(unittest.TestCase):
def test_noop_when_supervise_disabled(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_supervise_mcp(
_plan(supervise=False), bottle, _URL,
)
bottle.exec.assert_not_called()
def test_runs_claude_mcp_add_as_node(self):
bottle = _make_bottle()
ClaudeAgentProvider().provision_supervise_mcp(
_plan(supervise=True), bottle, _URL,
)
bottle.exec.assert_called_once()
script = bottle.exec.call_args.args[0]
self.assertEqual("node", bottle.exec.call_args.kwargs.get("user"))
self.assertIn("claude mcp add", script)
self.assertIn("--scope user", script)
self.assertIn("--transport http", script)
self.assertIn("supervise", script)
self.assertIn(_URL, script)
def test_logs_warning_on_failure_but_does_not_raise(self):
bottle = _make_bottle(
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
)
ClaudeAgentProvider().provision_supervise_mcp(
_plan(supervise=True), bottle, _URL,
)
class TestCodexSuperviseMcp(unittest.TestCase):
def test_noop_when_supervise_disabled(self):
bottle = _make_bottle()
CodexAgentProvider().provision_supervise_mcp(
_plan(supervise=False, template="codex"), bottle, _URL,
)
bottle.exec.assert_not_called()
def test_runs_codex_mcp_add_as_node(self):
bottle = _make_bottle()
CodexAgentProvider().provision_supervise_mcp(
_plan(supervise=True, template="codex"), bottle, _URL,
)
bottle.exec.assert_called_once()
script = bottle.exec.call_args.args[0]
self.assertEqual("node", bottle.exec.call_args.kwargs.get("user"))
self.assertIn("codex mcp add", script)
self.assertIn("--transport http", script)
self.assertIn("supervise", script)
self.assertIn(_URL, script)
def test_logs_warning_on_failure_but_does_not_raise(self):
bottle = _make_bottle(
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
)
CodexAgentProvider().provision_supervise_mcp(
_plan(supervise=True, template="codex"), bottle, _URL,
)
if __name__ == "__main__":
unittest.main()