Files
paperclip/doc/plans/cursor-cloud-adapter.md
Forgotten 21c506dcae docs: add deployment modes documentation and update plans
Add DEPLOYMENT-MODES.md with canonical mode taxonomy. Update CLI.md,
DEVELOPING.md, PRODUCT.md, and SPEC-implementation.md with local_trusted/
authenticated nomenclature. Revise humans-and-permissions plan with Better
Auth choice, bootstrap flow, unified invite semantics, and expanded criteria.
Add implementation guide and additional plan documents for cursor cloud
adapter and deployment auth mode consolidation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:41:35 -06:00

478 lines
13 KiB
Markdown

# Cursor Cloud Agent Adapter — Technical Plan
## Overview
This document defines the V1 design for a Paperclip adapter that integrates with
Cursor Background Agents via the Cursor REST API.
Primary references:
- https://docs.cursor.com/background-agent/api/overview
- https://docs.cursor.com/background-agent/api
- https://docs.cursor.com/background-agent/api/webhooks
Unlike `claude_local` and `codex_local`, this adapter is not a local subprocess.
It is a remote orchestration adapter with:
1. launch/follow-up over HTTP
2. webhook-driven status updates when possible
3. polling fallback for reliability
4. synthesized stdout events for Paperclip UI/CLI
## Key V1 Decisions
1. **Auth to Cursor API** uses `Authorization: Bearer <CURSOR_API_KEY>`.
2. **Callback URL** must be publicly reachable by Cursor VMs:
- local: Tailscale URL
- prod: public server URL
3. **Agent callback auth to Paperclip** uses a bootstrap exchange flow (no long-lived Paperclip key in prompt).
4. **Webhooks are V1**, polling remains fallback.
5. **Skill delivery** is fetch-on-demand from Paperclip endpoints, not full SKILL.md prompt injection.
---
## Cursor API Reference (Current)
Base URL: `https://api.cursor.com`
Authentication header:
- `Authorization: Bearer <CURSOR_API_KEY>`
Core endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
| `/v0/agents` | POST | Launch agent |
| `/v0/agents/{id}` | GET | Agent status |
| `/v0/agents/{id}/conversation` | GET | Conversation history |
| `/v0/agents/{id}/followup` | POST | Follow-up prompt |
| `/v0/agents/{id}/stop` | POST | Stop/pause running agent |
| `/v0/models` | GET | Recommended model list |
| `/v0/me` | GET | API key metadata |
| `/v0/repositories` | GET | Accessible repos (strictly rate-limited) |
Status handling policy for adapter:
- Treat `CREATING` and `RUNNING` as non-terminal.
- Treat `FINISHED` as success terminal.
- Treat `ERROR` as failure terminal.
- Treat unknown non-active statuses as terminal failure and preserve raw status in `resultJson`.
Webhook facts relevant to V1:
- Cursor emits `statusChange` webhooks.
- Terminal webhook statuses include `ERROR` and `FINISHED`.
- Webhook signatures use HMAC SHA256 (`X-Webhook-Signature: sha256=...`).
Operational limits:
- `/v0/repositories`: 1 req/user/min, 30 req/user/hour.
- MCP not supported in Cursor background agents.
---
## Package Structure
```
packages/adapters/cursor-cloud/
├── package.json
├── tsconfig.json
└── src/
├── index.ts
├── api.ts
├── server/
│ ├── index.ts
│ ├── execute.ts
│ ├── parse.ts
│ ├── test.ts
│ └── webhook.ts
├── ui/
│ ├── index.ts
│ ├── parse-stdout.ts
│ └── build-config.ts
└── cli/
├── index.ts
└── format-event.ts
```
`package.json` uses standard four exports (`.`, `./server`, `./ui`, `./cli`).
---
## API Client (`src/api.ts`)
`src/api.ts` is a typed wrapper over Cursor endpoints.
```ts
interface CursorClientConfig {
apiKey: string;
baseUrl?: string; // default https://api.cursor.com
}
interface CursorAgent {
id: string;
name: string;
status: "CREATING" | "RUNNING" | "FINISHED" | "ERROR" | string;
source: { repository: string; ref: string };
target: {
branchName?: string;
prUrl?: string;
url?: string;
autoCreatePr?: boolean;
openAsCursorGithubApp?: boolean;
skipReviewerRequest?: boolean;
};
summary?: string;
createdAt: string;
}
```
Client requirements:
- send `Authorization: Bearer ...` on all requests
- throw typed `CursorApiError` with `status`, parsed body, and request context
- preserve raw unknown fields for debugging in error metadata
---
## Adapter Config Contract (`src/index.ts`)
```ts
export const type = "cursor_cloud";
export const label = "Cursor Cloud Agent";
```
V1 config fields:
- `repository` (required): GitHub repo URL
- `ref` (optional, default `main`)
- `model` (optional, allow empty = auto)
- `autoCreatePr` (optional, default `false`)
- `branchName` (optional)
- `promptTemplate` / `bootstrapPromptTemplate`
- `pollIntervalSec` (optional, default `10`)
- `timeoutSec` (optional, default `0`)
- `graceSec` (optional, default `20`)
- `paperclipPublicUrl` (optional override; else `PAPERCLIP_PUBLIC_URL` env)
- `enableWebhooks` (optional, default `true`)
- `env.CURSOR_API_KEY` (required, secret_ref preferred)
- `env.CURSOR_WEBHOOK_SECRET` (required if `enableWebhooks=true`, min 32)
Important: do not store Cursor key in plain `apiKey` top-level field.
Use `adapterConfig.env` so secret references are supported by existing secret-resolution flow.
---
## Paperclip Callback + Auth Flow (V1)
Cursor agents run remotely, so we cannot inject local env like `PAPERCLIP_API_KEY`.
### Public URL
The adapter must resolve a callback base URL in this order:
1. `adapterConfig.paperclipPublicUrl`
2. `process.env.PAPERCLIP_PUBLIC_URL`
If empty, fail `testEnvironment` and runtime execution with a clear error.
### Bootstrap Exchange
Goal: avoid putting long-lived Paperclip credentials in prompt text.
Flow:
1. Before launch/follow-up, Paperclip mints a one-time bootstrap token bound to:
- `agentId`
- `companyId`
- `runId`
- short TTL (for example 10 minutes)
2. Adapter includes only:
- `paperclipPublicUrl`
- exchange endpoint path
- bootstrap token
3. Cursor agent calls:
- `POST /api/agent-auth/exchange`
4. Paperclip validates bootstrap token and returns a run-scoped bearer JWT.
5. Cursor agent uses returned bearer token for all Paperclip API calls.
This keeps long-lived keys out of prompt and supports clean revocation by TTL.
---
## Skills Delivery Strategy (V1)
Do not inline full SKILL.md content into the prompt.
Instead:
1. Prompt includes a compact instruction to fetch skills from Paperclip.
2. After auth exchange, agent fetches:
- `GET /api/skills/index`
- `GET /api/skills/paperclip`
- `GET /api/skills/paperclip-create-agent` when needed
3. Agent loads full skill content on demand.
Benefits:
- avoids prompt bloat
- keeps skill docs centrally updatable
- aligns with how local adapters expose skills as discoverable procedures
---
## Execution Flow (`src/server/execute.ts`)
### Step 1: Resolve Config and Secrets
- parse adapter config via `asString/asBoolean/asNumber/parseObject`
- resolve `env.CURSOR_API_KEY`
- resolve `paperclipPublicUrl`
- validate webhook secret when webhooks enabled
### Step 2: Session Resolution
Session identity is Cursor `agentId` (stored in `sessionParams`).
Reuse only when repository matches.
### Step 3: Render Prompt
Render template as usual, then append a compact callback block:
- public Paperclip URL
- bootstrap exchange endpoint
- bootstrap token
- skill index endpoint
- required run header behavior
### Step 4: Launch/Follow-up
- on resume: `POST /followup`
- else: `POST /agents`
- include webhook object when enabled:
- `url: <paperclipPublicUrl>/api/adapters/cursor-cloud/webhooks`
- `secret: CURSOR_WEBHOOK_SECRET`
### Step 5: Progress + Completion
Use hybrid strategy:
- webhook events are primary status signal
- polling is fallback and transcript source (`/conversation`)
Emit synthetic events to stdout (`init`, `status`, `assistant`, `user`, `result`).
Completion logic:
- success: `status === FINISHED`
- failure: `status === ERROR` or unknown terminal
- timeout: stop agent, mark timedOut
### Step 6: Result Mapping
`AdapterExecutionResult`:
- `exitCode: 0` on success, `1` on terminal failure
- `errorMessage` populated on failure/timeout
- `sessionParams: { agentId, repository }`
- `provider: "cursor"`
- `usage` and `costUsd`: unavailable/null
- `resultJson`: include raw status/target/conversation snapshot
Also ensure `result` event is emitted to stdout before return.
---
## Webhook Handling (`src/server/webhook.ts` + server route)
Add a server endpoint to receive Cursor webhook deliveries.
Responsibilities:
1. Verify HMAC signature from `X-Webhook-Signature`.
2. Deduplicate by `X-Webhook-ID`.
3. Validate event type (`statusChange`).
4. Route by Cursor `agentId` to active Paperclip run context.
5. Append `heartbeat_run_events` entries for audit/debug.
6. Update in-memory run signal so execute loop can short-circuit quickly.
Security:
- reject invalid signature (`401`)
- reject malformed payload (`400`)
- always return quickly after persistence (`2xx`)
---
## Environment Test (`src/server/test.ts`)
Checks:
1. `CURSOR_API_KEY` present
2. key validity via `GET /v0/me`
3. repository configured and URL shape valid
4. model exists (if set) via `/v0/models`
5. `paperclipPublicUrl` present and reachable shape-valid
6. webhook secret present/length-valid when webhooks enabled
Repository-access verification via `/v0/repositories` should be optional due strict rate limits.
Use a warning-level check only when an explicit `verifyRepositoryAccess` option is set.
---
## UI + CLI
### UI parser (`src/ui/parse-stdout.ts`)
Handle event types:
- `init`
- `status`
- `assistant`
- `user`
- `result`
- fallback `stdout`
On failure results, set `isError=true` and include error text.
### Config builder (`src/ui/build-config.ts`)
- map `CreateConfigValues.url -> repository`
- preserve env binding shape (`plain`/`secret_ref`)
- include defaults (`pollIntervalSec`, `timeoutSec`, `graceSec`, `enableWebhooks`)
### Adapter fields (`ui/src/adapters/cursor-cloud/config-fields.tsx`)
Add controls for:
- repository
- ref
- model
- autoCreatePr
- branchName
- poll interval
- timeout/grace
- paperclip public URL override
- enable webhooks
- env bindings for `CURSOR_API_KEY` and `CURSOR_WEBHOOK_SECRET`
### CLI formatter (`src/cli/format-event.ts`)
Format synthetic events similarly to local adapters.
Highlight terminal failures clearly.
---
## Server Registration and Cross-Layer Contract Sync
### Adapter registration
- `server/src/adapters/registry.ts`
- `ui/src/adapters/registry.ts`
- `cli/src/adapters/registry.ts`
### Shared contract updates (required)
- add `cursor_cloud` to `packages/shared/src/constants.ts` (`AGENT_ADAPTER_TYPES`)
- ensure validators accept it (`packages/shared/src/validators/agent.ts`)
- update UI labels/maps where adapter names are enumerated, including:
- `ui/src/components/agent-config-primitives.tsx`
- `ui/src/components/AgentProperties.tsx`
- `ui/src/pages/Agents.tsx`
- consider onboarding wizard support for adapter selection (`ui/src/components/OnboardingWizard.tsx`)
Without these updates, create/edit flows will reject the new adapter even if package code exists.
---
## Cancellation Semantics
Long-polling HTTP adapters must support run cancellation.
V1 requirement:
- register a cancellation handler per running adapter invocation
- `cancelRun` should invoke that handler (abort fetch/poll loop + optional Cursor stop call)
Current process-only cancellation maps are insufficient by themselves for Cursor.
---
## Comparison with `claude_local`
| Aspect | `claude_local` | `cursor_cloud` |
|---|---|---|
| Execution model | local subprocess | remote API |
| Updates | stream-json stdout | webhook + polling + synthesized stdout |
| Session id | Claude session id | Cursor agent id |
| Skill delivery | local skill dir injection | authenticated fetch from Paperclip skill endpoints |
| Paperclip auth | injected local run JWT env var | bootstrap token exchange -> run JWT |
| Cancellation | OS signals | abort polling + Cursor stop endpoint |
| Usage/cost | rich | not exposed by Cursor API |
---
## V1 Limitations
1. Cursor does not expose token/cost usage in API responses.
2. Conversation stream is text-only (`user_message`/`assistant_message`).
3. MCP/tool-call granularity is unavailable.
4. Webhooks currently deliver status-change events, not full transcript deltas.
---
## Future Enhancements
1. Reduce polling frequency further when webhook reliability is high.
2. Attach image payloads from Paperclip context.
3. Add richer PR metadata surfacing in Paperclip UI.
4. Add webhook replay UI for debugging.
---
## Implementation Checklist
### Adapter package
- [ ] `packages/adapters/cursor-cloud/package.json` exports wired
- [ ] `packages/adapters/cursor-cloud/tsconfig.json`
- [ ] `src/index.ts` metadata + configuration doc
- [ ] `src/api.ts` bearer-auth client + typed errors
- [ ] `src/server/execute.ts` hybrid webhook/poll orchestration
- [ ] `src/server/parse.ts` stream parser + not-found detection
- [ ] `src/server/test.ts` env diagnostics
- [ ] `src/server/webhook.ts` signature verification + payload helpers
- [ ] `src/server/index.ts` exports + session codec
- [ ] `src/ui/parse-stdout.ts`
- [ ] `src/ui/build-config.ts`
- [ ] `src/ui/index.ts`
- [ ] `src/cli/format-event.ts`
- [ ] `src/cli/index.ts`
### App integration
- [ ] register adapter in server/ui/cli registries
- [ ] add `cursor_cloud` to shared adapter constants/validators
- [ ] add adapter labels in UI surfaces
- [ ] add Cursor webhook route on server (`/api/adapters/cursor-cloud/webhooks`)
- [ ] add auth exchange route (`/api/agent-auth/exchange`)
- [ ] add skill serving routes (`/api/skills/index`, `/api/skills/:name`)
- [ ] add generic cancellation hook for non-subprocess adapters
### Tests
- [ ] api client auth/error mapping
- [ ] terminal status mapping (`FINISHED`, `ERROR`, unknown terminal)
- [ ] session codec round-trip
- [ ] config builder env binding handling
- [ ] webhook signature verification + dedupe
- [ ] bootstrap exchange happy path + expired/invalid token
### Verification
- [ ] `pnpm -r typecheck`
- [ ] `pnpm test:run`
- [ ] `pnpm build`