AgentCanvas / Pages / Developer Guide / Core / Decisions / Platform / ADR-platform-006
2026-05-10
Date
2026-05-10
Status
accepted **Supersedes**: ADR-platform-003 (credential-resolution policy only β€” CLI / mtime-invalidation / schema_version mechanisms preserved) ## Context ADR-platform-003 introduced layered config resolution where `profile.api_key` was the primary credential source and env vars were a field-level fallback (`AGENTCANVAS_API_KEY` / `VLM_API_KEY`). In practice: - Users saved API keys via the SettingsModal UI, which wrote them into `~/.agentcanvas/profiles.json` (chmod 0o600). - `_fill_from_env` only filled *empty* profile fields, so any populated `profile.api_key` permanently shadowed env vars. - Over time profiles accumulated stale keys that no longer matched the user's current shell env. A 2026-05-10 incident: DiscussNav cost analysis revealed OpenAI billing was being charged to project A (`sk-proj-yrxc...` in profiles.json) while the user's intended billing project B (`sk-proj-ofqr...` in `OPENAI_API_KEY` shell env) had idle balance. The user had set `export OPENAI_API_KEY=...` expecting it to be authoritative, per the convention every other LLM tool in the ecosystem (OpenAI SDK, Anthropic SDK, litellm, langchain, aider, cursor, claude-code, codex) follows. Profiles.json living in `~/.agentcanvas/` (outside the repo) compounded the problem: the file couldn't enter version control or dotfile sync without leaking secrets, multi-machine setups required manual key sync, and the file's existence was invisible to project-level tooling. ## Decision 1. **`LLMProfile` no longer carries `api_key`.** The dataclass field is removed. Loaders silently strip legacy `api_key` from existing profiles.json files (one-time migration on next save). 2. **Per-provider standard env var.** `ProviderDef` gains an `env_var` field naming the de-facto standard env var for each provider: - `openai` β†’ `OPENAI_API_KEY` - `anthropic` β†’ `ANTHROPIC_API_KEY` - `google` β†’ `GEMINI_API_KEY` - `moonshot` β†’ `MOONSHOT_API_KEY` - `mistral` β†’ `MISTRAL_API_KEY` - `xai` β†’ `XAI_API_KEY` - `deepseek` β†’ `DEEPSEEK_API_KEY` - `huggingface` β†’ `HF_TOKEN` - …23 entries total, matching litellm's internal convention so explicit-key and litellm-default resolution agree. - `ollama` β†’ `""` (no key needed) 3. **`get_provider_api_key(provider_id)` is the single source of truth.** Reads `os.environ.get(reg.env_var)`. Used by `resolve_provider_config()`, `api/platform/profiles.py:_profile_to_dict` (for the `api_key_set` UI flag), and the model-discovery endpoint. 4. **Private fallbacks retired.** `AGENTCANVAS_API_KEY` / `VLM_API_KEY` aliases removed from `_fill_from_env` and `_env_only_config`. `custom` provider keeps `AGENTCANVAS_API_KEY` as its env var only because it has no vendor-standard name. 5. **profiles.json moved into the repo.** Default location switched from `~/.agentcanvas/profiles.json` to `/agentcanvas/backend/profiles.json` (alongside `requirements.txt`). Override available via `AGENTCANVAS_PROFILES_FILE` env var. File permission relaxed from 0o600 to default umask (file is now key-free and committable). 6. **CLI updated.** `python -m app.llm.cli set` lost `--api-key`; new `cli env` subcommand lists all provider env vars with a βœ“/Β· status indicator. `cli show ` displays the provider's `env_var` name. 7. **API request bodies updated.** `CreateProfileBody` / `UpdateProfileBody` lost the `api_key` field. `BatchUpsertBody.keys` accepted but ignored (back-compat for older frontends). The frontend's `LLMProfile` type only ever exposed `api_key_set: boolean`, so no UI breakage. ## Alternatives a. **Keep `profile.api_key`, flip precedence so env wins.** Rejected: the UI's "set API key" interaction continues to write into the profile, recreating the same drift. The precedence flip surprises users who previously relied on the UI as authoritative. b. **Profile is the single source of truth, drop env vars entirely.** Rejected: profiles.json then must stay outside git (security), can't sync across machines without manual copy, and breaks the universal "export `OPENAI_API_KEY`" muscle memory every developer already has from other tools. c. **Hybrid: per-profile per-environment binding.** Rejected: introduces a per-profile `env_var` override that solves a problem nobody has β€” every provider already has a single canonical env var name in industry practice. d. **OS keychain / 1Password CLI integration.** Out of scope: complementary, not alternative. Users who want keychain β†’ env injection can do that in `.bashrc`; the env-only contract stays the same. ## Rationale - **Single source of truth eliminates the shadowing-bug class.** With profileβ†’env field-level fallback, the precedence depended on whether profile.api_key was empty, which depended on UI history, which was effectively a hidden state machine. Pulling the field entirely makes "where does the key come from" trivially answerable: from `os.environ[reg.env_var]`, always. - **Convention alignment with the broader ecosystem.** litellm's auto-resolution, OpenAI SDK's `OPENAI_API_KEY` default, Anthropic's `ANTHROPIC_API_KEY` default, and every CLI tool in the LLM space converge on the same env var names. Aligning saves users from re-learning a project-specific config layer. - **profiles.json becomes safe to commit.** Git, dotfile syncs, screenshots, IDE remote-pair sessions β€” none of these leak secrets anymore. The file becomes a reviewable artefact of the deployment's provider/model choices. - **CLI / mtime-invalidation / RLock / schema_version mechanisms from ADR-platform-003 are preserved.** This ADR scopes only the credential-resolution policy change. The rest of the LLM config plumbing keeps working. ## Affected docs - `decisions/platform/adr-platform-003` β€” superseded (credential-resolution policy only; supporting infra preserved) - `architecture.md` β€” config resolution section updated - `roadmap.md` β€” credential-related TODOs (if any) closed - `agentcanvas/backend/profiles.json` β€” new committed default
Field
platform
Supersedes
ADR-platform-003