1443376268
Lift the provider-specific blocks of agent_provision_plan into
contrib/claude/agent_provider.py and contrib/codex/agent_provider.py,
behind a new AgentProvider ABC and a lazy get_provider() registry
(mirrors PRD 0048's contrib convention).
agent_provision_plan and runtime_for stay as thin shims so existing
callers in backend/{docker,smolmachines}/prepare.py and cli/start.py
keep working without per-call edits — the shipping diff in this commit
is purely 'who owns the producer'.
Adds bot_bottle/_provision_apply.py — the backend-agnostic
skills / prompt / declarative-plan apply loops the per-provider
default methods will dispatch through in the next commit.
176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
"""Codex agent provider plugin (PRD 0050, contrib).
|
|
|
|
The Codex-specific behavior previously inlined under
|
|
`agent_provider.agent_provision_plan` (config.toml trust marker,
|
|
chatgpt.com / api.openai.com egress routes, optional host-credential
|
|
forwarding with dummy-auth.json + verify), plus the `codex mcp add`
|
|
invocation that registers the supervise sidecar in Codex's
|
|
~/.codex/config.toml (PRD 0050)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ...agent_provider import (
|
|
CODEX_HOST_CREDENTIAL_HOSTS,
|
|
GUEST_HOME,
|
|
AgentProvider,
|
|
AgentProviderRuntime,
|
|
AgentProvisionCommand,
|
|
AgentProvisionDir,
|
|
AgentProvisionFile,
|
|
AgentProvisionPlan,
|
|
)
|
|
from ...codex_auth import codex_host_access_token, write_codex_dummy_auth_file
|
|
from ...egress import CODEX_HOST_CREDENTIAL_TOKEN_REF, EgressRoute
|
|
from ...log import info, warn
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from ...backend import Bottle, BottlePlan
|
|
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
_SUPERVISE_MCP_NAME = "supervise"
|
|
|
|
_RUNTIME = AgentProviderRuntime(
|
|
template="codex",
|
|
command="codex",
|
|
image="bot-bottle-codex:latest",
|
|
dockerfile=str(_REPO_ROOT / "Dockerfile.codex"),
|
|
prompt_mode="read_prompt_file",
|
|
bypass_args=("--dangerously-bypass-approvals-and-sandbox",),
|
|
resume_args=("resume", "--last"),
|
|
remote_control_args=(),
|
|
)
|
|
|
|
|
|
class CodexAgentProvider(AgentProvider):
|
|
@property
|
|
def runtime(self) -> AgentProviderRuntime:
|
|
return _RUNTIME
|
|
|
|
def provision_plan(
|
|
self,
|
|
*,
|
|
dockerfile: str,
|
|
state_dir: Path,
|
|
guest_home: str = GUEST_HOME,
|
|
guest_env: dict[str, str] | None = None,
|
|
auth_token: str = "",
|
|
forward_host_credentials: bool = False,
|
|
host_env: dict[str, str] | None = None,
|
|
trusted_project_path: str = "",
|
|
) -> AgentProvisionPlan:
|
|
del auth_token # Claude-only knob
|
|
resolved_guest_env = dict(guest_env or {})
|
|
trusted_path = trusted_project_path or guest_home
|
|
|
|
env_vars: dict[str, str] = {
|
|
"CODEX_CA_CERTIFICATE": "/etc/ssl/certs/ca-certificates.crt",
|
|
}
|
|
auth_dir = resolved_guest_env.get("CODEX_HOME", f"{guest_home}/.codex")
|
|
if forward_host_credentials:
|
|
env_vars["CODEX_HOME"] = auth_dir
|
|
|
|
dirs = [AgentProvisionDir(auth_dir)]
|
|
files: list[AgentProvisionFile] = []
|
|
pre_copy: list[AgentProvisionCommand] = []
|
|
verify: list[AgentProvisionCommand] = []
|
|
provisioned_env: dict[str, str] = {}
|
|
|
|
config_path = f"{auth_dir}/config.toml"
|
|
config_file = state_dir / "codex-config.toml"
|
|
toml_path = trusted_path.replace("\\", "\\\\").replace('"', '\\"')
|
|
config_file.write_text(
|
|
f'[projects."{toml_path}"]\n'
|
|
'trust_level = "trusted"\n'
|
|
)
|
|
config_file.chmod(0o600)
|
|
files.append(AgentProvisionFile(config_file, config_path))
|
|
|
|
egress_routes: list[EgressRoute] = []
|
|
for host in CODEX_HOST_CREDENTIAL_HOSTS:
|
|
egress_routes.append(EgressRoute(
|
|
host=host,
|
|
auth_scheme="Bearer" if forward_host_credentials else "",
|
|
token_ref=CODEX_HOST_CREDENTIAL_TOKEN_REF if forward_host_credentials else "",
|
|
tls_passthrough=True,
|
|
))
|
|
|
|
if forward_host_credentials:
|
|
_host_env = host_env or dict(os.environ)
|
|
provisioned_env[CODEX_HOST_CREDENTIAL_TOKEN_REF] = (
|
|
codex_host_access_token(_host_env)
|
|
)
|
|
auth_file = state_dir / "codex-auth.json"
|
|
write_codex_dummy_auth_file(auth_file, _host_env)
|
|
files.append(AgentProvisionFile(auth_file, f"{auth_dir}/auth.json"))
|
|
pre_copy.append(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.append(AgentProvisionCommand((
|
|
"runuser", "-u", "node", "--",
|
|
"env",
|
|
f"HOME={guest_home}",
|
|
f"CODEX_HOME={auth_dir}",
|
|
"codex", "login", "status",
|
|
), (
|
|
"codex host credentials: dummy auth was copied into the "
|
|
"guest, but Codex did not accept it"
|
|
)))
|
|
|
|
return AgentProvisionPlan(
|
|
template=_RUNTIME.template,
|
|
command=_RUNTIME.command,
|
|
prompt_mode=_RUNTIME.prompt_mode,
|
|
image=_RUNTIME.image,
|
|
dockerfile=dockerfile,
|
|
env_vars=env_vars,
|
|
guest_env=resolved_guest_env,
|
|
dirs=tuple(dirs),
|
|
files=tuple(files),
|
|
pre_copy=tuple(pre_copy),
|
|
verify=tuple(verify),
|
|
egress_routes=tuple(egress_routes),
|
|
provisioned_env=provisioned_env,
|
|
)
|
|
|
|
def provision_supervise_mcp(
|
|
self,
|
|
plan: "BottlePlan",
|
|
bottle: "Bottle",
|
|
supervise_url: str,
|
|
) -> None:
|
|
"""Run `codex mcp add` inside the agent guest to register the
|
|
supervise sidecar in Codex's user config (~/.codex/config.toml).
|
|
|
|
Mirrors the Claude provider's `claude mcp add` flow — failure
|
|
is logged but not fatal."""
|
|
if plan.supervise_plan is None:
|
|
return
|
|
info(f"registering supervise MCP server in agent codex config → {supervise_url}")
|
|
r = bottle.exec(
|
|
f"codex mcp add --transport http "
|
|
f"{_SUPERVISE_MCP_NAME} {supervise_url}",
|
|
user="node",
|
|
)
|
|
if r.returncode != 0:
|
|
warn(
|
|
f"`codex mcp add supervise` failed (exit {r.returncode}): "
|
|
f"{(r.stderr or r.stdout or '').strip()}. Inside the bottle, "
|
|
f"register manually with: "
|
|
f"codex mcp add --transport http supervise {supervise_url}"
|
|
)
|