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
+19 -6
View File
@@ -11,7 +11,7 @@ import sys
from dataclasses import dataclass, field
from pathlib import Path
from ...agent_provider import PromptMode
from ...agent_provider import AgentProvisionPlan, PromptMode
from ...egress import EgressPlan
from ...git_gate import GitGatePlan
from ...log import info
@@ -52,10 +52,19 @@ class DockerBottlePlan(BottlePlan):
# is opt-in via the manifest's bottle.supervise field.
supervise_plan: SupervisePlan | None
use_runsc: bool
agent_command: str = "claude"
agent_prompt_mode: PromptMode = "append_file"
agent_provider_template: str = "claude"
codex_auth_file: Path | None = None
agent_provision: AgentProvisionPlan
@property
def agent_command(self) -> str:
return self.agent_provision.command
@property
def agent_prompt_mode(self) -> PromptMode:
return self.agent_provision.prompt_mode
@property
def agent_provider_template(self) -> str:
return self.agent_provision.template
def print(self, *, remote_control: bool) -> None:
"""Render the y/N preflight summary to stderr — compact form
@@ -75,7 +84,11 @@ class DockerBottlePlan(BottlePlan):
# upstream tokens in its own environ, so no token forwarding
# from the agent to the proxy is needed.
env_names = visible_agent_env_names(
sorted(set(bottle.env.keys()) | set(self.forwarded_env.keys())),
sorted(
set(bottle.env.keys())
| set(self.forwarded_env.keys())
| set(self.agent_provision.guest_env.keys())
),
agent_provider_template=self.agent_provider_template,
)
+2
View File
@@ -286,6 +286,8 @@ def _agent_service(plan: DockerBottlePlan) -> dict[str, Any]:
f"SSL_CERT_FILE={AGENT_CA_BUNDLE}",
f"REQUESTS_CA_BUNDLE={AGENT_CA_BUNDLE}",
]
for name, value in sorted(plan.agent_provision.guest_env.items()):
env.append(f"{name}={value}")
# Forwarded vars (OAuth token, manifest host-interpolations):
# bare name → inherits from compose-up process env, value
# never lands on argv or in the compose file.
+10 -9
View File
@@ -14,8 +14,7 @@ import os
from datetime import datetime, timezone
from pathlib import Path
from ...agent_provider import runtime_for
from ...codex_auth import write_codex_dummy_auth_file
from ...agent_provider import agent_provision_plan, runtime_for
from ...egress import Egress
from ...env import ResolvedEnv, resolve_env
from ...git_gate import GitGate
@@ -156,7 +155,6 @@ def resolve_plan(
agent_dir.mkdir(parents=True, exist_ok=True)
env_file = agent_dir / "agent.env"
prompt_file = agent_dir / "prompt.txt"
codex_auth_file = agent_dir / "codex-auth.json"
prompt_file.write_text("")
prompt_file.chmod(0o600)
@@ -221,12 +219,18 @@ def resolve_plan(
# error reporting) that egress can't gate by auth.
forwarded_env.setdefault("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1")
forwarded_env.setdefault("DISABLE_ERROR_REPORTING", "1")
if provider.forward_host_credentials:
write_codex_dummy_auth_file(codex_auth_file, dict(os.environ))
_write_env_file(resolved, env_file)
prompt_file.write_text(agent.prompt)
use_runsc = docker_mod.runsc_available()
agent_provision = agent_provision_plan(
template=provider.template,
dockerfile=dockerfile_path,
state_dir=agent_dir,
guest_home=os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node"),
forward_host_credentials=provider.forward_host_credentials,
host_env=dict(os.environ),
)
return DockerBottlePlan(
spec=spec,
@@ -246,10 +250,7 @@ def resolve_plan(
egress_plan=egress_plan,
supervise_plan=supervise_plan,
use_runsc=use_runsc,
agent_command=provider_runtime.command,
agent_prompt_mode=provider_runtime.prompt_mode,
agent_provider_template=provider.template,
codex_auth_file=codex_auth_file if provider.forward_host_credentials else None,
agent_provision=agent_provision,
)
@@ -2,87 +2,35 @@
from __future__ import annotations
import os
import shlex
import subprocess
from ..bottle_plan import DockerBottlePlan
_DEFAULT_GUEST_HOME = "/home/node"
def provision_provider_auth(plan: DockerBottlePlan, target: str) -> None:
"""Prepare Codex home state inside a Docker bottle.
"""Apply provider-owned guest setup through Docker primitives."""
provision = plan.agent_provision
for d in provision.dirs:
_exec(target, ["mkdir", "-p", d.guest_path])
_exec(target, ["chown", d.owner, d.guest_path])
_exec(target, ["chmod", d.mode, d.guest_path])
for command in provision.pre_copy:
_exec(target, list(command.argv))
for f in provision.files:
subprocess.run(
["docker", "cp", str(f.host_path), f"{target}:{f.guest_path}"],
stdout=subprocess.DEVNULL,
check=True,
)
_exec(target, ["chown", f.owner, f.guest_path])
_exec(target, ["chmod", f.mode, f.guest_path])
for command in provision.verify:
_exec(target, list(command.argv))
Every Codex bottle gets a minimal config.toml that trusts the
in-container launch directory. When host credentials are forwarded,
auth.json contains no real access or refresh token values; it only
nudges Codex into the same user/device auth branch as the host.
"""
if plan.agent_provider_template != "codex":
return
container_home = os.environ.get(
"BOT_BOTTLE_CONTAINER_HOME", _DEFAULT_GUEST_HOME,
)
auth_dir = f"{container_home}/.codex"
def _exec(target: str, argv: list[str]) -> None:
subprocess.run(
["docker", "exec", "-u", "0", target, "mkdir", "-p", auth_dir],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chown", "node:node", auth_dir],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chmod", "700", auth_dir],
stdout=subprocess.DEVNULL,
check=True,
)
config_path = f"{auth_dir}/config.toml"
config = (
f'[projects."{container_home}"]\n'
'trust_level = "trusted"\n'
)
subprocess.run(
[
"docker", "exec", "-u", "0", target,
"sh", "-c",
f"printf %s {shlex.quote(config)} > {shlex.quote(config_path)}",
],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chown", "node:node", config_path],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chmod", "600", config_path],
stdout=subprocess.DEVNULL,
check=True,
)
if not plan.codex_auth_file:
return
auth_path = f"{auth_dir}/auth.json"
subprocess.run(
["docker", "cp", str(plan.codex_auth_file), f"{target}:{auth_path}"],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chown", "node:node", auth_path],
stdout=subprocess.DEVNULL,
check=True,
)
subprocess.run(
["docker", "exec", "-u", "0", target, "chmod", "600", auth_path],
["docker", "exec", "-u", "0", target, *argv],
stdout=subprocess.DEVNULL,
check=True,
)