feat(manifest): add agent_provider.auth_token for Claude OAuth via egress

Operators can now declare:

  agent_provider:
    template: claude
    auth_token: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN

and the provisioner injects a provider-owned api.anthropic.com egress
route (Bearer, tls_passthrough) rather than requiring a manually
declared route with the former claude_code_oauth role.

Changes:
- Add auth_token field to AgentProvider; validate claude-only.
- Remove claude_code_oauth from EGRESS_ROLES / PROVIDER_EGRESS_ROLES.
  Manifests that declare the role now fail at parse time with "unknown
  role" — the provisioner owns the route.
- agent_provision_plan: replace manifest_egress_routes/has_provider_auth
  with auth_token; Claude branch injects the api.anthropic.com route,
  placeholder env, and nonessential-traffic flags when auth_token is set.
- Add hidden_env_names: frozenset[str] to AgentProvisionPlan; Claude
  branch populates it with CLAUDE_CODE_OAUTH_TOKEN.
- Remove auth_role from AgentProviderRuntime and placeholder_env_for().
- print_util.visible_agent_env_names: accept hidden_env_names from the
  plan instead of dispatching on agent_provider_template.
- Both backends: drop manifest_egress_routes call, pass auth_token.
- PRD 0029 rescoped to cover both Codex and Claude provider auth.

Assisted-by: Claude Code
This commit is contained in:
2026-06-02 01:24:18 +00:00
committed by didericis
parent 952dcd7eec
commit de9bd7eb83
11 changed files with 136 additions and 113 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ class DockerBottlePlan(BottlePlan):
| set(self.forwarded_env.keys())
| set(self.agent_provision.guest_env.keys())
),
agent_provider_template=self.agent_provider_template,
hidden_env_names=self.agent_provision.hidden_env_names,
)
print(file=sys.stderr)
+2 -2
View File
@@ -16,7 +16,7 @@ from dataclasses import replace
from pathlib import Path
from ...agent_provider import agent_provision_plan, runtime_for
from ...egress import Egress, egress_manifest_routes
from ...egress import Egress
from ...env import ResolvedEnv, resolve_env
from ...git_gate import GitGate
from ...log import die
@@ -178,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,
manifest_egress_routes=egress_manifest_routes(bottle),
auth_token=provider.auth_token,
host_env=dict(os.environ),
)
guest_env = dict(agent_provision.guest_env)
+6 -10
View File
@@ -9,7 +9,6 @@ from __future__ import annotations
from typing import Sequence
from ..agent_provider import placeholder_env_for
from ..log import info
@@ -30,16 +29,13 @@ def print_multi(label: str, values: Sequence[str]) -> None:
def visible_agent_env_names(
env_names: Sequence[str], *, agent_provider_template: str,
env_names: Sequence[str], *, hidden_env_names: frozenset[str],
) -> list[str]:
"""Env names worth showing in launch summaries.
Provider auth placeholders (currently `CLAUDE_CODE_OAUTH_TOKEN`)
are implementation details: they are non-secret dummy values that
satisfy the provider CLI while egress injects the real upstream
Authorization header. Showing them in preflight makes the operator
think a real key is entering the agent, so hide only the active
provider-owned placeholder.
Provider-injected placeholder env vars are implementation details:
they are non-secret dummy values that satisfy provider CLIs while
egress injects the real Authorization header. The plan's
`hidden_env_names` carries exactly which names to suppress.
"""
hidden = {placeholder_env_for(agent_provider_template)}
return sorted({name for name in env_names if name and name not in hidden})
return sorted({name for name in env_names if name and name not in hidden_env_names})
@@ -125,7 +125,7 @@ class SmolmachinesBottlePlan(BottlePlan):
set(bottle.env.keys())
| set(self.agent_provision.guest_env.keys())
),
agent_provider_template=self.agent_provider_template,
hidden_env_names=self.agent_provision.hidden_env_names,
)
upstreams = [
f"{g.Name}{g.Upstream}" for g in bottle.git
+1 -2
View File
@@ -16,7 +16,6 @@ from dataclasses import replace
from pathlib import Path
from ...agent_provider import agent_provision_plan, runtime_for
from ...egress import egress_manifest_routes
from ...backend import BottleSpec
from ...backend.docker.bottle_state import (
BottleMetadata,
@@ -134,7 +133,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,
manifest_egress_routes=egress_manifest_routes(bottle),
auth_token=provider.auth_token,
host_env=dict(os.environ),
)
merged_guest_env = dict(agent_provision.guest_env)