docs(agent): clarify claude oauth env
This commit is contained in:
@@ -313,7 +313,7 @@ egress:
|
|||||||
role: claude_code_oauth
|
role: claude_code_oauth
|
||||||
auth:
|
auth:
|
||||||
scheme: Bearer
|
scheme: Bearer
|
||||||
token_ref: BOT_BOTTLE_OAUTH_TOKEN
|
token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
|
||||||
---
|
---
|
||||||
|
|
||||||
Common Claude provider boundary.
|
Common Claude provider boundary.
|
||||||
@@ -389,11 +389,12 @@ Working examples live under `examples/`. Pipelock's design lives in
|
|||||||
rationale in `docs/research/pipelock-assessment.md`. The trust
|
rationale in `docs/research/pipelock-assessment.md`. The trust
|
||||||
boundary rationale lives in `docs/prds/0011-per-file-md-manifest.md`.
|
boundary rationale lives in `docs/prds/0011-per-file-md-manifest.md`.
|
||||||
|
|
||||||
## Auth: OAuth token, not API key
|
## Auth: Claude OAuth token, not API key
|
||||||
|
|
||||||
bot-bottle authenticates `claude` inside the container with the same
|
Bottles that use `agent_provider.template: claude` authenticate
|
||||||
Pro/Max subscription you already use on the host, via a long-lived OAuth
|
`claude` inside the container with the same Pro/Max subscription you
|
||||||
token. No `ANTHROPIC_API_KEY` is needed.
|
already use on the host, via a long-lived OAuth token. No
|
||||||
|
`ANTHROPIC_API_KEY` is needed.
|
||||||
|
|
||||||
**Why a token instead of mounting `~/.claude.json`:** on macOS, Claude
|
**Why a token instead of mounting `~/.claude.json`:** on macOS, Claude
|
||||||
Code stores OAuth credentials in the encrypted Keychain, not in
|
Code stores OAuth credentials in the encrypted Keychain, not in
|
||||||
@@ -409,28 +410,28 @@ claude setup-token # browser login, prints a ~1-year OAuth token
|
|||||||
```
|
```
|
||||||
|
|
||||||
Stash the token in your shell env (e.g. `~/.zshrc` or a secret manager)
|
Stash the token in your shell env (e.g. `~/.zshrc` or a secret manager)
|
||||||
as `BOT_BOTTLE_OAUTH_TOKEN`:
|
as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export BOT_BOTTLE_OAUTH_TOKEN="<token>"
|
export BOT_BOTTLE_CLAUDE_OAUTH_TOKEN="<token>"
|
||||||
```
|
```
|
||||||
|
|
||||||
The bottle reaches the Anthropic API only through the cred-proxy
|
The Claude bottle reaches the Anthropic API only through the cred-proxy
|
||||||
sidecar. To let `claude` authenticate, declare a route in
|
sidecar. To let `claude` authenticate, declare an egress route with
|
||||||
`bottle.cred_proxy.routes` with `role: "anthropic-base-url"` and
|
`role: claude_code_oauth` and
|
||||||
`token_ref: "BOT_BOTTLE_OAUTH_TOKEN"`:
|
`token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`:
|
||||||
|
|
||||||
```jsonc
|
```yaml
|
||||||
{
|
egress:
|
||||||
"path": "/anthropic/",
|
routes:
|
||||||
"upstream": "https://api.anthropic.com",
|
- host: api.anthropic.com
|
||||||
"auth_scheme": "Bearer",
|
role: claude_code_oauth
|
||||||
"token_ref": "BOT_BOTTLE_OAUTH_TOKEN",
|
auth:
|
||||||
"role": "anthropic-base-url"
|
scheme: Bearer
|
||||||
}
|
token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
|
||||||
```
|
```
|
||||||
|
|
||||||
At launch, `cli.py` reads `BOT_BOTTLE_OAUTH_TOKEN` from the host
|
At launch, `cli.py` reads `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` from the host
|
||||||
env and forwards it into the cred-proxy container's environ — never
|
env and forwards it into the cred-proxy container's environ — never
|
||||||
into the agent's. The agent receives `ANTHROPIC_BASE_URL` pointing at
|
into the agent's. The agent receives `ANTHROPIC_BASE_URL` pointing at
|
||||||
`http://cred-proxy:9099/anthropic` and a non-secret placeholder for
|
`http://cred-proxy:9099/anthropic` and a non-secret placeholder for
|
||||||
@@ -439,7 +440,7 @@ the proxy strips and replaces the header on every request). `printenv`
|
|||||||
inside the agent does not surface the real token, and the value is
|
inside the agent does not surface the real token, and the value is
|
||||||
never written to disk or placed on argv on the host.
|
never written to disk or placed on argv on the host.
|
||||||
|
|
||||||
A bottle without an `anthropic-base-url` route has no path to the
|
A Claude bottle without a `claude_code_oauth` route has no path to the
|
||||||
Anthropic API — there is no fallback that forwards the token directly
|
Anthropic API — there is no fallback that forwards the token directly
|
||||||
to the agent. Caveats: the token is bound to your subscription tier
|
to the agent. Caveats: the token is bound to your subscription tier
|
||||||
(Pro/Max/Team/Enterprise), it does not work with `claude --bare`
|
(Pro/Max/Team/Enterprise), it does not work with `claude --bare`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ entry and pushes straight at gitea/github with ssh-gate doing dumb
|
|||||||
L4 forwarding. There is no boundary between "the agent thinks this
|
L4 forwarding. There is no boundary between "the agent thinks this
|
||||||
commit is fine" and "the secret hits an external remote." If a
|
commit is fine" and "the secret hits an external remote." If a
|
||||||
compromised or careless agent stages a `.env`, slips a token into
|
compromised or careless agent stages a `.env`, slips a token into
|
||||||
a fixture, or commits the `BOT_BOTTLE_OAUTH_TOKEN` itself, `git
|
a fixture, or commits the `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` itself, `git
|
||||||
push` ships it.
|
push` ships it.
|
||||||
|
|
||||||
Host-side pre-commit / pre-push hooks are the usual defense, but
|
Host-side pre-commit / pre-push hooks are the usual defense, but
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as
|
|||||||
```
|
```
|
||||||
┌── Host (macOS) ──────────────────────────────────────────────────┐
|
┌── Host (macOS) ──────────────────────────────────────────────────┐
|
||||||
│ Secrets at rest (keychain / .env): │
|
│ Secrets at rest (keychain / .env): │
|
||||||
│ BOT_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN, │
|
│ BOT_BOTTLE_CLAUDE_OAUTH_TOKEN, GITHUB_TOKEN, │
|
||||||
│ GITEA_SERVER_TOKEN, NPM_TOKEN │
|
│ GITEA_SERVER_TOKEN, NPM_TOKEN │
|
||||||
│ │ docker run -e KEY (no =VALUE on argv) │
|
│ │ docker run -e KEY (no =VALUE on argv) │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
@@ -315,7 +315,7 @@ Why the agent can't reach the sidecar's environ:
|
|||||||
+ validate route shape, role enum, path uniqueness, singleton-
|
+ validate route shape, role enum, path uniqueness, singleton-
|
||||||
role constraints.
|
role constraints.
|
||||||
- **`bot_bottle/backend/docker/prepare.py`** — drop the
|
- **`bot_bottle/backend/docker/prepare.py`** — drop the
|
||||||
legacy `BOT_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN`
|
legacy `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN`
|
||||||
forward entirely. cred-proxy is the only path the Anthropic
|
forward entirely. cred-proxy is the only path the Anthropic
|
||||||
OAuth token reaches the bottle. When a route claims the
|
OAuth token reaches the bottle. When a route claims the
|
||||||
`anthropic-base-url` role, write `ANTHROPIC_BASE_URL`
|
`anthropic-base-url` role, write `ANTHROPIC_BASE_URL`
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ cred_proxy:
|
|||||||
- path: /anthropic/
|
- path: /anthropic/
|
||||||
upstream: https://api.anthropic.com
|
upstream: https://api.anthropic.com
|
||||||
auth_scheme: Bearer
|
auth_scheme: Bearer
|
||||||
token_ref: BOT_BOTTLE_OAUTH_TOKEN
|
token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
|
||||||
role: anthropic-base-url
|
role: anthropic-base-url
|
||||||
- path: /gitea/dideric/
|
- path: /gitea/dideric/
|
||||||
upstream: https://gitea.dideric.is
|
upstream: https://gitea.dideric.is
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ The remaining credible designs reduce to three:
|
|||||||
### Anthropic / Claude Code
|
### Anthropic / Claude Code
|
||||||
|
|
||||||
**Today's wiring** (`bot_bottle/cli/start.py`): the host's
|
**Today's wiring** (`bot_bottle/cli/start.py`): the host's
|
||||||
`BOT_BOTTLE_OAUTH_TOKEN` is forwarded into the bottle as
|
`BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` is forwarded into the bottle as
|
||||||
`CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e CLAUDE_CODE_OAUTH_TOKEN`
|
`CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e CLAUDE_CODE_OAUTH_TOKEN`
|
||||||
(no `=value`, so the value never lands on argv — good). Inside the
|
(no `=value`, so the value never lands on argv — good). Inside the
|
||||||
bottle, claude runs as `node` (UID 1000) with
|
bottle, claude runs as `node` (UID 1000) with
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ For a known-leaked or suspected-leaked token:
|
|||||||
1. Revoke the entry at `claude.ai/settings/claude-code`.
|
1. Revoke the entry at `claude.ai/settings/claude-code`.
|
||||||
2. Run "Log out all sessions" under Settings → Account → Active Sessions.
|
2. Run "Log out all sessions" under Settings → Account → Active Sessions.
|
||||||
3. Run `claude setup-token` to mint a replacement, and rotate it into
|
3. Run `claude setup-token` to mint a replacement, and rotate it into
|
||||||
`BOT_BOTTLE_OAUTH_TOKEN` immediately.
|
`BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` immediately.
|
||||||
4. Email Anthropic support at `support.anthropic.com`. Security issues
|
4. Email Anthropic support at `support.anthropic.com`. Security issues
|
||||||
sometimes get attention that GitHub issues do not.
|
sometimes get attention that GitHub issues do not.
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ already on the attacker's box. Detection has to be at *commit* time
|
|||||||
Two surfaces are exposed:
|
Two surfaces are exposed:
|
||||||
|
|
||||||
1. **The bot-bottle repo itself.** Development happens on a host
|
1. **The bot-bottle repo itself.** Development happens on a host
|
||||||
with `BOT_BOTTLE_OAUTH_TOKEN`, Gitea tokens, and other
|
with `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, Gitea tokens, and other
|
||||||
credentials in the environment. A fixture, test snapshot, log
|
credentials in the environment. A fixture, test snapshot, log
|
||||||
capture, or pasted-in debug output could carry one of them into a
|
capture, or pasted-in debug output could carry one of them into a
|
||||||
tracked file. The repo's Gitea remote is private, but mirrors or
|
tracked file. The repo's Gitea remote is private, but mirrors or
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ cred_proxy:
|
|||||||
- path: /anthropic/
|
- path: /anthropic/
|
||||||
upstream: https://api.anthropic.com
|
upstream: https://api.anthropic.com
|
||||||
auth_scheme: Bearer
|
auth_scheme: Bearer
|
||||||
token_ref: BOT_BOTTLE_OAUTH_TOKEN
|
token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
|
||||||
role: anthropic-base-url
|
role: anthropic-base-url
|
||||||
egress:
|
egress:
|
||||||
allowlist: [example.com]
|
allowlist: [example.com]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ on top of working onboarding.
|
|||||||
### Onboarding friction
|
### Onboarding friction
|
||||||
|
|
||||||
A first-time user today goes through five steps: install Docker,
|
A first-time user today goes through five steps: install Docker,
|
||||||
install `uv`, set `BOT_BOTTLE_OAUTH_TOKEN`, write
|
install `uv`, set `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, write
|
||||||
`bot-bottle.json`, run `./cli.py start`. One of those is
|
`bot-bottle.json`, run `./cli.py start`. One of those is
|
||||||
"author a JSON manifest." Polished tools in this category let
|
"author a JSON manifest." Polished tools in this category let
|
||||||
users skip that step on day one. The fix is an `init` subcommand
|
users skip that step on day one. The fix is an `init` subcommand
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ work:
|
|||||||
- **Typing latency.** Interactive Claude sessions over SSH have visible
|
- **Typing latency.** Interactive Claude sessions over SSH have visible
|
||||||
per-keystroke latency; usually fine on wired/fiber, less fine on
|
per-keystroke latency; usually fine on wired/fiber, less fine on
|
||||||
Wi-Fi-to-cloud. Mosh helps if it's bothersome.
|
Wi-Fi-to-cloud. Mosh helps if it's bothersome.
|
||||||
- **Token shipping.** `BOT_BOTTLE_OAUTH_TOKEN` has to live on the
|
- **Token shipping.** `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` has to live on the
|
||||||
remote box for the launcher to forward it into containers. Use the
|
remote box for the launcher to forward it into containers. Use the
|
||||||
provider's secret-injection path (cloud-init user-data,
|
provider's secret-injection path (cloud-init user-data,
|
||||||
`flyctl secrets`, Tailscale-served local file, etc.). Never echo the
|
`flyctl secrets`, Tailscale-served local file, etc.). Never echo the
|
||||||
@@ -130,7 +130,7 @@ The minimum-viable workflow, no bot-bottle code changes:
|
|||||||
(`curl -fsSL https://get.docker.com | sh`).
|
(`curl -fsSL https://get.docker.com | sh`).
|
||||||
3. SSH in.
|
3. SSH in.
|
||||||
4. `git clone` bot-bottle on the VM, drop a manifest in place,
|
4. `git clone` bot-bottle on the VM, drop a manifest in place,
|
||||||
inject `BOT_BOTTLE_OAUTH_TOKEN` via the provider's secrets path.
|
inject `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` via the provider's secrets path.
|
||||||
5. `./cli.py start <agent>` — the existing launcher handles the rest.
|
5. `./cli.py start <agent>` — the existing launcher handles the rest.
|
||||||
6. On exit: destroy the VM. No host artifacts persist.
|
6. On exit: destroy the VM. No host artifacts persist.
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ Build a custom OCI image `FROM docker:dind` that bakes in:
|
|||||||
the dind storage. Without this step, the first in-VM `docker build`
|
the dind storage. Without this step, the first in-VM `docker build`
|
||||||
runs `apt-get` and a global `npm install -g
|
runs `apt-get` and a global `npm install -g
|
||||||
@anthropic-ai/claude-code`, which adds 30–90 s to every cold start.
|
@anthropic-ai/claude-code`, which adds 30–90 s to every cold start.
|
||||||
- A `flyctl secrets`-injected `BOT_BOTTLE_OAUTH_TOKEN`, exposed to
|
- A `flyctl secrets`-injected `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, exposed to
|
||||||
the VM's PID 1 as an env var.
|
the VM's PID 1 as an env var.
|
||||||
- An entrypoint that starts dockerd, waits for it to be healthy, then
|
- An entrypoint that starts dockerd, waits for it to be healthy, then
|
||||||
either drops into a shell or directly runs `cli.py start <agent>`.
|
either drops into a shell or directly runs `cli.py start <agent>`.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ The agent's conversation channel is therefore wide open as an exfil
|
|||||||
path. A prompt-injected agent that has been told a secret can ship
|
path. A prompt-injected agent that has been told a secret can ship
|
||||||
it to Anthropic as conversation text, formatted however it likes,
|
it to Anthropic as conversation text, formatted however it likes,
|
||||||
and pipelock sees only `CONNECT api.anthropic.com:443`. The
|
and pipelock sees only `CONNECT api.anthropic.com:443`. The
|
||||||
`BOT_BOTTLE_OAUTH_TOKEN` itself rides this exact path.
|
`BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` itself rides this exact path.
|
||||||
|
|
||||||
### 3. Out-of-band channels exist regardless
|
### 3. Out-of-band channels exist regardless
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ per-bottle gate that:
|
|||||||
|
|
||||||
Two concrete instances worth implementing:
|
Two concrete instances worth implementing:
|
||||||
|
|
||||||
**Anthropic-API gate.** Holds `BOT_BOTTLE_OAUTH_TOKEN`. Agent's
|
**Anthropic-API gate.** Holds `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`. Agent's
|
||||||
`ANTHROPIC_BASE_URL` points at the gate; gate injects
|
`ANTHROPIC_BASE_URL` points at the gate; gate injects
|
||||||
`Authorization: Bearer …` and forwards to api.anthropic.com. The
|
`Authorization: Bearer …` and forwards to api.anthropic.com. The
|
||||||
token is no longer in the bottle's env. Once the token is out,
|
token is no longer in the bottle's env. Once the token is out,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ egress:
|
|||||||
role: claude_code_oauth
|
role: claude_code_oauth
|
||||||
auth:
|
auth:
|
||||||
scheme: Bearer
|
scheme: Bearer
|
||||||
token_ref: BOT_BOTTLE_OAUTH_TOKEN
|
token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
|
||||||
---
|
---
|
||||||
|
|
||||||
Common Claude provider boundary. Drop this file into
|
Common Claude provider boundary. Drop this file into
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ if ! command -v vhs >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${BOT_BOTTLE_OAUTH_TOKEN:-}" ]; then
|
if [ -z "${BOT_BOTTLE_CLAUDE_OAUTH_TOKEN:-}" ]; then
|
||||||
echo "demo-record: BOT_BOTTLE_OAUTH_TOKEN is unset; claude inside the bottle will not auth" >&2
|
echo "demo-record: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN is unset; claude inside the bottle will not auth" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,9 +14,9 @@ set -euo pipefail
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
if [ -z "${BOT_BOTTLE_OAUTH_TOKEN:-}" ]; then
|
if [ -z "${BOT_BOTTLE_CLAUDE_OAUTH_TOKEN:-}" ]; then
|
||||||
cat <<'EOF' >&2
|
cat <<'EOF' >&2
|
||||||
demo: BOT_BOTTLE_OAUTH_TOKEN is unset. The bottle launches claude,
|
demo: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN is unset. The bottle launches claude,
|
||||||
which needs the token to authenticate. Set it in your shell env (e.g.
|
which needs the token to authenticate. Set it in your shell env (e.g.
|
||||||
~/.zshrc) — see README §Auth — then re-run.
|
~/.zshrc) — see README §Auth — then re-run.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Reference in New Issue
Block a user