Skip to main content
When an AI agent calls Corsair, you need guardrails. Permissions let you set a policy per integration — reads go through, writes may need sign-off, destructive actions can be blocked entirely.
corsair.ts

How it works

Every plugin endpoint has a risk level (read, write, or destructive). Your permission mode maps each risk level to a policy. When an agent calls a gated endpoint:
  1. Corsair evaluates the policy for that endpoint
  2. If allow → the call proceeds immediately
  3. If deny → the call is blocked with no database record
  4. If require_approval → Corsair writes a row to corsair_permissions and blocks the call until a human approves
Approved actions are single-use. Once the endpoint runs successfully, the record moves to completed and cannot be replayed.
Permissions require a database. Without corsair_permissions, any endpoint that needs approval falls back to deny.

Add the permissions table

If you already ran the quick start migration, add this table once. The schema matches what Corsair expects at runtime.
permissions.sql

Column reference


Permission modes

Set a default mode per plugin with permissions.mode. Each mode maps risk levels to policies: cautious is a good default for agent workloads — agents can read and write freely, but destructive actions need a human in the loop.
corsair.ts

Approval policies

Three resolved policies control what happens at call time: Policies come from the mode matrix above, unless you override a specific endpoint.

Overrides

Use permissions.overrides to tighten or loosen individual endpoints beyond the mode default. Keys are dot-notation paths through the plugin’s endpoint tree — invalid paths are compile-time errors.
corsair.ts
Overrides take precedence over the mode matrix. An override of deny always wins — no approval record is created.

Status lifecycle

Each approval request moves through these states: Corsair deduplicates pending requests. If the same plugin, endpoint, args, and tenant already have a non-expired pending record, a second call returns the existing token instead of creating a duplicate.

Timeout

Configure timeouts and blocking behavior at the root with createCorsair({ permissions: ... }):
The old approval: { ... } key still works but is deprecated — rename it to permissions. TypeScript will strike through approval in your editor. A runtime warning is logged on startup if you still use it.
corsair.ts
  • timeout — how long a pending record stays valid. Defaults to 10m if not set. Written to expires_at when the record is created.
  • onTimeout — intended behavior when the window closes without a response. With deny, expired records are treated as blocked. Use approve only in low-risk, fully trusted environments.
After expires_at, the record is no longer actionable. Synchronous mode returns a timeout error; asynchronous retries see the request as expired.

Synchronous vs asynchronous

Control how blocked calls behave with permissions.mode. Agent-facing messages come from hub (hosted, automatic) or manual.onApprovalRequired (self-hosted review URLs).

Asynchronous (default)

The tool call returns immediately with an error. The agent sees the blocked result and must stop or retry after the user approves. Best when:
  • The agent should explicitly tell the user to visit a review page
  • You want the model to handle denial gracefully and not burn tokens polling
With hub config, Corsair automatically returns a hosted approval URL in the agent message (see Approvals on Hub). With manual config, set approvalBaseUrl and optionally customize via onApprovalRequired:
corsair.ts

Synchronous

The tool call blocks and polls corsair_permissions every 500 ms until the user approves, denies, or the timeout elapses. From the agent’s perspective, it is just a slow tool call — the model does not need to handle a separate approval step.
Many agents enforce automatic timeouts on tool calls to prevent hangs. If your agent cuts off long-running tools before you can approve, use asynchronous mode instead — the call returns immediately and the agent retries after approval.
Best when:
  • You have a review UI open alongside the agent session
  • You want approval to feel seamless — approve in the UI, the agent continues automatically
corsair.ts

Dynamic mode

Pass a function to switch modes per request — useful when approval behavior depends on runtime context:
corsair.ts

Handling permission approvals

When an action requires approval, Corsair inserts a row into corsair_permissions with a unique token. That token is what you put in review URLs, Slack messages, or anywhere else you surface the request. Look up the row by token to see exactly what the agent wants to do — the args column holds the JSON-encoded arguments frozen at request time.
To resolve the request, update status:
That’s the entire approval contract. Corsair handles the rest — polling in synchronous mode, retry matching in asynchronous mode, and execution once the status is approved.

Build your own review flow

How you approve is entirely up to you. A few common patterns: Manual review UI — Add a page in your app that lists pending requests, shows plugin, endpoint, and parsed args, and renders Approve / Deny buttons that run the UPDATE above. Automated reviewer agent — Send the pending request to a second agent that evaluates whether the action is safe, then programmatically sets status to approved or denied. Useful when you want policy checks without a human in the loop for every write.
review-page.ts
Once status is approved, either the original agent retries the call or you invoke executePermission yourself to run the action without waiting for a retry.

Integrating with an agent

MCP / coding agents

When using MCP adapters, permissions gate run_script calls automatically. Configure per-plugin permissions: { mode, overrides } and global permissions: { timeout, mode } on createCorsair. With hub config, blocked calls automatically include a hosted approval URL for the agent. With manual config, set manual.approvalBaseUrl (and optionally manual.onApprovalRequired) so agents receive a review link:
corsair.ts
After approval, the agent retries the same call. Corsair finds the approved record, runs the endpoint, and marks it completed.

executePermission (optional)

If you don’t want to wait for the agent to retry after approval, call executePermission once status is approved. It replays the frozen args directly — no LLM involved:
approve-handler.ts
executePermission scopes to the correct tenant via withTenant, navigates corsair[plugin].api[endpoint], and marks the record completed on success.
The corsair.permissions namespace exposes find_by_token and find_by_permission_id for reads, but intentionally does not include approve/deny transitions — those happen in your review flow.

Multi-tenancy

In multi-tenant setups, each approval record stores the tenant_id from the active withTenant() context. When the action executes, Corsair scopes to that tenant’s credentials and data.
See Multi-Tenancy for tenant scoping details.

What’s next

MCP Adapters

Wire Corsair into Cursor, Claude Code, or any MCP-compatible agent.

Multi-Tenancy

Scope approvals and credentials per user with withTenant().

Database

The four core tables Corsair uses for synced integration data.

Hooks

Add custom logic before and after API calls — logging, validation, side effects.