refactor(agent): group provider provisioning into plan

This commit is contained in:
2026-06-01 22:07:14 +00:00
committed by didericis
parent 36ce7aed4f
commit e808e81b87
13 changed files with 450 additions and 226 deletions
+57 -1
View File
@@ -2,9 +2,20 @@
from __future__ import annotations
import base64
import json
import tempfile
import unittest
from pathlib import Path
from bot_bottle.agent_provider import runtime_for
from bot_bottle.agent_provider import agent_provision_plan, runtime_for
def _jwt(exp: int) -> str:
def enc(obj: dict) -> str:
raw = json.dumps(obj, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
return f"{enc({'alg': 'none'})}.{enc({'exp': exp})}.sig"
class TestAgentProviderRuntime(unittest.TestCase):
@@ -18,6 +29,51 @@ class TestAgentProviderRuntime(unittest.TestCase):
self.assertEqual("", runtime.auth_role)
self.assertEqual("", runtime.placeholder_env)
def test_codex_plan_declares_home_state(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = agent_provision_plan(
template="codex",
dockerfile="/tmp/Dockerfile.codex",
state_dir=Path(tmp),
)
self.assertEqual("codex", plan.template)
self.assertEqual("codex", plan.command)
self.assertEqual("read_prompt_file", plan.prompt_mode)
self.assertEqual("/tmp/Dockerfile.codex", plan.dockerfile)
self.assertEqual(
"/etc/ssl/certs/ca-certificates.crt",
plan.guest_env["CODEX_CA_CERTIFICATE"],
)
self.assertEqual(("/home/node/.codex",), tuple(d.guest_path for d in plan.dirs))
self.assertEqual(
("/home/node/.codex/config.toml",),
tuple(f.guest_path for f in plan.files),
)
def test_codex_forward_host_credentials_adds_auth_and_verify(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
home = Path(tmp) / "host-codex"
home.mkdir()
(home / "auth.json").write_text(json.dumps({
"auth_mode": "chatgpt",
"tokens": {"access_token": _jwt(2000000000)},
}))
plan = agent_provision_plan(
template="codex",
dockerfile="",
state_dir=Path(tmp),
guest_env={"CODEX_HOME": "/run/codex-home"},
forward_host_credentials=True,
host_env={"CODEX_HOME": str(home)},
)
self.assertIn(
"/run/codex-home/auth.json",
{f.guest_path for f in plan.files},
)
self.assertEqual(1, len(plan.pre_copy))
self.assertEqual(1, len(plan.verify))
self.assertIn("CODEX_HOME=/run/codex-home", plan.verify[0].argv)
if __name__ == "__main__":
unittest.main()
+23
View File
@@ -14,6 +14,7 @@ import unittest
from pathlib import Path
from unittest import mock
from bot_bottle.agent_provider import AgentProvisionPlan
from bot_bottle.backend import BottleSpec
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
from bot_bottle.backend.docker.compose import (
@@ -180,6 +181,14 @@ def _plan(
egress_plan=_egress_plan(routes),
supervise_plan=_supervise_plan() if supervise else None,
use_runsc=False,
agent_provision=AgentProvisionPlan(
template="claude",
command="claude",
prompt_mode="append_file",
image="bot-bottle-claude:latest",
dockerfile="",
guest_env={},
),
)
@@ -250,6 +259,20 @@ class TestAgentAlwaysPresent(unittest.TestCase):
s = bottle_plan_to_compose(_plan())["services"]["agent"]
self.assertIn("CLAUDE_CODE_OAUTH_TOKEN", s["environment"])
def test_agent_provider_env_uses_literal_values(self):
plan = _plan()
provision = AgentProvisionPlan(
template="codex",
command="codex",
prompt_mode="read_prompt_file",
image="bot-bottle-codex:latest",
dockerfile="",
guest_env={"CODEX_HOME": "/home/node/.codex"},
)
plan = type(plan)(**{**vars(plan), "agent_provision": provision})
s = bottle_plan_to_compose(plan)["services"]["agent"]
self.assertIn("CODEX_HOME=/home/node/.codex", s["environment"])
def test_agent_runsc_runtime(self):
plan = _plan()
plan = type(plan)(**{**vars(plan), "use_runsc": True})
@@ -13,6 +13,7 @@ import unittest
from pathlib import Path
from unittest.mock import patch
from bot_bottle.agent_provider import AgentProvisionPlan
from bot_bottle.backend import BottleSpec
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
from bot_bottle.backend.docker.provision import git as _git
@@ -66,6 +67,14 @@ def _plan(*, git_user: dict | None = None,
),
supervise_plan=None,
use_runsc=False,
agent_provision=AgentProvisionPlan(
template="claude",
command="claude",
prompt_mode="append_file",
image="bot-bottle-claude:latest",
dockerfile="",
guest_env={},
),
)
@@ -6,6 +6,11 @@ import unittest
from pathlib import Path
from unittest.mock import patch
from bot_bottle.agent_provider import (
AgentProvisionDir,
AgentProvisionFile,
AgentProvisionPlan,
)
from bot_bottle.backend import BottleSpec
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
from bot_bottle.backend.docker.provision import provider_auth as _provider_auth
@@ -61,9 +66,44 @@ def _plan(
),
supervise_plan=None,
use_runsc=False,
agent_command="codex",
agent_provider_template=agent_provider_template,
codex_auth_file=codex_auth_file,
agent_provision=_agent_provision(
agent_provider_template, codex_auth_file=codex_auth_file,
),
)
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),
)
@@ -88,10 +128,12 @@ class TestProvisionProviderAuth(unittest.TestCase):
)
trust_config = next(
a for a in argvs
if a[:6] == ["docker", "exec", "-u", "0", "bot-bottle-demo-abc12", "sh"]
if a[:2] == ["docker", "cp"] and a[2] == "/tmp/codex-config.toml"
)
self.assertEqual(
"bot-bottle-demo-abc12:/home/node/.codex/config.toml",
trust_config[3],
)
self.assertIn('[projects."/home/node"]', trust_config[-1])
self.assertIn('trust_level = "trusted"', trust_config[-1])
self.assertIn(
["docker", "exec", "-u", "0", "bot-bottle-demo-abc12",
"chown", "node:node", "/home/node/.codex/config.toml"],
+93 -16
View File
@@ -13,6 +13,12 @@ from dataclasses import replace
from pathlib import Path
from unittest.mock import patch
from bot_bottle.agent_provider import (
AgentProvisionCommand,
AgentProvisionDir,
AgentProvisionFile,
AgentProvisionPlan,
)
from bot_bottle.backend import BottleSpec
from bot_bottle.backend.smolmachines.bottle_plan import (
SmolmachinesBottlePlan,
@@ -133,8 +139,69 @@ def _plan(
supervise_plan=supervise_plan,
agent_git_gate_host=agent_git_gate_host,
agent_supervise_url=agent_supervise_url,
codex_auth_file=codex_auth_file,
agent_provider_template=agent_provider_template,
agent_provision=_agent_provision(
agent_provider_template,
codex_auth_file=codex_auth_file,
guest_env=dict(guest_env or {}),
),
)
def _agent_provision(
template: str,
*,
codex_auth_file: Path | None = None,
guest_env: dict[str, str] | None = None,
) -> AgentProvisionPlan:
if template != "codex":
return AgentProvisionPlan(
template=template,
command=template,
prompt_mode="append_file",
image="",
dockerfile="",
guest_env=dict(guest_env or {}),
)
auth_dir = (guest_env or {}).get("CODEX_HOME", "/home/node/.codex")
files = [
AgentProvisionFile(
Path("/tmp/codex-config.toml"),
f"{auth_dir}/config.toml",
),
]
pre_copy: tuple[AgentProvisionCommand, ...] = ()
verify: tuple[AgentProvisionCommand, ...] = ()
if codex_auth_file is not None:
files.append(AgentProvisionFile(codex_auth_file, f"{auth_dir}/auth.json"))
pre_copy = (AgentProvisionCommand((
"find", auth_dir,
"-maxdepth", "1",
"-type", "f",
"(",
"-name", "*.sqlite",
"-o", "-name", "*.sqlite-*",
"-o", "-name", "*.codex-repair-*.bak",
")",
"-delete",
), "codex host credentials: could not reset runtime db files"),)
verify = (AgentProvisionCommand((
"runuser", "-u", "node", "--",
"env",
"HOME=/home/node",
f"CODEX_HOME={auth_dir}",
"codex", "login", "status",
), "codex host credentials: dummy auth was copied into the guest"),)
return AgentProvisionPlan(
template="codex",
command="codex",
prompt_mode="read_prompt_file",
image="bot-bottle-codex:latest",
dockerfile="",
guest_env=dict(guest_env or {}),
dirs=(AgentProvisionDir(auth_dir),),
files=tuple(files),
pre_copy=pre_copy,
verify=verify,
)
@@ -221,15 +288,12 @@ class TestProvisionProviderAuth(unittest.TestCase):
_plan(agent_provider_template="codex"),
"bot-bottle-demo-abc12",
)
self.assertEqual(0, cp.call_count)
cp.assert_called_once_with(
"/tmp/codex-config.toml",
"bot-bottle-demo-abc12:/home/node/.codex/config.toml",
)
argv_seen = [call.args[1] for call in ex.call_args_list]
self.assertIn(["mkdir", "-p", "/home/node/.codex"], argv_seen)
trust_config = next(
a for a in argv_seen
if a[:2] == ["sh", "-c"] and "config.toml" in a[2]
)
self.assertIn('[projects."/home/node"]', trust_config[2])
self.assertIn('trust_level = "trusted"', trust_config[2])
self.assertIn(
["chown", "node:node", "/home/node/.codex/config.toml"],
argv_seen,
@@ -247,9 +311,16 @@ class TestProvisionProviderAuth(unittest.TestCase):
),
"bot-bottle-demo-abc12",
)
cp.assert_called_once_with(
"/tmp/codex-auth.json",
"bot-bottle-demo-abc12:/home/node/.codex/auth.json",
cp_calls = [call.args for call in cp.call_args_list]
self.assertIn(
("/tmp/codex-config.toml",
"bot-bottle-demo-abc12:/home/node/.codex/config.toml"),
cp_calls,
)
self.assertIn(
("/tmp/codex-auth.json",
"bot-bottle-demo-abc12:/home/node/.codex/auth.json"),
cp_calls,
)
argv_seen = [call.args[1] for call in ex.call_args_list]
self.assertIn(["mkdir", "-p", "/home/node/.codex"], argv_seen)
@@ -303,9 +374,16 @@ class TestProvisionProviderAuth(unittest.TestCase):
),
"bot-bottle-demo-abc12",
)
cp.assert_called_once_with(
"/tmp/codex-auth.json",
"bot-bottle-demo-abc12:/run/codex-home/auth.json",
cp_calls = [call.args for call in cp.call_args_list]
self.assertIn(
("/tmp/codex-config.toml",
"bot-bottle-demo-abc12:/run/codex-home/config.toml"),
cp_calls,
)
self.assertIn(
("/tmp/codex-auth.json",
"bot-bottle-demo-abc12:/run/codex-home/auth.json"),
cp_calls,
)
argv_seen = [call.args[1] for call in ex.call_args_list]
self.assertIn(
@@ -343,7 +421,6 @@ class TestProvisionProviderAuth(unittest.TestCase):
SmolvmRunResult(0, "", ""), # chown CODEX_HOME
SmolvmRunResult(0, "", ""), # chmod CODEX_HOME
SmolvmRunResult(0, "", ""), # reset runtime db files
SmolvmRunResult(0, "", ""), # write config.toml
SmolvmRunResult(0, "", ""), # chown config.toml
SmolvmRunResult(0, "", ""), # chmod config.toml
SmolvmRunResult(0, "", ""), # chown auth.json