docs: update AI copilot spec — OAuth PKCE auth against Max subscription (no API key)

This commit is contained in:
Vantz Stockwell 2026-03-17 08:55:40 -04:00
parent 1962d2c9bc
commit 1793576030

View File

@ -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