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>
This commit is contained in:
Forgotten
2026-02-23 14:41:35 -06:00
parent 2ec45c49af
commit 21c506dcae
9 changed files with 1708 additions and 46 deletions

View File

@@ -25,6 +25,18 @@ Choose local instance:
pnpm paperclip run --instance dev
```
## Deployment Modes
Mode taxonomy and design intent are documented in `doc/DEPLOYMENT-MODES.md`.
Current CLI behavior:
- `paperclip onboard` and `paperclip configure --section server` set deployment mode in config
- runtime can override mode with `PAPERCLIP_DEPLOYMENT_MODE`
- `paperclip run` and `paperclip doctor` do not yet expose a direct `--mode` flag
Target behavior (planned) is documented in `doc/DEPLOYMENT-MODES.md` section 5.
All client commands support:
- `--api-base <url>`

107
doc/DEPLOYMENT-MODES.md Normal file
View File

@@ -0,0 +1,107 @@
# Deployment Modes
Status: Canonical deployment and auth mode model
Date: 2026-02-23
## 1. Purpose
Paperclip supports two runtime modes:
1. `local_trusted`
2. `authenticated`
`authenticated` supports two exposure policies:
1. `private`
2. `public`
This keeps one authenticated auth stack while still separating low-friction private-network defaults from internet-facing hardening requirements.
## 2. Canonical Model
| Runtime Mode | Exposure | Human auth | Primary use |
|---|---|---|---|
| `local_trusted` | n/a | No login required | Single-operator local machine workflow |
| `authenticated` | `private` | Login required | Private-network access (for example Tailscale/VPN/LAN) |
| `authenticated` | `public` | Login required | Internet-facing/cloud deployment |
## 3. Security Policy
## `local_trusted`
- loopback-only host binding
- no human login flow
- optimized for fastest local startup
## `authenticated + private`
- login required
- low-friction URL handling (`auto` base URL mode)
- private-host trust policy required
## `authenticated + public`
- login required
- explicit public URL required
- stricter deployment checks and failures in doctor
## 4. Onboarding UX Contract
Default onboarding remains interactive and flagless:
```sh
pnpm paperclip onboard
```
Server prompt behavior:
1. ask mode, default `local_trusted`
2. option copy:
- `local_trusted`: "Easiest for local setup (no login, localhost-only)"
- `authenticated`: "Login required; use for private network or public hosting"
3. if `authenticated`, ask exposure:
- `private`: "Private network access (for example Tailscale), lower setup friction"
- `public`: "Internet-facing deployment, stricter security requirements"
4. ask explicit public URL only for `authenticated + public`
`configure --section server` follows the same interactive behavior.
## 5. Doctor UX Contract
Default doctor remains flagless:
```sh
pnpm paperclip doctor
```
Doctor reads configured mode/exposure and applies mode-aware checks. Optional override flags are secondary.
## 6. Board/User Integration Contract
Board identity must be represented by a real DB user principal for user-based features to work consistently.
Required integration points:
- real user row in `authUsers` for Board identity
- `instance_user_roles` entry for Board admin authority
- `company_memberships` integration for user-level task assignment and access
This is required because user assignment paths validate active membership for `assigneeUserId`.
## 7. Current Code Reality (As Of 2026-02-23)
- runtime values are `local_trusted | authenticated`
- `authenticated` uses Better Auth sessions and bootstrap invite flow
- `local_trusted` ensures a real local Board user principal in `authUsers` with `instance_user_roles` admin access
- company creation ensures creator membership in `company_memberships` so user assignment/access flows remain consistent
## 8. Naming and Compatibility Policy
- canonical naming is `local_trusted` and `authenticated` with `private/public` exposure
- no long-term compatibility alias layer for discarded naming variants
## 9. Relationship to Other Docs
- implementation plan: `doc/plans/deployment-auth-mode-consolidation.md`
- V1 contract: `doc/SPEC-implementation.md`
- operator workflows: `doc/DEVELOPING.md` and `doc/CLI.md`

View File

@@ -2,6 +2,14 @@
This project can run fully in local dev without setting up PostgreSQL manually.
## Deployment Modes
For mode definitions and intended CLI behavior, see `doc/DEPLOYMENT-MODES.md`.
Current implementation status:
- canonical model: `local_trusted` and `authenticated` (with `private/public` exposure)
## Prerequisites
- Node.js 20+

View File

@@ -84,10 +84,12 @@ More detailed task structure TBD.
## Guidelines
There are two deployment scenarios that need to be maintained:
There are two runtime modes Paperclip must support:
- a single user, local trusted deployment - this should be easy to install with a single `npx paperclip run` command and the environment is trusted and self-contained on a local machine (e.g. local files, agents, embedded db, easy to use)
- multi-user cloud deployment - allows for a hosted deploy (remote deployment, user logins, hosted db, scalable)
- `local_trusted` (default): single-user local trusted deployment with no login friction
- `authenticated`: login-required mode that supports both private-network and public deployment exposure policies
Canonical mode design and command expectations live in `doc/DEPLOYMENT-MODES.md`.
## Further Detail

View File

@@ -39,10 +39,10 @@ These decisions close open questions from `SPEC.md` for V1.
| Task ownership | Single assignee; atomic checkout required for `in_progress` transition |
| Recovery | No automatic reassignment; stale work is surfaced, not silently fixed |
| Agent adapters | Built-in `process` and `http` adapters |
| Auth | Session auth for board, API keys for agents |
| Auth | Mode-dependent human auth (`local_trusted` implicit board in current code; authenticated mode uses sessions), API keys for agents |
| Budget period | Monthly UTC calendar window |
| Budget enforcement | Soft alerts + hard limit auto-pause |
| Deployment modes | Embedded PostgreSQL default; Docker/hosted Postgres supported |
| Deployment modes | Canonical model is `local_trusted` + `authenticated` with `private/public` exposure policy (see `doc/DEPLOYMENT-MODES.md`) |
## 4. Current Baseline (Repo Snapshot)

View File

@@ -0,0 +1,643 @@
# Humans and Permissions Implementation (V1)
Status: Draft
Date: 2026-02-21
Owners: Server + UI + CLI + DB + Shared
Companion plan: `doc/plan/humans-and-permissions.md`
## 1. Document role
This document is the engineering implementation contract for the humans-and-permissions plan.
It translates product decisions into concrete schema, API, middleware, UI, CLI, and test work.
If this document conflicts with prior exploratory notes, this document wins for V1 execution.
## 2. Locked V1 decisions
1. Two deployment modes remain:
- `local_trusted`
- `cloud_hosted`
2. `local_trusted`:
- no login UX
- implicit local instance admin actor
- loopback-only server binding
- full admin/settings/invite/approval capabilities available locally
3. `cloud_hosted`:
- Better Auth for humans
- email/password only
- no email verification requirement in V1
4. Permissions:
- one shared authorization system for humans and agents
- normalized grants table (`principal_permission_grants`)
- no separate “agent permissions engine”
5. Invites:
- copy-link only (no outbound email sending in V1)
- unified `company_join` link that supports human or agent path
- acceptance creates `pending_approval` join request
- no access until admin approval
6. Join review metadata:
- source IP required
- no GeoIP/country lookup in V1
7. Agent API keys:
- indefinite by default
- hash at rest
- display once on claim
- revoke/regenerate supported
8. Local ingress:
- public/untrusted ingress is out of scope for V1
- no `--dangerous-agent-ingress` in V1
## 3. Current baseline and delta
Current baseline (repo as of this doc):
- server actor model defaults to `board` in `server/src/middleware/auth.ts`
- authorization is mostly `assertBoard` + company check in `server/src/routes/authz.ts`
- no human auth/session tables in local schema
- no principal membership or grants tables
- no invite or join-request lifecycle
Required delta:
- move from board-vs-agent authz to principal-based authz
- add Better Auth integration in cloud mode
- add membership/grants/invite/join-request persistence
- add approval inbox signals and actions
- preserve local no-login UX without weakening cloud security
## 4. Architecture
## 4.1 Deployment mode contract
Add explicit runtime mode:
- `deployment.mode = local_trusted | cloud_hosted`
Config behavior:
- mode stored in config file (`packages/shared/src/config-schema.ts`)
- loaded in server config (`server/src/config.ts`)
- surfaced in `/api/health`
Startup guardrails:
- `local_trusted`: fail startup if bind host is not loopback
- `cloud_hosted`: fail startup if Better Auth is not configured
## 4.2 Actor model
Replace implicit “board” semantics with explicit actors:
- `user` (session-authenticated human)
- `agent` (bearer API key)
- `local_implicit_admin` (local_trusted only)
Implementation note:
- keep `req.actor` shape backward-compatible during migration by introducing a normalizer helper
- remove hard-coded `"board"` checks route-by-route after new authz helpers are in place
## 4.3 Authorization model
Authorization input tuple:
- `(company_id, principal_type, principal_id, permission_key, scope_payload)`
Principal types:
- `user`
- `agent`
Role layers:
- `instance_admin` (instance-wide)
- company-scoped grants via `principal_permission_grants`
Evaluation order:
1. resolve principal from actor
2. resolve instance role (`instance_admin` short-circuit for admin-only actions)
3. resolve company membership (`active` required for company access)
4. resolve grant + scope for requested action
## 5. Data model
## 5.1 Better Auth tables
Managed by Better Auth adapter/migrations (expected minimum):
- `user`
- `session`
- `account`
- `verification`
Note:
- use Better Auth canonical table names/types to avoid custom forks
## 5.2 New Paperclip tables
1. `instance_user_roles`
- `id` uuid pk
- `user_id` text not null
- `role` text not null (`instance_admin`)
- `created_at`, `updated_at`
- unique index: `(user_id, role)`
2. `company_memberships`
- `id` uuid pk
- `company_id` uuid fk `companies.id` not null
- `principal_type` text not null (`user | agent`)
- `principal_id` text not null
- `status` text not null (`pending | active | suspended`)
- `membership_role` text null
- `created_at`, `updated_at`
- unique index: `(company_id, principal_type, principal_id)`
- index: `(principal_type, principal_id, status)`
3. `principal_permission_grants`
- `id` uuid pk
- `company_id` uuid fk `companies.id` not null
- `principal_type` text not null (`user | agent`)
- `principal_id` text not null
- `permission_key` text not null
- `scope` jsonb null
- `granted_by_user_id` text null
- `created_at`, `updated_at`
- unique index: `(company_id, principal_type, principal_id, permission_key)`
- index: `(company_id, permission_key)`
4. `invites`
- `id` uuid pk
- `company_id` uuid fk `companies.id` not null
- `invite_type` text not null (`company_join | bootstrap_ceo`)
- `token_hash` text not null
- `allowed_join_types` text not null (`human | agent | both`) for `company_join`
- `defaults_payload` jsonb null
- `expires_at` timestamptz not null
- `invited_by_user_id` text null
- `revoked_at` timestamptz null
- `accepted_at` timestamptz null
- `created_at` timestamptz not null default now()
- unique index: `(token_hash)`
- index: `(company_id, invite_type, revoked_at, expires_at)`
5. `join_requests`
- `id` uuid pk
- `invite_id` uuid fk `invites.id` not null
- `company_id` uuid fk `companies.id` not null
- `request_type` text not null (`human | agent`)
- `status` text not null (`pending_approval | approved | rejected`)
- `request_ip` text not null
- `requesting_user_id` text null
- `request_email_snapshot` text null
- `agent_name` text null
- `adapter_type` text null
- `capabilities` text null
- `agent_defaults_payload` jsonb null
- `created_agent_id` uuid fk `agents.id` null
- `approved_by_user_id` text null
- `approved_at` timestamptz null
- `rejected_by_user_id` text null
- `rejected_at` timestamptz null
- `created_at`, `updated_at`
- index: `(company_id, status, request_type, created_at desc)`
- unique index: `(invite_id)` to enforce one request per consumed invite
## 5.3 Existing table changes
1. `issues`
- add `assignee_user_id` text null
- enforce single-assignee invariant:
- at most one of `assignee_agent_id` and `assignee_user_id` is non-null
2. `agents`
- keep existing `permissions` JSON for transition only
- mark as deprecated in code path once principal grants are live
## 5.4 Migration strategy
Migration ordering:
1. add new tables/columns/indexes
2. backfill minimum memberships/grants for existing data:
- create local implicit admin membership context in local mode at runtime (not persisted as Better Auth user)
- for cloud, bootstrap creates first admin user role on acceptance
3. switch authz reads to new tables
4. remove legacy board-only checks
## 6. API contract (new/changed)
All under `/api`.
## 6.1 Health
`GET /api/health` response additions:
- `deploymentMode`
- `authReady`
- `bootstrapStatus` (`ready | bootstrap_pending`)
## 6.2 Invites
1. `POST /api/companies/:companyId/invites`
- create `company_join` invite
- copy-link value returned once
2. `GET /api/invites/:token`
- validate token
- return invite landing payload
- includes `allowedJoinTypes`
3. `POST /api/invites/:token/accept`
- body:
- `requestType: human | agent`
- human path: no extra payload beyond authenticated user
- agent path: `agentName`, `adapterType`, `capabilities`, optional adapter defaults
- consumes invite token
- creates `join_requests(status=pending_approval)`
4. `POST /api/invites/:inviteId/revoke`
- revokes non-consumed invite
## 6.3 Join requests
1. `GET /api/companies/:companyId/join-requests?status=pending_approval&requestType=...`
2. `POST /api/companies/:companyId/join-requests/:requestId/approve`
- human:
- create/activate `company_memberships`
- apply default grants
- agent:
- create `agents` row
- create pending claim context for API key
- create/activate agent membership
- apply default grants
3. `POST /api/companies/:companyId/join-requests/:requestId/reject`
4. `POST /api/join-requests/:requestId/claim-api-key`
- approved agent request only
- returns plaintext key once
- stores hash in `agent_api_keys`
## 6.4 Membership and grants
1. `GET /api/companies/:companyId/members`
- returns both principal types
2. `PATCH /api/companies/:companyId/members/:memberId/permissions`
- upsert/remove grants
3. `PUT /api/admin/users/:userId/company-access`
- instance admin only
4. `GET /api/admin/users/:userId/company-access`
5. `POST /api/admin/users/:userId/promote-instance-admin`
6. `POST /api/admin/users/:userId/demote-instance-admin`
## 6.5 Inbox
`GET /api/companies/:companyId/inbox` additions:
- pending join request alert items when actor can `joins:approve`
- each item includes inline action metadata:
- join request id
- request type
- source IP
- human email snapshot when applicable
## 7. Server implementation details
## 7.1 Config and startup
Files:
- `packages/shared/src/config-schema.ts`
- `server/src/config.ts`
- `server/src/index.ts`
- `server/src/startup-banner.ts`
Changes:
- add deployment mode + bind host settings
- enforce loopback-only for `local_trusted`
- enforce Better Auth readiness in `cloud_hosted`
- banner shows mode and bootstrap status
## 7.2 Better Auth integration
Files:
- `server/package.json` (dependency)
- `server/src/auth/*` (new)
- `server/src/app.ts` (mount auth handler endpoints + session middleware)
Changes:
- add Better Auth server instance
- cookie/session handling for cloud mode
- no-op session auth in local mode
## 7.3 Actor middleware
Files:
- `server/src/middleware/auth.ts`
- `server/src/routes/authz.ts`
- `server/src/middleware/board-mutation-guard.ts`
Changes:
- stop defaulting every request to board in cloud mode
- map local requests to `local_implicit_admin` actor in local mode
- map Better Auth session to `user` actor in cloud mode
- preserve agent bearer path
- replace `assertBoard` with permission-oriented helpers:
- `requireInstanceAdmin(req)`
- `requireCompanyAccess(req, companyId)`
- `requireCompanyPermission(req, companyId, permissionKey, scope?)`
## 7.4 Authorization services
Files:
- `server/src/services` (new modules)
- `memberships.ts`
- `permissions.ts`
- `invites.ts`
- `join-requests.ts`
- `instance-admin.ts`
Changes:
- centralized permission evaluation
- centralized membership resolution
- one place for principal-type branching
## 7.5 Routes
Files:
- `server/src/routes/index.ts` and new route modules:
- `auth.ts` (if needed)
- `invites.ts`
- `join-requests.ts`
- `members.ts`
- `instance-admin.ts`
- `inbox.ts` (or extension of existing inbox source)
Changes:
- add new endpoints listed above
- apply company and permission checks consistently
- log all mutations through activity log service
## 7.6 Activity log and audit
Files:
- `server/src/services/activity-log.ts`
- call sites in invite/join/member/admin routes
Required actions:
- `invite.created`
- `invite.revoked`
- `join.requested`
- `join.approved`
- `join.rejected`
- `membership.activated`
- `permission.granted`
- `permission.revoked`
- `instance_admin.promoted`
- `instance_admin.demoted`
- `agent_api_key.claimed`
- `agent_api_key.revoked`
## 7.7 Real-time and inbox propagation
Files:
- `server/src/services/live-events.ts`
- `server/src/realtime/live-events-ws.ts`
- inbox data source endpoint(s)
Changes:
- emit join-request events
- ensure inbox refresh path includes join alerts
## 8. CLI implementation
Files:
- `cli/src/index.ts`
- `cli/src/commands/onboard.ts`
- `cli/src/commands/configure.ts`
- `cli/src/prompts/server.ts`
Commands:
1. `paperclip auth bootstrap-ceo`
- create bootstrap invite
- print one-time URL
2. `paperclip onboard`
- in cloud mode with `bootstrap_pending`, print bootstrap URL and next steps
- in local mode, skip bootstrap requirement
Config additions:
- deployment mode
- bind host (validated against mode)
## 9. UI implementation
Files:
- routing: `ui/src/App.tsx`
- API clients: `ui/src/api/*`
- pages/components (new):
- `AuthLogin` / `AuthSignup` (cloud mode)
- `BootstrapPending` page
- `InviteLanding` page
- `InstanceSettings` page
- join approval components in `Inbox`
- member/grant management in company settings
Required UX:
1. Cloud unauthenticated user:
- redirect to login/signup
2. Cloud bootstrap pending:
- block app with setup command guidance
3. Invite landing:
- choose human vs agent path (respect `allowedJoinTypes`)
- submit join request
- show pending approval confirmation
4. Inbox:
- show join approval cards with approve/reject actions
- include source IP and human email snapshot when applicable
5. Local mode:
- no login prompts
- full settings/invite/approval UI available
## 10. Security controls
1. Token handling
- invite tokens hashed at rest
- API keys hashed at rest
- one-time plaintext key reveal only
2. Local mode isolation
- loopback bind enforcement
- startup hard-fail on non-loopback host
3. Cloud auth
- no implicit board fallback
- session auth mandatory for human mutations
4. Join workflow hardening
- one request per invite token
- pending request has no data access
- approval required before membership activation
5. Abuse controls
- rate limit invite accept and key claim endpoints
- structured logging for join and claim failures
## 11. Migration and compatibility
## 11.1 Runtime compatibility
- keep existing board-dependent routes functional while migrating authz helper usage
- phase out `assertBoard` calls only after permission helpers cover all routes
## 11.2 Data compatibility
- do not delete `agents.permissions` in V1
- stop reading it once grants are wired
- remove in post-V1 cleanup migration
## 11.3 Better Auth user ID handling
- treat `user.id` as text end-to-end
- existing `created_by_user_id` and similar text fields remain valid
## 12. Testing strategy
## 12.1 Unit tests
- permission evaluator:
- instance admin bypass
- grant checks
- scope checks
- join approval state machine
- invite token lifecycle
## 12.2 Integration tests
- cloud mode unauthenticated mutation -> `401`
- local mode implicit admin mutation -> success
- invite accept -> pending join -> no access
- join approve (human) -> membership/grants active
- join approve (agent) -> key claim once
- cross-company access denied for user and agent principals
- local mode non-loopback bind -> startup failure
## 12.3 UI tests
- login gate in cloud mode
- bootstrap pending screen
- invite landing choose-path UX
- inbox join alert approve/reject flows
## 12.4 Regression tests
- existing agent API key flows still work
- task assignment and checkout invariants unchanged
- activity logging still emitted for all mutations
## 13. Delivery plan
## Phase A: Foundations
- config mode/bind host support
- startup guardrails
- Better Auth integration skeleton
- actor type expansion
## Phase B: Schema and authz core
- add membership/grants/invite/join tables
- add permission service and helpers
- wire company/member/instance admin checks
## Phase C: Invite + join backend
- invite create/revoke
- invite accept -> pending request
- approve/reject + key claim
- activity log + live events
## Phase D: UI + CLI
- cloud login/bootstrap screens
- invite landing
- inbox join approval actions
- instance settings and member permissions
- bootstrap CLI command and onboarding updates
## Phase E: Hardening
- full integration/e2e coverage
- docs updates (`SPEC-implementation`, `DEVELOPING`, `CLI`)
- cleanup of legacy board-only codepaths
## 14. Verification gate
Before handoff:
```sh
pnpm -r typecheck
pnpm test:run
pnpm build
```
If any command is skipped, record exactly what was skipped and why.
## 15. Done criteria
1. Behavior matches locked V1 decisions in this doc and `doc/plan/humans-and-permissions.md`.
2. Cloud mode requires auth; local mode has no login UX.
3. Unified invite + pending approval flow works for both humans and agents.
4. Shared principal membership + permission system is live for users and agents.
5. Local mode remains loopback-only and fails otherwise.
6. Inbox shows actionable join approvals.
7. All new mutating paths are activity-logged.

View File

@@ -1,7 +1,7 @@
# Humans and Permissions Plan
Status: Draft
Date: 2026-02-20
Date: 2026-02-21
Owner: Server + UI + Shared + DB
## Goal
@@ -19,15 +19,22 @@ Current V1 assumptions are centered on one board operator. We now need:
- safe cloud deployment defaults (no accidental loginless production)
- local mode that still feels instant (`npx paperclip run` and go)
- agent-to-human task delegation, including a human inbox
- one user account with access to multiple companies in one deployment
- instance admins who can manage company access across the instance
- join approvals surfaced as actionable inbox alerts, not buried in admin-only pages
- a symmetric invite-and-approve onboarding path for both humans and agents
- one shared membership and permission model for both humans and agents
## Product constraints
1. Keep company scoping strict for every new table, endpoint, and permission check.
2. Preserve existing control-plane invariants:
- single-assignee task model
- approval gates
- budget hard-stop behavior
- mutation activity logging
3. Keep local mode easy and trusted, but prevent unsafe cloud posture.
## Deployment modes
@@ -40,11 +47,13 @@ Behavior:
- browser opens directly into board context
- embedded DB and local storage defaults remain
- a local implicit human actor exists for attribution
- local implicit actor has effective `instance_admin` authority for that instance
- full invite/approval/permission settings flows remain available in local mode (including agent enrollment)
Guardrails:
- server binds to loopback by default
- refuse startup if mode is `local_trusted` with non-loopback bind, unless explicit `--allow-unsafe-local-network`
- fail startup if mode is `local_trusted` with non-loopback bind
- UI shows a persistent "Local trusted mode" badge
## Mode B: `cloud_hosted`
@@ -52,6 +61,9 @@ Guardrails:
Behavior:
- login required for all human endpoints
- Better Auth for human auth
- initial auth method: email + password
- email verification is not required for initial release
- hosted DB and remote deployment supported
- multi-user sessions and role/permission enforcement
@@ -61,6 +73,13 @@ Guardrails:
- fail startup if insecure auth bypass flag is set
- health payload includes mode and auth readiness
## Authentication choice
- use Better Auth for human users
- start with email/password login only
- no email confirmation requirement in V1
- keep implementation structured so social/SSO providers can be added later without changing membership/permission semantics
## Auth and actor model
Unify request actors into a single model:
@@ -73,28 +92,78 @@ Rules:
- in `cloud_hosted`, only `user` and `agent` are valid actors
- in `local_trusted`, unauthenticated browser/API requests resolve to `local_board_implicit`
- `local_board_implicit` is authorized as an instance admin principal for local operations
- all mutating actions continue writing `activity_log` with actor type/id
## First admin bootstrap
Problem:
- new cloud deployments need a safe, explicit first human admin path
- app cannot assume a pre-existing admin account
- `local_trusted` does not use bootstrap flow because implicit local instance admin already exists
Bootstrap flow:
1. If no `instance_admin` user exists for the deployment, instance is in `bootstrap_pending` state.
2. CLI command `pnpm paperclip auth bootstrap-ceo` creates a one-time CEO onboarding invite URL for that instance.
3. `pnpm paperclip onboard` runs this bootstrap check and prints the invite URL automatically when `bootstrap_pending`.
4. Visiting the app while `bootstrap_pending` shows a blocking setup page with the exact CLI command to run (`pnpm paperclip onboard`).
5. Accepting that CEO invite creates the first admin user and exits bootstrap mode.
Security rules:
- bootstrap invite is single-use, short-lived, and token-hash stored at rest
- only one active bootstrap invite at a time per instance (regeneration revokes prior token)
- bootstrap actions are audited in `activity_log`
## Data model additions
## New tables
1. `users`
- identity record for human users (email-based)
- optional instance-level role field (or companion table) for admin rights
2. `company_memberships`
- `company_id`, `user_id`, status, role metadata
- stores effective permissions and optional org scope constraints
- `company_id`, `principal_type` (`user | agent`), `principal_id`
- status (`pending | active | suspended`), role metadata
- stores effective access state for both humans and agents
- many-to-many: one principal can belong to multiple companies
3. `invites`
- `company_id`, invite email, token hash, expires_at, invited_by, revoked_at, accepted_at
- optional default permissions payload at invite time
4. `user_permission_grants` (or JSON grant blob in membership)
- `company_id`, `invite_type` (`company_join | bootstrap_ceo`), token hash, expires_at, invited_by, revoked_at, accepted_at
- one-time share link (no pre-bound invite email)
- `allowed_join_types` (`human | agent | both`) for `company_join` links
- optional defaults payload keyed by join type:
- human defaults: initial permissions/membership role
- agent defaults: proposed role/title/adapter defaults
4. `principal_permission_grants`
- `company_id`, `principal_type` (`user | agent`), `principal_id`, `permission_key`
- explicit grants such as `agents:create`
- includes scope payload for chain-of-command limits
- normalized table (not JSON blob) for auditable grant/revoke history
5. `join_requests`
- `invite_id`, `company_id`, `request_type` (`human | agent`)
- `status` (`pending_approval | approved | rejected`)
- common review metadata:
- `request_ip`
- `approved_by_user_id`, `approved_at`, `rejected_by_user_id`, `rejected_at`
- human request fields:
- `requesting_user_id`, `request_email_snapshot`
- agent request fields:
- `agent_name`, `adapter_type`, `capabilities`, `created_agent_id` nullable until approved
- each consumed invite creates exactly one join request record after join type is selected
6. `issues` extension
5. `issues` extension
- add `assignee_user_id` nullable
- preserve single-assignee invariant with XOR check:
- exactly zero or one of `assignee_agent_id` / `assignee_user_id`
@@ -102,10 +171,21 @@ Rules:
## Compatibility
- existing `created_by_user_id` / `author_user_id` fields remain and become fully active
- agent API key model remains unchanged, still company-scoped
- agent API keys remain auth credentials; membership + grants remain authorization source
## Permission model (initial set)
Principle:
- humans and agents use the same membership + grant evaluation engine
- permission checks resolve against `(company_id, principal_type, principal_id)` for both actor types
- this avoids separate authz codepaths and keeps behavior consistent
Role layers:
- `instance_admin`: deployment-wide admin, can access/manage all companies and user-company access mapping
- `company_member`: company-scoped permissions only
Core grants:
1. `agents:create`
@@ -113,11 +193,13 @@ Core grants:
3. `users:manage_permissions`
4. `tasks:assign`
5. `tasks:assign_scope` (org-constrained delegation)
6. `joins:approve` (approve/reject human and agent join requests)
Additional behavioral rules:
- board-level users can manage all grants
- non-board users can only act within explicit grants
- instance admins can promote/demote instance admins and manage user access across companies
- board-level users can manage company grants inside companies they control
- non-admin principals can only act within explicit grants
- assignment checks apply to both agent and human assignees
## Chain-of-command scope design
@@ -137,18 +219,44 @@ Enforcement:
## Invite and signup flow
1. Authorized user creates invite with email + grants + optional expiry.
1. Authorized user creates one `company_join` invite share link with optional defaults + expiry.
2. System sends invite URL containing one-time token.
3. Invitee signs up/logs in.
4. Email on authenticated account must match invite email.
5. Accepting invite creates active `company_membership` and permission grants.
6. Inviter/admin can revoke invite before acceptance.
3. Invite landing page presents two paths: `Join as human` or `Join as agent` (subject to `allowed_join_types`).
4. Requester selects join path and submits required data.
5. Submission consumes token and creates a `pending_approval` join request (no access yet).
6. Join request captures review metadata:
- human: authenticated email
- both: source IP
- agent: proposed agent metadata
7. Company admin/instance admin reviews request and approves or rejects.
8. On approval:
- human: activate `company_membership` and apply permission grants
- agent: create agent record and enable API-key claim flow
9. Link is one-time and cannot be reused.
10. Inviter/admin can revoke invite before acceptance.
Security rules:
- store invite token hashed at rest
- one-time use token with short expiry
- all invite lifecycle events logged in `activity_log`
- pending users cannot read or mutate any company data until approved
## Join approval inbox
- join requests generate inbox alerts for eligible approvers (`joins:approve` or admin role)
- alerts appear in both:
- global/company inbox feed
- dedicated pending-approvals UI
- each alert includes approve/reject actions inline (no context switch required)
- alert payload must include:
- requester email when `request_type=human`
- source IP
- request type (`human | agent`)
## Human inbox and agent-to-human delegation
@@ -158,22 +266,49 @@ Behavior:
- humans see assigned tasks in inbox view (including in local trusted mode)
- comment and status transitions follow same issue lifecycle guards
## Agent join path (via unified invite link)
1. Authorized user shares one `company_join` invite link (with `allowed_join_types` including `agent`).
2. Agent operator opens link, chooses `Join as agent`, and submits join payload (name/role/adapter metadata).
3. System creates `pending_approval` agent join request and captures source IP.
4. Approver sees alert in inbox and approves or rejects.
5. On approval, server creates the agent record and mints a long-lived API key.
6. API key is shown exactly once via secure claim flow with explicit "save now" instruction.
Long-lived token policy:
- default to long-lived revocable API keys (hash stored at rest)
- show plaintext key once only
- support immediate revoke/regenerate from admin UI
- optionally add expirations/rotation policy later without changing join flow
API additions (proposed):
- `GET /companies/:companyId/inbox` (human actor scoped to self)
- `GET /companies/:companyId/inbox` (human actor scoped to self; includes task items + pending join approval alerts when authorized)
- `POST /companies/:companyId/issues/:issueId/assign-user`
- `POST /companies/:companyId/invites`
- `POST /invites/:token/accept`
- `GET /invites/:token` (invite landing payload with `allowed_join_types`)
- `POST /invites/:token/accept` (body includes `requestType=human|agent` and request metadata)
- `POST /invites/:inviteId/revoke`
- `GET /companies/:companyId/members`
- `PATCH /companies/:companyId/members/:userId/permissions`
- `GET /companies/:companyId/join-requests?status=pending_approval&requestType=human|agent`
- `POST /companies/:companyId/join-requests/:requestId/approve`
- `POST /companies/:companyId/join-requests/:requestId/reject`
- `POST /join-requests/:requestId/claim-api-key` (approved agent requests only)
- `GET /companies/:companyId/members` (returns both human and agent principals)
- `PATCH /companies/:companyId/members/:memberId/permissions`
- `POST /admin/users/:userId/promote-instance-admin`
- `POST /admin/users/:userId/demote-instance-admin`
- `PUT /admin/users/:userId/company-access` (set accessible companies for a user)
- `GET /admin/users/:userId/company-access`
## Local mode UX policy
- no login prompt or account setup required
- local implicit board user is auto-provisioned for audit attribution
- invite/multi-user screens can be hidden or marked unavailable in local mode
- if operator wants collaboration, they must switch to `cloud_hosted`
- local operator can still use instance settings and company settings as effective instance admin
- invite, join approval, and permission-management UI is available in local mode
- agent onboarding is expected in local mode, including creating invite links and approving join requests
- public/untrusted network ingress is out of scope for V1 local mode
## Cloud agents in this model
@@ -181,6 +316,16 @@ API additions (proposed):
- same-company boundary checks remain mandatory
- agent ability to assign human tasks is permission-gated, not implicit
## Instance settings surface
This plan introduces instance-level concerns (for example bootstrap state, instance admins, invite defaults, and token policy). There is no dedicated UI surface today.
V1 approach:
- add a minimal `Instance Settings` page for instance admins
- expose key instance settings in API + CLI (`paperclip configure` / `paperclip onboard`)
- show read-only instance status indicators in the main UI until full settings UX exists
## Implementation phases
## Phase 1: Mode and guardrails
@@ -188,47 +333,89 @@ API additions (proposed):
- add explicit deployment mode config (`local_trusted | cloud_hosted`)
- enforce startup safety checks and health visibility
- implement actor resolution for local implicit board
- map local implicit board actor to instance-admin authorization context
- add bootstrap status signal in health/config surface (`ready | bootstrap_pending`)
- add minimal instance settings API/CLI surface and read-only UI indicators
## Phase 2: Human identity and memberships
- add schema + migrations for users/memberships/invites
- wire auth middleware for cloud mode
- add membership lookup and company access checks
- implement Better Auth email/password flow (no email verification)
- implement first-admin bootstrap invite command and onboard integration
- implement one-time share-link invite acceptance flow with `pending_approval` join requests
## Phase 3: Permissions and assignment scope
- add grant model and enforcement helpers
- add shared principal grant model and enforcement helpers
- add chain-of-command scope checks for assignment APIs
- add tests for forbidden assignment (for example, cannot assign to CEO)
- add instance-admin promotion/demotion and global company-access management APIs
- add `joins:approve` permission checks for human and agent join approvals
## Phase 4: Invite workflow
- invite create/send/accept/revoke endpoints
- email-match enforcement and token security
- UI for invite management and membership permissions
- unified `company_join` invite create/landing/accept/revoke endpoints
- join request approve/reject endpoints with review metadata (email when applicable, IP)
- one-time token security and revocation semantics
- UI for invite management, pending join approvals, and membership permissions
- inbox alert generation for pending join requests
- ensure invite and approval UX is enabled in both `cloud_hosted` and `local_trusted`
## Phase 5: Human inbox + task assignment updates
- extend issue assignee model for human users
- inbox API and UI
- inbox API and UI for:
- task assignments
- pending join approval alerts with inline approve/reject actions
- agent-to-human assignment flow with policy checks
## Phase 6: Agent self-join and token claim
- add agent join path on unified invite landing page
- capture agent join requests and admin approval flow
- create one-time API-key claim flow after approval (display once)
## Acceptance criteria
1. `local_trusted` starts with no login and shows board UI immediately.
2. `cloud_hosted` cannot start without auth configured.
3. No request in `cloud_hosted` can mutate data without authenticated actor.
4. Humans can be invited by email, accepted with matching email, and revoked.
5. Permissions can be granted/revoked per company member.
6. Assignment scope prevents out-of-hierarchy or protected-role assignments.
7. Agents can assign tasks to humans only when allowed.
8. Humans can view assigned tasks in inbox and act on them per permissions.
9. All new mutations are company-scoped and logged in `activity_log`.
2. `local_trusted` does not expose optional human login UX in V1.
3. `local_trusted` local implicit actor can manage instance settings, invite links, join approvals, and permission grants.
4. `cloud_hosted` cannot start without auth configured.
5. No request in `cloud_hosted` can mutate data without authenticated actor.
6. If no initial admin exists, app shows bootstrap instructions with CLI command.
7. `pnpm paperclip onboard` outputs a CEO onboarding invite URL when bootstrap is pending.
8. One `company_join` link supports both human and agent onboarding via join-type selection on the invite landing page.
9. Invite delivery in V1 is copy-link only (no built-in email delivery).
10. Share-link acceptance creates a pending join request; it does not grant immediate access.
11. Pending join requests appear as inbox alerts with inline approve/reject actions.
12. Admin review view includes join metadata before decision (human email when applicable, source IP, and agent metadata for agent requests).
13. Only approved join requests unlock access:
## Open decisions
- human: active company membership + permission grants
- agent: agent creation + API-key claim eligibility
1. Auth provider choice for cloud mode (Auth.js vs hosted provider).
2. Whether local mode supports optional login in addition to implicit board.
3. Exact representation of permission grants (normalized table vs JSON schema).
4. Whether a user can belong to multiple companies in initial release.
5. Whether invite email delivery is built-in or webhook/provider integration only.
14. Agent enrollment follows the same link -> pending approval -> approve flow.
15. Approved agents can claim a long-lived API key exactly once, with plaintext display-once semantics.
16. Agent API keys are indefinite by default in V1 and revocable/regenerable by admins.
17. Public/untrusted ingress for `local_trusted` is not supported in V1 (loopback-only local server).
18. One user can hold memberships in multiple companies.
19. Instance admins can promote another user to instance admin.
20. Instance admins can manage which companies each user can access.
21. Permissions can be granted/revoked per member principal (human or agent) through one shared grant system.
22. Assignment scope prevents out-of-hierarchy or protected-role assignments.
23. Agents can assign tasks to humans only when allowed.
24. Humans can view assigned tasks in inbox and act on them per permissions.
25. All new mutations are company-scoped and logged in `activity_log`.
## V1 decisions (locked)
1. `local_trusted` will not support login UX in V1; implicit local board actor only.
2. Permissions use a normalized shared table: `principal_permission_grants` with scoped grants.
3. Invite delivery is copy-link only in V1 (no built-in email sending).
4. Bootstrap invite creation should require local shell access only (CLI path only, no HTTP bootstrap endpoint).
5. Approval review shows source IP only; no GeoIP/country lookup in V1.
6. Agent API-key lifetime is indefinite by default in V1, with explicit revoke/regenerate controls.
7. Local mode keeps full admin/settings/invite capabilities through the implicit local instance-admin actor.
8. Public/untrusted ingress for local mode is out of scope for V1; no `--dangerous-agent-ingress` in V1.

View File

@@ -0,0 +1,477 @@
# 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`

View File

@@ -0,0 +1,226 @@
# Deployment/Auth Mode Consolidation Plan
Status: Proposal
Owner: Server + CLI + UI
Date: 2026-02-23
## Goal
Keep Paperclip low-friction while making the mode model simpler and safer:
1. `local_trusted` remains the default and easiest path.
2. one authenticated runtime mode supports both private-network local use and public cloud use.
3. onboarding/configure/doctor stay primarily interactive and flagless.
4. Board identity is represented by a real user row in the database, with explicit role/membership integration points.
## Product Constraints (From Review)
1. `onboard` default flow is interactive (no flags required).
2. first mode choice defaults to `local_trusted`, with clear UX copy.
3. authenticated flow gives guidance for private vs public exposure.
4. `doctor` should also be flagless by default (read config and evaluate the selected mode/profile).
5. do not add backward-compatibility alias layers for abandoned mode names.
6. plan must explicitly cover how users/Board are represented in DB and how that affects task assignment and permissions.
## Current Implementation Audit (As Of 2026-02-23)
## Runtime/Auth
- Runtime deployment modes are currently `local_trusted | cloud_hosted` (`packages/shared/src/constants.ts`).
- `local_trusted` actor is currently synthetic:
- `req.actor = { type: "board", userId: "local-board", source: "local_implicit" }` (`server/src/middleware/auth.ts`).
- this is not a real auth user row by default.
- `cloud_hosted` uses Better Auth sessions and `authUsers` rows (`server/src/auth/better-auth.ts`, `packages/db/src/schema/auth.ts`).
## Bootstrap/Admin
- `cloud_hosted` requires `BETTER_AUTH_SECRET` and reports bootstrap status from `instance_user_roles` (`server/src/index.ts`, `server/src/routes/health.ts`).
- bootstrap invite acceptance promotes the signed-in user to `instance_admin` (`server/src/routes/access.ts`, `server/src/services/access.ts`).
## Membership/Assignment Integration
- User task assignment requires active `company_memberships` entry for that user (`server/src/services/issues.ts`).
- Local implicit board identity is not automatically a real membership principal; this is a gap for “board as assignable user” semantics.
## Proposed Runtime Model
## Modes
1. `local_trusted`
- no login required
- localhost/loopback only
- optimized for single-operator local setup
2. `authenticated`
- login required for human actions
- same auth stack for both private and public deployments
## Exposure Policy (Within `authenticated`)
1. `private`
- private-network deployments (LAN, VPN, Tailscale)
- low-friction URL handling (`auto` base URL)
- strict host allow policy for private targets
2. `public`
- internet-facing deployments
- explicit public base URL required
- stricter deployment checks in doctor
This is one authenticated mode with two safety policies, not two different auth systems.
## UX Contract
## Onboard (Primary Path: Interactive)
Default command remains:
```sh
pnpm paperclip onboard
```
Interactive server step:
1. ask mode with default selection `local_trusted`
2. copy for options:
- `local_trusted`: "Easiest for local setup (no login, localhost-only)"
- `authenticated`: "Login required; use for private network or public hosting"
3. if `authenticated`, ask exposure:
- `private`: "Private network access (for example Tailscale), lower setup friction"
- `public`: "Internet-facing deployment, stricter security requirements"
4. only if `authenticated + public`, ask for explicit public URL
Flags are optional power-user overrides, not required for normal setup.
## Configure
Default command remains interactive:
```sh
pnpm paperclip configure --section server
```
Same mode/exposure questions and defaults as onboarding.
## Doctor
Default command remains flagless:
```sh
pnpm paperclip doctor
```
Doctor reads configured mode/exposure and applies relevant checks.
Optional flags may exist for override/testing, but are not required for normal operation.
## Board/User Data Model Integration (Required)
## Requirement
Board must be a real DB user principal so user-centric features (task assignment, membership, audit identity) work consistently.
## Target Behavior
1. `local_trusted`
- seed/ensure a deterministic local board user row in `authUsers` during setup/startup.
- actor middleware uses that real user id instead of synthetic-only identity.
- ensure:
- `instance_user_roles` includes `instance_admin` for this user.
- company membership can be created/maintained for this user where needed.
2. `authenticated`
- Better Auth sign-up creates user row.
- bootstrap/admin flow promotes that real user to `instance_admin`.
- first company creation flow should ensure creator membership is active.
## Why This Matters
- `assigneeUserId` validation checks company membership.
- without a real board user + membership path, assigning tasks to board user is inconsistent.
## Configuration Contract (Target)
- `server.mode`: `local_trusted | authenticated`
- `server.exposure`: `private | public` (required when mode is `authenticated`)
- `auth.baseUrlMode`: `auto | explicit`
- `auth.publicBaseUrl`: required when `authenticated + public`
No compatibility aliases for discarded naming variants.
## No Backward-Compatibility Layer
This change is a clean cut:
- remove use of old split terminology in code and prompts.
- config schema uses only canonical fields/values above.
- existing dev instances can rerun onboarding or update config once.
## Implementation Phases
## Phase 1: Shared Schema + Config Surface
- `packages/shared/src/constants.ts`: define canonical mode/exposure constants.
- `packages/shared/src/config-schema.ts`: add mode/exposure/auth URL fields.
- `server/src/config.ts` and CLI config types: consume canonical fields only.
## Phase 2: CLI Interactive UX
- `cli/src/prompts/server.ts`: implement defaulted mode prompt and authenticated exposure guidance copy.
- `cli/src/commands/onboard.ts`: keep interactive-first flow; optional overrides only.
- `cli/src/commands/configure.ts`: same behavior for server section.
- `cli/src/commands/doctor.ts`: mode-aware checks from config, flagless default flow.
## Phase 3: Runtime/Auth Policy
- `server/src/index.ts`: enforce mode-specific startup constraints.
- `server/src/auth/better-auth.ts`: implement `auto` vs `explicit` base URL behavior.
- host/origin trust helper for `authenticated + private`.
## Phase 4: Board Principal Integration
- add ensure-board-user startup/setup step:
- real local board user row
- instance admin role row
- ensure first-company creation path grants creator membership.
- remove synthetic-only assumptions where they break user assignment/membership semantics.
## Phase 5: UI + Docs
- update UI labels/help text around mode and exposure guidance.
- update docs:
- `doc/DEPLOYMENT-MODES.md`
- `doc/DEVELOPING.md`
- `doc/CLI.md`
- `doc/SPEC-implementation.md`
## Test Plan
- config schema tests for canonical mode/exposure/auth fields.
- CLI prompt tests for default interactive selections and copy.
- doctor tests by mode/exposure.
- runtime tests:
- authenticated/private works without explicit URL
- authenticated/public requires explicit URL
- private host policy rejects untrusted hosts
- Board principal tests:
- local_trusted board user exists as real DB user
- board can be assigned tasks via `assigneeUserId` after membership setup
- creator membership behavior for authenticated flows
## Acceptance Criteria
1. `pnpm paperclip onboard` is interactive-first and defaults to `local_trusted`.
2. authenticated mode is one runtime mode with `private/public` exposure guidance.
3. `pnpm paperclip doctor` works flagless with mode-aware checks.
4. no extra compatibility aliases for dropped naming variants.
5. Board identity is represented by real DB user/role/membership integration points, enabling consistent task assignment and permission behavior.
## Verification Gate
Before merge:
```sh
pnpm -r typecheck
pnpm test:run
pnpm build
```