Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

.nornir/claude-token-path.md

dwarves Claude pane — the token path

The dwarves GUI mounts a Claude tab (dwarves-gui/src/tabbed.rs → ClaudeTab) that hosts the shared facett_claude::ClaudePane (a pure facett-Elm leaf). The pane emits a ClaudeEffect::Send (data); a host (ClaudeTab) pumps that effect through an injected facett_claude::ClaudeTransport and feeds the reply back. That transport is the shared nornir-claude edda leaf (the ONE constellation Anthropic client), adapted onto the facett seam by the EddaBackend newtype (guide §10). This doc pins down where the Claude credential comes from.

Current state — offline by default, live behind a feature

ClaudeTab::offline() (the demo default) injects the leaf's nornir_claude::OfflineTransport through EddaBackend: it makes no live Anthropic call — a labelled (offline demo — no live Claude wired)… reply. The demo is robot-tested end-to-end (robot_claude_pane_drives_over_the_seam plus the L5 guard claude_tab_drives_the_shared_nornir_claude_leaf) with zero network and zero credential, so the send→reply loop is proven in code (no AI, no key), AND that the pane genuinely drives the shared leaf transport.

ClaudeTab::live() (behind dwarves-gui's claude-http feature, which maps to the leaf's http feature) injects nornir_claude::HttpTransport through the SAME EddaBackend — a live blocking POST to /v1/messages, credential resolved at send time. The ClaudeTab::new(transport) seam is unchanged; only the boxed transport differs. Build/run it with cargo run -p dwarves-gui --features claude-http.

The live token path — nornir_claude::resolve_credential

The leaf resolves its credential exactly the way the Anthropic SDKs and the ant CLI do — first match wins (nornir_claude::resolve_credential(session) / the pure resolve_credential_from(api_key, auth_token, session)); an unset ANTHROPIC_API_KEY does NOT mean "no credential":

  1. ANTHROPIC_API_KEY (env) — sent as the x-api-key header (Credential::ApiKey).
  2. ANTHROPIC_AUTH_TOKEN (env) — sent as Authorization: Bearer … plus the anthropic-beta: oauth-2025-04-20 header (Credential::Bearer; OAuth tokens go on Authorization, never x-api-key).
  3. Per-session token — threaded in as the session_key arg and used as x-api-key when neither env source is set. In dwarves this rung is filled from the vault (the key you configure in Settings) — see below.

The vault rung — WHERE YOU CONFIGURE IT (dwarves, claude-token)

Rung 3 used to be unwired in dwarves, so the two env vars were the ONLY way to give the app a credential and there was nowhere in the UI to configure one. That is closed:

The funnel pane takes the same label over {"claude_source": "…"} and shows "no Claude credential — configure it in Settings to expand ideas" when it is none, so a funnel that cannot expand ideas says so instead of silently producing nothing.

Coverage: claude_token unit tests (round-trip through a REAL temp vault + a not-plaintext-on-disk assert, the blank-key refusal, the pure ladder's precedence, the locked-vault degradation, a no-secret-material assert), VaultKeyed transport tests (the key reaches req.session_key; a blank is not a credential; an explicit key is not overwritten), and two robot-UI scenarios + matrix cells (claude_token_control_discoverable, claude_token_stored_in_vault_from_settings — the latter proving the key never enters state_json and is encrypted at rest).

Not yet in the leaf (deferred, edda D1): the ant auth login on-disk profile (~/.config/anthropic/) and Workload Identity Federation. Those need profile selection + short-lived-token refresh / a JWT exchange (heavier than a dep-light leaf carries); resolve_credential's session_key arg is the single insertion point when a consumer wants to thread them in. Until then the live path is the two env sources + the per-session paste.

A set-but-empty ANTHROPIC_API_KEY="" is treated as blank and falls through (the leaf trims + skips empty), so it does NOT wedge out a real token. If both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN are set the first (x-api-key) wins by precedence — unset it to rely on the bearer token.

L5 (reuse) — RESOLVED: consumes the shared nornir-claude leaf

The standing L5 debt is closed. dwarves no longer hand-rolls a Claude client: the bespoke OfflineClaudeTransport twin was retired, and ClaudeTab now CONSUMES the shared nornir-claude edda leaf (crate edda/crates/nornir-claude) — the ONE primitive carrying the offline stub, the live HTTP transport, and the credential-resolution chain. The only dwarves-side glue is EddaBackend (the ~3-line newtype in tabbed.rs) mapping facett_claude::WireMsg → nornir_claude::WireMsg and forwarding to the leaf's byte-identical send_wire. nornir's viz::claude::HttpTransport repoints at the same leaf (its adopting agent's task), so both brains share one client. See the shared claude-api reference for the exact SDK/HTTP shapes.

Observability

The pane folds its state under __dwarves_state()["claude"] (kind:"claude", turns, sending, last_assistant). The robot scenario drives set_tab → prompt_changed → send over the JSON seam and asserts the reply landed (offline transport, in code) — the same channel a live transport would answer on, so promoting to a live client needs no test rewrite, only a credential + a network-gated arm.