mirror of
https://github.com/paperclipai/paperclip-docs.git
synced 2026-11-08 17:37:51 -10:00
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]>
589 lines
33 KiB
Markdown
589 lines
33 KiB
Markdown
---
|
||
paperclip_version: v2026.916.0
|
||
seo_title: Tool Gateway API
|
||
seo_description: Agents decide what to do; the tool gateway decides whether they may. It sits between every agent and every external tool you have connected.
|
||
---
|
||
|
||
# Tool Gateway
|
||
|
||
Your agents are good at deciding *what* to do. The **tool gateway** is what decides whether they are allowed to actually do it. It sits between an agent and every external tool you have connected — a Model Context Protocol (MCP) server, a spreadsheet, an internal HTTP service — and brokers each call: it looks up who is asking, checks the tool-access policy, and then allows the call, blocks it, asks a human first, or defers it to a runtime that isn't ready yet. Everything it decides lands in an audit trail you can read back.
|
||
|
||
Around that broker sit the pieces you configure. **Applications** and **connections** are the things you connect to. A **catalog** is the list of tools a connection actually offers. A **profile** is a named bundle of "these tools are in scope", which you **bind** to a company, an agent, a project, a routine, an issue, or a gateway. **Policies** are the rules layered on top — block this, rate-limit that, ask a person before this one. And a **named MCP gateway** is a front door you can hand to an outside MCP client, so that client speaks plain MCP while Paperclip quietly applies all of the above.
|
||
|
||
Every route below is company-scoped, and company access is checked on every request. If you are new to the API, read the [API Overview](./overview.md) first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.
|
||
|
||
> **Experimental — off by default.** The Tools, Profiles, and Apps surfaces are gated behind an instance experimental setting: an operator has to turn on `enableApps` before any of this appears in your instance. The Smoke Lab described near the end of this page is gated separately behind `enableSmokeLab`, and while that flag is off every Smoke Lab route responds `404 Not Found` with `{ "error": "Smoke lab is disabled" }`. Because the whole surface is experimental, treat the request and response shapes below as subject to change between releases.
|
||
|
||
---
|
||
|
||
## Permissions
|
||
|
||
Board callers need a permission key for the more sensitive routes. A local implicit board actor or an instance admin skips these checks; everyone else needs the key granted, or the request comes back `403 Forbidden` with `{ "error": "Missing permission: <key>" }`.
|
||
|
||
| Permission key | Covers |
|
||
|---|---|
|
||
| `tools:admin` | Creating, listing, and updating named MCP gateways; minting and revoking gateway tokens; managing approved stdio command templates. |
|
||
| `tools:manage_runtime` | Listing runtime slots, and stopping or restarting them. |
|
||
| `tools:view_audit` | Reading the gateway audit feed. |
|
||
| `tools:manage_connections` | Syncing a connection's installs. |
|
||
| `tools:use` | Running test calls against a connection (either this or `tools:manage_connections` is enough). |
|
||
|
||
Several routes that create or edit configuration don't use a permission key at all — instead they require an **active, non-viewer** company membership. A viewer gets `403 Forbidden` with `{ "error": "Viewer access is read-only" }`, and someone with no active membership gets `{ "error": "User does not have active company access" }`.
|
||
|
||
---
|
||
|
||
## The moving parts
|
||
|
||
### Applications and connections
|
||
|
||
An **application** is the thing you are connecting to, and a **connection** is one configured way of reaching it. An application has a `type` and a `status`; a connection has a `transport` — `remote_http` or `local_stdio` — plus its own `status` (`draft`, `active`, `disabled`, `archived`) and an `enabled` flag. A connection that is disabled, archived, or simply not `enabled` short-circuits every call through it with a denial (`deny_disabled_connection`), and the same is true one level up for an application that is `disabled` or `archived`.
|
||
|
||
### Catalog entries
|
||
|
||
Refreshing a connection pulls in its **catalog** — one entry per tool the connection offers, with the tool's `name`, `description`, `inputSchema`, and a `riskLevel`. Risk levels come from a fixed set: `low`, `medium`, `high`, `critical`, `read`, `write`, `destructive`. Each entry also carries a `versionHash` and a `schemaHash`, which is how Paperclip notices that a tool you previously trusted has quietly changed shape.
|
||
|
||
### Profiles, entries, and bindings
|
||
|
||
A **profile** is a named set of tools, and it has a `defaultAction` of either `deny` or `allow`. Its **entries** narrow that down. Every entry has a `selectorType` — `application`, `connection`, `catalog_entry`, `tool_name`, or `risk_level` — and an `effect` of `include` or `exclude`.
|
||
|
||
A profile does nothing until you **bind** it. A binding has a `targetType` of `company`, `agent`, `project`, `routine`, `issue`, or `gateway`, and when several bindings match a single call, the narrowest scope wins.
|
||
|
||
### Policies
|
||
|
||
**Policies** are the rules evaluated on every call, in `priority` order (lower first). A policy has a `policyType`, a set of `selectors` that decide which calls it applies to, optional `conditions` that must also hold, and — for rate limits — a `config`.
|
||
|
||
The useful types are `allow`, `block`, `require_approval`, and `rate_limit`. Two others are special: `trust_rule` policies are managed only through the trust-rule promotion and revoke routes (trying to create one directly returns `422` with `Trust rules are managed through the trust-rule promotion and revoke endpoints`), and `redact` and `validate` are rejected outright with `Tool policy type '<type>' is not supported at runtime`.
|
||
|
||
A `rate_limit` policy needs a positive `limit` and `windowSeconds` in its `config`, or you get `422 Unprocessable Entity` with `Rate-limit policy config requires positive numeric limit and windowSeconds`. Its optional `keyBy` decides what the limit counts against; the default buckets by company, agent, connection, and tool.
|
||
|
||
Conditions come in named groups — `arguments`, `actor`, `context`, `risk`, `credentialScope`, `trustBoundary`, and `timeWindow` — and all of them must hold for the policy to apply. The `arguments` group supports `fieldEquals`, `fieldNotEquals`, `fieldIn`, `fieldMatches`, `fieldExists`, and `fieldAbsent`. A `fieldMatches` pattern that looks dangerous to evaluate is rejected up front with `Tool policy fieldMatches includes an unsafe regular expression`.
|
||
|
||
### What a decision looks like
|
||
|
||
Every brokered call ends in one of five decisions: `allow`, `deny`, `require_approval`, `rate_limited`, or `defer_runtime`. Alongside the decision you get a `reasonCode` (`deny_disabled_connection`, `allow_trust_rule`, `deny_policy_block`, and friends), a human-readable `explanation`, the `effectiveProfileIds` that were in play, and the `matchedPolicyIds` that fired.
|
||
|
||
---
|
||
|
||
## Named MCP Gateways
|
||
|
||
A named gateway is how you hand an outside MCP client — an editor, a desktop agent, another service — a single governed endpoint. The client authenticates with a bearer token you mint, asks for a tool list, and calls tools; Paperclip resolves the token to the gateway, applies the gateway's profile and your policies, and forwards only what is allowed.
|
||
|
||
### List gateways
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/gateways
|
||
```
|
||
|
||
Requires `tools:admin`. Returns `{ "gateways": [ ... ] }`.
|
||
|
||
### Create a gateway
|
||
|
||
```
|
||
POST /api/companies/{companyId}/tools/gateways
|
||
```
|
||
|
||
Requires `tools:admin`. Responds `201 Created`.
|
||
|
||
| Field | Required | Notes |
|
||
|---|---|---|
|
||
| `name` | yes | 1–160 characters, unique within the company. |
|
||
| `profileId` | yes | The tool profile this gateway serves. |
|
||
| `slug` / `displaySlug` | no | 1–120 characters each. |
|
||
| `description` | no | Up to 4,000 characters, or `null`. |
|
||
| `defaultProfileMode` | no | `gateway_only` (the default), `inherit_context_then_gateway`, or `gateway_then_context`. |
|
||
| `contextScopeType` | no | Defaults to `none`. |
|
||
| `contextScopeId` | no | 1–200 characters, or `null`. |
|
||
| `agentId`, `projectId`, `issueId`, `approvalIssueId` | no | UUIDs, or `null`. |
|
||
| `authConfig`, `headerPolicy`, `metadataPolicy`, `onDemandToolsConfig` | no | See below. |
|
||
| `metadata` | no | Arbitrary JSON object, or `null`. |
|
||
|
||
An invalid payload comes back `422 Unprocessable Entity` as `{ "error": "Invalid gateway payload", "issues": [ ... ] }`.
|
||
|
||
The four config blocks are where a gateway's manners live, and each has a sensible default:
|
||
|
||
- **`headerPolicy`** — whether caller headers pass through (`callerPassthrough`, off by default with an empty `allowedHeaders`), any `staticHeaders` you always attach, `generatedMetadata` headers, and which `responseHeaders` come back. By default it forwards MCP-required headers and safe cache headers.
|
||
- **`metadataPolicy`** — which identifiers Paperclip is allowed to tell the upstream tool about: `forwardCompanyId`, `forwardGatewayId`, `forwardProjectId`, `forwardIssueId`, `forwardAgentId`, `forwardRunId`, and `forwardCorrelationId`. Only `forwardCorrelationId` is on by default, so nothing about your company leaks unless you say so.
|
||
- **`authConfig`** — bearer-token behaviour. By default bearer auth is enabled with a `pcgw` token prefix, a 90-day default TTL, `requireFiniteExpiry` on, and `longLivedTokenRequiresOverride` on. OAuth is present but disabled.
|
||
- **`onDemandToolsConfig`** — off by default. When enabled it exposes two meta-tools, `search_tools` and `run_tool`, instead of a long static list, which keeps a large catalog from flooding the client's context.
|
||
|
||
### Update a gateway
|
||
|
||
```
|
||
PATCH /api/tool-gateway/gateways/{gatewayId}
|
||
```
|
||
|
||
Requires `tools:admin`. Send `companyId` in the body — without it you get `400 Bad Request` and `{ "error": "companyId is required" }`. Every create field is optional here, plus `status` (`draft`, `active`, `disabled`, `archived`). At least one field must be present.
|
||
|
||
### Mint a gateway token
|
||
|
||
```
|
||
POST /api/tool-gateway/gateways/{gatewayId}/tokens
|
||
```
|
||
|
||
Requires `tools:admin`, and `companyId` in the body. Responds `201 Created`.
|
||
|
||
| Field | Required | Notes |
|
||
|---|---|---|
|
||
| `name` | yes | 1–160 characters. |
|
||
| `clientLabel` | yes | 1–160 characters — which client this token is for. |
|
||
| `ownerNote` | yes | 1–1,000 characters — who owns it and why it exists. |
|
||
| `subjectType` | no | Only `gateway_client` is accepted today. |
|
||
| `subjectId` | no | 1–240 characters, or `null`. |
|
||
| `allowedActions` | no | Any of `tools/list` and `tools/call`. Defaults to both. |
|
||
| `expiresAt` | no | A date, or `null` for a non-expiring token. |
|
||
| `expiryOverrideReason` | no | Required when `expiresAt` is `null`. |
|
||
|
||
Two rules are worth knowing before you script this. Asking for anything other than `gateway_client` as `subjectType` is rejected — `heartbeat_run` is runtime-managed, and `board_user` and `agent` are reserved for later OAuth and user-bound flows. And a token with no expiry is deliberately awkward: set `expiresAt` to `null` and you must also supply `expiryOverrideReason`, or you get `Non-expiring gateway tokens require an override reason`.
|
||
|
||
### Revoke a gateway token
|
||
|
||
```
|
||
POST /api/tool-gateway/gateway-tokens/{tokenId}/revoke
|
||
```
|
||
|
||
Requires `tools:admin`, and `companyId` in the body.
|
||
|
||
### The MCP endpoint itself
|
||
|
||
There are two ways to reach the same gateway — by its public id, or by its internal UUID:
|
||
|
||
```
|
||
GET /mcp/gateways/{gatewayPublicId}
|
||
POST /mcp/gateways/{gatewayPublicId}
|
||
GET /api/tool-gateway/gateways/{gatewayId}/mcp
|
||
POST /api/tool-gateway/gateways/{gatewayId}/mcp
|
||
```
|
||
|
||
The `GET` is a small discovery document telling a client how to connect:
|
||
|
||
```json
|
||
{
|
||
"transport": "streamable_http",
|
||
"endpoint": "/mcp/gateways/{gatewayPublicId}",
|
||
"authentication": "bearer"
|
||
}
|
||
```
|
||
|
||
The `POST` speaks JSON-RPC. Authenticate with `Authorization: Bearer <gateway-token>`; without it you get `401 Unauthorized` and `{ "error": "Bearer token is required" }`.
|
||
|
||
| Method | What happens |
|
||
|---|---|
|
||
| `initialize` | Returns `protocolVersion` `2025-03-26`, a `capabilities` block advertising `tools`, and `serverInfo` naming `Paperclip MCP Gateway`. |
|
||
| `notifications/initialized` | Acknowledged with `202 Accepted` and no body. |
|
||
| `tools/list` | Returns the tools this gateway's profile allows, each with `name`, `title`, `description`, and `inputSchema`. |
|
||
| `tools/call` | Runs a tool. Takes `params.name` and `params.arguments`; a missing name is a JSON-RPC error `-32602` with `params.name is required`. |
|
||
|
||
Anything else returns `404` with JSON-RPC error `-32601`, `Method not found`. When a call is refused, the JSON-RPC `error` object carries a `data.reasonCode` so you can tell a policy block apart from a broken upstream.
|
||
|
||
---
|
||
|
||
## Agent Sessions
|
||
|
||
Named gateways are for outside clients. An agent working inside Paperclip uses a **session** instead: a short-lived token scoped to one agent and one run.
|
||
|
||
### Create a session
|
||
|
||
```
|
||
POST /api/tool-gateway/sessions
|
||
```
|
||
|
||
Board or agent callers. An agent's own `companyId`, `agentId`, and `runId` are taken from its token; a board caller supplies them in the body along with optional `issueId`, `projectId`, and `ttlMs`. Missing any of the three required ids gives `400 Bad Request` with `{ "error": "companyId, agentId, and runId are required" }`.
|
||
|
||
The `201 Created` response hands back everything the agent needs:
|
||
|
||
```json
|
||
{
|
||
"sessionId": "...",
|
||
"token": "...",
|
||
"expiresAt": "2026-07-20T12:00:00.000Z",
|
||
"toolsUrl": "/api/tool-gateway/tools",
|
||
"callUrl": "/api/tool-gateway/tools/call"
|
||
}
|
||
```
|
||
|
||
### Revoke a session
|
||
|
||
```
|
||
POST /api/tool-gateway/sessions/{sessionId}/revoke
|
||
```
|
||
|
||
Returns `{ "sessionId": ..., "revokedAt": ... }`. An agent can only revoke its own sessions.
|
||
|
||
### List and call tools
|
||
|
||
```
|
||
GET /api/tool-gateway/tools
|
||
POST /api/tool-gateway/tools/call
|
||
```
|
||
|
||
Both authenticate with the session token in the `x-paperclip-tool-gateway-token` header — not `Authorization`. Without it you get `401 Unauthorized` and `{ "error": "Tool gateway session token is required" }`.
|
||
|
||
The call body takes:
|
||
|
||
| Field | Required | Notes |
|
||
|---|---|---|
|
||
| `tool` | yes | The tool name. A missing or non-string value returns `400` with `"tool" is required and must be a string`. |
|
||
| `parameters` | no | The tool's arguments. Defaults to `{}`. |
|
||
| `timeoutMs` | no | How long to wait on the upstream tool. |
|
||
| `approvedActionRequestId` | no | The approval you are cashing in, when a previous attempt returned `require_approval`. |
|
||
| `idempotencyKey` | no | Makes a retry safe — the same key won't run the tool twice. |
|
||
|
||
### An agent authorizing its own connection
|
||
|
||
Some connections need the agent to authorize itself — for example, to complete an OAuth flow or to mint a short-lived credential for the upstream service. Two routes let an agent do that for itself, using its own run token; both refuse anyone who isn't an agent on an active run.
|
||
|
||
```
|
||
POST /api/agents/me/connections/{connectionId}/start-authorization
|
||
POST /api/agents/me/connections/{connectionId}/token
|
||
```
|
||
|
||
`start-authorization` kicks off an OAuth authorization and returns `{ "url": ... }` (plus a `handoff` block when one applies) — send the person to `url` to finish consent. Its body takes an optional `subjectUserId`, `scopes`, and `returnTo`.
|
||
|
||
`token` mints a short-lived token the agent can present to the upstream service. It needs a run id, so a token without one is refused `401` with `{ "error": "Agent run id required", "code": "run_id_required" }`, and if you also send an `X-Paperclip-Run-Id` header it must match the token's run or you get `403` with `run_id_mismatch`. A successful mint returns `200` with `status` `minted` and the `token`, `tokenType`, `expiresAt`, `ttlSeconds`, `scope`, and `grantId`. If the connection is backed by static credentials rather than a broker, minting is refused with `409 Conflict` and `status` `use_env_lease` — the signal to reach the credential through an audited environment lease instead.
|
||
|
||
---
|
||
|
||
## Approvals
|
||
|
||
When a policy says `require_approval`, the call becomes an **action request** rather than an immediate result. A human then decides.
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/action-requests
|
||
POST /api/tool-gateway/action-requests/{id}/approve
|
||
POST /api/tool-gateway/action-requests/{id}/decline
|
||
```
|
||
|
||
The list route takes a `status` query parameter and defaults to `pending`. Approve and decline both need `companyId`, either in the body or as a query parameter, and both require an active non-viewer membership.
|
||
|
||
Each action request stores a `canonicalArgumentsHash` and a redacted `canonicalArgumentsSummary`, so what a person approves is exactly what later runs — approving one call doesn't quietly bless a different set of arguments. Many requests also carry `previewMarkdown`, a readable summary of what the tool is about to do.
|
||
|
||
Once you have approved the same thing a few times, you can promote it into a standing rule:
|
||
|
||
```
|
||
POST /api/companies/{companyId}/tools/action-requests/{actionRequestId}/trust-rule
|
||
POST /api/companies/{companyId}/tools/trust-rules/{policyId}/revoke
|
||
GET /api/companies/{companyId}/tools/trust-rules
|
||
```
|
||
|
||
A trust rule remembers the catalog `versionHash` and `schemaHash` it was created against. If the upstream tool is later quarantined, removed, or changes shape, the rule stops applying on its own and the call goes back to asking — trust is pinned to the tool you actually approved, not to its name.
|
||
|
||
---
|
||
|
||
## Redaction
|
||
|
||
Paperclip never stores raw tool arguments. Before anything is written to the audit trail, values are summarized and scrubbed: keys that look like secrets (`api_key`, `authorization`, `password`, `token`, `client_secret`, and similar) are replaced with `[REDACTED]`, and so are values matching known credential shapes. Long strings are truncated, and long arrays are capped. What you get back is a `redactionPlan` with a `redactedFieldCount` and the list of `redactedFields`, plus a hash you can compare across calls without ever seeing the values.
|
||
|
||
---
|
||
|
||
## Runtime Slots
|
||
|
||
A `local_stdio` connection runs a real process, and a **runtime slot** is Paperclip's handle on it — its status, health, last error, and idle deadline.
|
||
|
||
```
|
||
GET /api/tool-gateway/runtime-slots?companyId={companyId}
|
||
POST /api/tool-gateway/runtime-slots/{slotId}/stop
|
||
POST /api/tool-gateway/runtime-slots/{slotId}/restart
|
||
```
|
||
|
||
All three require `tools:manage_runtime`. The same operations are also available under the company path:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/runtime-slots
|
||
POST /api/companies/{companyId}/tools/runtime-slots/{id}/stop
|
||
POST /api/companies/{companyId}/tools/runtime-slots/{id}/restart
|
||
GET /api/companies/{companyId}/tools/runtime-health
|
||
```
|
||
|
||
Because stdio connections launch commands, they are constrained by an approved list of **command templates**:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/stdio-templates
|
||
POST /api/companies/{companyId}/tools/stdio-templates
|
||
POST /api/companies/{companyId}/tools/stdio-templates/{templateId}/disable
|
||
```
|
||
|
||
All three require `tools:admin`. A template pins a `command`, its `args`, the `envKeys` it may read, and the `tools` it provides — so a connection can only ever start something an admin has already vetted.
|
||
|
||
---
|
||
|
||
## The Audit Feed
|
||
|
||
```
|
||
GET /api/tool-gateway/audit?companyId={companyId}
|
||
```
|
||
|
||
Requires `tools:view_audit`. This is the record of everything the gateway did.
|
||
|
||
| Query parameter | Notes |
|
||
|---|---|
|
||
| `window` | `1h`, `24h`, `7d`, or `30d`. Defaults to `24h`. Anything else returns `400` with `window must be one of 1h, 24h, 7d, 30d`. |
|
||
| `limit` | 1–100. Defaults to `100`. |
|
||
| `app` | An `applicationId` or `connectionId` UUID. A non-UUID returns `400` with `app must be an applicationId or connectionId UUID`. |
|
||
| `agent` | An `agentId` UUID; likewise validated. |
|
||
| `outcome` | See the table below. |
|
||
| `search` | Free text, matched server-side against agent, app, and connection names as well as action names, tool names, and reason codes. |
|
||
| `cursor` | The `nextCursor` from a previous page. An unreadable cursor returns `400` with `Invalid audit cursor`. |
|
||
|
||
Each event carries a `normalizedOutcome` — a plain-language version of what happened, so you don't have to memorize the underlying action names:
|
||
|
||
| `normalizedOutcome` | Means |
|
||
|---|---|
|
||
| `allowed` | The call went through. |
|
||
| `asked_first` | An approval was requested before running. |
|
||
| `waiting` | The call was deferred, usually to a runtime that wasn't ready. |
|
||
| `blocked` | Policy or a rate limit stopped it. |
|
||
| `failed` | It ran and errored. |
|
||
| `unknown` | Doesn't map to any of the above. |
|
||
|
||
Events are also enriched for display with `agentDisplayName`, `appDisplayName`, `applicationDisplayName`, `connectionDisplayName`, and `toolDisplayName`. The response is `{ "events": [ ... ], "nextCursor": ... }`; page by feeding `nextCursor` back in until it comes back `null`.
|
||
|
||
The underlying actions, if you need them, are `tool_gateway.session_created`, `session_revoked`, `session_rejected`, `discovery`, `call_allowed`, `call_denied`, `call_completed`, `call_failed`, `call_deferred`, `approval_requested`, and `runtime_mcp_delivery`.
|
||
|
||
---
|
||
|
||
## Wiring up an App
|
||
|
||
The Apps surface is the friendly path to all of the above — pick something from a gallery, connect it, and let Paperclip generate a sensible profile and a set of ask-first rules for you.
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/gallery
|
||
GET /api/companies/{companyId}/tools/apps/{galleryKey}/preflight
|
||
POST /api/companies/{companyId}/tools/apps/connect
|
||
POST /api/companies/{companyId}/tools/apps/{connectionId}/finish
|
||
GET /api/companies/{companyId}/tools/apps/attention
|
||
```
|
||
|
||
`preflight` returns the metadata Paperclip needs to render the connect screen for a gallery app before you commit to it — pass an optional `methodKey` query to preview one specific auth method. `connect` creates the application, the connection, and the catalog in one step, and responds `201 Created`. If the app authenticates with OAuth, the response's `auth` block comes back with a `startUrl` you send the person to. `finish` then decides `access` and, from it, builds the profile, its entries, its bindings, and the ask-first policies — the response tells you how many of each it made.
|
||
|
||
The `attention` route is the one to poll on a dashboard: it lists apps that need a human — a broken health check, an expired credential, newly appeared tools nobody has reviewed.
|
||
|
||
OAuth has its own two routes:
|
||
|
||
```
|
||
POST /api/tools/oauth/{connectionId}/start
|
||
GET /api/tools/oauth/callback
|
||
```
|
||
|
||
These need a public base URL configured; without one you get `422 Unprocessable Entity` with `OAuth connections require PAPERCLIP_PUBLIC_URL or an auth public base URL`.
|
||
|
||
You can also start from a config file you already have:
|
||
|
||
```
|
||
POST /api/companies/{companyId}/tools/mcp/import-json
|
||
```
|
||
|
||
This previews an MCP JSON config as a set of connection drafts. It only previews — nothing is created until you say so.
|
||
|
||
Examples are prebuilt, working setups you can install and smoke-test in place:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/examples
|
||
POST /api/companies/{companyId}/tools/examples/{id}/install
|
||
POST /api/companies/{companyId}/tools/examples/{id}/smoke
|
||
```
|
||
|
||
---
|
||
|
||
## Managing Applications, Connections, and Profiles Directly
|
||
|
||
If you would rather build it yourself, every object has a plain CRUD surface.
|
||
|
||
Applications:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/applications
|
||
POST /api/companies/{companyId}/tools/applications
|
||
PATCH /api/tool-applications/{applicationId}
|
||
DELETE /api/tool-applications/{applicationId}
|
||
```
|
||
|
||
Connections:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/connections
|
||
POST /api/companies/{companyId}/tools/connections
|
||
GET /api/tool-connections/{connectionId}
|
||
PATCH /api/tool-connections/{connectionId}
|
||
DELETE /api/tool-connections/{connectionId}
|
||
POST /api/tool-connections/{connectionId}/health-check
|
||
POST /api/tool-connections/{connectionId}/reconnect
|
||
POST /api/tool-connections/{connectionId}/catalog/refresh
|
||
GET /api/tool-connections/{connectionId}/catalog
|
||
GET /api/tool-connections/{connectionId}/activity
|
||
```
|
||
|
||
Deleting a connection archives it rather than dropping it, and if it was the last connection on its application, that application is archived too.
|
||
|
||
A connection can be **installed** onto a `company` or an `agent`, which is how you say who gets to see it at all:
|
||
|
||
```
|
||
GET /api/tool-connections/{connectionId}/installs
|
||
PUT /api/tool-connections/{connectionId}/installs
|
||
```
|
||
|
||
The `PUT` replaces the whole install set and requires `tools:manage_connections`.
|
||
|
||
#### Connection identities (grants)
|
||
|
||
A single connection can carry more than one **identity**. A grant with `kind` `organization` is a shared identity everyone on the connection uses; a grant with `kind` `user` is one member's personal identity. This is how, for example, five people can all use the same Gmail connection while each acts as themselves.
|
||
|
||
```
|
||
GET /api/tool-connections/{connectionId}/grants
|
||
POST /api/tool-connections/{connectionId}/grants/installations
|
||
DELETE /api/tool-connections/{connectionId}/grants/{grantId}
|
||
PUT /api/tool-connections/{connectionId}/grants/{grantId}/members
|
||
POST /api/tool-connections/{connectionId}/grants/{grantId}/delegations
|
||
DELETE /api/tool-connections/{connectionId}/grants/{grantId}/delegations/{delegationId}
|
||
```
|
||
|
||
The **list** route returns the grants along with a `capabilities` block, the caller's `currentUserId`, and — for a connection manager — the audience `members` to choose from. It filters what you can see: a regular member sees the organization grants and their own personal grant, but not other people's personal identities. Seeing everyone's identities is a connection-manager power (`tools:manage_connections`).
|
||
|
||
The **`installations`** route adds a grant; its body takes an optional `providerTenant`, a list of `credentialSecretRefs`, and `isDefault`, and it responds `201 Created`. The **`members`** `PUT` replaces a grant's audience with a `memberUserIds` array — the grant's creator can do this, anyone else needs configure access. **Deleting** a grant revokes it; you can always revoke a grant you own (you are its subject or its creator), otherwise you need configure access.
|
||
|
||
A **delegation** lets the owner of a personal grant lend that identity to an agent — the `POST` takes an `agentId` and must be called by the owning user, and the matching `DELETE` (owner or connection manager only) takes it back.
|
||
|
||
Most of these routes gate on **configure access** — you are the connection's creator, or you hold `tools:manage_connections` — rather than on a single permission key. Viewers and members with no active company access are refused before anything else.
|
||
|
||
#### Toolkit services and usage
|
||
|
||
When a connection fronts a provider that bundles many toolkits, you connect and disconnect each toolkit **service** on its own:
|
||
|
||
```
|
||
GET /api/tool-connections/{connectionId}/services
|
||
POST /api/tool-connections/{connectionId}/services/{toolkitSlug}/connect
|
||
GET /api/tool-connections/{connectionId}/services/{toolkitSlug}/status
|
||
DELETE /api/tool-connections/{connectionId}/services/{toolkitSlug}
|
||
```
|
||
|
||
All four require configure access. `connect` takes an optional `authConfigId` and `callbackUrl` and responds `201 Created`; `status` polls where an in-flight connect got to; and the `DELETE` disconnects the toolkit and removes the catalog tools it contributed.
|
||
|
||
A usage summary rounds out the per-connection view:
|
||
|
||
```
|
||
GET /api/tool-connections/{connectionId}/usage?range={7d|30d}
|
||
```
|
||
|
||
`range` defaults to `7d` and must be `7d` or `30d` — anything else returns `400 Bad Request` with `Usage range must be 7d or 30d`.
|
||
|
||
Profiles, entries, and bindings:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/profiles
|
||
POST /api/companies/{companyId}/tools/profiles
|
||
PATCH /api/tool-profiles/{profileId}
|
||
DELETE /api/tool-profiles/{profileId}
|
||
POST /api/tool-profiles/{profileId}/duplicate
|
||
POST /api/tool-profiles/{profileId}/entries
|
||
PATCH /api/tool-profile-entries/{entryId}
|
||
DELETE /api/tool-profile-entries/{entryId}
|
||
POST /api/companies/{companyId}/tools/profiles/{profileId}/bind
|
||
POST /api/companies/{companyId}/tools/profiles/{profileId}/unbind
|
||
GET /api/companies/{companyId}/tools/profiles/effective/agents/{agentId}
|
||
```
|
||
|
||
That last one answers the question you'll actually ask most often: *given everything I have configured, what can this agent really use?*
|
||
|
||
When a connection's catalog refresh turns up tools your profile has never seen, they are held back rather than silently allowed:
|
||
|
||
```
|
||
GET /api/tool-profiles/{profileId}/new-tools
|
||
POST /api/tool-profiles/{profileId}/new-tools/review
|
||
```
|
||
|
||
Reviewing them returns an `allowedCount` and a `keptBlockedCount`, so an upstream server can't widen its own access just by adding a tool.
|
||
|
||
Policies:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/policies
|
||
POST /api/companies/{companyId}/tools/policies
|
||
PATCH /api/companies/{companyId}/tools/policies/{policyId}
|
||
DELETE /api/companies/{companyId}/tools/policies/{policyId}
|
||
POST /api/companies/{companyId}/tools/policies/{policyId}/duplicate
|
||
POST /api/companies/{companyId}/tools/policies/reorder
|
||
```
|
||
|
||
Reordering rewrites `priority` in steps of 100 and must include **every** non-trust policy in the company — a partial list returns `422` with `Reorder must include every non-trust policy for the company`. A duplicated policy is always created **disabled**, so you can edit the copy before it starts affecting anything.
|
||
|
||
---
|
||
|
||
## Testing Before You Trust
|
||
|
||
Three routes let you rehearse a call without committing to it.
|
||
|
||
The dry run answers "what *would* happen":
|
||
|
||
```
|
||
POST /api/companies/{companyId}/tools/policy/test
|
||
```
|
||
|
||
This runs the real decision engine over a hypothetical call and returns `{ "decision": ..., "auditEvent": ... }`. Nothing is executed. Set `writeAuditEvent` to `true` if you want the rehearsal recorded; otherwise `auditEvent` comes back `null`.
|
||
|
||
The live test actually runs the tool, as a chosen agent:
|
||
|
||
```
|
||
GET /api/tool-connections/{connectionId}/test-agents
|
||
GET /api/tool-connections/{connectionId}/test-agents/{agentId}/access
|
||
POST /api/tool-connections/{connectionId}/test-calls
|
||
GET /api/tool-connections/{connectionId}/test-calls/{actionRequestId}
|
||
```
|
||
|
||
All of these need `tools:use` or `tools:manage_connections`, and you can only test as an agent you would be allowed to assign work to. `test-agents` lists those agents along with each one's `effectiveAccess` for that connection; the `test-agents/{agentId}/access` route drills into a single agent and returns its full `access` summary for the connection. `test-calls` takes an `agentId`, a `toolName`, and optional `parameters`. If the call needs approval, you get an action request back and poll the third route for its status. When the gateway service isn't configured at all, these respond `501 Not Implemented` with `{ "error": "Tool gateway service is not configured" }`.
|
||
|
||
You can also look up how decisions landed for a specific agent run:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/tools/runs/{runId}/decisions
|
||
```
|
||
|
||
---
|
||
|
||
## Smoke Lab
|
||
|
||
The Smoke Lab is a self-contained rehearsal environment: a fake OAuth provider and a set of loopback fixture services, so you can exercise the whole connect-authorize-call-audit path without touching a real vendor.
|
||
|
||
> **Gated twice.** Smoke Lab needs the `enableSmokeLab` experimental setting turned on, *and* it refuses to run on a publicly exposed instance. While it is off, every route here responds `404 Not Found` with `{ "error": "Smoke lab is disabled" }`.
|
||
|
||
```
|
||
GET /api/companies/{companyId}/smoke-lab/services
|
||
POST /api/companies/{companyId}/smoke-lab/services/start
|
||
POST /api/companies/{companyId}/smoke-lab/services/stop
|
||
POST /api/companies/{companyId}/smoke-lab/install-fixtures
|
||
POST /api/companies/{companyId}/smoke-lab/reset
|
||
```
|
||
|
||
`install-fixtures` creates a ready-made application, connection, catalog, and profile, responding `201 Created` the first time and `200 OK` if they already exist. `reset` puts everything back.
|
||
|
||
Runs record what a rehearsal actually did:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/smoke-lab/runs
|
||
POST /api/companies/{companyId}/smoke-lab/runs
|
||
GET /api/companies/{companyId}/smoke-lab/runs/{runId}
|
||
PATCH /api/companies/{companyId}/smoke-lab/runs/{runId}
|
||
POST /api/companies/{companyId}/smoke-lab/runs/{runId}/steps
|
||
```
|
||
|
||
A run has a `trigger` and a `status`; each recorded step has a `path` and a `status`, and both board users and agents can create runs and record steps — which is what lets an agent drive its own end-to-end check.
|
||
|
||
The lab's fake OAuth provider lives under the same prefix and behaves like a real one, so a connection you configure against it needs no special handling:
|
||
|
||
```
|
||
GET /api/companies/{companyId}/smoke-lab/oauth/authorize
|
||
POST /api/companies/{companyId}/smoke-lab/oauth/authorize
|
||
POST /api/companies/{companyId}/smoke-lab/oauth/token
|
||
GET /api/companies/{companyId}/smoke-lab/oauth/userinfo
|
||
POST /api/companies/{companyId}/smoke-lab/oauth/revoke
|
||
```
|
||
|
||
Redirect URIs are checked against the instance's own origin, so a fixture authorization code can never be handed to an outside host.
|
||
|
||
---
|
||
|
||
## Where to go next
|
||
|
||
- [API Overview](./overview.md) — base URL, authentication, company scoping, and the shared error-code table.
|
||
- [Company Skill Policy](./company-skill-policy.md) — the same guardrail idea applied to the skills your agents can pick up.
|
||
- [Agents](./agents.md) — the agents that profiles and gateway sessions are scoped to.
|
||
- [Secrets](./secrets.md) — where the credentials behind a connection are stored.
|