303 lines
9.1 KiB
Markdown
303 lines
9.1 KiB
Markdown
|
|
# API Key Manager — Implementation Plan
|
||
|
|
|
||
|
|
## Problem Statement
|
||
|
|
|
||
|
|
GSD has solid API key infrastructure (AuthStorage, OAuth flows, rate-limit backoff, multi-key rotation) but lacks a user-facing CLI for day-to-day key management. Users currently must either:
|
||
|
|
- Run the full onboarding wizard to add keys
|
||
|
|
- Manually edit `~/.gsd/agent/auth.json`
|
||
|
|
- Use the limited `/gsd setup keys` flow (only covers 5 tool keys, no LLM keys)
|
||
|
|
|
||
|
|
There's no way to list, test, remove, or inspect key health from the CLI.
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
|
||
|
|
Build `/gsd keys` — a comprehensive API key management command with subcommands:
|
||
|
|
|
||
|
|
```
|
||
|
|
/gsd keys → Show key status dashboard
|
||
|
|
/gsd keys list → List all configured keys with status
|
||
|
|
/gsd keys add <provider> → Add/replace a key for a provider
|
||
|
|
/gsd keys remove <provider> → Remove a key for a provider
|
||
|
|
/gsd keys test [provider] → Validate key(s) by making a lightweight API call
|
||
|
|
/gsd keys rotate <provider> → Remove old key and prompt for new one
|
||
|
|
/gsd keys doctor → Health check all keys (expired OAuth, empty keys, backoff state)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### New Files
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `src/resources/extensions/gsd/key-manager.ts` | Core key manager logic (list, add, remove, test, rotate, doctor) |
|
||
|
|
| `src/resources/extensions/gsd/tests/key-manager.test.ts` | Unit tests |
|
||
|
|
|
||
|
|
### Modified Files
|
||
|
|
|
||
|
|
| File | Change |
|
||
|
|
|------|--------|
|
||
|
|
| `src/resources/extensions/gsd/commands.ts` | Add `/gsd keys` subcommand routing + completions |
|
||
|
|
|
||
|
|
### No changes to core packages
|
||
|
|
|
||
|
|
All work stays in the GSD extension layer. We use `AuthStorage` as-is — no modifications to `pi-coding-agent` or `pi-ai`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 1: Key Status Dashboard (`/gsd keys` and `/gsd keys list`)
|
||
|
|
|
||
|
|
### What it shows
|
||
|
|
|
||
|
|
```
|
||
|
|
GSD API Key Manager
|
||
|
|
|
||
|
|
LLM Providers
|
||
|
|
✓ anthropic — OAuth (expires in 23h 41m)
|
||
|
|
✓ openai — API key (sk-...a4Bf)
|
||
|
|
✗ google — not configured
|
||
|
|
✗ groq — not configured (env: GROQ_API_KEY)
|
||
|
|
|
||
|
|
Tool Keys
|
||
|
|
✓ tavily — API key (tvly-...x92k)
|
||
|
|
✓ context7 — API key (c7-...m3np)
|
||
|
|
✗ brave — not configured (env: BRAVE_API_KEY)
|
||
|
|
✗ jina — not configured (env: JINA_API_KEY)
|
||
|
|
|
||
|
|
Remote Integrations
|
||
|
|
✓ discord_bot — API key (configured)
|
||
|
|
✗ slack_bot — not configured
|
||
|
|
✗ telegram_bot — not configured
|
||
|
|
|
||
|
|
Search Providers
|
||
|
|
✓ tavily — API key (tvly-...x92k)
|
||
|
|
|
||
|
|
Source: ~/.gsd/agent/auth.json
|
||
|
|
3 keys configured | 2 from env vars | 1 OAuth token
|
||
|
|
```
|
||
|
|
|
||
|
|
### Implementation
|
||
|
|
|
||
|
|
- Read all known provider IDs from `env-api-keys.ts` envMap + LLM_PROVIDER_IDS + TOOL_KEYS
|
||
|
|
- Check `authStorage.has()`, `authStorage.get()`, and `getEnvApiKey()` for each
|
||
|
|
- For API keys: show masked preview (first 4 + last 4 chars)
|
||
|
|
- For OAuth: show expiration time remaining
|
||
|
|
- For env vars: indicate source is environment
|
||
|
|
- Group by category (LLM, Tools, Remote, Search)
|
||
|
|
- Show backoff status if any keys are currently backed off
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 2: Add Key (`/gsd keys add <provider>`)
|
||
|
|
|
||
|
|
### Flow
|
||
|
|
|
||
|
|
1. If `<provider>` not specified → show interactive provider picker (grouped by category)
|
||
|
|
2. If provider has OAuth available → offer "Browser login" or "API key" choice
|
||
|
|
3. For API key: masked password input → prefix validation → save to auth.json
|
||
|
|
4. For OAuth: delegate to existing `authStorage.login()` flow
|
||
|
|
5. Confirm save with masked preview
|
||
|
|
|
||
|
|
### Provider Registry
|
||
|
|
|
||
|
|
Build a unified provider registry that merges:
|
||
|
|
- `LLM_PROVIDER_IDS` from onboarding.ts
|
||
|
|
- `TOOL_KEYS` from commands.ts
|
||
|
|
- `envMap` from env-api-keys.ts
|
||
|
|
- Remote bot tokens (discord_bot, slack_bot, telegram_bot)
|
||
|
|
|
||
|
|
Each entry has:
|
||
|
|
```typescript
|
||
|
|
interface ProviderInfo {
|
||
|
|
id: string
|
||
|
|
label: string
|
||
|
|
category: 'llm' | 'tool' | 'search' | 'remote'
|
||
|
|
envVar?: string // Known env var name
|
||
|
|
prefixes?: string[] // Expected key prefixes for validation
|
||
|
|
hasOAuth?: boolean // Whether OAuth login is available
|
||
|
|
dashboardUrl?: string // Where to get the key
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 3: Remove Key (`/gsd keys remove <provider>`)
|
||
|
|
|
||
|
|
### Flow
|
||
|
|
|
||
|
|
1. If `<provider>` not specified → show picker of configured keys only
|
||
|
|
2. Confirm removal (show what will be removed)
|
||
|
|
3. Call `authStorage.remove(provider)`
|
||
|
|
4. Clear corresponding env var from process.env
|
||
|
|
5. Notify success
|
||
|
|
|
||
|
|
### Multi-key handling
|
||
|
|
|
||
|
|
If a provider has multiple keys (round-robin), show:
|
||
|
|
```
|
||
|
|
anthropic has 3 API keys configured:
|
||
|
|
[1] sk-ant-...a4Bf
|
||
|
|
[2] sk-ant-...x92k
|
||
|
|
[3] sk-ant-...m3np
|
||
|
|
Remove: all | specific index?
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 4: Test Key (`/gsd keys test [provider]`)
|
||
|
|
|
||
|
|
### Validation Strategy
|
||
|
|
|
||
|
|
For each provider, make the lightest possible API call:
|
||
|
|
|
||
|
|
| Provider | Test Method |
|
||
|
|
|----------|------------|
|
||
|
|
| anthropic | `POST /v1/messages` with `max_tokens: 1` and a trivial prompt |
|
||
|
|
| openai | `GET /v1/models` (list models endpoint) |
|
||
|
|
| google | `GET /v1beta/models` |
|
||
|
|
| groq | `GET /openai/v1/models` |
|
||
|
|
| brave | `GET /res/v1/web/search?q=test&count=1` |
|
||
|
|
| tavily | `POST /search` with minimal params |
|
||
|
|
| context7 | Lightweight search query |
|
||
|
|
| jina | `GET /` health check |
|
||
|
|
| discord_bot | `GET /api/v10/users/@me` |
|
||
|
|
| slack_bot | `POST auth.test` |
|
||
|
|
| telegram_bot | `GET /getMe` |
|
||
|
|
|
||
|
|
### Output
|
||
|
|
|
||
|
|
```
|
||
|
|
Testing API keys...
|
||
|
|
|
||
|
|
✓ anthropic — valid (claude-sonnet-4-20250514 available) 142ms
|
||
|
|
✓ openai — valid (gpt-4o available) 89ms
|
||
|
|
✗ groq — invalid (401 Unauthorized)
|
||
|
|
✓ tavily — valid 203ms
|
||
|
|
⚠ brave — rate limited (retry in 28s)
|
||
|
|
— jina — skipped (not configured)
|
||
|
|
|
||
|
|
3 valid | 1 invalid | 1 rate-limited | 1 skipped
|
||
|
|
```
|
||
|
|
|
||
|
|
### Error Classification
|
||
|
|
|
||
|
|
- 401/403 → "invalid key"
|
||
|
|
- 429 → "rate limited (retry in Xs)"
|
||
|
|
- 5xx → "server error"
|
||
|
|
- timeout → "unreachable"
|
||
|
|
- success → "valid" + model info if available
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 5: Rotate Key (`/gsd keys rotate <provider>`)
|
||
|
|
|
||
|
|
### Flow
|
||
|
|
|
||
|
|
1. Show current key (masked)
|
||
|
|
2. Prompt for new key
|
||
|
|
3. Validate prefix format
|
||
|
|
4. Optionally test the new key before saving (`/gsd keys test` logic)
|
||
|
|
5. Replace in auth.json
|
||
|
|
6. Update process.env
|
||
|
|
7. Confirm
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 6: Key Doctor (`/gsd keys doctor`)
|
||
|
|
|
||
|
|
### Checks
|
||
|
|
|
||
|
|
1. **Expired OAuth tokens** — OAuth credentials past their expiration
|
||
|
|
2. **Empty keys** — Providers with empty string keys (from skipped onboarding)
|
||
|
|
3. **Duplicate keys** — Same key stored under multiple providers
|
||
|
|
4. **Missing required keys** — LLM provider not configured at all
|
||
|
|
5. **Backoff state** — Keys currently in rate-limit backoff
|
||
|
|
6. **Env var conflicts** — Key in auth.json differs from env var
|
||
|
|
7. **File permissions** — auth.json not 0o600
|
||
|
|
|
||
|
|
### Output
|
||
|
|
|
||
|
|
```
|
||
|
|
API Key Health Check
|
||
|
|
|
||
|
|
⚠ anthropic: OAuth token expires in 4m — will auto-refresh
|
||
|
|
✗ groq: empty key stored (from skipped setup) — run /gsd keys add groq
|
||
|
|
⚠ openai: env var OPENAI_API_KEY differs from auth.json — env takes priority
|
||
|
|
✗ auth.json permissions: 0644 (should be 0600) — fixing...
|
||
|
|
✓ No duplicate keys found
|
||
|
|
✓ No keys in backoff state
|
||
|
|
|
||
|
|
2 warnings | 1 issue fixed | 1 action needed
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Integration Points
|
||
|
|
|
||
|
|
### Command Registration (commands.ts)
|
||
|
|
|
||
|
|
Add to the `/gsd` subcommand router:
|
||
|
|
```typescript
|
||
|
|
if (trimmed === "keys" || trimmed.startsWith("keys ")) {
|
||
|
|
const keysArgs = trimmed.replace(/^keys\s*/, "").trim();
|
||
|
|
await handleKeys(keysArgs, ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Add tab completions for `keys` subcommands:
|
||
|
|
```typescript
|
||
|
|
if (parts[0] === "keys" && parts.length <= 2) {
|
||
|
|
// list, add, remove, test, rotate, doctor
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Redirect `/gsd setup keys`
|
||
|
|
|
||
|
|
Update `handleSetup` to route `keys` to the new handler instead of `handleConfig`.
|
||
|
|
|
||
|
|
### Help Text
|
||
|
|
|
||
|
|
Add to the help output in the appropriate category.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests (`key-manager.test.ts`)
|
||
|
|
|
||
|
|
1. **Provider registry** — All known providers have correct metadata
|
||
|
|
2. **Key masking** — Masks correctly for various key lengths
|
||
|
|
3. **Status formatting** — Dashboard output matches expected format
|
||
|
|
4. **Add key** — Stores via AuthStorage.inMemory()
|
||
|
|
5. **Remove key** — Removes correctly, handles multi-key providers
|
||
|
|
6. **Doctor checks** — Detects expired OAuth, empty keys, permission issues
|
||
|
|
7. **Test key result formatting** — Correct status symbols and messages
|
||
|
|
|
||
|
|
### Integration-level (manual)
|
||
|
|
|
||
|
|
- Full add → test → rotate → remove flow
|
||
|
|
- OAuth provider login flow
|
||
|
|
- Multi-key round-robin after adding multiple keys
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Order
|
||
|
|
|
||
|
|
1. **Phase 1** — `key-manager.ts` with provider registry + list/status dashboard
|
||
|
|
2. **Phase 2** — Add key (interactive picker + validation)
|
||
|
|
3. **Phase 3** — Remove key (with multi-key handling)
|
||
|
|
4. **Phase 4** — Test key (lightweight API calls per provider)
|
||
|
|
5. **Phase 5** — Rotate key (remove + add in one flow)
|
||
|
|
6. **Phase 6** — Key doctor (health checks)
|
||
|
|
7. **Wire up** — Command registration, completions, help text, redirect setup keys
|
||
|
|
8. **Tests** — Unit tests for all phases
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Out of Scope
|
||
|
|
|
||
|
|
- Encrypted-at-rest storage (would require a master password / keyring integration — separate effort)
|
||
|
|
- Per-project key scoping (would require project-level auth.json — separate effort)
|
||
|
|
- Key usage tracking/audit log (would require persistent metrics — separate effort)
|
||
|
|
- Changes to `pi-coding-agent` or `pi-ai` packages
|