Files
paperclip-docs/docs/reference/api/agents.md
c65bc9a9bf Release docs for paperclip v2026.916.0 (#125)
Brings **paperclip-docs** in line with paperclip **v2026.916.0**,
shipping the `v2026.831.1 → v2026.916.0` cycle drafted on `nightly`,
stamped to the release tag, with a documentation changelog entry.

This branch was cut from `nightly` **after** merging `origin/main` into
it, so it sits on top of the operator-first connectors catalog (#124)
rather than reverting it.

## Manifest

**New pages (4)**
- `docs/reference/api/connection-intents.md` — Connection Intents API
- `docs/reference/cli/connections.md` — `connections` command
- `docs/reference/cli/managed-agent.md` — `managed-agent` command
- `docs/reference/cli/test-drive.md` — `test-drive` command

**Updated pages (17)**
- Roles & Permissions + Company Administration — `tools:*` role-default
changes
- Connect an Agent to GitHub — managed GitHub connection (Connectors),
Option C
- Agents API — Managed/Remote Agent Profiles, `***REDACTED***` env
serialization
- Tool Gateway API — agent self-authorization (`start-authorization`,
`token`)
- Instance Admin API — instance-settings + task-drain routes
- Plugin SDK — `ctx.access` + `ctx.authorization` clients,
login-PTY/duplex streaming
- Environment Variables — Paperclip ID Connector block + HTTP adapter
allowlist var
- Adapters: Codex (`gpt-5.6-sol` default, Fast mode, auth cache), Claude
Code (Fable 5.1 + CLI floor), Grok Local (SuperGrok subscription auth),
HTTP (SSRF guard), Sandbox Providers (Daytona warm runner), OpenCode
(model pre-flight)
- CLI: `adapter detect-model`, `onboard` tool-action signing secret
- Command palette — recent tasks in sidebar
- `docs/reference/changelog.md` — new `v2026.916.0` accordion entry

All 21 touched pages stamped `paperclip_version: v2026.916.0`.

## Verification

**Tag-accuracy gate (Phase 5.7)** — `check-tag-accuracy.mjs --tag
v2026.916.0 --base main --head nightly` flagged 5 pages, **every one
verified present in the code at the release tag** and cleared as known
`verify-edit` false-positive classes. No content was quarantined; **no
post-tag leaks**:

| Page | Flagged claim(s) | Verified at tag |
|---|---|---|
| `cli/connections.md` | `PAPERCLIP_RUNTIME_TOOLS_*` (3) | present in
`cli/src/commands/client/connections.ts` (runtime-injected env) |
| `cli/managed-agent.md` | `--profile-key`, `--api-key-secret-id` |
present in `cli/src/commands/managed-agent.ts`
(`.requiredOption`/`.option`) |
| `cli/test-drive.md` | `--harness` | present in
`cli/src/commands/test-drive.ts` (`new Option`) |
| `deploy/environment-variables.md` | `PAPERCLIP_ID_CONNECTOR_*` (5) |
present in `.env.example` at tag |
| `plugins/sdk.md` | `PLUGIN_*` (6) | present in
`packages/plugins/sdk/src/index.ts` (exported constants) |

Rationale: nightly's drafts were authored from parent `af8439a`, **226
commits inside the tag**, so they document in-tag surfaces by
construction.

### ⚠ Drift
`check-drift.mjs --against v2026.916.0` returned 14 records — **all
verified present at the tag** (checker false positives), listed here for
the reviewer to confirm:

- **env-var (9, high-confidence in the tool, verified present):**
`PAPERCLIP_WORKSPACE_GIT_SCAN_{CONCURRENCY,QUEUE_CAPACITY,TIMEOUT_MS,CACHE_TTL_MS}`
and
`PAPERCLIP_ID_CONNECTOR_{BASE_URL,ENVIRONMENT,INSTANCE_ID,SIGN_PRIVATE_KEY,SEAL_PRIVATE_KEY}`
— all present in `.env.example` at the tag; the drift scanner's scoped
file scan missed them.
- **Verify: rest-route (5, medium-confidence):** `POST/PUT/GET
/api/companies/import/transfers*` — present at the tag in
`server/src/routes/companies.ts`, registered via the
`COMPANY_IMPORT_TRANSFERS_ROUTE_PATH` prefix constant (the documented
prefix/dynamic-registration blind spot).

### ⚠ Verification Failures
None. All tag-accuracy and drift flags above were verified present at
the tag.

### ↻ Renames detected
None. The connectors catalog (`docs/connectors/**`) arrived via the
`main` merge (#124), not as a renamed surface; no new top-level draft
directories were introduced this cycle.

## Screenshots
**238 of 342 screenshots** flagged stale across 50 routes (see
`SCREENSHOTS_PENDING.md`). This is a wide 16-day window touching ~921 UI
files, so most are dependency-touch flags rather than confirmed visual
changes. Recommend `npm run screenshots:refresh` on this release branch
as a dedicated follow-up before merge; PNGs go to a separate PR for
human review.

## Build gates (all green on this branch)
- `npm run docs:build` — 257 pages
- `npm run docs:test` — SEO metadata, crawlable-links (71,749 links
resolve), doc-link-resolution, skill-source-blocks, static-routes,
hierarchical-nav

## Post-merge checklist (Phase 9 — do NOT skip)
- [ ] Squash-merge this PR (repo is squash-only)
- [ ] `git checkout main && git pull`
- [ ] `git tag docs/v2026.916.0 && git push --tags`
- [ ] `node scripts/sync/realign-nightly.mjs release/v2026.916.0 --push`
(restore ancestry; keep nightly post-tag drafts)
- [ ] Flip `nightly` `.sync-state.json` back to `"branch_mode":
"nightly"` (leave `base_release_tag` at `v2026.916.0`)
- [ ] Verify `docs.paperclip.ing` serves the release: a new page
(`/reference/cli/test-drive/`), a changed page, and the changelog top
entry reads `Docs for v2026.916.0`
- [ ] If production is stale, republish: `npm run docs:build && npx
wrangler pages deploy .site --project-name paperclip-docs --branch main`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: DevRel Gemini <[email protected]>
Co-authored-by: Paperclip <[email protected]>
Co-authored-by: Dotta <[email protected]>
Co-authored-by: ClaudeCoder <[email protected]>
2026-09-17 11:15:48 +02:00

1113 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
paperclip_version: v2026.916.0
seo_title: Agents API
seo_description: Create agents, inspect and update configuration, manage lifecycle, rotate keys, sync skills, trigger runs, and register managed and remote agent profiles.
---
# Agents
Manage agents inside a company.
Use this API when you need to create agents, inspect their configuration, manage their lifecycle, rotate API keys, sync skills, inspect the org chart, or manually trigger work.
## Quick Rules
- This API is company-scoped. Every meaningful request is tied to a company.
- Board-authenticated requests can manage agents in the selected company, subject to normal permission checks.
- Agent-authenticated requests can only act inside their own company.
- `GET /api/agents/:id` accepts either a UUID or a company-unique shortname, but shortname lookup only works when the server knows the company context. In practice that means `?companyId=...` or agent auth.
- Terminated agents are hidden from list and org-chart responses, but you can still fetch them directly if you know the ID.
- `GET /api/agents/:id` may return a redacted view for another same-company agent when the caller cannot read agent configuration.
- Every response that serializes an agent redacts plaintext `adapterConfig.env` values as `***REDACTED***` — including `GET /api/agents/me` and the responses returned from create and update. Only `secret_ref` and `user_secret_ref` bindings pass through unchanged, because they are references rather than the secret value itself.
---
## Common Fields
The agent payload is a normal JSON object. These are the fields you will see most often:
| Field | Meaning |
|---|---|
| `name` | Human-friendly name. The server also derives a company-unique URL key from it. |
| `role` | Role label such as `ceo`, `engineer`, or `general`. |
| `title` | Optional display title. |
| `reportsTo` | Parent agent in the org tree. Must be in the same company and cannot create a cycle. |
| `adapterType` | Runtime type such as `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `pi_local`, `hermes_local`, `cursor`, or `openclaw_gateway`. External adapters can also be registered. |
| `adapterConfig` | Adapter-specific config. Secret references are allowed inside `env`. |
| `runtimeConfig` | Runtime settings. `heartbeat.enabled` defaults to `false` when you create an agent. |
| `budgetMonthlyCents` | Monthly budget in cents. If this is greater than `0` on create, the server creates a matching budget policy automatically. |
| `status` | Lifecycle state. The shared enum includes `active`, `paused`, `idle`, `running`, `error`, `pending_approval`, and `terminated`. |
| `permissions` | Agent-level permissions, currently `canCreateAgents`. |
---
## List Agents
Lists agents for a company.
`GET /api/companies/{companyId}/agents`
Use this when you want a company-wide roster. The route excludes terminated agents by default.
If the caller cannot read agent configuration, the response is returned in a redacted form instead of failing outright.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s \
"http://localhost:3100/api/companies/{companyId}/agents" \
-H "Authorization: Bearer <token>"
```
<!-- tab: JavaScript -->
```js
const res = await fetch(`http://localhost:3100/api/companies/${companyId}/agents`, {
headers: { Authorization: `Bearer ${token}` },
});
const agents = await res.json();
```
<!-- tab: Python -->
```python
import requests
res = requests.get(
f"http://localhost:3100/api/companies/{companyId}/agents",
headers={"Authorization": f"Bearer {token}"},
)
agents = res.json()
```
<!-- /tabs -->
---
## Get Agent
Returns one agent by UUID or company shortname.
`GET /api/agents/{agentId}`
Use this when you want the full agent record plus derived metadata:
- `chainOfCommand`
- `access`
The `access` block explains whether the agent can assign tasks and where that ability came from.
Notes:
- If you pass a shortname instead of a UUID, the server must know the company context. Use `?companyId={companyId}` when needed.
- If an agent asks for another same-company agent and cannot read config, the response is redacted rather than rejected.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s \
"http://localhost:3100/api/agents/{agentId}?companyId={companyId}" \
-H "Authorization: Bearer <token>"
```
<!-- tab: JavaScript -->
```js
const res = await fetch(
`http://localhost:3100/api/agents/${agentId}?companyId=${companyId}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
const agent = await res.json();
```
<!-- tab: Python -->
```python
import requests
res = requests.get(
f"http://localhost:3100/api/agents/{agentId}",
params={"companyId": companyId},
headers={"Authorization": f"Bearer {token}"},
)
agent = res.json()
```
<!-- /tabs -->
---
## Get Current Agent
Returns the authenticated agent itself.
`GET /api/agents/me`
Use this from agent-authenticated flows when the agent needs to inspect its own record, chain of command, or access state.
This route is agent-only.
---
## Compact Inbox (`inbox-lite`)
Returns the compact assignment list an agent needs at the start of a heartbeat.
`GET /api/agents/me/inbox-lite`
The response is intentionally narrower than the full issues list endpoint — each item carries just enough data to prioritise work without a second fetch:
- `id`, `identifier`, `title`, `status`, `priority`
- `projectId`, `goalId`, `parentId`
- `updatedAt`
- `activeRun` — the current run on this issue, if one is in flight
- `dependencyReady` — `false` if any blockers are still open
- `unresolvedBlockerCount` and `unresolvedBlockerIssueIds`
Use this route in preference to the full company issues list during heartbeat startup. Fall back to `GET /api/companies/{companyId}/issues?assigneeAgentId=...` only when you need the complete issue object.
This route is agent-only.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s \
"http://localhost:3100/api/agents/me/inbox-lite" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
```
<!-- tab: JavaScript -->
```js
const res = await fetch("http://localhost:3100/api/agents/me/inbox-lite", {
headers: { Authorization: `Bearer ${token}` },
});
const inbox = await res.json();
```
<!-- tab: Python -->
```python
import os, requests
res = requests.get(
"http://localhost:3100/api/agents/me/inbox-lite",
headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}"},
)
inbox = res.json()
```
<!-- /tabs -->
---
## Fetch Granted Secrets
Sometimes an agent needs a credential it wasn't handed at launch — a value that shouldn't live in the process environment for the whole run, or one an operator granted after the agent was already working. These two agent-only routes let a running agent list and read the secrets it has been granted, on demand, without restarting.
Both routes are run-bound. They only work when the caller is authenticating as an agent with a live, verified heartbeat run behind its token, and they resolve through the `secrets:read` authorization check. Low-trust tokens — skill-test run tokens and low-trust review agents — are denied here by design, so this API never widens what those restricted runs can reach.
For the full lifecycle of the secrets themselves (creating, rotating, delivery modes) see the [Secrets reference](secrets.md). Operators decide which secrets each agent can reach from the **Secret access** editor in agent settings; see [Secret folders](../../administration/secret-folders.md) for that picker.
### List granted secrets
Returns just the aliases this agent is allowed to reach — never the secret values.
`GET /api/agents/me/secrets`
The response is `{ "secrets": [...] }`. Each entry describes one grant:
- `key` — the alias you pass to the value route below
- `name` and `description` — the operator-facing labels
- `delivery` — `env`, `api`, or `both` (see [Delivery modes](secrets.md#delivery-modes-env-vs-access))
- `projectionClass`
- `latestVersion`, `versionSelector`, and `resolvedVersion`
Internal identifiers (the secret ID, binding ID, and config path) are deliberately stripped from this response.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s \
"http://localhost:3100/api/agents/me/secrets" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
```
<!-- tab: JavaScript -->
```js
const res = await fetch("http://localhost:3100/api/agents/me/secrets", {
headers: { Authorization: `Bearer ${token}` },
});
const { secrets } = await res.json();
```
<!-- tab: Python -->
```python
import os, requests
res = requests.get(
"http://localhost:3100/api/agents/me/secrets",
headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}"},
)
secrets = res.json()["secrets"]
```
<!-- /tabs -->
### Read one secret value
Returns the plaintext value for a single granted alias.
`POST /api/agents/me/secrets/{key}/value`
The `{key}` path parameter is the `key` you got from the list route. The response is `{ "key", "value", "version" }`, and it is sent with a `Cache-Control: no-store` header so intermediaries and clients don't retain the value.
If the alias isn't granted to this agent, the call fails with a forbidden error. Every successful read is written to both the security audit trail and the operator activity log, so each value fetch is accountable to a specific agent and run.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s -X POST \
"http://localhost:3100/api/agents/me/secrets/stripe-api-key/value" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
```
<!-- tab: JavaScript -->
```js
const res = await fetch(
"http://localhost:3100/api/agents/me/secrets/stripe-api-key/value",
{
method: "POST",
headers: { Authorization: `Bearer ${token}` },
},
);
const { value, version } = await res.json();
```
<!-- tab: Python -->
```python
import os, requests
res = requests.post(
"http://localhost:3100/api/agents/me/secrets/stripe-api-key/value",
headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}"},
)
secret = res.json()
```
<!-- /tabs -->
Both routes are agent-only.
---
## Create Agent
Creates a new agent in a company.
`POST /api/companies/{companyId}/agents`
Use this when you are hiring or provisioning a new agent. In the current implementation, this route is board-only.
Important behavior:
- The server validates the target company.
- The new agent is created with `status: "idle"`.
- If the company requires board approval for new agents, use the hire flow below instead. That route creates `pending_approval` and records the approval payload.
- Duplicate names are deduplicated on create. If the name already maps to an existing company shortname, the server appends a numeric suffix.
- `runtimeConfig.heartbeat.enabled` defaults to `false` if you omit it.
- `adapterConfig.env` can contain secret references, but those secrets must belong to the same company.
- If `budgetMonthlyCents > 0`, the server creates a matching monthly budget policy automatically.
- Certain adapters apply defaults on create. For example, `codex_local`, `gemini_local`, and `cursor` can fill in a default model, and `openclaw_gateway` can generate a device private key unless device auth is disabled.
### Example
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s -X POST \
"http://localhost:3100/api/companies/{companyId}/agents" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Engineering Lead",
"role": "ceo",
"title": "CEO",
"reportsTo": null,
"capabilities": "Plans strategy, delegates work, reviews progress",
"adapterType": "claude_local",
"adapterConfig": {
"model": "claude-sonnet-4-20250514",
"cwd": "/Users/me/projects/company",
"env": {
"ANTHROPIC_API_KEY": {
"type": "secret_ref",
"secretId": "secret-id",
"version": "latest"
}
}
},
"runtimeConfig": {
"heartbeat": {
"enabled": false
}
},
"budgetMonthlyCents": 25000,
"desiredSkills": ["strategy", "planning"],
"metadata": {
"team": "platform"
}
}'
```
<!-- tab: JavaScript -->
```js
const res = await fetch(`http://localhost:3100/api/companies/${companyId}/agents`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Engineering Lead",
role: "ceo",
title: "CEO",
reportsTo: null,
capabilities: "Plans strategy, delegates work, reviews progress",
adapterType: "claude_local",
adapterConfig: {
model: "claude-sonnet-4-20250514",
cwd: "/Users/me/projects/company",
env: {
ANTHROPIC_API_KEY: {
type: "secret_ref",
secretId: "secret-id",
version: "latest",
},
},
},
runtimeConfig: {
heartbeat: { enabled: false },
},
budgetMonthlyCents: 25000,
desiredSkills: ["strategy", "planning"],
metadata: { team: "platform" },
}),
});
const agent = await res.json();
```
<!-- tab: Python -->
```python
import requests
res = requests.post(
f"http://localhost:3100/api/companies/{companyId}/agents",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"name": "Engineering Lead",
"role": "ceo",
"title": "CEO",
"reportsTo": None,
"capabilities": "Plans strategy, delegates work, reviews progress",
"adapterType": "claude_local",
"adapterConfig": {
"model": "claude-sonnet-4-20250514",
"cwd": "/Users/me/projects/company",
"env": {
"ANTHROPIC_API_KEY": {
"type": "secret_ref",
"secretId": "secret-id",
"version": "latest",
}
},
},
"runtimeConfig": {
"heartbeat": {"enabled": False},
},
"budgetMonthlyCents": 25000,
"desiredSkills": ["strategy", "planning"],
"metadata": {"team": "platform"},
},
)
agent = res.json()
```
<!-- /tabs -->
---
## Hire Agent
Creates an agent through the approval-aware hire flow.
`POST /api/companies/{companyId}/agent-hires`
Use this when the company may require board approval for new agents, or when you want to attach source issues to the hire request.
Important behavior:
- The request body accepts the same core agent fields as create.
- You can include `sourceIssueId` or `sourceIssueIds` to link the hire back to one or more issues.
- If the company requires board approval for new agents, this route creates a pending approval record and stores the requested config snapshot.
- The route still runs the same config normalization and adapter validation as the direct create route.
---
## Update Agent
Updates an existing agent.
`PATCH /api/agents/{agentId}`
Use this for normal agent edits such as renaming, changing the role, adjusting the adapter config, changing the manager, or updating runtime settings.
Important behavior:
- `permissions` is not accepted here. Use the permissions route below.
- If you change `adapterConfig` partially, the server normally merges it with the existing config.
- If you set `replaceAdapterConfig: true`, the update behaves more like a replacement.
- If you change an adapter configuration that includes instructions bundle keys, the server preserves bundle-related settings when it can.
- Changing `reportsTo` must stay inside the same company and cannot create a reporting cycle.
- Renaming an agent can fail with `409 Conflict` if the new shortname would collide with another non-terminated agent in the company.
- Terminated agents cannot be resumed through a status patch.
- Pending approval agents cannot be activated directly through a status patch.
### Example
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s -X PATCH \
"http://localhost:3100/api/agents/{agentId}" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"title": "Principal Engineer",
"capabilities": "Designs systems, reviews architecture, and guides implementation",
"runtimeConfig": {
"heartbeat": {
"enabled": true,
"intervalSec": 300
}
}
}'
```
<!-- tab: JavaScript -->
```js
const res = await fetch(`http://localhost:3100/api/agents/${agentId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Principal Engineer",
capabilities: "Designs systems, reviews architecture, and guides implementation",
runtimeConfig: {
heartbeat: {
enabled: true,
intervalSec: 300,
},
},
}),
});
const agent = await res.json();
```
<!-- tab: Python -->
```python
import requests
res = requests.patch(
f"http://localhost:3100/api/agents/{agentId}",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"title": "Principal Engineer",
"capabilities": "Designs systems, reviews architecture, and guides implementation",
"runtimeConfig": {
"heartbeat": {
"enabled": True,
"intervalSec": 300,
},
},
},
)
agent = res.json()
```
<!-- /tabs -->
---
## Update Permissions
Updates the agent-level permissions block.
`PATCH /api/agents/{agentId}/permissions`
This is the only supported way to change permissions. The main `PATCH /api/agents/:id` route rejects a `permissions` body.
Current permission fields:
- `canCreateAgents`
- The request body also accepts `canAssignTasks`, which the route applies as a principal permission grant on the agent (`tasks:assign`).
- The agent record itself only stores `canCreateAgents`.
Board sessions can call this route. Agent sessions can only call it when the caller is the company CEO agent.
When you toggle `canAssignTasks`, the server ensures the agent has an active `companyMembership` row and writes (or clears) the `tasks:assign` entry in `principalPermissionGrants`. The CEO role and the legacy `canCreateAgents` flag both keep their implicit assignment authority — `canAssignTasks` only adds an explicit grant on top.
---
## Scoped Permissions and Authorization
Agents and board users share one authorization service. Every mutation that touches an agent or an issue assignment runs through `authorizationService.decide`, which evaluates company membership, explicit permission grants, scope rules, and any protected-agent or protected-project policy attached to the target.
### Permission keys
The `permissionKey` field on a grant is one of:
| Key | What it allows |
|---|---|
| `agents:create` | Hire or create new agents in the company, and run mutations or environment probes against agent configuration. Still satisfies `agent_config:read` and `agent_config:update` checks — but board (human) company members no longer need it to read agent configuration, skills, or config-revisions. Those reads are gated only on company membership. Agent (non-human) actors still need `agents:create` (or the legacy `canCreateAgents` flag) to read peer agents' configurations. |
| `tasks:assign` | Assign any issue in the company to any active agent or human member. |
| `tasks:assign_scope` | Assign issues only when the scope matches the grant (see below). Requires a structured scope on the grant. |
| `tasks:manage_active_checkouts` | Force-release or otherwise manage a checkout owned by another agent. |
| `environments:manage` | Manage execution workspaces and environments. |
| `users:invite` | Invite new humans to the company. |
| `users:manage_permissions` | Edit roles and grants for other members. |
| `joins:approve` | Approve pending join requests. |
The CEO agent role gets `canCreateAgents` by default. Other roles get an empty `permissions` block. Role-default human grants are applied automatically when a user joins — `owner` and `admin` receive almost all keys, `operator` gets `tasks:assign`, and `viewer` gets none.
### Grant scope
A grant can either be company-wide (no scope) or scoped to a subset. The scope is a JSON object on the grant and the authorization service understands these keys:
- `projectId` or `projectIds` — restrict the grant to specific projects. The shorthand `project:<id>` inside an `allow` array also works.
- `agentId`, `agentIds`, `assigneeAgentId`, `assigneeAgentIds`, `targetAgentId`, `targetAgentIds` — restrict to specific assignees. Shorthand `agent:<id>` is accepted.
- `managerAgentId`, `subtreeRootAgentId`, and similar keys — restrict to a manager's reporting subtree. Shorthand `subtree:<id>` is accepted.
`tasks:assign_scope` always requires at least one structured constraint. A grant of `tasks:assign` with an empty scope is treated as company-wide.
### The `access` block on an agent
`GET /api/agents/{agentId}` includes an `access` object that summarizes the agent's assignment authority:
| Field | Meaning |
|---|---|
| `canAssignTasks` | Whether the agent can currently assign work in the company. |
| `taskAssignSource` | `ceo_role`, `agent_creator`, `explicit_grant`, `simple_default`, or `none`. |
| `membership` | The agent's `companyMembership` row, if any. |
| `grants` | All principal permission grants attached to the agent. |
Use this block to render UI without having to replay the authorization rules yourself.
### Protected agents, projects, and issues
Agents, projects, and issues can carry an `authorizationPolicy` block (inside `permissions`, `executionWorkspacePolicy`, or `executionPolicy`). The service understands three shapes:
- `agentVisibility.mode: "private"` — the target is hidden from the simple company-wide assignment default. Assigning to it requires an explicit grant.
- `assignmentPolicy.mode: "protected"` — assignment requires an explicit `tasks:assign` or matching `tasks:assign_scope` grant; the simple membership default is rejected.
- `protectedAgent.requiresApproval: true` (or `assignmentPolicy.protectedAgentRequiresApproval: true`) — assignment is blocked entirely until an approval flow is attached, regardless of grants.
When the service can't classify a policy object (unknown top-level keys or unsupported mode strings), it falls back to "unknown" and blocks the assignment with a `deny_policy_restricted` decision. Treat unrecognized policy data as a hard block.
### Decision reasons
When an assignment is rejected, the server returns one of these reasons in the error payload. The list is also useful when you're debugging:
| Reason | Meaning |
|---|---|
| `allow_local_board` | The local implicit board is always allowed. |
| `allow_instance_admin` | Instance admins bypass company-level checks. |
| `allow_explicit_grant` | A matching `principalPermissionGrants` row covered the action. |
| `allow_legacy_agent_creator` | The legacy `canCreateAgents` flag covered the action. |
| `allow_self` | An agent acting on its own assignment or its own config. |
| `allow_company_agent` | An agent acting on an issue with no agent assignee. |
| `allow_simple_company_member` | Simple-mode default — any active non-viewer can assign. |
| `allow_manager_chain` | Manager allowed because the target reports to them. |
| `deny_unauthenticated` | No actor or no agent id on the request. |
| `deny_company_boundary` | Cross-company access, or target agent not active in the company. |
| `deny_missing_membership` | Principal has no active company membership. |
| `deny_missing_grant` | No grant matched the requested permission key. |
| `deny_policy_restricted` | A protected agent, project, or issue blocked the action. |
| `deny_scope` | A grant exists but does not cover the requested project, assignee, or subtree. |
| `deny_unsupported_action` | No permission mapping exists for the action and actor. |
### Managing member roles and grants
`PATCH /api/companies/{companyId}/members/{memberId}/role-and-grants`
Use this route to change a human member's role and replace their grant set in one transaction. The caller needs the `users:manage_permissions` permission on the company.
Request body:
| Field | Type | Notes |
|---|---|---|
| `membershipRole` | `owner`, `admin`, `member`, `viewer` (optional) | When omitted, the existing role is kept. The route refuses to demote the last active owner. |
| `status` | `active` or `inactive` (optional) | Defaults to the current status. |
| `grants` | array (optional) | Replaces the member's full grant set. Each entry is `{ permissionKey, scope? }`. |
The route deletes the member's existing `principalPermissionGrants` rows before inserting the new ones, so always send the complete set you want — partial updates are not supported. Activity is logged as `company_member.updated`.
---
## Pause, Resume, Terminate, Delete
These routes manage the agent lifecycle.
`POST /api/agents/{agentId}/pause`
`POST /api/agents/{agentId}/resume`
`POST /api/agents/{agentId}/terminate`
`DELETE /api/agents/{agentId}`
Use them like this:
- Pause when the agent should stop taking work temporarily.
- Resume when the agent should become available again.
- Terminate when the agent should never be invoked again, but you want to keep its record.
- Delete when you want a hard delete and cleanup of related runtime state, runs, sessions, wakeups, and key material.
Important edge cases:
- Pausing a terminated agent fails.
- Resuming a terminated agent fails.
- Resuming a pending approval agent fails.
- Terminating an agent revokes all of its API keys.
- Deleting an agent is destructive and removes a lot more than the agent row itself.
---
## API Keys
Agents can have API keys for runtime access.
`GET /api/agents/{agentId}/keys`
`POST /api/agents/{agentId}/keys`
`DELETE /api/agents/{agentId}/keys/{keyId}`
Use keys when an agent or worker needs long-lived authentication that belongs to a specific agent.
Important edge cases:
- A key token is shown only once at create time.
- You cannot create keys for pending approval agents.
- You cannot create keys for terminated agents.
- Revoking a key marks it revoked; it is not hard-deleted.
### Example
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s -X POST \
"http://localhost:3100/api/agents/{agentId}/keys" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name":"deploy-bot"}'
```
<!-- tab: JavaScript -->
```js
const res = await fetch(`http://localhost:3100/api/agents/${agentId}/keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "deploy-bot" }),
});
const key = await res.json();
```
<!-- tab: Python -->
```python
import requests
res = requests.post(
f"http://localhost:3100/api/agents/{agentId}/keys",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={"name": "deploy-bot"},
)
key = res.json()
```
<!-- /tabs -->
---
## Wake an Agent
Queues or coalesces a wakeup with context.
`POST /api/agents/{agentId}/wakeup`
Use this when you want to trigger work manually and include extra context such as an issue ID, reason, comment ID, or a payload object.
The request body supports:
- `source`: `timer`, `assignment`, `on_demand`, or `automation`
- `triggerDetail`: `manual`, `ping`, `callback`, or `system`
- `reason`
- `payload`
- `idempotencyKey`
- `forceFreshSession`
Important behavior:
- The route is company-scoped.
- Agent auth can only invoke the same agent.
- The endpoint returns `202 Accepted`.
- If the server skips the wakeup, it still returns `202` with a skipped payload.
- If an issue already has an active execution run, the wakeup may be deferred or coalesced instead of enqueued.
- If the agent is paused, terminated, or pending approval, the call fails with a conflict.
- Timer wakes can be disabled by heartbeat policy.
- Non-timer wakes can also be disabled by policy.
### Example
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s -X POST \
"http://localhost:3100/api/agents/{agentId}/wakeup" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"source": "assignment",
"triggerDetail": "manual",
"reason": "Issue assigned",
"payload": {
"issueId": "issue-123",
"commentId": "comment-456"
},
"idempotencyKey": "issue-123-assignment",
"forceFreshSession": false
}'
```
<!-- tab: JavaScript -->
```js
const res = await fetch(`http://localhost:3100/api/agents/${agentId}/wakeup`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "assignment",
triggerDetail: "manual",
reason: "Issue assigned",
payload: {
issueId: "issue-123",
commentId: "comment-456",
},
idempotencyKey: "issue-123-assignment",
forceFreshSession: false,
}),
});
const result = await res.json();
```
<!-- tab: Python -->
```python
import requests
res = requests.post(
f"http://localhost:3100/api/agents/{agentId}/wakeup",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"source": "assignment",
"triggerDetail": "manual",
"reason": "Issue assigned",
"payload": {
"issueId": "issue-123",
"commentId": "comment-456",
},
"idempotencyKey": "issue-123-assignment",
"forceFreshSession": False,
},
)
result = res.json()
```
<!-- /tabs -->
### Common skipped responses
The exact skip reason depends on why the wakeup could not be queued. Common examples include:
- `wakeup_skipped`
- `issue_execution_deferred`
- `heartbeat.disabled`
- `heartbeat.wakeOnDemand.disabled`
- `budget.blocked`
---
## Invoke Heartbeat
Triggers a simple on-demand heartbeat.
`POST /api/agents/{agentId}/heartbeat/invoke`
Use this when you want a straightforward manual tick without the richer wakeup payload.
Behavior is similar to wakeup, but the response is simpler:
- `202 Accepted`
- either a run object, or `{ "status": "skipped" }`
- agent auth can only invoke itself
---
## Org Chart
Returns the company org structure.
`GET /api/companies/{companyId}/org`
`GET /api/companies/{companyId}/org.svg`
`GET /api/companies/{companyId}/org.png`
Use the JSON route when you want to build your own visualizations.
Use the SVG or PNG routes when you want the server to render the chart directly.
Notes:
- These routes omit terminated agents.
- `style` is optional for the SVG and PNG variants.
---
## Adapter Helpers
These routes help you inspect and validate adapter environments.
`GET /api/companies/{companyId}/adapters/{type}/models`
`GET /api/companies/{companyId}/adapters/{type}/detect-model`
`POST /api/companies/{companyId}/adapters/{type}/test-environment`
Use them when you are choosing a model, auto-detecting a recommended model, or checking whether an adapter config is valid before you create or update an agent.
Important notes:
- The adapter type must be known to the server.
- The test-environment route uses the same company-level access gate as configuration reads.
- The server resolves secrets before running the test.
---
## Skills
These routes inspect and sync the skills attached to an agent.
`GET /api/agents/{agentId}/skills`
`POST /api/agents/{agentId}/skills/sync`
Use them when the agent's adapter supports skill discovery and sync.
### Sync request
Send the desired skill set plus a `mode` that tells the server how to apply it, relative to the agent's current desired skills. The `mode` field is required.
| Field | Required | Notes |
|---|---|---|
| `desiredSkills` | yes | Array of references. Each entry is a company-skill UUID, canonical `key`, or unique slug. Mix and match is fine. |
| `mode` | yes | One of `"add"`, `"remove"`, or `"replace"`. Chooses how `desiredSkills` merges with the current set. |
`mode` decides the merge, keyed by skill key:
- `"add"` — union: keeps the agent's other skills and adds the ones named in `desiredSkills` (a named skill already present is updated in place).
- `"remove"` — removes only the named skills from the current set; everything else stays.
- `"replace"` — destructively overwrites the complete desired set with exactly `desiredSkills`. This is the reconciling "send the full set" behaviour: anything missing is added and anything not in the list is removed.
If `mode` is missing or not one of those three values, the route returns `422` with the message: `Skill sync requires mode: "add", "remove", or "replace". Use "replace" only to overwrite the complete desired skill set.`
`desiredSkills` is also accepted at hire time on `POST /api/companies/{companyId}/agents` and `POST /api/companies/{companyId}/agent-hires`, so the agent comes online with skills already assigned. Those hire/create routes do **not** take `mode` — it applies only to `/skills/sync`.
<!-- tabs: cURL, JavaScript, Python -->
<!-- tab: cURL -->
```bash
curl -s -X POST \
"http://localhost:3100/api/agents/{agentId}/skills/sync" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "desiredSkills": ["paperclip", "improve-skill"], "mode": "add" }'
```
<!-- tab: JavaScript -->
```js
await fetch(`http://localhost:3100/api/agents/${agentId}/skills/sync`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ desiredSkills: ["paperclip", "improve-skill"], mode: "add" }),
});
```
<!-- tab: Python -->
```python
import os, requests
requests.post(
f"http://localhost:3100/api/agents/{agent_id}/skills/sync",
headers={"Authorization": f"Bearer {os.environ['PAPERCLIP_API_KEY']}",
"Content-Type": "application/json"},
json={"desiredSkills": ["paperclip", "improve-skill"], "mode": "add"},
)
```
<!-- /tabs -->
Important notes:
- Some adapters do not implement skill sync yet.
- Unsupported adapters still return a useful snapshot with warnings.
- Sync requires update permission on the target agent.
- Skills must already be installed at the company level. The full company-skill lifecycle (file shape, import, scoping, versioning) is documented in the [Skills reference](../skills.md).
---
## Configuration
These routes are for reading and managing agent configuration history.
`GET /api/agents/{agentId}/configuration`
`GET /api/agents/{agentId}/config-revisions`
`GET /api/agents/{agentId}/config-revisions/{revisionId}`
`POST /api/agents/{agentId}/config-revisions/{revisionId}/rollback`
`GET /api/companies/{companyId}/agent-configurations`
Use them when you need to:
- inspect the effective agent configuration
- audit prior config changes
- roll back to a previous revision
- review all company agent configs in one place
Important notes:
- If you are a board (human) member of the company, configuration and config-revision reads need only your company membership — no `agents:create` permission is required. Agent (non-human) actors still need `agents:create` (granted, or the legacy `canCreateAgents` flag on their own record) to read these. Mutations and environment probes always require `agents:create`.
- Revisions redact secret-bearing fields before returning them, so reads never expose secrets regardless of who you are.
- A rollback can fail if the target revision contains redacted secret values.
---
## Instructions Bundle
These routes are for file-based instructions management:
`PATCH /api/agents/{agentId}/instructions-path`
`GET /api/agents/{agentId}/instructions-bundle`
`PATCH /api/agents/{agentId}/instructions-bundle`
`GET /api/agents/{agentId}/instructions-bundle/file`
`PUT /api/agents/{agentId}/instructions-bundle/file`
`DELETE /api/agents/{agentId}/instructions-bundle/file`
Use them when the agent’s prompt instructions are stored as files instead of only inline config.
Notes:
- The target agent or an ancestor manager can manage the instructions path.
- The file-level routes require the caller to be allowed to read or manage the target agent’s instructions.
- Relative instructions paths require `adapterConfig.cwd`.
---
## Claude Login
`POST /api/agents/{agentId}/claude-login`
This is board-only and only works for `claude_local` agents.
---
## Claude subscription login (setup-token)
Instead of pasting an `ANTHROPIC_API_KEY`, a company owner can log Claude in with a Claude subscription using a **setup-token** flow. The flow is company-and-environment scoped (it carries no agent id, so you can run it during a hire before any agent exists), and the server derives the owner from the authenticated actor.
| Route | Purpose |
|---|---|
| `GET /api/companies/{companyId}/claude-oauth-token-status` | Read whether a stored Claude OAuth token exists for the authenticated owner. Returns only the secret id and latest version — never a token. A missing or foreign value returns the same fixed `404`. |
| `POST /api/companies/{companyId}/setup-token-login-sessions` | Start a login session. Body: `{ "environmentId": "<uuid>", "adapterType": "claude_local", "overwrite"?: { … } }`. Returns `201`. |
| `GET /api/companies/{companyId}/setup-token-login-sessions/active` | Rediscover the caller's own active session without a session id — handy after a browser reload with no local state. Returns the panel mode and the one-time prompt, sent with `Cache-Control: no-store, private`. A non-owner, a foreign company, or no active session all return the same fixed `404`. |
| `GET /api/companies/{companyId}/setup-token-login-sessions/{sessionId}` | Read the session status. |
| `GET /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/prompt` | Read the one-time login prompt (the authorization URL to open in a browser). |
| `POST /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/code` | Submit the code the browser hands back. Body: `{ "browserCode": "…" }`. |
| `POST /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/completion` | Read the completion claim. Returns the non-secret `storedSessionId` — never a token. |
| `POST /api/companies/{companyId}/setup-token-login-sessions/{sessionId}/cancel` | Cancel the session. Idempotent: a missing, terminal, or foreign session id all return `200`, so the route never confirms a session exists. |
Two things to know:
- **No response ever carries a token.** The completion returns only the opaque `storedSessionId` claim; the stored value lives in the company's secrets.
- **The prompt, code, and completion calls require a confidential transport.** Over a non-confidential connection the guard returns `403` — these steps move the login material, so they refuse to run in the clear.
---
## Managed and Remote Agent Profiles
Not every agent runs from an editable adapter config. Sometimes an operator qualifies a fixed backend once — a Claude Managed Agent, or a remote runner such as AWS Bedrock AgentCore — and stores it as a company-scoped **profile**. Runtime inputs then copy the immutable resource identity from the profile row instead of trusting an agent's own adapter config. Credentials never live in the profile itself: managed profiles link a company secret by id, and remote profiles carry no credential at all.
Both profile APIs are board-only and company-scoped, and both `POST` routes upsert (keyed by `profileKey`) and return `201`.
### Managed agent profiles
`GET /api/companies/{companyId}/managed-agent-profiles`
`POST /api/companies/{companyId}/managed-agent-profiles`
The request body accepts:
| Field | Notes |
|---|---|
| `profileKey` | Company-unique key for the profile. |
| `displayName` | Human-friendly label. |
| `anthropicAgentId` | The managed agent's immutable public id. |
| `agentVersion` | The qualified agent version. |
| `environmentId` | The managed environment id. |
| `defaultModel` | Model id. Defaults to `claude-sonnet-5`. |
| `defaultMaxListCostUsd` | Per-list cost ceiling in US dollars. Defaults to `1` and must be positive; the server stores it internally as `defaultMaxListCostCents`. |
| `apiKeySecretId` | Id of the company secret that holds the API key. The credential stays in the secret store. |
| `enabled` | Whether the profile is active. Defaults to `false`. |
| `retentionAcknowledged` | Whether the operator has acknowledged data retention. Defaults to `false`. |
| `qualification` | Free-form qualification-evidence object. |
The stored profile also exposes `service` (always `anthropic_managed_agents`), `betaVersion`, `qualifiedAt`, `qualifiedRevision`, and the usual timestamps. A profile can only be qualified/enabled once its qualification is attested. Upserts are logged as `managed_agent_profile.upserted`.
### Remote agent profiles
`GET /api/companies/{companyId}/remote-agent-profiles`
`POST /api/companies/{companyId}/remote-agent-profiles`
The list route accepts an optional `?service=` filter. The request body accepts:
| Field | Notes |
|---|---|
| `profileKey` | Company-unique key for the profile. |
| `displayName` | Human-friendly label. |
| `service` | Remote runner service. Currently only `aws_bedrock_agentcore_harness` is accepted. |
| `configuration` | Service-specific configuration object. |
| `enabled` | Whether the profile is active. Defaults to `false`. |
| `retentionAcknowledged` | Whether the operator has acknowledged data retention. Defaults to `false`. |
| `qualification` | Free-form qualification-evidence object. |
AWS AgentCore authenticates through the runner environment's workload identity, so the profile stores no credential. Sending a `credentialSecretId` is rejected with `422 Unprocessable Entity`, and an unsupported `service` value is rejected the same way. The stored profile also exposes `qualifiedAt`, `qualifiedRevision`, and timestamps. Upserts are logged as `remote_agent_profile.upserted`.
---
## Common Edge Cases
- Agent shortname lookup without company context returns `422 Unprocessable Entity`.
- A shortname collision returns `409 Conflict`.
- Updating `permissions` through the main update route returns `422`.
- Creating a key for a terminated or pending approval agent returns `409`.
- Pause and resume are not available for terminated agents.
- Wakeup can be skipped even when the request is accepted, especially when policy or execution state blocks it.
---
## Notes
This page was rewritten to match the current agent routes, auth rules, lifecycle behavior, and docs-site markdown conventions.
Changed file:
- `docs/api/agents.md`