Skip to content

Commit ebc2610

Browse files
committed
docs(profile): document auth.type=profile + auto_login Beta + ant v1.5.0+
- README + ja/README: third union branch, profile snippet, active_config caveat, link to upstream PR anthropics/anthropic-cli#45. - docs/api-key-helper + ja: new "Profile-based authentication" section (Quick start, Auto-login bootstrap Beta, env-var migration), updated field table, 401/403 matrix gains a profile column, Recovery checklist gains every profile_load + auto_login error_class label. - docs/configuration + ja: profile credential_source value, profile_load reason row. - defaults.jsonnet (claude / codex): auth example block now lists the profile shape with the [Beta] auto_login variant. - examples/{claude,codex}/ccgate.jsonnet: commented-out profile + Beta variants alongside the existing provider block. All copy is honest about the Beta scope: ccgate does not save / restore active_config in this release; root fix is upstream. Refs #69
1 parent 8527898 commit ebc2610

10 files changed

Lines changed: 184 additions & 26 deletions

File tree

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ Project-local configs are loaded only when **not tracked by Git**.
252252
| `provider.name` | string | `"anthropic"` | Provider name. One of `"anthropic"`, `"openai"`, `"gemini"`. |
253253
| `provider.model` | string | `"claude-haiku-4-5"` | Model name. Examples: `claude-haiku-4-5` / `claude-sonnet-4-6` (anthropic), `gpt-5.4-nano-2026-03-17` (openai), `gemini-3-flash-preview` (gemini). When routing through a compatible proxy, use whatever model name the proxy exposes (e.g. `anthropic/claude-haiku-4-5`). |
254254
| `provider.base_url` | string | `""` | Override the provider's API base URL. Empty = use the SDK default. Use this to route through an OpenAI- / Anthropic-compatible proxy (LiteLLM proxy, Azure OpenAI, on-prem gateway, regional endpoint, ...). |
255-
| `provider.auth` | object (`{type, ...}`) | (omit = env var) | Discriminated union for short-lived / rotating credentials. `type=exec` (run a shell command), `type=file` (read a rotator-managed file). See [docs/api-key-helper.md](docs/api-key-helper.md) for full reference. |
255+
| `provider.auth` | object (`{type, ...}`) | (omit = env var) | Discriminated union for short-lived / rotating credentials. `type=exec` (run a shell command), `type=file` (read a rotator-managed file), `type=profile` (Anthropic-only, reads `ant auth login` credentials; `auto_login: true` is **Beta** and spawns `ant auth login` on credentials-missing). See [docs/api-key-helper.md](docs/api-key-helper.md) for full reference. |
256256
| `provider.timeout_ms` | int | `20000` | API timeout (ms). `0` = no timeout. |
257257
| `log_path` | string | `$XDG_STATE_HOME/ccgate/<target>/ccgate.log` | Log file path. Supports `~` for home directory. |
258258
| `log_disabled` | bool | `false` | Disable logging entirely |
@@ -329,7 +329,7 @@ Export the proxy's API key as `CCGATE_ANTHROPIC_API_KEY`. The Anthropic SDK appe
329329

330330
### Short-lived / rotating API keys
331331

332-
When the credential rotates faster than a static env var can keep up (AWS STS, Vertex ADC, OpenAI-compatible gateways with virtual keys, internal key brokers), use `provider.auth`. It's a discriminated union over two shapes — pick the one that matches your setup:
332+
When the credential rotates faster than a static env var can keep up (AWS STS, Vertex ADC, OpenAI-compatible gateways with virtual keys, internal key brokers), use `provider.auth`. It's a discriminated union over three shapes — pick the one that matches your setup:
333333

334334
```jsonnet
335335
// Run a shell helper to mint a credential on demand
@@ -356,18 +356,35 @@ When the credential rotates faster than a static env var can keep up (AWS STS, V
356356
},
357357
},
358358
}
359+
360+
// Or read credentials from an `ant auth login` profile (Anthropic only;
361+
// the SDK refreshes the access token on its own, ccgate stays out of the
362+
// credential path entirely)
363+
{
364+
provider: {
365+
name: 'anthropic',
366+
model: 'claude-haiku-4-5',
367+
auth: {
368+
type: 'profile',
369+
name: 'ccgate', // matches `ant auth login --profile ccgate`
370+
// auto_login: true, // [Beta] spawns `ant auth login` on miss; requires ant v1.5.0+
371+
},
372+
},
373+
}
359374
```
360375

361376
The helper / file content is one of:
362377

363378
- **JSON** `{"key":"sk-...","expires_at":"<RFC3339>"}` — for `auth.type=exec`, memoized in `$XDG_CACHE_HOME/ccgate/<target>/` and refreshed early.
364379
- **Plain string** — a single non-empty line, not cached.
365380

381+
`auth.type=profile` is different: ccgate hands the loaded profile to anthropic-sdk-go via `option.WithConfig`, and the SDK's refresh-token loop owns the credential lifecycle. ccgate also calls `option.WithoutEnvironmentDefaults` so a leftover `ANTHROPIC_API_KEY` cannot silently shadow your declared profile. After any `ant auth login` (auto-login or manual) check `ant auth status``ant` rewrites `<config_dir>/active_config` (shared with Claude Code) as a side effect of `--profile`; upstream PR [anthropics/anthropic-cli#45](https://github.com/anthropics/anthropic-cli/pull/45) adds a `--no-activate` flag to drop that side effect.
382+
366383
Resolution order: `provider.auth` (when configured) > `CCGATE_*_API_KEY` > `*_API_KEY`. When `auth` is configured ccgate **does not silently fall back to env vars on failure** — the hook falls through with `kind=credential_unavailable` instead.
367384

368385
ccgate also registers `std.native('env')(name)` (returns empty string for undefined) and `std.native('must_env')(name)` (raises a config-load error) as Jsonnet helpers, so any string field can pull values from the environment without ccgate-specific syntax.
369386

370-
See [docs/api-key-helper.md](docs/api-key-helper.md) for the full helper contract, runnable examples, account-aware caching via `auth.cache_key`, browser-based first-run auth, the 401/403 behaviour matrix, and the operational recovery checklist.
387+
See [docs/api-key-helper.md](docs/api-key-helper.md) for the full helper contract, runnable examples, account-aware caching via `auth.cache_key`, browser-based first-run auth, the profile auto-login Beta + `active_config` caveat, the 401/403 behaviour matrix, and the operational recovery checklist.
371388
## Default Rules
372389

373390
ccgate ships built-in default rules per target. They are always applied as the base; your global / project-local configs layer on top.

docs/api-key-helper.md

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ Linux, macOS, *BSD, and Windows are supported. The shell that runs the helper co
4545

4646
| Field | Type | Default | Description |
4747
|---|---|---|---|
48-
| Field | Type | Default | Description |
49-
|---|---|---|---|
50-
| `auth.type` | `"exec"` / `"file"` | (required when `auth` is set) | Resolution mode. |
48+
| `auth.type` | `"exec"` / `"file"` / `"profile"` | (required when `auth` is set) | Resolution mode. `profile` is Anthropic-only; see [Profile-based authentication](#profile-based-authentication-anthropic-only). |
5149
| `auth.command` | string | `""` | (`exec` only, required) Shell command; stdout is the credential. |
5250
| `auth.shell` | `"bash"` / `"powershell"` | `"bash"` | (`exec` only) Shell. `powershell` tries `pwsh` first, falls back to `powershell`. |
5351
| `auth.path` | string | `$XDG_STATE_HOME/ccgate/<target>/auth_key.json` | (`file` only) Credential file path. Omit to use the default. |
54-
| `auth.refresh_margin_ms` | int (ms) | `60000` | Treat credentials as expired this many ms before `expires_at`. `0` disables. |
55-
| `auth.timeout_ms` | int (ms) | `30000` | Hard cap on one Resolve call. `> 0`. |
52+
| `auth.name` | string | `""` | (`profile` only) Anthropic profile name. Empty/omitted lets the SDK resolve `$ANTHROPIC_PROFILE``<config_dir>/active_config``"default"`. Required when `auto_login=true`. |
53+
| `auth.auto_login` | bool | `false` | (`profile` only, **Beta**) When true, ccgate spawns `ant auth login --profile <name>` if the credentials file is missing at preflight. Requires `name` and `ant` v1.5.0+. |
54+
| `auth.refresh_margin_ms` | int (ms) | `60000` | Treat credentials as expired this many ms before `expires_at`. `0` disables. (`exec` / `file` only — `profile` delegates refresh to the SDK.) |
55+
| `auth.timeout_ms` | int (ms) | `30000` (`exec` / `file`); `360000` (`profile` + `auto_login`) | Hard cap on one Resolve call (`exec` / `file`) or on the `ant` subprocess (`profile` + `auto_login`, passed to `ant --timeout`; ccgate's CommandContext kill cap is `timeout_ms + 30000`). `> 0`. |
5656
| `auth.cache_key` | string | `""` | (`exec` only) Salt added to the cache fingerprint. See [Account isolation](#account-isolation). |
5757

5858
Relative paths in `auth.command` and `auth.path` resolve from the hook's working directory at fire time, not from the config file's directory.
@@ -86,6 +86,47 @@ Helpers like `gcloud auth print-access-token`, `aws sso login`, or an internal S
8686

8787
This pattern is supported. The default `auth.timeout_ms` (`30000`) covers most non-interactive helpers; for browser-based first-run flows that involve the user clicking through a consent screen, raise it to e.g. `120000` so the first Permission Request after a long idle does not surface as `reason=timeout`.
8888

89+
## Profile-based authentication (Anthropic only)
90+
91+
Anthropic provider only. The official `ant` CLI (`ant auth login` for browser-based OAuth, `ant profile activate <name>` to switch the active profile) writes credentials to `<config_dir>/credentials/<name>.json` (mode 0600). `<config_dir>` defaults to `~/.config/anthropic`. The Anthropic profile resolution chain (`$ANTHROPIC_PROFILE` → `<config_dir>/active_config` → `"default"`) is shared between the Go SDK, Claude Code, and the Claude Agent SDK ([wif-reference doc](https://platform.claude.com/docs/en/api/authentication/wif-reference)). The Anthropic SDK refreshes the access token itself using the stored refresh token; ccgate stays out of the credential path entirely and only disables the SDK's static-credential env autoload (`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`) so a leftover env var does not silently shadow your declared profile. Workload Identity Federation is supported only via a profile config that declares `authentication.type=oidc_federation`; env-only WIF (no profile) is out of scope today.
92+
93+
### Quick start
94+
95+
```sh
96+
brew install anthropics/tap/ant # or download a release from anthropics/anthropic-cli
97+
ant auth login --profile ccgate # opens a browser, writes ~/.config/anthropic/credentials/ccgate.json
98+
# add to ccgate.jsonnet:
99+
# provider: { ..., auth: { type: 'profile', name: 'ccgate' } }
100+
# tail -f $XDG_STATE_HOME/ccgate/<target>/ccgate.log
101+
# → expect: rg 'credential source selected.*source=profile.*profile_name_set=true'
102+
```
103+
104+
> **⚠ Recommended — non-default profile name on Claude Max / Pro**: `ant auth login` without `--profile` creates a `default` profile and points `<config_dir>/active_config` at it. Because Anthropic profile resolution is shared with Claude Code, reusing `default` for ccgate can route Claude Code's credential lookup through the same OAuth profile and shift it from your subscription onto pay-as-you-go API billing. Use a non-default profile name (e.g. `ccgate`) and declare it explicitly in ccgate.jsonnet.
105+
106+
### Auto-login bootstrap (opt-in, **Beta**)
107+
108+
**Prerequisites**: `ant` v1.5.0 or newer on `PATH`. Verify with `ant --version`.
109+
110+
```sh
111+
brew install anthropics/tap/ant
112+
ant --version # confirm v1.5.0+
113+
# add to ccgate.jsonnet:
114+
# provider: { ..., auth: { type: 'profile', name: 'ccgate', auto_login: true } }
115+
# the first hook fire after credentials go missing spawns
116+
# `ant auth login --profile ccgate --timeout 6m`
117+
# (a browser opens for OAuth approval)
118+
```
119+
120+
`auto_login: true` requires a non-empty `name`; ccgate validates this so the bootstrap never silently writes a `default` profile. `auth.timeout_ms` (default `360000` ms = 6 min) becomes the value passed to `ant --timeout`, and ccgate's CommandContext kill cap is that value + 30 s. Per-profile flock (target-agnostic; under `$XDG_STATE_HOME/ccgate/auto_login.<sha>.lock`) keeps two concurrent hook fires from racing the same browser callback. When ant is missing or fails, ccgate falls through with `kind=credential_unavailable, reason=profile_load`; `error_class` in the slog warning narrows the cause.
121+
122+
> **🚧 Beta — `active_config` side effect not mitigated**: `ant auth login --profile <name>` rewrites `<config_dir>/active_config` to `<name>` unconditionally, and `<config_dir>/active_config` is shared with Claude Code and the Claude Agent SDK. ccgate **does not** save / restore `active_config` in this release; mitigating the race in ccgate would itself be racy. The proper fix is upstream PR [anthropics/anthropic-cli#45](https://github.com/anthropics/anthropic-cli/pull/45) (`--no-activate` flag); once that lands, a follow-up ccgate PR will pass the new flag and drop this warning. After auto-login (or a manual `ant auth login`) always run `ant auth status` to confirm the active profile and, if needed, `ant profile activate <claude-profile>` to point Claude Code back. Also: `ant` is invoked via `PATH` lookup, so the binary your `PATH` resolves to must be the trusted upstream `ant` — declaring `auth.auto_login` opts you into trusting that lookup.
123+
124+
### Migrating from env-var auth
125+
126+
- The hook behaves exactly like before until you add `auth: { type: 'profile', ... }` (env var path stays in effect).
127+
- Once `auth.type=profile` is declared, ccgate disables the SDK's env autoload, so a leftover `ANTHROPIC_API_KEY` cannot silently shadow your profile.
128+
- Removing the `auth` block reverts to the env-var path immediately; no cache or state is left behind.
129+
89130
## Examples
90131

91132
### Wrap an existing env-var credential
@@ -194,10 +235,10 @@ ccgate emits a `slog.Warn` when `auth.path` *or* the cache file has any group/ot
194235

195236
When the provider rejects the credential ccgate just used, the HTTP status alone determines the reaction.
196237

197-
| HTTP status | `auth.type=exec` | `auth.type=file` | env var |
198-
|---------------------|-------------------------------------------|------------------------------------------|--------------|
199-
| 401 / 403 | `provider_auth`, **invalidate cache + fallthrough** | `provider_auth`, fallthrough only (no cache) | **exit 1** |
200-
| 5xx / 429 / network | exit 1 (existing behaviour) | exit 1 | exit 1 |
238+
| HTTP status | `auth.type=exec` | `auth.type=file` | `auth.type=profile` | env var |
239+
|---------------------|-------------------------------------------|------------------------------------------|------------------------------------------------------------------------|--------------|
240+
| 401 / 403 | `provider_auth`, **invalidate cache + fallthrough** | `provider_auth`, fallthrough only (no cache) | `provider_auth`, fallthrough (SDK refresh-token loop owns the credential, no ccgate cache) | **exit 1** |
241+
| 5xx / 429 / network | exit 1 (existing behaviour) | exit 1 | exit 1 | exit 1 |
201242

202243
The env-var path keeps the existing exit-1 behaviour on 401/403 because ccgate cannot rotate env vars; swallowing the rejection would hide a user-side configuration error.
203244

@@ -218,5 +259,19 @@ To opt out of caching, return JSON without `expires_at` (or plain string) and th
218259
4. For `expired`, compare the helper's `expires_at` with `date -u`. Clock skew or a broken TTL inside the helper is the usual cause.
219260
5. For `command_exit` on a fresh setup, check first whether the configured `auth.shell` binary is on `$PATH`. `bash` is universal on Linux / macOS; for `powershell`, at least one of `pwsh` (preferred) or `powershell` must resolve via `$PATH`. When ccgate cannot find the shell at all, the failure surfaces as `command_exit` from `os/exec`'s lookup error.
220261
6. For repeated `provider_auth` even after cache invalidation, the helper itself is producing a credential the provider rejects. Re-run the helper manually with the same shell ccgate would use — `bash -c "$your_command"`, or `pwsh -Command "$your_command"` / `powershell -Command "$your_command"` for `auth.shell: 'powershell'` — and inspect the stdout that reached the SDK.
262+
7. For `profile_load` (`auth.type=profile`), the slog `error_class` field narrows the cause:
263+
- `profile_config_missing`: run `ant auth login --profile <name>` to create the profile.
264+
- `profile_config_parse` / `profile_config_invalid`: restore `<config_dir>/configs/<name>.json` to mode `0644`, or delete it and re-run `ant auth login --profile <name>`.
265+
- `credentials_missing` (`auto_login=false`): run `ant auth login --profile <name>` to publish credentials.
266+
- `credentials_stat_failed`: the credentials file or its parent dir failed `os.Stat` for a reason other than "missing" (typically permissions). Restore mode 0700 on `<config_dir>/credentials/`.
267+
- `auto_login_requires_profile`: declare `auth.name` (the bootstrap requires a non-default profile name).
268+
- `credentials_path_auto_login_unsupported`: the profile config sets a custom `credentials_path`; auto-login only knows the SDK default. Either drop the custom path or set `auto_login: false`.
269+
- `ant_lock_unavailable`: another hook fire is bootstrapping the same profile; the next fire reads the published credentials.
270+
- `ant_not_found`: install `ant` (`brew install anthropics/tap/ant`) and confirm `ant --version` reports v1.5.0+.
271+
- `ant_lookup_failed`: the `ant` entry on `PATH` is not a working executable; check with `which ant` / `ls -l $(which ant)`.
272+
- `ant_timeout`: nobody approved the browser login within `auth.timeout_ms`; raise it or run `ant auth login --profile <name>` manually.
273+
- `ant_failed`: ant exited non-zero — re-run it manually to inspect the error.
274+
- `credentials_missing_after_login`: ant exited 0 but no credentials file appeared (rare ant bug). Run `ant auth status`.
275+
8. After any `ant auth login` (auto-login or manual), confirm `<config_dir>/active_config` with `ant auth status`. If Claude Code starts using an unexpected profile, run `ant profile activate <claude-profile>`. Upstream PR [anthropics/anthropic-cli#45](https://github.com/anthropics/anthropic-cli/pull/45) (`--no-activate`) will let ccgate stop touching `active_config` once it lands.
221276

222277
The full reason list is in [docs/configuration.md](configuration.md#reason-values-for-credential_unavailable).

0 commit comments

Comments
 (0)