PRD 0029: provision egress routes via AgentProvisionPlan #115
@@ -34,7 +34,6 @@ class AgentProviderRuntime:
|
||||
image: str
|
||||
dockerfile: str
|
||||
auth_role: str
|
||||
placeholder_env: str
|
||||
prompt_mode: PromptMode
|
||||
bypass_args: tuple[str, ...]
|
||||
resume_args: tuple[str, ...]
|
||||
@@ -100,7 +99,6 @@ _RUNTIMES = {
|
||||
image="bot-bottle-claude:latest",
|
||||
dockerfile=str(_REPO_ROOT / "Dockerfile.claude"),
|
||||
auth_role="claude_code_oauth",
|
||||
|
|
||||
placeholder_env="CLAUDE_CODE_OAUTH_TOKEN",
|
||||
prompt_mode="append_file",
|
||||
bypass_args=("--dangerously-skip-permissions",),
|
||||
resume_args=("--continue",),
|
||||
@@ -112,7 +110,6 @@ _RUNTIMES = {
|
||||
image="bot-bottle-codex:latest",
|
||||
dockerfile=str(_REPO_ROOT / "Dockerfile.codex"),
|
||||
auth_role="",
|
||||
placeholder_env="",
|
||||
prompt_mode="read_prompt_file",
|
||||
bypass_args=("--dangerously-bypass-approvals-and-sandbox",),
|
||||
resume_args=("resume", "--last"),
|
||||
@@ -125,6 +122,13 @@ def runtime_for(template: str) -> AgentProviderRuntime:
|
||||
return _RUNTIMES[template]
|
||||
|
||||
|
||||
def placeholder_env_for(template: str) -> str:
|
||||
|
didericis
commented
This doesn't need to be a separate function, and the organization is wrong/we want the code for a specific provider to be making the decision about placeholder envs (a function which switches on different provider templates is the wrong abstraction) This doesn't need to be a separate function, and the organization is wrong/we want the code for a specific provider to be making the decision about placeholder envs (a function which switches on different provider templates is the wrong abstraction)
|
||||
"""Return the provider auth placeholder env var name, or empty string."""
|
||||
if template == PROVIDER_CLAUDE:
|
||||
return "CLAUDE_CODE_OAUTH_TOKEN"
|
||||
return ""
|
||||
|
||||
|
||||
def agent_provision_plan(
|
||||
*,
|
||||
template: str,
|
||||
@@ -133,10 +137,13 @@ def agent_provision_plan(
|
||||
guest_home: str = "/home/node",
|
||||
guest_env: dict[str, str] | None = None,
|
||||
forward_host_credentials: bool = False,
|
||||
has_provider_auth: bool = False,
|
||||
manifest_egress_routes: tuple[EgressRoute, ...] = (),
|
||||
host_env: dict[str, str] | None = None,
|
||||
) -> AgentProvisionPlan:
|
||||
runtime = runtime_for(template)
|
||||
has_provider_auth = bool(runtime.auth_role) and any(
|
||||
runtime.auth_role in r.roles for r in manifest_egress_routes
|
||||
)
|
||||
resolved_guest_env = dict(guest_env or {})
|
||||
env_vars: dict[str, str] = {}
|
||||
dirs: list[AgentProvisionDir] = []
|
||||
@@ -193,6 +200,7 @@ def agent_provision_plan(
|
||||
"guest, but Codex did not accept it"
|
||||
)))
|
||||
if template == PROVIDER_CLAUDE and has_provider_auth:
|
||||
env_vars["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-placeholder"
|
||||
env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1"
|
||||
env_vars["DISABLE_ERROR_REPORTING"] = "1"
|
||||
|
||||
|
||||
@@ -168,19 +168,6 @@ def resolve_plan(
|
||||
# never lands on argv or in env_file) goes into one dict. Nothing
|
||||
# mutates the host os.environ.
|
||||
forwarded_env: dict[str, str] = dict(resolved.forwarded)
|
||||
# Some provider CLIs refuse to start without *some* credential
|
||||
# env var even when egress will strip + re-inject the real
|
||||
# Authorization header. For those providers, auth_role names the
|
||||
# route marker that enables a non-secret placeholder env. Codex is
|
||||
# intentionally absent here: it should use its device/ChatGPT login
|
||||
# state, and an OPENAI_API_KEY placeholder would force API-key auth.
|
||||
has_provider_auth = any(
|
||||
provider_runtime.auth_role
|
||||
and provider_runtime.auth_role in r.roles
|
||||
for r in egress_manifest_routes(bottle)
|
||||
)
|
||||
if has_provider_auth and provider_runtime.placeholder_env:
|
||||
forwarded_env[provider_runtime.placeholder_env] = "egress-placeholder"
|
||||
_write_env_file(resolved, env_file)
|
||||
prompt_file.write_text(agent.prompt)
|
||||
|
||||
@@ -191,7 +178,7 @@ def resolve_plan(
|
||||
state_dir=agent_dir,
|
||||
guest_home=os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node"),
|
||||
forward_host_credentials=provider.forward_host_credentials,
|
||||
has_provider_auth=has_provider_auth,
|
||||
manifest_egress_routes=egress_manifest_routes(bottle),
|
||||
host_env=dict(os.environ),
|
||||
)
|
||||
guest_env = dict(agent_provision.guest_env)
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from ..agent_provider import runtime_for
|
||||
from ..agent_provider import placeholder_env_for
|
||||
from ..log import info
|
||||
|
||||
|
||||
@@ -41,5 +41,5 @@ def visible_agent_env_names(
|
||||
think a real key is entering the agent, so hide only the active
|
||||
provider-owned placeholder.
|
||||
"""
|
||||
hidden = {runtime_for(agent_provider_template).placeholder_env}
|
||||
hidden = {placeholder_env_for(agent_provider_template)}
|
||||
return sorted({name for name in env_names if name and name not in hidden})
|
||||
|
||||
@@ -100,20 +100,6 @@ def resolve_plan(
|
||||
git_gate_dir.mkdir(parents=True, exist_ok=True)
|
||||
git_gate_plan = GitGate().prepare(bottle, slug, git_gate_dir)
|
||||
|
||||
# Some provider CLIs refuse to start without *some* credential
|
||||
# env var even when egress will strip + re-inject the real
|
||||
# Authorization header. For those providers, auth_role names the
|
||||
# route marker that enables a non-secret placeholder env. Codex is
|
||||
# intentionally absent here: it should use its device/ChatGPT login
|
||||
# state, and an OPENAI_API_KEY placeholder would force API-key auth.
|
||||
has_provider_auth = any(
|
||||
provider_runtime.auth_role
|
||||
and provider_runtime.auth_role in r.roles
|
||||
for r in egress_manifest_routes(bottle)
|
||||
)
|
||||
if has_provider_auth and provider_runtime.placeholder_env:
|
||||
guest_env[provider_runtime.placeholder_env] = "egress-placeholder"
|
||||
|
||||
# Prompt file is always written (mode 0o600) so the in-VM
|
||||
# path always exists. Content is the agent's `prompt`
|
||||
# field (markdown body) — empty for agents with no prompt.
|
||||
@@ -148,7 +134,7 @@ def resolve_plan(
|
||||
guest_home=os.environ.get("BOT_BOTTLE_GUEST_HOME", "/home/node"),
|
||||
guest_env=guest_env,
|
||||
forward_host_credentials=provider.forward_host_credentials,
|
||||
has_provider_auth=has_provider_auth,
|
||||
manifest_egress_routes=egress_manifest_routes(bottle),
|
||||
host_env=dict(os.environ),
|
||||
)
|
||||
merged_guest_env = dict(agent_provision.guest_env)
|
||||
|
||||
@@ -13,7 +13,7 @@ from bot_bottle.agent_provider import (
|
||||
agent_provision_plan,
|
||||
runtime_for,
|
||||
)
|
||||
from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF
|
||||
from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF, EgressRoute
|
||||
|
||||
|
||||
def _jwt(exp: int) -> str:
|
||||
@@ -24,15 +24,13 @@ def _jwt(exp: int) -> str:
|
||||
|
||||
|
||||
class TestAgentProviderRuntime(unittest.TestCase):
|
||||
def test_claude_keeps_oauth_placeholder(self):
|
||||
def test_claude_has_auth_role(self):
|
||||
runtime = runtime_for("claude")
|
||||
self.assertEqual("claude_code_oauth", runtime.auth_role)
|
||||
self.assertEqual("CLAUDE_CODE_OAUTH_TOKEN", runtime.placeholder_env)
|
||||
|
||||
def test_codex_does_not_inject_openai_api_key_placeholder(self):
|
||||
def test_codex_has_no_auth_role(self):
|
||||
runtime = runtime_for("codex")
|
||||
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:
|
||||
@@ -81,18 +79,19 @@ class TestAgentProviderRuntime(unittest.TestCase):
|
||||
self.assertEqual(1, len(plan.verify))
|
||||
self.assertIn("CODEX_HOME=/run/codex-home", plan.verify[0].argv)
|
||||
|
||||
def test_claude_with_provider_auth_disables_nonessential_traffic(self):
|
||||
def test_claude_with_provider_auth_sets_placeholder_and_disables_nonessential_traffic(self):
|
||||
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
|
||||
plan = agent_provision_plan(
|
||||
template="claude",
|
||||
dockerfile="/tmp/Dockerfile.claude",
|
||||
state_dir=Path(tmp),
|
||||
has_provider_auth=True,
|
||||
manifest_egress_routes=(EgressRoute(
|
||||
host="api.anthropic.com",
|
||||
roles=("claude_code_oauth",),
|
||||
),),
|
||||
)
|
||||
self.assertEqual(
|
||||
"1",
|
||||
plan.env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"],
|
||||
)
|
||||
self.assertEqual("egress-placeholder", plan.env_vars["CLAUDE_CODE_OAUTH_TOKEN"])
|
||||
self.assertEqual("1", plan.env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"])
|
||||
self.assertEqual("1", plan.env_vars["DISABLE_ERROR_REPORTING"])
|
||||
|
||||
def test_codex_forward_host_credentials_populates_egress_routes(self):
|
||||
|
||||
Reference in New Issue
Block a user
I'm not sure if auth_role needs to be declared in the agent runtime anymore either: in the original plan, we were going to have the user manually specify egress routes. But given all of the provider specific logic we already need to do in the agent provider and the fact that we've already started added to egress routes in there means we should probably be doing that for claude as well, NOT looking for this specific auth role on user defined egress routes