Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions docs/02-User-Guide/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,41 @@ Include your client API key in the Authorization header:
Authorization: Bearer cnp_live_YOUR_KEY
```

### Train & Account Headers
### Project & Account Headers

Use the `MSL-Project-Id` header to associate requests with a project/train, and optionally
`MSL-Account` to select a specific Anthropic account credential:
The `MSL-Project-Id` header is **mandatory** for all requests to identify the project:

```bash
MSL-Project-Id: project-alpha
MSL-Account: acc_abc123xyz # optional - account ID to override project default
MSL-Project-Id: project-alpha # REQUIRED
MSL-Account: acc_abc123xyz # optional - override account selection
```

**Account Selection Priority:**
**Authentication Priority:**

1. If `MSL-Account` header is present, use the specified account ID
2. Otherwise, use the project's configured default account
3. If no default account is configured, return an error
The proxy determines which Anthropic credentials to use in this order:

1. **User Passthrough Mode** (highest priority): If `Authorization: Bearer <token>` header contains an Anthropic API token, forward it directly to Anthropic API
2. **MSL-Account Header**: If present, use the specified organization account ID
3. **Project Default Account**: Use the project's configured default account
4. **Error**: If no credentials available, return error

### User Passthrough Mode

Projects can be configured to accept user-provided Anthropic credentials instead of using organization accounts:

```bash
MSL-Project-Id: my-project
Authorization: Bearer sk-ant-api03-YOUR_ANTHROPIC_TOKEN
```

In this mode:

- Your personal Anthropic API token is forwarded directly to Anthropic
- Billing goes to your Anthropic account (not organization account)
- Works regardless of project's default account configuration
- All client headers are preserved for maximum API compatibility

To obtain an Anthropic API token, visit: https://console.anthropic.com/settings/keys

## Endpoints

Expand Down
243 changes: 243 additions & 0 deletions docs/04-Architecture/ADRs/adr-030-user-credential-passthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# ADR-030: User Credential Passthrough Mode

**Status:** Accepted

**Date:** 2025-11-04

**Context:** Project-level authentication with user-provided credentials

**Decision Makers:** AI Agent

---

## Context

Currently, all projects must have a `default_account_id` configured, which references an organization-level Anthropic credential stored in the `anthropic_credentials` table. When users make API requests, the proxy uses this organization account to authenticate with Anthropic's API.

This model works well for centralized account management, but creates limitations:

1. **No support for individual user accounts**: Users cannot use their personal Anthropic accounts
2. **Billing complexity**: All usage is attributed to the organization account
3. **Access control limitations**: Cannot leverage user-specific Anthropic permissions
4. **Development friction**: Developers must use shared credentials instead of their own

We need a way for projects to operate in "user passthrough mode" where authenticated users provide their own Anthropic credentials via the `Authorization` header, and the proxy forwards these directly to Anthropic's API.

## Decision

We will implement **user credential passthrough mode** with the following design:

### 1. Optional Default Account

- The `default_account_id` column in the `projects` table remains nullable (already supported)
- When `default_account_id` is `null`, the project operates in user passthrough mode
- Existing projects continue to work unchanged (backward compatible)

### 2. Mandatory Project Identification

The `MSL-Project-Id` header is now **mandatory** for all requests:

- Requests without this header are rejected with a clear error message
- This ensures proper project identification for authentication lookup
- Required to determine if project uses organization account or user passthrough mode
- Removes fallback to default project ID for better security and explicit configuration

### 3. Authentication Priority

The `AuthenticationService.authenticate()` method follows this priority order:

1. **Bearer token in Authorization header** (highest priority) → user passthrough mode
2. **MSL-Account header** (explicit account override) → uses specified organization account
3. **Project default account** → uses `default_account_id` from database

**Rationale**: User-provided credentials always take precedence to ensure user choice is respected, even when MSL-Account header or default account are present. This allows users to use their own credentials regardless of project configuration.

### 4. Error Handling

When a project has no default account (`default_account_id = null`):

- If user provides `Authorization: Bearer <token>` → passthrough succeeds
- If user provides no Authorization header → returns clear error:
```
No default account configured for this project and no user credentials provided.
Either set a default account via the dashboard, or provide your Anthropic credentials via Authorization header.
```

### 5. Dashboard Integration

**Project Creation:**

- Add dropdown: "👤 User Account (passthrough mode)" + organization accounts
- Default selection remains random organization account (backward compatible)
- Users can explicitly select "User Account" for new projects

**Project Management:**

- Display "👤 User Account" badge when `default_account_id = null`
- Allow switching between User Account and organization accounts
- Add explanatory text about passthrough mode

### 6. Implementation Details

**Migration:** `015-user-passthrough-support.ts`

- Ensures `default_account_id` column is nullable
- Adds column documentation explaining passthrough behavior
- No data migration needed (idempotent)

**Type Updates:**

- `CreateProjectRequest.default_account_id?: string | null`
- `UpdateProjectRequest.default_account_id?: string | null`

**Query Updates:**

- `createProject()`: Accepts explicit `null` for passthrough mode
- `updateProject()`: Allows changing `default_account_id` to `null`

**Request Context:**

- `RequestContext.fromHono()`: Enforces mandatory `MSL-Project-Id` header
- Throws clear error if header is missing

**UI Sentinel Value:**

- Dashboard uses `"__user__"` string value in forms
- Converted to `null` before database insertion
- Prevents dropdown from submitting empty string

### 7. Header Passthrough

In user passthrough mode, **all client headers are forwarded** to Anthropic API (except blacklisted infrastructure headers):

**Headers Blacklisted (NOT forwarded):**

- `host` - Proxy host (would be incorrect for Anthropic API)
- `connection` - Proxy connection handling
- `content-length` - Recalculated by fetch
- `accept-encoding` - Handled by fetch
- `authorization` - Filtered from client headers, provided by auth service to prevent duplicates
- `x-forwarded-for`, `x-real-ip` - Proxy headers
- `msl-project-id`, `msl-account` - Internal routing headers
- `x-api-key` - Internal authentication

**Headers Forwarded (examples):**

- `anthropic-beta` - API beta feature flags (critical for compatibility)
- `anthropic-version` - API version
- `anthropic-dangerous-direct-browser-access` - Browser access flag
- `user-agent` - Client identification
- `x-stainless-*` - SDK telemetry headers
- `x-app` - Application identification
- `baggage`, `sentry-trace`, `sec-fetch-mode` - Tracing/security headers
- All other client-provided headers

**Rationale**: This ensures maximum compatibility with Anthropic API by preserving all client-specific headers (SDK versions, beta feature flags, etc.) while removing only infrastructure headers that would be incorrect or duplicate.

**Logging**: Authorization headers are logged in plain text (not masked) to enable debugging of authentication issues. This is intentional for troubleshooting user passthrough mode.

## Consequences

### Positive

✅ **User choice**: Projects can choose organization OR user credentials
✅ **Backward compatible**: Existing projects continue working unchanged
✅ **Simple implementation**: Leverages existing nullable column, no new tables
✅ **Clear intent**: Explicit "User Account" option in dashboard
✅ **Flexible**: Can switch between modes via dashboard
✅ **No token storage**: User tokens never stored in database (security benefit)

### Negative

⚠️ **No automatic refresh**: User must manage their own token expiration (unlike organization accounts)
⚠️ **Manual token provision**: Users must pass `Authorization` header in every request
⚠️ **No centralized tracking**: Cannot track individual user costs in dashboard (shows project-level only)
⚠️ **Documentation burden**: Users need clear instructions on obtaining/using their Anthropic tokens

### Neutral

- User authentication in dashboard (ADR-027) is separate from API authentication
- Request tracking continues to work (uses `account_id: 'user-passthrough'` for attribution)
- Slack notifications continue to work (project-level configuration)

## Alternatives Considered

### Alternative 1: Store user OAuth tokens in database

**Approach**: Create `user_anthropic_credentials` table, implement OAuth flow in dashboard, automatically refresh user tokens

**Rejected because:**

- Significantly more complex (OAuth flow, token refresh, user credential management)
- Security risk (storing user tokens in database)
- Not needed for MVP use case (users can manage their own tokens)
- Can be added later if demand exists

### Alternative 2: Project-level passthrough flag

**Approach**: Add `use_user_passthrough: boolean` column instead of implicit `default_account_id = null`

**Rejected because:**

- Adds unnecessary column when nullable `default_account_id` already expresses the same intent
- More complex validation (what if both flag is true AND default_account_id is set?)
- Less intuitive ("what does this flag do?" vs "no default account = user provides credentials")

### Alternative 3: Dashboard-only passthrough

**Approach**: Dashboard forwards user auth headers to proxy, only works from dashboard UI

**Rejected because:**

- Doesn't solve the primary use case (developers using Claude Code CLI)
- Creates two different authentication flows (dashboard vs direct API)
- More complex (dashboard must capture and forward user Anthropic tokens)

## Related ADRs

- **ADR-004**: Proxy Authentication - Established multi-account model
- **ADR-024**: Header-Based Project and Account Routing - Defines MSL-Account header priority
- **ADR-027**: Mandatory User Authentication - Dashboard authentication (separate concern)
- **ADR-028**: Proxy Service Operational Modes - Passthrough works in all modes
- **ADR-029**: Project Privacy Model - User passthrough respects project membership

## Implementation Notes

### Testing User Passthrough Mode

1. Create project with "User Account" selected
2. Obtain Anthropic API token from https://console.anthropic.com/settings/keys
3. Make API request with headers:
```
MSL-Project-Id: your-project-id
Authorization: Bearer your-anthropic-token
```
4. Proxy forwards your token to Anthropic API
5. Request tracked under `account_id: 'user-passthrough'`

### Switching Modes

- **User Account → Organization Account**: Select organization account in dashboard
- **Organization Account → User Account**: Select "User Account" in dashboard
- No data loss, instant switch

### Monitoring

- Dashboard shows requests under "User Account" when `default_account_id = null`
- Individual user tokens not tracked (privacy by design)
- Project-level metrics still work (conversation tracking, token usage aggregates)

## Migration Path

**Existing Projects:** No changes required, continue using organization accounts

**New Projects:** Can choose User Account at creation time

**Future Enhancement:** If user credential storage becomes needed, can add:

- `user_anthropic_credentials` table
- OAuth flow in dashboard
- Automatic token refresh
- Per-user cost tracking

This decision provides immediate value while keeping the door open for future enhancements.
27 changes: 20 additions & 7 deletions packages/shared/src/database/queries/project-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@ import type {
import { toSafeCredential } from './credential-queries-internal'

/**
* Create a new project with a randomly selected default account
* Create a new project
* - If default_account_id is explicitly provided, use it
* - If default_account_id is null, project will use user passthrough mode
* - If default_account_id is undefined, randomly select an account
*/
export async function createProject(pool: Pool, request: CreateProjectRequest): Promise<Project> {
// Get a random credential to use as default
const credentialResult = await pool.query<{ id: string }>(
'SELECT id FROM anthropic_credentials ORDER BY RANDOM() LIMIT 1'
)

const defaultAccountId = credentialResult.rows[0]?.id || null
let defaultAccountId: string | null = null

if (request.default_account_id === undefined) {
// Undefined = auto-assign random account (backward compatibility)
const credentialResult = await pool.query<{ id: string }>(
'SELECT id FROM anthropic_credentials ORDER BY RANDOM() LIMIT 1'
)
defaultAccountId = credentialResult.rows[0]?.id || null
} else {
// Explicit value (including null) = use as-is
defaultAccountId = request.default_account_id
}

const result = await pool.query<Project>(
`
Expand Down Expand Up @@ -143,6 +152,10 @@ export async function updateProject(
updates.push(`name = $${paramIndex++}`)
values.push(request.name)
}
if (request.default_account_id !== undefined) {
updates.push(`default_account_id = $${paramIndex++}`)
values.push(request.default_account_id)
}
if (request.slack_enabled !== undefined) {
updates.push(`slack_enabled = $${paramIndex++}`)
values.push(request.slack_enabled)
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface UpdateCredentialTokensRequest {
export interface CreateProjectRequest {
project_id: string
name: string
default_account_id?: string | null // null = user passthrough mode
slack_enabled?: boolean
slack_webhook_url?: string
slack_channel?: string
Expand All @@ -111,6 +112,7 @@ export interface CreateProjectRequest {

export interface UpdateProjectRequest {
name?: string
default_account_id?: string | null // null = user passthrough mode
slack_enabled?: boolean
slack_webhook_url?: string
slack_channel?: string
Expand Down
Loading