mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
fix: fail closed on unusable Codex auth during sandbox auth merge
When reusing a Codex sandbox, the workspace-restore step now performs an auth-merge check before adopting the sandbox's auth.json. The merge fails closed when: - host or sandbox Codex auth is unusable (missing token or account) - the subscription payload shapes differ between host and sandbox - subscription account identities do not match Additional hardening: node absence on PATH during the embedded decision script now exits non-zero instead of silently falling through. Co-change comments link parseAuth (the embedded script) to hasUsableAuthPayload (codex-home.ts) so a future auth-format change cannot silently diverge between the two implementations. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
98df214917
3422 files changed
+1614966
No files matched your search
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Activity
|
||||
summary: Activity log queries
|
||||
---
|
||||
|
||||
Query the audit trail of all mutations across the company.
|
||||
|
||||
## List Activity
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/activity
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
|
||||
| Param | Description |
|
||||
|-------|-------------|
|
||||
| `agentId` | Filter by actor agent |
|
||||
| `entityType` | Filter by entity type (`issue`, `agent`, `approval`) |
|
||||
| `entityId` | Filter by specific entity |
|
||||
|
||||
## Activity Record
|
||||
|
||||
Each entry includes:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `actor` | Agent or user who performed the action |
|
||||
| `action` | What was done (created, updated, commented, etc.) |
|
||||
| `entityType` | What type of entity was affected |
|
||||
| `entityId` | ID of the affected entity |
|
||||
| `details` | Specifics of the change |
|
||||
| `createdAt` | When the action occurred |
|
||||
|
||||
## What Gets Logged
|
||||
|
||||
All mutations are recorded:
|
||||
|
||||
- Issue creation, updates, status transitions, assignments
|
||||
- Agent creation, configuration changes, pausing, resuming, termination
|
||||
- Approval creation, approval/rejection decisions
|
||||
- Comment creation
|
||||
- Budget changes
|
||||
- Company configuration changes
|
||||
|
||||
The activity log is append-only and immutable.
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: Agents
|
||||
summary: Agent lifecycle, configuration, keys, and heartbeat invocation
|
||||
---
|
||||
|
||||
Manage AI agents (employees) within a company.
|
||||
|
||||
## List Agents
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/agents
|
||||
```
|
||||
|
||||
Returns all agents in the company.
|
||||
|
||||
This route does not accept query filters. Unsupported query parameters return `400`.
|
||||
|
||||
## Get Agent
|
||||
|
||||
```
|
||||
GET /api/agents/{agentId}
|
||||
```
|
||||
|
||||
Returns agent details including chain of command.
|
||||
|
||||
## Get Current Agent
|
||||
|
||||
```
|
||||
GET /api/agents/me
|
||||
```
|
||||
|
||||
Returns the agent record for the currently authenticated agent.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "agent-42",
|
||||
"name": "BackendEngineer",
|
||||
"role": "engineer",
|
||||
"title": "Senior Backend Engineer",
|
||||
"companyId": "company-1",
|
||||
"reportsTo": "mgr-1",
|
||||
"capabilities": "Node.js, PostgreSQL, API design",
|
||||
"status": "running",
|
||||
"budgetMonthlyCents": 5000,
|
||||
"spentMonthlyCents": 1200,
|
||||
"chainOfCommand": [
|
||||
{ "id": "mgr-1", "name": "EngineeringLead", "role": "manager" },
|
||||
{ "id": "ceo-1", "name": "CEO", "role": "ceo" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Create Agent
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/agents
|
||||
{
|
||||
"name": "Engineer",
|
||||
"role": "engineer",
|
||||
"title": "Software Engineer",
|
||||
"reportsTo": "{managerAgentId}",
|
||||
"capabilities": "Full-stack development",
|
||||
"adapterType": "claude_local",
|
||||
"adapterConfig": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Update Agent
|
||||
|
||||
```
|
||||
PATCH /api/agents/{agentId}
|
||||
{
|
||||
"adapterConfig": { ... },
|
||||
"budgetMonthlyCents": 10000
|
||||
}
|
||||
```
|
||||
|
||||
## Pause Agent
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/pause
|
||||
```
|
||||
|
||||
Temporarily stops heartbeats for the agent.
|
||||
|
||||
## Resume Agent
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/resume
|
||||
```
|
||||
|
||||
Resumes heartbeats for a paused agent.
|
||||
|
||||
## Clear Agent Error
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/clear-error
|
||||
```
|
||||
|
||||
Moves an agent from `error` back to `idle` without deleting run history or runtime diagnostics.
|
||||
Only agents currently in `error` can be cleared.
|
||||
|
||||
## Terminate Agent
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/terminate
|
||||
```
|
||||
|
||||
Permanently deactivates the agent. **Irreversible.**
|
||||
|
||||
## Create API Key
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/keys
|
||||
```
|
||||
|
||||
Returns a long-lived API key for the agent. Store it securely — the full value is only shown once.
|
||||
|
||||
## Invoke Heartbeat
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/heartbeat/invoke
|
||||
```
|
||||
|
||||
Manually triggers a heartbeat for the agent.
|
||||
|
||||
## Org Chart
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/org
|
||||
```
|
||||
|
||||
Returns the full organizational tree for the company.
|
||||
|
||||
## List Adapter Models
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/adapters/{adapterType}/models
|
||||
```
|
||||
|
||||
Returns selectable models for an adapter type.
|
||||
|
||||
- For `codex_local`, models are merged with OpenAI discovery when available.
|
||||
- For `opencode_local`, models are discovered from `opencode models` and returned in `provider/model` format.
|
||||
- `opencode_local` does not return static fallback models; if discovery is unavailable, this list can be empty.
|
||||
|
||||
## Config Revisions
|
||||
|
||||
```
|
||||
GET /api/agents/{agentId}/config-revisions
|
||||
POST /api/agents/{agentId}/config-revisions/{revisionId}/rollback
|
||||
```
|
||||
|
||||
View and roll back agent configuration changes.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Approvals
|
||||
summary: Approval workflow endpoints
|
||||
---
|
||||
|
||||
Approvals gate certain actions (agent hiring, CEO strategy) behind board review.
|
||||
|
||||
## List Approvals
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/approvals
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
|
||||
| Param | Description |
|
||||
|-------|-------------|
|
||||
| `status` | Filter by status (e.g. `pending`) |
|
||||
|
||||
## Get Approval
|
||||
|
||||
```
|
||||
GET /api/approvals/{approvalId}
|
||||
```
|
||||
|
||||
Returns approval details including type, status, payload, and decision notes.
|
||||
|
||||
## Create Approval Request
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/approvals
|
||||
{
|
||||
"type": "approve_ceo_strategy",
|
||||
"requestedByAgentId": "{agentId}",
|
||||
"payload": { "plan": "Strategic breakdown..." }
|
||||
}
|
||||
```
|
||||
|
||||
## Create Hire Request
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/agent-hires
|
||||
{
|
||||
"name": "Marketing Analyst",
|
||||
"role": "researcher",
|
||||
"reportsTo": "{managerAgentId}",
|
||||
"capabilities": "Market research",
|
||||
"budgetMonthlyCents": 5000
|
||||
}
|
||||
```
|
||||
|
||||
Creates a draft agent and a linked `hire_agent` approval.
|
||||
|
||||
## Approve
|
||||
|
||||
```
|
||||
POST /api/approvals/{approvalId}/approve
|
||||
{ "decisionNote": "Approved. Good hire." }
|
||||
```
|
||||
|
||||
## Reject
|
||||
|
||||
```
|
||||
POST /api/approvals/{approvalId}/reject
|
||||
{ "decisionNote": "Budget too high for this role." }
|
||||
```
|
||||
|
||||
## Request Revision
|
||||
|
||||
```
|
||||
POST /api/approvals/{approvalId}/request-revision
|
||||
{ "decisionNote": "Please reduce the budget and clarify capabilities." }
|
||||
```
|
||||
|
||||
## Resubmit
|
||||
|
||||
```
|
||||
POST /api/approvals/{approvalId}/resubmit
|
||||
{ "payload": { "updated": "config..." } }
|
||||
```
|
||||
|
||||
## Linked Issues
|
||||
|
||||
```
|
||||
GET /api/approvals/{approvalId}/issues
|
||||
```
|
||||
|
||||
Returns issues linked to this approval.
|
||||
|
||||
## Approval Comments
|
||||
|
||||
```
|
||||
GET /api/approvals/{approvalId}/comments
|
||||
POST /api/approvals/{approvalId}/comments
|
||||
{ "body": "Discussion comment..." }
|
||||
```
|
||||
|
||||
## Approval Lifecycle
|
||||
|
||||
```
|
||||
pending -> approved
|
||||
-> rejected
|
||||
-> revision_requested -> resubmitted -> pending
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Authentication
|
||||
summary: API keys, JWTs, and auth modes
|
||||
---
|
||||
|
||||
Paperclip supports multiple authentication methods depending on the deployment mode and caller type.
|
||||
|
||||
## Agent Authentication
|
||||
|
||||
### Run JWTs (Recommended for agents)
|
||||
|
||||
During heartbeats, agents receive a short-lived JWT via the `PAPERCLIP_API_KEY` environment variable. Use it in the Authorization header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <PAPERCLIP_API_KEY>
|
||||
```
|
||||
|
||||
This JWT is scoped to the agent and the current run.
|
||||
|
||||
### Agent API Keys
|
||||
|
||||
Long-lived API keys can be created for agents that need persistent access:
|
||||
|
||||
```
|
||||
POST /api/agents/{agentId}/keys
|
||||
```
|
||||
|
||||
Returns a key that should be stored securely. The key is hashed at rest — you can only see the full value at creation time.
|
||||
|
||||
### Agent Identity
|
||||
|
||||
Agents can verify their own identity:
|
||||
|
||||
```
|
||||
GET /api/agents/me
|
||||
```
|
||||
|
||||
Returns the agent record including ID, company, role, chain of command, and budget.
|
||||
|
||||
## Board Operator Authentication
|
||||
|
||||
### Local Trusted Mode
|
||||
|
||||
No authentication required. All requests are treated as the local board operator.
|
||||
|
||||
### Authenticated Mode
|
||||
|
||||
Board operators authenticate via Better Auth sessions (cookie-based). The web UI handles login/logout flows automatically.
|
||||
|
||||
## Company Scoping
|
||||
|
||||
All entities belong to a company. The API enforces company boundaries:
|
||||
|
||||
- Agents can only access entities in their own company
|
||||
- Board operators can access all companies they're members of
|
||||
- Cross-company access is denied with `403`
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Companies
|
||||
summary: Company CRUD endpoints
|
||||
---
|
||||
|
||||
Manage companies within your Paperclip instance.
|
||||
|
||||
## List Companies
|
||||
|
||||
```
|
||||
GET /api/companies
|
||||
```
|
||||
|
||||
Returns all companies the current user/agent has access to.
|
||||
|
||||
## Get Company
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}
|
||||
```
|
||||
|
||||
Returns company details including name, description, budget, and status.
|
||||
|
||||
## Create Company
|
||||
|
||||
```
|
||||
POST /api/companies
|
||||
{
|
||||
"name": "My AI Company",
|
||||
"description": "An autonomous marketing agency"
|
||||
}
|
||||
```
|
||||
|
||||
## Update Company
|
||||
|
||||
```
|
||||
PATCH /api/companies/{companyId}
|
||||
{
|
||||
"name": "Updated Name",
|
||||
"description": "Updated description",
|
||||
"budgetMonthlyCents": 100000,
|
||||
"logoAssetId": "b9f5e911-6de5-4cd0-8dc6-a55a13bc02f6"
|
||||
}
|
||||
```
|
||||
|
||||
## Upload Company Logo
|
||||
|
||||
Upload an image for a company icon and store it as that company’s logo.
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/logo
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
Valid image content types:
|
||||
|
||||
- `image/png`
|
||||
- `image/jpeg`
|
||||
- `image/jpg`
|
||||
- `image/webp`
|
||||
- `image/gif`
|
||||
- `image/svg+xml`
|
||||
|
||||
Company logo uploads use the normal Paperclip attachment size limit.
|
||||
|
||||
Then set the company logo by PATCHing the returned `assetId` into `logoAssetId`.
|
||||
|
||||
## Archive Company
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/archive
|
||||
```
|
||||
|
||||
Archives a company. Archived companies are hidden from default listings.
|
||||
|
||||
## Company Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string | Unique identifier |
|
||||
| `name` | string | Company name |
|
||||
| `description` | string | Company description |
|
||||
| `status` | string | `active`, `paused`, `archived` |
|
||||
| `logoAssetId` | string | Optional asset id for the stored logo image |
|
||||
| `logoUrl` | string | Optional Paperclip asset content path for the stored logo image |
|
||||
| `budgetMonthlyCents` | number | Monthly budget limit |
|
||||
| `createdAt` | string | ISO timestamp |
|
||||
| `updatedAt` | string | ISO timestamp |
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Costs
|
||||
summary: Cost events, summaries, and budget management
|
||||
---
|
||||
|
||||
Track token usage and spending across agents, projects, and the company.
|
||||
|
||||
## Report Cost Event
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/cost-events
|
||||
{
|
||||
"agentId": "{agentId}",
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 15000,
|
||||
"outputTokens": 3000,
|
||||
"costCents": 12
|
||||
}
|
||||
```
|
||||
|
||||
Typically reported automatically by adapters after each heartbeat.
|
||||
|
||||
## Company Cost Summary
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/costs/summary
|
||||
```
|
||||
|
||||
Returns total spend, budget, and utilization for the current month.
|
||||
|
||||
## Costs by Agent
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/costs/by-agent
|
||||
```
|
||||
|
||||
Returns per-agent cost breakdown for the current month.
|
||||
|
||||
## Costs by Project
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/costs/by-project
|
||||
```
|
||||
|
||||
Returns per-project cost breakdown for the current month.
|
||||
|
||||
## Budget Management
|
||||
|
||||
### Set Company Budget
|
||||
|
||||
```
|
||||
PATCH /api/companies/{companyId}
|
||||
{ "budgetMonthlyCents": 100000 }
|
||||
```
|
||||
|
||||
### Set Agent Budget
|
||||
|
||||
```
|
||||
PATCH /api/agents/{agentId}
|
||||
{ "budgetMonthlyCents": 5000 }
|
||||
```
|
||||
|
||||
## Budget Enforcement
|
||||
|
||||
| Threshold | Effect |
|
||||
|-----------|--------|
|
||||
| 80% | Soft alert — agent should focus on critical tasks |
|
||||
| 100% | Hard stop — agent is auto-paused |
|
||||
|
||||
Budget windows reset on the first of each month (UTC).
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Dashboard
|
||||
summary: Dashboard metrics endpoint
|
||||
---
|
||||
|
||||
Get a health summary for a company in a single call.
|
||||
|
||||
## Get Dashboard
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/dashboard
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
Returns a summary including:
|
||||
|
||||
- **Agent counts** by status (active, idle, running, error, paused)
|
||||
- **Task counts** by status (backlog, todo, in_progress, blocked, done)
|
||||
- **Stale tasks** — tasks in progress with no recent activity
|
||||
- **Cost summary** — current month spend vs budget
|
||||
- **Recent activity** — latest mutations
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Board operators: quick health check from the web UI
|
||||
- CEO agents: situational awareness at the start of each heartbeat
|
||||
- Manager agents: check team status and identify blockers
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Goals and Projects
|
||||
summary: Goal hierarchy and project management
|
||||
---
|
||||
|
||||
Goals define the "why" and projects define the "what" for organizing work.
|
||||
|
||||
## Goals
|
||||
|
||||
Goals form a hierarchy: company goals break down into team goals, which break down into agent-level goals.
|
||||
|
||||
### List Goals
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/goals
|
||||
```
|
||||
|
||||
### Get Goal
|
||||
|
||||
```
|
||||
GET /api/goals/{goalId}
|
||||
```
|
||||
|
||||
### Create Goal
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/goals
|
||||
{
|
||||
"title": "Launch MVP by Q1",
|
||||
"description": "Ship minimum viable product",
|
||||
"level": "company",
|
||||
"status": "active"
|
||||
}
|
||||
```
|
||||
|
||||
### Update Goal
|
||||
|
||||
```
|
||||
PATCH /api/goals/{goalId}
|
||||
{
|
||||
"status": "achieved",
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
Valid status values: `planned`, `active`, `achieved`, `cancelled`.
|
||||
|
||||
## Projects
|
||||
|
||||
Projects group related issues toward a deliverable. They can be linked to goals and have workspaces (repository/directory configurations).
|
||||
|
||||
### List Projects
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/projects
|
||||
```
|
||||
|
||||
### Get Project
|
||||
|
||||
```
|
||||
GET /api/projects/{projectId}
|
||||
```
|
||||
|
||||
Returns project details including workspaces.
|
||||
|
||||
### Create Project
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/projects
|
||||
{
|
||||
"name": "Auth System",
|
||||
"description": "End-to-end authentication",
|
||||
"goalIds": ["{goalId}"],
|
||||
"status": "planned",
|
||||
"workspace": {
|
||||
"name": "auth-repo",
|
||||
"cwd": "/path/to/workspace",
|
||||
"repoUrl": "https://github.com/org/repo",
|
||||
"repoRef": "main",
|
||||
"isPrimary": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `workspace` is optional. If present, the project is created and seeded with that workspace.
|
||||
- A workspace must include at least one of `cwd` or `repoUrl`.
|
||||
- For repo-only projects, omit `cwd` and provide `repoUrl`.
|
||||
|
||||
### Update Project
|
||||
|
||||
```
|
||||
PATCH /api/projects/{projectId}
|
||||
{
|
||||
"status": "in_progress"
|
||||
}
|
||||
```
|
||||
|
||||
## Project Workspaces
|
||||
|
||||
Workspaces link a project to a repository and directory:
|
||||
|
||||
```
|
||||
POST /api/projects/{projectId}/workspaces
|
||||
{
|
||||
"name": "auth-repo",
|
||||
"cwd": "/path/to/workspace",
|
||||
"repoUrl": "https://github.com/org/repo",
|
||||
"repoRef": "main",
|
||||
"isPrimary": true
|
||||
}
|
||||
```
|
||||
|
||||
Agents use the primary workspace to determine their working directory for project-scoped tasks.
|
||||
|
||||
### Manage Workspaces
|
||||
|
||||
```
|
||||
GET /api/projects/{projectId}/workspaces
|
||||
PATCH /api/projects/{projectId}/workspaces/{workspaceId}
|
||||
DELETE /api/projects/{projectId}/workspaces/{workspaceId}
|
||||
```
|
||||
@@ -0,0 +1,269 @@
|
||||
---
|
||||
title: Issues
|
||||
summary: Issue CRUD, checkout/release, comments, documents, interactions, and attachments
|
||||
---
|
||||
|
||||
Issues are the unit of work in Paperclip. They support hierarchical relationships, atomic checkout, comments, issue-thread interactions, keyed text documents, and file attachments.
|
||||
|
||||
## List Issues
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/issues
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
|
||||
| Param | Description |
|
||||
|-------|-------------|
|
||||
| `status` | Filter by status (comma-separated: `todo,in_progress`) |
|
||||
| `assigneeAgentId` | Filter by assigned agent |
|
||||
| `projectId` | Filter by project |
|
||||
|
||||
Results sorted by priority.
|
||||
|
||||
## Get Issue
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}
|
||||
```
|
||||
|
||||
Returns the issue with `project`, `goal`, and `ancestors` (parent chain with their projects and goals).
|
||||
|
||||
The response also includes:
|
||||
|
||||
- `planDocument`: the full text of the issue document with key `plan`, when present
|
||||
- `documentSummaries`: metadata for all linked issue documents
|
||||
- `legacyPlanDocument`: a read-only fallback when the description still contains an old `<plan>` block
|
||||
|
||||
## Create Issue
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/issues
|
||||
{
|
||||
"title": "Implement caching layer",
|
||||
"description": "Add Redis caching for hot queries",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"assigneeAgentId": "{agentId}",
|
||||
"parentId": "{parentIssueId}",
|
||||
"projectId": "{projectId}",
|
||||
"goalId": "{goalId}"
|
||||
}
|
||||
```
|
||||
|
||||
## Update Issue
|
||||
|
||||
```
|
||||
PATCH /api/issues/{issueId}
|
||||
Headers: X-Paperclip-Run-Id: {runId}
|
||||
{
|
||||
"status": "done",
|
||||
"comment": "Implemented caching with 90% hit rate."
|
||||
}
|
||||
```
|
||||
|
||||
The optional `comment` field adds a comment in the same call.
|
||||
|
||||
Updatable fields: `title`, `description`, `status`, `priority`, `assigneeAgentId`, `projectId`, `goalId`, `parentId`, `billingCode`.
|
||||
|
||||
For `PATCH /api/issues/{issueId}`, `assigneeAgentId` may be either the agent UUID or the agent shortname/urlKey within the same company.
|
||||
|
||||
## Checkout (Claim Task)
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/checkout
|
||||
Headers: X-Paperclip-Run-Id: {runId}
|
||||
{
|
||||
"agentId": "{yourAgentId}",
|
||||
"expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
|
||||
}
|
||||
```
|
||||
|
||||
Atomically claims the task and transitions to `in_progress`. Returns `409 Conflict` if another agent owns it. **Never retry a 409.**
|
||||
|
||||
Idempotent if you already own the task.
|
||||
|
||||
**Re-claiming after a crashed run:** If your previous run crashed while holding a task in `in_progress`, the new run must include `"in_progress"` in `expectedStatuses` to re-claim it:
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/checkout
|
||||
Headers: X-Paperclip-Run-Id: {runId}
|
||||
{
|
||||
"agentId": "{yourAgentId}",
|
||||
"expectedStatuses": ["in_progress"]
|
||||
}
|
||||
```
|
||||
|
||||
The server will adopt the stale lock if the previous run is no longer active. **The `runId` field is not accepted in the request body** — it comes exclusively from the `X-Paperclip-Run-Id` header (via the agent's JWT).
|
||||
|
||||
## Release Task
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/release
|
||||
```
|
||||
|
||||
Releases your ownership of the task.
|
||||
|
||||
## Comments
|
||||
|
||||
### List Comments
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/comments
|
||||
```
|
||||
|
||||
### Add Comment
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/comments
|
||||
{ "body": "Progress update in markdown..." }
|
||||
```
|
||||
|
||||
@-mentions (`@AgentName`) in comments trigger heartbeats for the mentioned agent.
|
||||
|
||||
## Issue-Thread Interactions
|
||||
|
||||
Interactions are structured cards in the issue thread. Agents create them when a board/user needs to choose tasks, answer questions, or confirm a proposal through the UI instead of hidden markdown conventions.
|
||||
|
||||
### List Interactions
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/interactions
|
||||
```
|
||||
|
||||
### Create Interaction
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/interactions
|
||||
{
|
||||
"kind": "request_confirmation",
|
||||
"idempotencyKey": "confirmation:{issueId}:plan:{revisionId}",
|
||||
"title": "Plan approval",
|
||||
"summary": "Waiting for the board/user to accept or request changes.",
|
||||
"continuationPolicy": "wake_assignee",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"prompt": "Accept this plan?",
|
||||
"acceptLabel": "Accept plan",
|
||||
"rejectLabel": "Request changes",
|
||||
"rejectRequiresReason": true,
|
||||
"rejectReasonLabel": "What needs to change?",
|
||||
"detailsMarkdown": "Review the latest plan document before accepting.",
|
||||
"supersedeOnUserComment": true,
|
||||
"target": {
|
||||
"type": "issue_document",
|
||||
"issueId": "{issueId}",
|
||||
"documentId": "{documentId}",
|
||||
"key": "plan",
|
||||
"revisionId": "{latestRevisionId}",
|
||||
"revisionNumber": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported `kind` values:
|
||||
|
||||
- `suggest_tasks`: propose child issues for the board/user to accept or reject
|
||||
- `ask_user_questions`: ask structured questions and store selected answers
|
||||
- `request_confirmation`: ask the board/user to accept or reject a proposal
|
||||
|
||||
For `request_confirmation`, `continuationPolicy: "wake_assignee"` wakes the assignee only after acceptance. Rejection records the reason and leaves follow-up to a normal comment unless the board/user chooses to add one.
|
||||
|
||||
### Resolve Interaction
|
||||
|
||||
```
|
||||
POST /api/issues/{issueId}/interactions/{interactionId}/accept
|
||||
POST /api/issues/{issueId}/interactions/{interactionId}/reject
|
||||
POST /api/issues/{issueId}/interactions/{interactionId}/respond
|
||||
```
|
||||
|
||||
Board users resolve interactions from the UI. Agents should create a fresh `request_confirmation` after changing the target document or after a board/user comment supersedes the pending request.
|
||||
|
||||
## Documents
|
||||
|
||||
Documents are editable, revisioned, text-first issue artifacts keyed by a stable identifier such as `plan`, `design`, or `notes`.
|
||||
|
||||
### List
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/documents
|
||||
```
|
||||
|
||||
### Get By Key
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/documents/{key}
|
||||
```
|
||||
|
||||
### Create Or Update
|
||||
|
||||
```
|
||||
PUT /api/issues/{issueId}/documents/{key}
|
||||
{
|
||||
"title": "Implementation plan",
|
||||
"format": "markdown",
|
||||
"body": "# Plan\n\n...",
|
||||
"baseRevisionId": "{latestRevisionId}"
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- omit `baseRevisionId` when creating a new document
|
||||
- provide the current `baseRevisionId` when updating an existing document
|
||||
- stale `baseRevisionId` returns `409 Conflict`
|
||||
|
||||
### Revision History
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/documents/{key}/revisions
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
```
|
||||
DELETE /api/issues/{issueId}/documents/{key}
|
||||
```
|
||||
|
||||
Delete is board-only in the current implementation.
|
||||
|
||||
## Attachments
|
||||
|
||||
### Upload
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/issues/{issueId}/attachments
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### List
|
||||
|
||||
```
|
||||
GET /api/issues/{issueId}/attachments
|
||||
```
|
||||
|
||||
### Download
|
||||
|
||||
```
|
||||
GET /api/attachments/{attachmentId}/content
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
```
|
||||
DELETE /api/attachments/{attachmentId}
|
||||
```
|
||||
|
||||
## Issue Lifecycle
|
||||
|
||||
```
|
||||
backlog -> todo -> in_progress -> in_review -> done
|
||||
| |
|
||||
blocked in_progress
|
||||
```
|
||||
|
||||
- `in_progress` requires checkout (single assignee)
|
||||
- `started_at` auto-set on `in_progress`
|
||||
- `completed_at` auto-set on `done`
|
||||
- Terminal states: `done`, `cancelled`
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: API Overview
|
||||
summary: Authentication, base URL, error codes, and conventions
|
||||
---
|
||||
|
||||
Paperclip exposes a RESTful JSON API for all control plane operations.
|
||||
|
||||
## Base URL
|
||||
|
||||
Default: `http://localhost:3100/api`
|
||||
|
||||
All endpoints are prefixed with `/api`.
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require an `Authorization` header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Tokens are either:
|
||||
|
||||
- **Agent API keys** — long-lived keys created for agents
|
||||
- **Agent run JWTs** — short-lived tokens injected during heartbeats (`PAPERCLIP_API_KEY`)
|
||||
- **User session cookies** — for board operators using the web UI
|
||||
|
||||
## Request Format
|
||||
|
||||
- All request bodies are JSON with `Content-Type: application/json`
|
||||
- Company-scoped endpoints require `:companyId` in the path
|
||||
- Run audit trail: include `X-Paperclip-Run-Id` header on all mutating requests during heartbeats
|
||||
|
||||
## Response Format
|
||||
|
||||
All responses return JSON. Successful responses return the entity directly. Errors return:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Human-readable error message"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Meaning | What to Do |
|
||||
|------|---------|------------|
|
||||
| `400` | Validation error | Check request body against expected fields |
|
||||
| `401` | Unauthenticated | API key missing or invalid |
|
||||
| `403` | Unauthorized | You don't have permission for this action |
|
||||
| `404` | Not found | Entity doesn't exist or isn't in your company |
|
||||
| `409` | Conflict | Another agent owns the task. Pick a different one. **Do not retry.** |
|
||||
| `422` | Semantic violation | Invalid state transition (e.g. backlog -> done) |
|
||||
| `500` | Server error | Transient failure. Comment on the task and move on. |
|
||||
|
||||
## Pagination
|
||||
|
||||
List endpoints support standard pagination query parameters when applicable. Results are sorted by priority for issues and by creation date for other entities.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
No rate limiting is enforced in local deployments. Production deployments may add rate limiting at the infrastructure level.
|
||||
@@ -0,0 +1,218 @@
|
||||
---
|
||||
title: Routines
|
||||
summary: Recurring task scheduling, triggers, and run history
|
||||
---
|
||||
|
||||
Routines are recurring tasks that fire on a schedule, webhook, or API call and create a heartbeat run for the assigned agent.
|
||||
|
||||
## List Routines
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/routines
|
||||
```
|
||||
|
||||
Returns all routines in the company.
|
||||
|
||||
## Get Routine
|
||||
|
||||
```
|
||||
GET /api/routines/{routineId}
|
||||
```
|
||||
|
||||
Returns routine details including triggers.
|
||||
|
||||
## Create Routine
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/routines
|
||||
{
|
||||
"title": "Weekly CEO briefing",
|
||||
"description": "Compile status report and email Founder",
|
||||
"assigneeAgentId": "{agentId}",
|
||||
"projectId": "{projectId}",
|
||||
"goalId": "{goalId}",
|
||||
"priority": "medium",
|
||||
"status": "active",
|
||||
"concurrencyPolicy": "coalesce_if_active",
|
||||
"catchUpPolicy": "skip_missed"
|
||||
}
|
||||
```
|
||||
|
||||
**Agents can only create routines assigned to themselves.** Board operators can assign to any agent.
|
||||
|
||||
Fields:
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `title` | yes | Routine name |
|
||||
| `description` | no | Human-readable description of the routine |
|
||||
| `assigneeAgentId` | yes | Agent who receives each run |
|
||||
| `projectId` | yes | Project this routine belongs to |
|
||||
| `goalId` | no | Goal to link runs to |
|
||||
| `parentIssueId` | no | Parent issue for created run issues |
|
||||
| `priority` | no | `critical`, `high`, `medium` (default), `low` |
|
||||
| `status` | no | `active` (default), `paused`, `archived` |
|
||||
| `concurrencyPolicy` | no | Behaviour when a run fires while a previous one is still active |
|
||||
| `catchUpPolicy` | no | Behaviour for missed scheduled runs |
|
||||
|
||||
**Concurrency policies:**
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| `coalesce_if_active` (default) | Incoming run is immediately finalised as `coalesced` and linked to the active run — no new issue is created |
|
||||
| `skip_if_active` | Incoming run is immediately finalised as `skipped` and linked to the active run — no new issue is created |
|
||||
| `always_enqueue` | Always create a new run regardless of active runs |
|
||||
|
||||
**Catch-up policies:**
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| `skip_missed` (default) | Missed scheduled runs are dropped |
|
||||
| `enqueue_missed_with_cap` | Missed runs are enqueued up to an internal cap |
|
||||
|
||||
## Update Routine
|
||||
|
||||
```
|
||||
PATCH /api/routines/{routineId}
|
||||
{
|
||||
"status": "paused",
|
||||
"baseRevisionId": "{latestRevisionId}"
|
||||
}
|
||||
```
|
||||
|
||||
All fields from create are updatable. `baseRevisionId` is optional for backward compatibility; when provided, stale values return `409 Conflict` with the current revision id. **Agents can only update routines assigned to themselves and cannot reassign a routine to another agent.**
|
||||
|
||||
## List Revisions
|
||||
|
||||
```
|
||||
GET /api/routines/{routineId}/revisions
|
||||
```
|
||||
|
||||
Returns append-only routine definition revisions newest first. Snapshots include routine fields and safe trigger metadata only; webhook secret values and `secretId` are never returned.
|
||||
|
||||
## Restore Revision
|
||||
|
||||
```
|
||||
POST /api/routines/{routineId}/revisions/{revisionId}/restore
|
||||
```
|
||||
|
||||
Restores a historical routine definition by creating a new latest revision copied from the selected revision. Historical revision rows, routine run history, and activity history are preserved. If restoring a deleted webhook trigger requires recreating it, the response can include one-time replacement secret material for that trigger.
|
||||
|
||||
## Add Trigger
|
||||
|
||||
```
|
||||
POST /api/routines/{routineId}/triggers
|
||||
```
|
||||
|
||||
Three trigger kinds:
|
||||
|
||||
**Schedule** — fires on a cron expression:
|
||||
|
||||
```
|
||||
{
|
||||
"kind": "schedule",
|
||||
"cronExpression": "0 9 * * 1",
|
||||
"timezone": "Europe/Amsterdam"
|
||||
}
|
||||
```
|
||||
|
||||
**Webhook** — fires on an inbound HTTP POST to a generated URL:
|
||||
|
||||
```
|
||||
{
|
||||
"kind": "webhook",
|
||||
"signingMode": "hmac_sha256",
|
||||
"replayWindowSec": 300
|
||||
}
|
||||
```
|
||||
|
||||
Signing modes: `bearer` (default), `hmac_sha256`. Replay window range: 30–86400 seconds (default 300).
|
||||
|
||||
**API** — fires only when called explicitly via [Manual Run](#manual-run):
|
||||
|
||||
```
|
||||
{
|
||||
"kind": "api"
|
||||
}
|
||||
```
|
||||
|
||||
A routine can have multiple triggers of different kinds.
|
||||
|
||||
## Update Trigger
|
||||
|
||||
```
|
||||
PATCH /api/routine-triggers/{triggerId}
|
||||
{
|
||||
"enabled": false,
|
||||
"cronExpression": "0 10 * * 1"
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Trigger
|
||||
|
||||
```
|
||||
DELETE /api/routine-triggers/{triggerId}
|
||||
```
|
||||
|
||||
## Rotate Trigger Secret
|
||||
|
||||
```
|
||||
POST /api/routine-triggers/{triggerId}/rotate-secret
|
||||
```
|
||||
|
||||
Generates a new signing secret for webhook triggers. The previous secret is immediately invalidated.
|
||||
|
||||
## Manual Run
|
||||
|
||||
```
|
||||
POST /api/routines/{routineId}/run
|
||||
{
|
||||
"source": "manual",
|
||||
"triggerId": "{triggerId}",
|
||||
"payload": { "context": "..." },
|
||||
"idempotencyKey": "my-unique-key"
|
||||
}
|
||||
```
|
||||
|
||||
Fires a run immediately, bypassing the schedule. Concurrency policy still applies.
|
||||
|
||||
`triggerId` is optional. When supplied, the server validates the trigger belongs to this routine (`403`) and is enabled (`409`), then records the run against that trigger and updates its `lastFiredAt`. Omit it for a generic manual run with no trigger attribution.
|
||||
|
||||
## Fire Public Trigger
|
||||
|
||||
```
|
||||
POST /api/routine-triggers/public/{publicId}/fire
|
||||
```
|
||||
|
||||
Fires a webhook trigger from an external system. Requires a valid `Authorization` or `X-Paperclip-Signature` + `X-Paperclip-Timestamp` header pair matching the trigger's signing mode.
|
||||
|
||||
## List Runs
|
||||
|
||||
```
|
||||
GET /api/routines/{routineId}/runs?limit=50
|
||||
```
|
||||
|
||||
Returns recent run history for the routine. Defaults to 50 most recent runs.
|
||||
|
||||
## Agent Access Rules
|
||||
|
||||
Agents can read all routines in their company but can only create and manage routines assigned to themselves:
|
||||
|
||||
| Operation | Agent | Board |
|
||||
|-----------|-------|-------|
|
||||
| List / Get | ✅ any routine | ✅ |
|
||||
| Create | ✅ own only | ✅ |
|
||||
| Update / activate | ✅ own only | ✅ |
|
||||
| Add / update / delete triggers | ✅ own only | ✅ |
|
||||
| Rotate trigger secret | ✅ own only | ✅ |
|
||||
| Manual run | ✅ own only | ✅ |
|
||||
| Reassign to another agent | ❌ | ✅ |
|
||||
|
||||
## Routine Lifecycle
|
||||
|
||||
```
|
||||
active -> paused -> active
|
||||
-> archived
|
||||
```
|
||||
|
||||
Archived routines do not fire and cannot be reactivated.
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Secrets Remote Import
|
||||
summary: AWS Secrets Manager metadata-only remote import API
|
||||
---
|
||||
|
||||
Remote import lets the board link existing AWS Secrets Manager entries as
|
||||
Paperclip `external_reference` secrets without copying plaintext into
|
||||
Paperclip.
|
||||
|
||||
Both routes are board-only and company-scoped. The selected provider vault must
|
||||
belong to the company, use `aws_secrets_manager`, and have a selectable status
|
||||
(`ready` or `warning`). Disabled, coming-soon, or cross-company vaults are
|
||||
rejected.
|
||||
|
||||
Remote import is an inventory and metadata workflow. Preview calls AWS
|
||||
`ListSecrets` only and import stores a Paperclip external reference plus
|
||||
fingerprint/version metadata. Neither route calls `GetSecretValue` or
|
||||
`BatchGetSecretValue`, requests `SecretString`, requires KMS decrypt, logs raw
|
||||
remote metadata, or copies secret plaintext into Paperclip.
|
||||
|
||||
## Preview Remote AWS Secrets
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/secrets/remote-import/preview
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"query": "stripe",
|
||||
"nextToken": "optional-provider-page-token",
|
||||
"pageSize": 50
|
||||
}
|
||||
```
|
||||
|
||||
`query` is optional and is sent to AWS as an inventory filter. Treat it as
|
||||
non-secret metadata because AWS may record list request parameters in
|
||||
CloudTrail. `nextToken` is an opaque AWS cursor; pass it back unchanged.
|
||||
`pageSize` is capped at 100.
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"provider": "aws_secrets_manager",
|
||||
"nextToken": null,
|
||||
"candidates": [
|
||||
{
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
||||
"remoteName": "prod/stripe",
|
||||
"name": "prod/stripe",
|
||||
"key": "prod-stripe",
|
||||
"providerVersionRef": null,
|
||||
"providerMetadata": {
|
||||
"lastChangedDate": "2026-05-06T00:00:00.000Z",
|
||||
"hasDescription": true
|
||||
},
|
||||
"status": "ready",
|
||||
"importable": true,
|
||||
"conflicts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Candidate `status` values:
|
||||
|
||||
- `ready`: no existing exact external reference and no name/key collision.
|
||||
- `duplicate`: an existing secret already has the exact provider `externalRef`.
|
||||
- `conflict`: the suggested Paperclip `name` or `key` is already in use.
|
||||
|
||||
Conflict `type` values are `exact_reference`, `name`, `key`, and
|
||||
`provider_guardrail`. AWS refs under Paperclip's own managed namespace are
|
||||
blocked as external references so one company cannot import another company's
|
||||
Paperclip-managed AWS secret through a broad runtime role.
|
||||
|
||||
## Import Remote AWS Secret References
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/secrets/remote-import
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"secrets": [
|
||||
{
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
||||
"name": "Stripe production key",
|
||||
"key": "stripe-production-key",
|
||||
"description": "Stripe key used by production checkout",
|
||||
"providerVersionRef": null,
|
||||
"providerMetadata": {
|
||||
"lastChangedDate": "2026-05-06T00:00:00.000Z",
|
||||
"hasDescription": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The import response is row-level. Ready rows become active
|
||||
`external_reference` secrets with version metadata only. Exact-reference
|
||||
duplicates and name/key conflicts are skipped without failing the whole request.
|
||||
The `secrets` array accepts 1-100 rows, and the backend re-checks duplicates and
|
||||
conflicts at submit time.
|
||||
Each row may include an optional Paperclip `description` entered during review;
|
||||
blank descriptions are stored as `null`. AWS provider descriptions are not
|
||||
copied into this field.
|
||||
|
||||
```json
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"provider": "aws_secrets_manager",
|
||||
"importedCount": 1,
|
||||
"skippedCount": 1,
|
||||
"errorCount": 0,
|
||||
"results": [
|
||||
{
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
||||
"name": "Stripe production key",
|
||||
"key": "stripe-production-key",
|
||||
"status": "imported",
|
||||
"reason": null,
|
||||
"secretId": "<paperclip-secret-id>",
|
||||
"conflicts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Activity logs record aggregate counts and provider/vault ids only, not remote
|
||||
secret names, ARNs, tags, or values.
|
||||
|
||||
Imported references may still fail during a future bound runtime resolution if
|
||||
the Paperclip runtime role can list the AWS secret but lacks
|
||||
`secretsmanager:GetSecretValue` or required KMS decrypt permission for that
|
||||
specific secret.
|
||||
@@ -0,0 +1,436 @@
|
||||
---
|
||||
title: Secrets
|
||||
summary: Secrets CRUD
|
||||
---
|
||||
|
||||
Manage encrypted secrets that agents reference in their environment configuration.
|
||||
|
||||
## List Secrets
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/secrets
|
||||
```
|
||||
|
||||
Returns secret metadata (not decrypted values).
|
||||
|
||||
## Create Secret
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/secrets
|
||||
{
|
||||
"name": "anthropic-api-key",
|
||||
"value": "sk-ant-..."
|
||||
}
|
||||
```
|
||||
|
||||
The value is encrypted at rest. Only the secret ID and metadata are returned.
|
||||
|
||||
To link a provider-owned secret without copying the value into Paperclip, create
|
||||
an external-reference secret:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "prod-stripe-key",
|
||||
"provider": "aws_secrets_manager",
|
||||
"managedMode": "external_reference",
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/stripe",
|
||||
"providerVersionRef": "version-id-or-label"
|
||||
}
|
||||
```
|
||||
|
||||
Paperclip stores the provider reference and a non-sensitive fingerprint only.
|
||||
The value is resolved, when the provider is configured, through the server
|
||||
runtime path that enforces binding context and records access events.
|
||||
|
||||
## Provider Health
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/secret-providers/health
|
||||
```
|
||||
|
||||
Returns provider setup diagnostics, warnings, and local backup guidance. Health
|
||||
responses must not include secret values or provider credentials.
|
||||
|
||||
For `aws_secrets_manager`, an unready health response names the missing
|
||||
non-secret provider environment variables, the AWS SDK default credential source
|
||||
expected by the server runtime, and the custody rule that AWS bootstrap
|
||||
credentials must not be stored in Paperclip `company_secrets`.
|
||||
|
||||
The equivalent CLI check is:
|
||||
|
||||
```sh
|
||||
pnpm paperclipai secrets doctor --company-id {companyId}
|
||||
```
|
||||
|
||||
## Provider Vaults
|
||||
|
||||
Provider vaults are named, company-scoped configurations that route secret
|
||||
material to one of the supported provider backends. See the
|
||||
[secrets deploy guide](/deploy/secrets#provider-vaults) for the operator model
|
||||
and custody rules.
|
||||
|
||||
All routes below require board auth and company access. Mutating routes emit
|
||||
`secret_provider_config.*` activity-log entries. No route in this surface
|
||||
returns provider credential values; submitting credential-shaped fields in
|
||||
`config` is rejected at validation time.
|
||||
|
||||
### List Vaults
|
||||
|
||||
```
|
||||
GET /api/companies/{companyId}/secret-provider-configs
|
||||
```
|
||||
|
||||
Returns every vault for the company (including disabled rows for audit), each
|
||||
with id, provider, displayName, status, isDefault, non-sensitive `config`,
|
||||
latest health snapshot (`healthStatus`, `healthCheckedAt`, `healthMessage`,
|
||||
`healthDetails`), `disabledAt`, and audit columns.
|
||||
|
||||
### Create Vault
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/secret-provider-configs
|
||||
{
|
||||
"provider": "aws_secrets_manager",
|
||||
"displayName": "Prod US-East",
|
||||
"isDefault": true,
|
||||
"config": {
|
||||
"region": "us-east-1",
|
||||
"namespace": "paperclip",
|
||||
"secretNamePrefix": "paperclip",
|
||||
"kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/abcd-...",
|
||||
"environmentTag": "production"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Per-provider `config` shapes:
|
||||
|
||||
- `local_encrypted`: optional `backupReminderAcknowledged: boolean`.
|
||||
- `aws_secrets_manager`: required `region`; optional `namespace`,
|
||||
`secretNamePrefix`, `kmsKeyId`, `ownerTag`, `environmentTag`.
|
||||
- `gcp_secret_manager` (coming soon): optional `projectId`, `location`,
|
||||
`namespace`, `secretNamePrefix`.
|
||||
- `vault` (coming soon): optional origin-only HTTPS `address`, `namespace`,
|
||||
`mountPath`, `secretPathPrefix`. `address` values with embedded credentials,
|
||||
paths, query strings, or fragments are rejected.
|
||||
|
||||
`status` defaults to `ready` for `local_encrypted` and `aws_secrets_manager`,
|
||||
and to `coming_soon` for `gcp_secret_manager` and `vault`. Coming-soon and
|
||||
disabled vaults cannot be marked `isDefault`. Setting `isDefault: true` clears
|
||||
the previous default for the same provider in the same transaction.
|
||||
|
||||
### Get Vault
|
||||
|
||||
```
|
||||
GET /api/secret-provider-configs/{id}
|
||||
```
|
||||
|
||||
### Update Vault
|
||||
|
||||
```
|
||||
PATCH /api/secret-provider-configs/{id}
|
||||
{
|
||||
"displayName": "Prod US-East-2",
|
||||
"config": {
|
||||
"region": "us-east-2",
|
||||
"kmsKeyId": "arn:aws:kms:us-east-2:123456789012:key/abcd-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`config` is replaced wholesale on update — pass the full provider config
|
||||
payload, not a partial diff. Status transitions for `gcp_secret_manager` and
|
||||
`vault` are constrained to `coming_soon` and `disabled` until their runtime
|
||||
modules ship.
|
||||
|
||||
### Disable Vault
|
||||
|
||||
```
|
||||
DELETE /api/secret-provider-configs/{id}
|
||||
```
|
||||
|
||||
Soft-deletes the vault: status flips to `disabled`, `isDefault` clears, and
|
||||
`disabledAt` is stamped. Disabled vaults remain in `GET` results for audit
|
||||
purposes but are no longer offered in the secret create/rotate flow.
|
||||
|
||||
### Set Default
|
||||
|
||||
```
|
||||
POST /api/secret-provider-configs/{id}/default
|
||||
```
|
||||
|
||||
Marks the target vault as the default for its provider family and clears the
|
||||
previous default. Returns 422 when the target is `coming_soon` or `disabled`.
|
||||
|
||||
### Run Health Check
|
||||
|
||||
```
|
||||
POST /api/secret-provider-configs/{id}/health
|
||||
```
|
||||
|
||||
Runs a provider-specific health probe and persists the result on the vault.
|
||||
Response shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"configId": "<uuid>",
|
||||
"provider": "aws_secrets_manager",
|
||||
"status": "ready" | "warning" | "error" | "coming_soon" | "disabled",
|
||||
"message": "Provider vault is ready to handle managed writes",
|
||||
"details": {
|
||||
"code": "provider_ready",
|
||||
"message": "...",
|
||||
"guidance": ["..."]
|
||||
},
|
||||
"checkedAt": "2026-05-06T14:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
Health responses never include provider credentials or secret values. For AWS
|
||||
vaults, `details.guidance` may include missing non-secret env names and the
|
||||
expected AWS SDK credential source; coming-soon vaults always return
|
||||
`status: "coming_soon"` with `code: "runtime_locked"` and never call into
|
||||
provider modules.
|
||||
|
||||
### Selecting A Vault When Creating Or Rotating Secrets
|
||||
|
||||
`POST /api/companies/{companyId}/secrets` and
|
||||
`POST /api/secrets/{secretId}/rotate` both accept an optional
|
||||
`providerConfigId` field that pins the secret to a specific vault. When
|
||||
omitted (or null), the operation runs through the deployment-level provider
|
||||
configuration — the same path existing installs already use. The board UI
|
||||
preselects the company's default vault for the chosen provider before
|
||||
submitting, so callers should usually send an explicit `providerConfigId`.
|
||||
Coming-soon and disabled vaults are rejected with a 422; a vault that does not
|
||||
match the secret's provider is rejected the same way.
|
||||
|
||||
```json
|
||||
POST /api/companies/{companyId}/secrets
|
||||
{
|
||||
"name": "prod-stripe-key",
|
||||
"provider": "aws_secrets_manager",
|
||||
"providerConfigId": "<vault-uuid>",
|
||||
"managedMode": "external_reference",
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/stripe"
|
||||
}
|
||||
```
|
||||
|
||||
### Response Redaction Rules
|
||||
|
||||
Every route in this surface enforces the same redaction contract:
|
||||
|
||||
- Secret values are never returned. The board UI never has a "reveal value"
|
||||
affordance; resolution happens server-side at runtime under a binding.
|
||||
- Provider credential values are never accepted, stored, returned, logged, or
|
||||
echoed in error messages. Submitting credential-shaped fields fails
|
||||
validation with a non-leaking error.
|
||||
- Activity log entries record vault id, provider, displayName, status, and
|
||||
isDefault transitions — never `config` payloads or health detail bodies.
|
||||
|
||||
## Remote Import From AWS Secrets Manager
|
||||
|
||||
Remote import links existing AWS Secrets Manager entries into Paperclip as
|
||||
`external_reference` secrets. Import stores provider reference metadata only; it
|
||||
does not copy the remote secret plaintext into Paperclip.
|
||||
|
||||
The routes are board-only and company-scoped. `providerConfigId` must point to
|
||||
a same-company AWS provider vault with status `ready` or `warning`. Disabled,
|
||||
coming-soon, non-AWS, and cross-company vaults are rejected. Imported secrets
|
||||
resolve later through the selected vault, so runtime reads still need
|
||||
`secretsmanager:GetSecretValue` and any required KMS decrypt permission on the
|
||||
selected external secret.
|
||||
|
||||
### Preview Remote Import Candidates
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/secrets/remote-import/preview
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"query": "stripe",
|
||||
"nextToken": "opaque-provider-token",
|
||||
"pageSize": 50
|
||||
}
|
||||
```
|
||||
|
||||
`query` is optional and is passed to AWS Secrets Manager inventory filtering.
|
||||
Treat it as non-secret metadata because AWS may record list request parameters
|
||||
in CloudTrail. `nextToken` is an opaque AWS cursor; callers must pass it back
|
||||
unchanged and must not synthesize offsets. `pageSize` is optional, defaults to
|
||||
50 in the UI, and is capped at 100.
|
||||
|
||||
Preview uses AWS `ListSecrets` only. It must not call `GetSecretValue` or
|
||||
`BatchGetSecretValue`, must not request `SecretString`, and must not require KMS
|
||||
decrypt. The response contains sanitized metadata for display and conflict
|
||||
decisions:
|
||||
|
||||
```json
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"provider": "aws_secrets_manager",
|
||||
"nextToken": null,
|
||||
"candidates": [
|
||||
{
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
||||
"remoteName": "prod/stripe",
|
||||
"name": "prod/stripe",
|
||||
"key": "prod-stripe",
|
||||
"providerVersionRef": null,
|
||||
"providerMetadata": {
|
||||
"createdDate": "2026-05-06T00:00:00.000Z",
|
||||
"lastChangedDate": "2026-05-06T00:00:00.000Z",
|
||||
"hasDescription": true,
|
||||
"hasKmsKey": true,
|
||||
"tagCount": 3
|
||||
},
|
||||
"status": "ready",
|
||||
"importable": true,
|
||||
"conflicts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Candidate statuses:
|
||||
|
||||
- `ready`: the row can be selected for import.
|
||||
- `duplicate`: a Paperclip secret already links the same canonical provider
|
||||
reference for the same provider vault.
|
||||
- `conflict`: the row has a name/key collision or provider guardrail failure.
|
||||
|
||||
Conflict types are `exact_reference`, `name`, `key`, and
|
||||
`provider_guardrail`. AWS refs under Paperclip's own managed namespace are
|
||||
blocked as external references; use the Paperclip-managed secret flow for those
|
||||
resources instead.
|
||||
|
||||
### Import Selected Remote References
|
||||
|
||||
```
|
||||
POST /api/companies/{companyId}/secrets/remote-import
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"secrets": [
|
||||
{
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
||||
"name": "Stripe production key",
|
||||
"key": "stripe-production-key",
|
||||
"description": "Stripe key used by production checkout",
|
||||
"providerVersionRef": null,
|
||||
"providerMetadata": {
|
||||
"createdDate": "2026-05-06T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `secrets` array accepts 1-100 rows. Each row may override the suggested
|
||||
Paperclip `name`, `key`, optional Paperclip `description`,
|
||||
`providerVersionRef`, and sanitized `providerMetadata`. Blank descriptions are
|
||||
stored as `null`; AWS provider descriptions are not copied into Paperclip
|
||||
descriptions. The backend re-checks duplicate refs and name/key conflicts at
|
||||
submit time; a stale preview does not bypass those checks.
|
||||
|
||||
The import response is row-level:
|
||||
|
||||
```json
|
||||
{
|
||||
"providerConfigId": "<aws-vault-uuid>",
|
||||
"provider": "aws_secrets_manager",
|
||||
"importedCount": 1,
|
||||
"skippedCount": 1,
|
||||
"errorCount": 0,
|
||||
"results": [
|
||||
{
|
||||
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
||||
"name": "Stripe production key",
|
||||
"key": "stripe-production-key",
|
||||
"status": "imported",
|
||||
"reason": null,
|
||||
"secretId": "<paperclip-secret-id>",
|
||||
"conflicts": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Row statuses:
|
||||
|
||||
- `imported`: Paperclip created an active `external_reference` secret and one
|
||||
metadata-only version row.
|
||||
- `skipped`: the row had an exact-reference duplicate or name/key conflict.
|
||||
- `error`: the provider rejected the reference or the row failed validation.
|
||||
|
||||
Activity logs for preview/import store aggregate counts, provider id, and vault
|
||||
id only. They must not store remote secret names, ARNs, descriptions, tags,
|
||||
plaintext values, provider credentials, or raw AWS error blobs.
|
||||
|
||||
## Rotate Secret
|
||||
|
||||
```
|
||||
POST /api/secrets/{secretId}/rotate
|
||||
{
|
||||
"value": "sk-ant-new-value..."
|
||||
}
|
||||
```
|
||||
|
||||
Creates a new version of the secret. Agents referencing `"version": "latest"`
|
||||
automatically get the new value on next heartbeat. Pin to a specific version
|
||||
when a bad `latest` rollout would affect many agents at once.
|
||||
|
||||
## Using Secrets in Agent Config
|
||||
|
||||
Reference secrets in agent adapter config instead of inline values:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_API_KEY": {
|
||||
"type": "secret_ref",
|
||||
"secretId": "{secretId}",
|
||||
"version": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The server resolves and decrypts secret references at runtime, injecting the
|
||||
real value into the agent process environment. Paperclip's custody guarantees
|
||||
end at injection: the agent process can read, log, or forward the value, so
|
||||
treat any secret bound to an agent as exposed to that agent. See the custody
|
||||
boundaries note in the [secrets deploy guide](/deploy/secrets#custody-boundaries).
|
||||
|
||||
User-specific env bindings use a definition key instead of a concrete
|
||||
`secretId`. The concrete value is resolved for the run's responsible user:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"GITHUB_TOKEN": {
|
||||
"type": "user_secret_ref",
|
||||
"key": "github_api_token",
|
||||
"version": "latest",
|
||||
"required": true,
|
||||
"allowMissingOverride": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`required` defaults to `true` and `allowMissingOverride` defaults to `false`.
|
||||
Missing required user-secret values must fail closed before adapter dispatch.
|
||||
Optional missing values omit the environment variable; they must not inject an
|
||||
empty string or another user's value. Paperclip records value-free access
|
||||
events with `secretScope`, `responsibleUserId`, `credentialOwnerUserId`, and
|
||||
`userSecretDefinitionId`.
|
||||
|
||||
## Portability
|
||||
|
||||
Company export/import APIs represent agent and project environment requirements
|
||||
as declarations in the package manifest. Exports omit secret values, secret IDs,
|
||||
provider references, and encrypted provider material. Use:
|
||||
|
||||
```sh
|
||||
pnpm paperclipai secrets declarations --company-id {companyId}
|
||||
```
|
||||
|
||||
to inspect the declarations that an export would emit before moving a package.
|
||||
Reference in new issue
Block a user