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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user