|
| 1 | +# Env Var API Key Priority Over Stored Key Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Make environment variable API keys (e.g. `OPENCODE_API_KEY`) take precedence over API keys stored via the interactive UI, so users can switch keys at launch time without touching stored configuration. |
| 6 | + |
| 7 | +**Architecture:** In the provider loading sequence inside `provider.ts`, the `// load apikeys` section currently runs after `// load env` and overwrites whatever env var key was loaded. The fix adds a 2-line guard: before merging a stored API key into a provider, check whether that provider's env vars already provided a key. If so, skip the stored key. |
| 8 | + |
| 9 | +**Tech Stack:** TypeScript, Effect, Bun test runner |
| 10 | + |
| 11 | +## Global Constraints |
| 12 | + |
| 13 | +- Run tests from `packages/opencode`, never from repo root |
| 14 | +- Use `bun test` (not `bun run test`) to run tests |
| 15 | +- Use `bun typecheck` (not `tsc`) for type checking |
| 16 | +- Do not move or restructure code blocks — only add the guard inside the existing loop |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +### Task 1: Write the failing test |
| 21 | + |
| 22 | +**Files:** |
| 23 | +- Modify: `packages/opencode/test/provider/provider.test.ts` (append at end of file) |
| 24 | + |
| 25 | +**Interfaces:** |
| 26 | +- Consumes: `Auth.Service` (already imported as `import { Auth } from "@/auth"`), `set` helper (already defined at line 36), `Global` from `@opencode-ai/core/global`, `Filesystem` from `@/util/filesystem` |
| 27 | +- Produces: test case `"env var takes precedence over stored API key"` |
| 28 | + |
| 29 | +- [ ] **Step 1: Append the new test to the test file** |
| 30 | + |
| 31 | +Open `packages/opencode/test/provider/provider.test.ts` and append at the very end: |
| 32 | + |
| 33 | +```typescript |
| 34 | +it.instance("env var takes precedence over stored API key", () => |
| 35 | + Effect.gen(function* () { |
| 36 | + // Set a stored API key for anthropic |
| 37 | + const auth = yield* Auth.Service |
| 38 | + yield* auth.set("anthropic", { type: "api", key: "stored-key" }) |
| 39 | + |
| 40 | + // Set an env var key — this should win |
| 41 | + yield* set("ANTHROPIC_API_KEY", "env-key") |
| 42 | + |
| 43 | + const providers = yield* list |
| 44 | + const anthropic = providers[ProviderV2.ID.anthropic] |
| 45 | + expect(anthropic).toBeDefined() |
| 46 | + // The loaded key should be the env var, not the stored key |
| 47 | + expect(anthropic.key).toBe("env-key") |
| 48 | + }), |
| 49 | +) |
| 50 | +``` |
| 51 | + |
| 52 | +- [ ] **Step 2: Run the test to verify it fails** |
| 53 | + |
| 54 | +```bash |
| 55 | +cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key" |
| 56 | +``` |
| 57 | + |
| 58 | +Expected: test **FAILS** — currently `anthropic.key` will be `"stored-key"` (stored key wins over env). |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +### Task 2: Implement the fix |
| 63 | + |
| 64 | +**Files:** |
| 65 | +- Modify: `packages/opencode/src/provider/provider.ts:1501-1512` |
| 66 | + |
| 67 | +**Interfaces:** |
| 68 | +- Consumes: `database` (already in scope — the provider registry with `env` arrays), `envs` (already in scope — loaded env vars map) |
| 69 | +- Produces: modified `// load apikeys` loop that skips stored keys when env var is already providing a key |
| 70 | + |
| 71 | +- [ ] **Step 1: Locate the `// load apikeys` loop** |
| 72 | + |
| 73 | +In `packages/opencode/src/provider/provider.ts`, find this block (around line 1501): |
| 74 | + |
| 75 | +```typescript |
| 76 | + // load apikeys |
| 77 | + const auths = yield* auth.all().pipe(Effect.orDie) |
| 78 | + for (const [id, provider] of Object.entries(auths)) { |
| 79 | + const providerID = ProviderV2.ID.make(id) |
| 80 | + if (disabled.has(providerID)) continue |
| 81 | + if (provider.type === "api") { |
| 82 | + mergeProvider(providerID, { |
| 83 | + source: "api", |
| 84 | + key: provider.key, |
| 85 | + }) |
| 86 | + } |
| 87 | + } |
| 88 | +``` |
| 89 | + |
| 90 | +- [ ] **Step 2: Add the env-priority guard** |
| 91 | + |
| 92 | +Replace only the inner `if (provider.type === "api")` block: |
| 93 | + |
| 94 | +```typescript |
| 95 | + // load apikeys |
| 96 | + const auths = yield* auth.all().pipe(Effect.orDie) |
| 97 | + for (const [id, provider] of Object.entries(auths)) { |
| 98 | + const providerID = ProviderV2.ID.make(id) |
| 99 | + if (disabled.has(providerID)) continue |
| 100 | + if (provider.type === "api") { |
| 101 | + const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean) |
| 102 | + if (envKey) continue |
| 103 | + mergeProvider(providerID, { |
| 104 | + source: "api", |
| 105 | + key: provider.key, |
| 106 | + }) |
| 107 | + } |
| 108 | + } |
| 109 | +``` |
| 110 | + |
| 111 | +The two new lines are: |
| 112 | +1. `const envKey = database[providerID]?.env.map((item) => envs[item]).find(Boolean)` — reuse the same env-var lookup already used in `// load env` |
| 113 | +2. `if (envKey) continue` — skip stored key when env var is present |
| 114 | + |
| 115 | +- [ ] **Step 3: Run type check** |
| 116 | + |
| 117 | +```bash |
| 118 | +cd packages/opencode && bun typecheck |
| 119 | +``` |
| 120 | + |
| 121 | +Expected: no errors. |
| 122 | + |
| 123 | +- [ ] **Step 4: Run the new test to verify it passes** |
| 124 | + |
| 125 | +```bash |
| 126 | +cd packages/opencode && bun test test/provider/provider.test.ts --test-name-pattern "env var takes precedence over stored API key" |
| 127 | +``` |
| 128 | + |
| 129 | +Expected: **PASS** |
| 130 | + |
| 131 | +- [ ] **Step 5: Run the full provider test suite to check for regressions** |
| 132 | + |
| 133 | +```bash |
| 134 | +cd packages/opencode && bun test test/provider/provider.test.ts |
| 135 | +``` |
| 136 | + |
| 137 | +Expected: all existing tests **PASS**. |
| 138 | + |
| 139 | +- [ ] **Step 6: Commit** |
| 140 | + |
| 141 | +```bash |
| 142 | +git add packages/opencode/src/provider/provider.ts packages/opencode/test/provider/provider.test.ts |
| 143 | +git commit -m "fix(core): env var api key takes precedence over stored key" |
| 144 | +``` |
0 commit comments