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:
- Corsair evaluates the policy for that endpoint
- If
allow→ the call proceeds immediately - If
deny→ the call is blocked with no database record - If
require_approval→ Corsair writes a row tocorsair_permissionsand blocks the call until a human approves
completed and cannot be replayed.
Add the permissions table
If you already ran the quick start migration, add this table once. The schema matches what Corsair expects at runtime.- SQLite
- PostgreSQL
View migration SQL
View migration SQL
permissions.sql
Column reference
| Column | Purpose |
|---|---|
token | 64-character hex token embedded in review URLs — the public handle for approve/deny |
plugin | Plugin id, e.g. github |
endpoint | Dot-notation path, e.g. repositories.delete |
args | JSON-encoded arguments frozen at request time — replayed exactly on approval |
tenant_id | Tenant scope for multi-tenant instances. Defaults to default |
status | Lifecycle state (see below) |
expires_at | ISO8601 timestamp — when the request becomes invalid |
error | Error message when status is failed |
Permission modes
Set a default mode per plugin withpermissions.mode. Each mode maps risk levels to policies:
| Mode | Read | Write | Destructive |
|---|---|---|---|
open | allow | allow | allow |
cautious | allow | allow | require_approval |
strict | allow | require_approval | deny |
readonly | allow | deny | deny |
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:| Policy | Behavior |
|---|---|
allow | Executes immediately — no approval record |
deny | Blocked by policy. Logs a message pointing you to the corsair config |
require_approval | Creates a pending record in corsair_permissions and blocks until approved |
Overrides
Usepermissions.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
deny always wins — no approval record is created.
Status lifecycle
Each approval request moves through these states:| Status | Meaning |
|---|---|
pending | Waiting for human approval |
approved | Human signed off — ready to execute (single-use) |
executing | executePermission is running the frozen args |
completed | Action ran successfully — approval consumed |
denied | Human declined the request |
expired | expires_at passed before a decision |
failed | Endpoint threw during execution — see error column |
pending record, a second call returns the existing token instead of creating a duplicate.
Timeout
Configure the approval window at the root withcreateCorsair({ approval: ... }):
corsair.ts
timeout— how long apendingrecord stays valid. Defaults to10mif not set. Written toexpires_atwhen the record is created.onTimeout— intended behavior when the window closes without a response. Withdeny, expired records are treated as blocked. Useapproveonly in low-risk, fully trusted environments.
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 withapproval.mode:
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
corsair.ts
Synchronous
The tool call blocks and pollscorsair_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.
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 intocorsair_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.
status:
| Decision | Set status to |
|---|---|
| Approve | approved |
| Deny | denied |
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, showsplugin, 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
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 gaterun_script calls automatically. No extra wiring — configure permissions on each plugin and approval on createCorsair.
In asynchronous mode, customize the error the LLM sees with formatAsyncMessage. Point the agent (and the user) at your review page:
corsair.ts
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 thetenant_id from the active withTenant() context. When the action executes, Corsair scopes to that tenant’s credentials and data.
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.