0efc07ba67
Closes #178. The backend provision functions now receive a Bottle handle with exec / cp_in methods instead of a raw target string. Provisioner modules use bottle.exec and bottle.cp_in in place of inlined subprocess.run(["docker", "exec"/"cp", ...]) and direct _smolvm.machine_cp / machine_exec calls. This decouples the provisioners from backend-specific runtime primitives so future refactors (e.g. the supervise rework) can swap the bottle's exec implementation without touching every provisioner. Each launch.py constructs the Bottle handle before calling provision so it can be passed in; provision_prompt's return value is wired back onto the bottle's prompt path attribute after the fact.
174 lines
5.6 KiB
Python
174 lines
5.6 KiB
Python
"""Unit: docker provider auth marker provisioning."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
from bot_bottle.agent_provider import (
|
|
AgentProvisionDir,
|
|
AgentProvisionFile,
|
|
AgentProvisionPlan,
|
|
)
|
|
from bot_bottle.backend import Bottle, BottleSpec, ExecResult
|
|
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
|
from bot_bottle.backend.docker.provision import provider_auth as _provider_auth
|
|
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.workspace import workspace_plan
|
|
|
|
|
|
def _plan(
|
|
*,
|
|
codex_auth_file: Path | None = None,
|
|
agent_provider_template: str = "codex",
|
|
) -> DockerBottlePlan:
|
|
manifest = Manifest.from_json_obj({
|
|
"bottles": {"dev": {"agent_provider": {"template": "codex"}}},
|
|
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
|
})
|
|
spec = BottleSpec(
|
|
manifest=manifest,
|
|
agent_name="demo",
|
|
copy_cwd=False,
|
|
user_cwd="/tmp/x",
|
|
)
|
|
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-codex:latest",
|
|
derived_image="",
|
|
runtime_image="bot-bottle-codex: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=None,
|
|
use_runsc=False,
|
|
agent_provision=_agent_provision(
|
|
agent_provider_template, codex_auth_file=codex_auth_file,
|
|
),
|
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
|
)
|
|
|
|
|
|
def _agent_provision(
|
|
template: str, *, codex_auth_file: Path | None = None,
|
|
) -> AgentProvisionPlan:
|
|
if template != "codex":
|
|
return AgentProvisionPlan(
|
|
template=template,
|
|
command=template,
|
|
prompt_mode="append_file",
|
|
image="",
|
|
dockerfile="",
|
|
guest_env={},
|
|
)
|
|
files = [
|
|
AgentProvisionFile(
|
|
Path("/tmp/codex-config.toml"),
|
|
"/home/node/.codex/config.toml",
|
|
),
|
|
]
|
|
if codex_auth_file is not None:
|
|
files.append(AgentProvisionFile(
|
|
codex_auth_file,
|
|
"/home/node/.codex/auth.json",
|
|
))
|
|
return AgentProvisionPlan(
|
|
template="codex",
|
|
command="codex",
|
|
prompt_mode="read_prompt_file",
|
|
image="bot-bottle-codex:latest",
|
|
dockerfile="",
|
|
guest_env={},
|
|
dirs=(AgentProvisionDir("/home/node/.codex"),),
|
|
files=tuple(files),
|
|
)
|
|
|
|
|
|
def _make_bottle(name: str = "bot-bottle-demo-abc12") -> MagicMock:
|
|
bottle = MagicMock(spec=Bottle)
|
|
bottle.name = name
|
|
bottle.exec.return_value = ExecResult(returncode=0, stdout="", stderr="")
|
|
return bottle
|
|
|
|
|
|
class TestProvisionProviderAuth(unittest.TestCase):
|
|
def test_noop_for_non_codex_provider(self):
|
|
bottle = _make_bottle()
|
|
_provider_auth.provision_provider_auth(
|
|
_plan(agent_provider_template="claude"), bottle,
|
|
)
|
|
self.assertEqual(0, bottle.cp_in.call_count)
|
|
self.assertEqual(0, bottle.exec.call_count)
|
|
|
|
def test_codex_provider_trusts_launch_dir_without_auth_file(self):
|
|
bottle = _make_bottle()
|
|
_provider_auth.provision_provider_auth(_plan(), bottle)
|
|
scripts = [c.args[0] for c in bottle.exec.call_args_list]
|
|
self.assertTrue(
|
|
any("mkdir -p" in s and "/home/node/.codex" in s for s in scripts)
|
|
)
|
|
cp_calls = [c.args for c in bottle.cp_in.call_args_list]
|
|
self.assertIn(
|
|
("/tmp/codex-config.toml", "/home/node/.codex/config.toml"),
|
|
cp_calls,
|
|
)
|
|
self.assertTrue(
|
|
any("chown" in s and "/home/node/.codex/config.toml" in s for s in scripts)
|
|
)
|
|
self.assertTrue(
|
|
any("chmod" in s and "/home/node/.codex/config.toml" in s for s in scripts)
|
|
)
|
|
|
|
def test_copies_dummy_auth_json_to_codex_home(self):
|
|
bottle = _make_bottle()
|
|
_provider_auth.provision_provider_auth(
|
|
_plan(codex_auth_file=Path("/tmp/codex-auth.json")),
|
|
bottle,
|
|
)
|
|
cp_calls = [c.args for c in bottle.cp_in.call_args_list]
|
|
self.assertIn(
|
|
("/tmp/codex-config.toml", "/home/node/.codex/config.toml"),
|
|
cp_calls,
|
|
)
|
|
self.assertIn(
|
|
("/tmp/codex-auth.json", "/home/node/.codex/auth.json"),
|
|
cp_calls,
|
|
)
|
|
scripts = [c.args[0] for c in bottle.exec.call_args_list]
|
|
self.assertTrue(
|
|
any("chown" in s and "/home/node/.codex/auth.json" in s for s in scripts)
|
|
)
|
|
self.assertTrue(
|
|
any("chmod" in s and "/home/node/.codex/auth.json" in s for s in scripts)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|