diff --git a/docs/superpowers/specs/2026-03-17-wraith-ai-copilot-design.md b/docs/superpowers/specs/2026-03-17-wraith-ai-copilot-design.md index ff48f81..d5ef4c2 100644 --- a/docs/superpowers/specs/2026-03-17-wraith-ai-copilot-design.md +++ b/docs/superpowers/specs/2026-03-17-wraith-ai-copilot-design.md @@ -77,13 +77,85 @@ This is NOT a chatbot sidebar. It's a second operator with the same access as th ## 3. AI Service Layer (`internal/ai/`) -### 3.1 Claude API Client +### 3.1 Authentication — OAuth PKCE (Max Subscription) + +Wraith authenticates against the user's Claude Max subscription via OAuth Authorization Code Flow with PKCE. No API key needed. No per-token billing. Same auth path as Claude Code, but with Wraith's own independent token set (no shared credential file, no race conditions). + +**OAuth Parameters:** + +| Parameter | Value | +|---|---| +| Authorize URL | `https://claude.ai/oauth/authorize` | +| Token URL | `https://platform.claude.com/v1/oauth/token` | +| Client ID | `9d1c250a-e61b-44d9-88ed-5944d1962f5e` | +| PKCE Method | S256 | +| Code Verifier | 32 random bytes, base64url (no padding) | +| Code Challenge | SHA-256(verifier), base64url (no padding) | +| Redirect URI | `http://localhost:{dynamic_port}/callback` | +| Scopes | `user:inference user:profile` | +| State | 32 random bytes, base64url | + +**Auth Flow:** + +``` +1. User clicks "Connect to Claude" in Wraith copilot settings +2. Wraith generates PKCE code_verifier + code_challenge +3. Wraith starts a local HTTP server on a random port +4. Wraith opens browser to: + https://claude.ai/oauth/authorize + ?code=true + &client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e + &response_type=code + &redirect_uri=http://localhost:{port}/callback + &scope=user:inference user:profile + &code_challenge={challenge} + &code_challenge_method=S256 + &state={state} +5. User logs in with their Anthropic/Claude account +6. Browser redirects to http://localhost:{port}/callback?code={auth_code}&state={state} +7. Wraith validates state, exchanges code for tokens: + POST https://platform.claude.com/v1/oauth/token + { + "grant_type": "authorization_code", + "code": "{auth_code}", + "redirect_uri": "http://localhost:{port}/callback", + "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e", + "code_verifier": "{verifier}", + "state": "{state}" + } +8. Response: { access_token, refresh_token, expires_in, scope } +9. Wraith encrypts tokens with vault and stores in SQLite settings: + - ai_access_token (vault-encrypted) + - ai_refresh_token (vault-encrypted) + - ai_token_expires_at (unix timestamp) +10. Done — copilot is authenticated +``` + +**Token Refresh (automatic, silent):** + +``` +When access_token is expired (checked before each API call): + POST https://platform.claude.com/v1/oauth/token + { + "grant_type": "refresh_token", + "refresh_token": "{decrypted_refresh_token}", + "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e", + "scope": "user:inference user:profile" + } + → New access_token + refresh_token stored in vault +``` + +**Implementation:** `internal/ai/oauth.go` — Go HTTP server for callback, PKCE helpers, token exchange, token refresh. Uses `pkg/browser` to open the authorize URL. + +**Fallback:** For users without a Max subscription, allow raw API key input (stored in vault). The client checks which auth method is configured and uses the appropriate header. + +### 3.2 Claude API Client Direct HTTP client — no Python sidecar, no external SDK. Pure Go. ```go type ClaudeClient struct { - apiKey string // decrypted from vault on demand + auth *OAuthManager // handles token refresh + auth header model string // configurable: claude-sonnet-4-5-20250514, etc. httpClient *http.Client baseURL string // https://api.anthropic.com @@ -91,9 +163,11 @@ type ClaudeClient struct { // SendMessage sends a messages API request with tool use + vision support. // Returns a streaming response channel for token-by-token delivery. -func (c *ClaudeClient) SendMessage(req *MessageRequest) (<-chan StreamEvent, error) +func (c *ClaudeClient) SendMessage(messages []Message, tools []Tool, systemPrompt string) (<-chan StreamEvent, error) ``` +**Auth header:** `Authorization: Bearer {access_token}` (from OAuth). Falls back to `x-api-key: {api_key}` if using raw API key auth. + **Message format:** Anthropic Messages API v1 (`/v1/messages`). **Streaming:** SSE (`stream: true`). Parse `event: content_block_delta`, `event: content_block_stop`, `event: message_delta`, `event: tool_use` events. Emit to frontend via Wails events. @@ -322,6 +396,8 @@ internal/ ai/ service.go # AIService — orchestrates everything service_test.go + oauth.go # OAuth PKCE flow — authorize, callback, token exchange, refresh + oauth_test.go client.go # ClaudeClient — HTTP + SSE to Anthropic API client_test.go tools.go # Tool definitions (JSON schema) @@ -432,8 +508,12 @@ Track cumulative token usage per day/month. When approaching the configured budg ## 10. Security Considerations -- **API key** stored in vault (same AES-256-GCM encryption as SSH keys) -- **API key never logged** — mask in all log output +- **OAuth tokens** stored in vault (same AES-256-GCM encryption as SSH keys). Access token + refresh token both encrypted at rest. +- **Tokens never logged** — mask in all log output. Only log token expiry times and auth status. +- **Token refresh is automatic and silent** — no user interaction needed after initial login. Refresh token rotation handled properly (new refresh token replaces old). +- **Independent from Claude Code** — Wraith has its own OAuth session. No shared credential files, no race conditions with other Anthropic apps. +- **Fallback API key** also stored in vault if used instead of OAuth. - **Conversation content** may contain sensitive data (terminal output, file contents, screenshots of desktops). Stored in SQLite alongside other encrypted data. Consider encrypting the messages JSON blob with the vault key. - **Tool access is unrestricted** — the XO has the same access as the Commander. This is by design. The human is always watching and can take control. - **No autonomous session creation without Commander context** — the XO can open sessions, but the connections (with credentials) were set up by the Commander +- **PKCE prevents token interception** — authorization code flow with S256 challenge ensures the code can only be exchanged by the app that initiated the flow