Files
1jehuang--jcode/docs/AUTH_CREDENTIAL_SOURCES.md
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

128 lines
6.9 KiB
Markdown

# Auth Credential Sources (single source of truth)
This document exists because the same confusion keeps recurring: an agent (or a
human) greps for `ANTHROPIC_API_KEY` / `sk-ant-api`, finds nothing, reads a stale
`auth-validation.json` entry that says "expired", and wrongly concludes "there is
no working credential". In reality the credential is present and working; it just
lives somewhere the naive search did not look.
If you are debugging "does provider X have a credential?", read this first.
## The two "dual-auth" providers: Anthropic and OpenAI
Anthropic/Claude and OpenAI each support **two completely independent credential
paths**, surfaced as **two separate login providers**:
| Concept | Login provider id | Auth kind | Where the credential lives |
|--------------------|-------------------|-----------|-------------------------------------------------------------|
| Claude, OAuth/sub | `claude` | OAuth | `~/.jcode/auth.json``anthropic_accounts[].access` (`sk-ant-oat...`) |
| Claude, API key | `anthropic-api` | API key | `ANTHROPIC_API_KEY` env **or** `~/.config/jcode/anthropic.env` |
| OpenAI, OAuth | `openai` | OAuth | `~/.jcode/openai-auth.json` (Codex/ChatGPT login) |
| OpenAI, API key | `openai-api` | API key | `OPENAI_API_KEY` env **or** `~/.config/jcode/openai.env` |
Key facts that trip people up:
- The **OAuth token is not an API key.** Anthropic OAuth tokens are `sk-ant-oat01-...`
(and refresh tokens `sk-ant-ort01-...`). A direct API key is `sk-ant-api03-...`.
Grepping for `sk-ant-api` will miss an OAuth-only setup, and vice versa.
- The **API key is usually in the app config dir, not an env var.** The canonical
store is `~/.config/jcode/anthropic.env` (XDG `$XDG_CONFIG_HOME/jcode/anthropic.env`),
written by `jcode login --provider anthropic-api`. `printenv ANTHROPIC_API_KEY`
returning nothing does **not** mean there is no key.
- `~/.jcode/auth.json` holds **only OAuth accounts**, never the API key.
- `claude` and `anthropic-api` are **different providers** with different
availability. Having a Claude subscription login (OAuth) does **not** make
`anthropic-api` usable, and vice versa.
### How to actually check (don't guess)
```sh
# The honest, normalized answer for every provider:
jcode auth status --json
```
Each provider entry reports `status`, `auth_kind` ("OAuth" vs "API key"),
`credential_source` (env var / app config file / jcode-managed file), and the
exact `method`. This is the canonical surface; prefer it over grepping files.
Programmatically, the single source of truth is
`AuthStatus::assessment_for_provider(descriptor)` in
`crates/jcode-base/src/auth/mod.rs`, which returns a `ProviderAuthAssessment`.
## Selecting a default via config
`~/.jcode/config.toml`:
```toml
[provider]
default_provider = "claude" # Claude subscription (OAuth)
# default_provider = "anthropic-api" # Claude via direct Anthropic API key
default_model = "claude-opus-4-8"
anthropic_reasoning_effort = "xhigh"
```
- `default_provider = "claude"` uses the OAuth/subscription credential.
- `default_provider = "anthropic-api"` uses the direct API key. In this mode the
runtime **does not** fall back to OAuth: if no API key is configured the request
fails. Make sure `~/.config/jcode/anthropic.env` (or `ANTHROPIC_API_KEY`) exists.
The full alias/vocabulary mapping (runtime env, route stable-id, CLI `--provider`,
model prefix) is centralized in
`crates/jcode-provider-core/src/auth_mode.rs` (`AuthRoute`). Do not re-parse these
strings by hand; go through `AuthRoute`.
## Why "expired" was misleading: validation cache is not live state
`~/.jcode/auth-validation.json` caches the result of the **last** runtime
auth-test per provider. It is a historical record, not the current credential
state. An OAuth token that has since auto-refreshed can still show a days-old
"validation failed / expired" entry here.
To avoid presenting stale records as current fact, `format_record_label`
(`crates/jcode-base/src/auth/validation.rs`) flags any record older than
`doctor::VALIDATION_STALE_AFTER_MS` (7 days) as `stale, ... re-validate`. Treat a
stale record as "unknown, re-check", never as ground truth. Re-validate with:
```sh
jcode auth-test --provider <id>
```
## Quick decision tree for "is provider X authenticated?"
1. Run `jcode auth status --json` and read the entry for the **specific** login
provider id (`claude` vs `anthropic-api` are different!).
2. If you must inspect files: OAuth → `~/.jcode/auth.json` (and external imports);
API key → `ANTHROPIC_API_KEY` env or `~/.config/jcode/<provider>.env`.
3. Ignore `auth-validation.json` verdicts older than 7 days (shown as `stale`);
re-run `jcode auth-test` instead.
## Importing credentials from other agent tools
On a fresh install jcode can **reuse logins left behind by other coding
agents**, both OAuth tokens and API keys. Detection is consent-gated: jcode
lists the sources it found and only reads them after you approve each one
(`crates/jcode-base/src/auth/external.rs`, `unconsented_sources` /
`trust_external_auth_source`). Nothing is copied into jcode's own stores; the
external file is read in place.
Shared `auth.json`-style sources (`ExternalAuthSource`):
| Tool | Auth file path | On-disk shape |
|-----------|------------------------------------|------------------------------------------------------------------------------|
| OpenCode | `~/.local/share/opencode/auth.json`| flat `{ provider: { type: "oauth", access, refresh, expires } \| { type: "api", key } }` |
| pi | `~/.pi/agent/auth.json` | flat `{ provider: { type: "oauth", ... } \| { type: "api_key", key } }` (key may be `$ENV` ref) |
| OpenClaw | `~/.openclaw/agent/auth.json`, `~/.openclaw/agents/<id>/agent/auth-profiles.json`, `~/.openclaw/agents/<id>/agent/auth.json`, `~/.openclaw/credentials/oauth.json` | legacy flat pi shape, or the current `{ "profiles": { "<provider>:<name>": ... } }` store (first existing path wins; `main` agent and `:default` profiles preferred) |
| Hermes | `~/.hermes/auth.json` | nested `{ credential_pool: { provider: [ { auth_type, access_token, refresh_token, expires_at_ms } ] }, providers: {...} }` |
Notes:
- pi/OpenClaw API-key values that are `$ENV_VAR` references are resolved against
the environment; values that begin with `!` (shell commands) are **never
executed** and are skipped.
- Hermes stores literal API keys in the `access_token` field of `api_key`
credential-pool entries; many of its providers store only env-var *names*, so
those import nothing unless the env var is set.
- Other tool-specific importers exist for Claude Code, Codex, Gemini CLI,
GitHub Copilot, and Cursor (see `auth/claude.rs`, `auth/codex.rs`,
`auth/gemini.rs`, `auth/copilot.rs`, `auth/cursor.rs`).