diff --git a/README.md b/README.md index 9326be2..43bbfc3 100644 --- a/README.md +++ b/README.md @@ -284,20 +284,36 @@ as `CLAUDE_BOTTLE_OAUTH_TOKEN`: export CLAUDE_BOTTLE_OAUTH_TOKEN="" ``` -By default `cli.py` forwards the token into the agent container as -`CLAUDE_CODE_OAUTH_TOKEN`. Declare a `bottle.cred_proxy.routes` entry -with `role: "anthropic-base-url"` and `token_ref: -"CLAUDE_BOTTLE_OAUTH_TOKEN"` to route via cred-proxy instead: the -token then lives only in the cred-proxy sidecar's environ, the agent's -`ANTHROPIC_BASE_URL` points at the proxy, and `printenv` inside the -agent does not surface the real token. Either way the value is never -written to disk or placed on argv on the host. +The bottle reaches the Anthropic API only through the cred-proxy +sidecar. To let `claude` authenticate, declare a route in +`bottle.cred_proxy.routes` with `role: "anthropic-base-url"` and +`token_ref: "CLAUDE_BOTTLE_OAUTH_TOKEN"`: -Inside the container, `claude` picks up `CLAUDE_CODE_OAUTH_TOKEN` and -authenticates against your subscription. Caveats: the token is bound -to your subscription tier (Pro/Max/Team/Enterprise), it does not work -with `claude --bare` (which only reads `ANTHROPIC_API_KEY`), and if it -leaks, regenerate via `claude setup-token` again. Reference: +```jsonc +{ + "path": "/anthropic/", + "upstream": "https://api.anthropic.com", + "auth_scheme": "Bearer", + "token_ref": "CLAUDE_BOTTLE_OAUTH_TOKEN", + "role": "anthropic-base-url" +} +``` + +At launch, `cli.py` reads `CLAUDE_BOTTLE_OAUTH_TOKEN` from the host +env and forwards it into the cred-proxy container's environ — never +into the agent's. The agent receives `ANTHROPIC_BASE_URL` pointing at +`http://cred-proxy:9099/anthropic` and a non-secret placeholder for +`CLAUDE_CODE_OAUTH_TOKEN` (claude-code refuses to start without one; +the proxy strips and replaces the header on every request). `printenv` +inside the agent does not surface the real token, and the value is +never written to disk or placed on argv on the host. + +A bottle without an `anthropic-base-url` route has no path to the +Anthropic API — there is no fallback that forwards the token directly +to the agent. Caveats: the token is bound to your subscription tier +(Pro/Max/Team/Enterprise), it does not work with `claude --bare` +(which only reads `ANTHROPIC_API_KEY`), and if it leaks, regenerate +via `claude setup-token` again. Reference: . ## Trademarks diff --git a/claude_bottle/backend/__init__.py b/claude_bottle/backend/__init__.py index cb04496..45c65ac 100644 --- a/claude_bottle/backend/__init__.py +++ b/claude_bottle/backend/__init__.py @@ -53,7 +53,6 @@ class BottleSpec: agent_name: str copy_cwd: bool user_cwd: str - forward_oauth_token: bool @dataclass(frozen=True) diff --git a/claude_bottle/backend/docker/prepare.py b/claude_bottle/backend/docker/prepare.py index be0ddb5..8c23f38 100644 --- a/claude_bottle/backend/docker/prepare.py +++ b/claude_bottle/backend/docker/prepare.py @@ -118,17 +118,15 @@ def resolve_plan( forwarded_env: dict[str, str] = dict(resolved.forwarded) # Find the (at most one) cred-proxy route claiming the # anthropic-base-url role. Manifest validation enforces the - # singleton constraint. + # singleton constraint. cred-proxy is the only path the Anthropic + # OAuth token reaches the bottle — there is no fallback that + # forwards it into the agent's environ directly. Bottles that + # need claude-code to authenticate must declare an + # anthropic-base-url route. anthropic_route = next( (r for r in cred_proxy_plan.routes if "anthropic-base-url" in r.roles), None, ) - if spec.forward_oauth_token and anthropic_route is None: - # Pre-PRD 0010 behavior: agent reads CLAUDE_CODE_OAUTH_TOKEN - # directly. Still the path when no cred_proxy.routes entry - # is tagged anthropic-base-url; otherwise the sidecar holds - # the token. - forwarded_env["CLAUDE_CODE_OAUTH_TOKEN"] = os.environ["CLAUDE_BOTTLE_OAUTH_TOKEN"] if anthropic_route is not None: # Point claude-code at the cred-proxy. The sidecar holds the # OAuth token; the agent's environ does not. Strip the diff --git a/claude_bottle/cli/start.py b/claude_bottle/cli/start.py index 585bd75..a98e330 100644 --- a/claude_bottle/cli/start.py +++ b/claude_bottle/cli/start.py @@ -42,7 +42,6 @@ def cmd_start(argv: list[str]) -> int: agent_name=args.name, copy_cwd=args.cwd, user_cwd=USER_CWD, - forward_oauth_token=bool(os.environ.get("CLAUDE_BOTTLE_OAUTH_TOKEN")), ) stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage.")) diff --git a/docs/prds/0010-cred-proxy.md b/docs/prds/0010-cred-proxy.md index 35789ec..4d58c83 100644 --- a/docs/prds/0010-cred-proxy.md +++ b/docs/prds/0010-cred-proxy.md @@ -306,14 +306,16 @@ Why the agent can't reach the sidecar's environ: `CredProxyConfig`, `Bottle.cred_proxy: CredProxyConfig`. Parse + validate route shape, role enum, path uniqueness, singleton- role constraints. -- **`claude_bottle/backend/docker/prepare.py`** — switch the - agent's OAuth handling: when a route claims the - `anthropic-base-url` role, write `ANTHROPIC_BASE_URL` (pointing - at the proxy) plus a non-secret placeholder for +- **`claude_bottle/backend/docker/prepare.py`** — drop the + legacy `CLAUDE_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN` + forward entirely. cred-proxy is the only path the Anthropic + OAuth token reaches the bottle. When a route claims the + `anthropic-base-url` role, write `ANTHROPIC_BASE_URL` + (pointing at the proxy) plus a non-secret placeholder for `CLAUDE_CODE_OAUTH_TOKEN` (claude-code refuses to start otherwise; the proxy strips & replaces on every request). - When no such route exists, fall back to the pre-PRD-0010 path - (forward `CLAUDE_BOTTLE_OAUTH_TOKEN` as `CLAUDE_CODE_OAUTH_TOKEN`). + Bottles that need claude-code to authenticate must declare + the route; there is no fallback. - **`claude_bottle/backend/docker/backend.py`** — instantiate `DockerCredProxy` alongside `DockerPipelockProxy` and `DockerGitGate`; thread its `prepare` / `start` / `stop` diff --git a/tests/integration/test_pipelock_allow_node.py b/tests/integration/test_pipelock_allow_node.py index 1d68d57..20bf1d1 100644 --- a/tests/integration/test_pipelock_allow_node.py +++ b/tests/integration/test_pipelock_allow_node.py @@ -79,7 +79,6 @@ class TestPipelockAllowsNode(unittest.TestCase): agent_name="demo", copy_cwd=False, user_cwd=str(stage_dir), - forward_oauth_token=False, ) plan = backend.prepare(spec, stage_dir=stage_dir) with backend.launch(plan) as bottle: diff --git a/tests/integration/test_pipelock_allows_normal_https.py b/tests/integration/test_pipelock_allows_normal_https.py index 97b1732..41acabe 100644 --- a/tests/integration/test_pipelock_allows_normal_https.py +++ b/tests/integration/test_pipelock_allows_normal_https.py @@ -44,7 +44,6 @@ class TestPipelockAllowsNormalHttps(unittest.TestCase): agent_name="demo", copy_cwd=False, user_cwd=str(stage_dir), - forward_oauth_token=False, ) plan = backend.prepare(spec, stage_dir=stage_dir) with backend.launch(plan) as bottle: diff --git a/tests/integration/test_pipelock_block_node.py b/tests/integration/test_pipelock_block_node.py index ba95888..62708f2 100644 --- a/tests/integration/test_pipelock_block_node.py +++ b/tests/integration/test_pipelock_block_node.py @@ -75,7 +75,6 @@ class TestPipelockBlocksNode(unittest.TestCase): agent_name="demo", copy_cwd=False, user_cwd=str(stage_dir), - forward_oauth_token=False, ) plan = backend.prepare(spec, stage_dir=stage_dir) with backend.launch(plan) as bottle: diff --git a/tests/integration/test_pipelock_blocks_secret_https_post.py b/tests/integration/test_pipelock_blocks_secret_https_post.py index 92f9f80..2b597ae 100644 --- a/tests/integration/test_pipelock_blocks_secret_https_post.py +++ b/tests/integration/test_pipelock_blocks_secret_https_post.py @@ -63,7 +63,6 @@ class TestPipelockBlocksSecretHttpsPost(unittest.TestCase): agent_name="demo", copy_cwd=False, user_cwd=str(stage_dir), - forward_oauth_token=False, ) plan = backend.prepare(spec, stage_dir=stage_dir) with backend.launch(plan) as bottle: diff --git a/tests/integration/test_pipelock_blocks_secret_post.py b/tests/integration/test_pipelock_blocks_secret_post.py index 6d6fb72..8c58bb6 100644 --- a/tests/integration/test_pipelock_blocks_secret_post.py +++ b/tests/integration/test_pipelock_blocks_secret_post.py @@ -99,7 +99,6 @@ class TestPipelockBlocksSecretPost(unittest.TestCase): agent_name="demo", copy_cwd=False, user_cwd=str(stage_dir), - forward_oauth_token=False, ) plan = backend.prepare(spec, stage_dir=stage_dir) with backend.launch(plan) as bottle: diff --git a/tests/integration/test_pipelock_llm_passthrough.py b/tests/integration/test_pipelock_llm_passthrough.py index 7fecb3b..bca19b7 100644 --- a/tests/integration/test_pipelock_llm_passthrough.py +++ b/tests/integration/test_pipelock_llm_passthrough.py @@ -60,7 +60,6 @@ class TestPipelockLlmPassthrough(unittest.TestCase): agent_name="demo", copy_cwd=False, user_cwd=str(stage_dir), - forward_oauth_token=False, ) plan = backend.prepare(spec, stage_dir=stage_dir) with backend.launch(plan) as bottle: