Skip to main content
Permissions gate risky agent actions behind human approval. The policy engine, the modes, and the corsair_permissions table work the same regardless of mode. The only thing Hub changes is who hosts the approve/deny UI.
  • Manual — you build a review page and wire manual.onApprovalRequired so the agent receives a link to it.
  • Hub — Corsair hosts the approval page. When a call is gated, the SDK returns a Hub approval link automatically. Nothing to build.

How it works on Hub

The approval record still lives in your corsair_permissions table. Hub renders the UI and delivers the signed decision to your handler, which writes it to your database. Hub never touches your database and stores no approval data of its own. Delivery uses the same environment-specific transport as connect flows — browser redirect in development, signed POST in production.

Configuration

With hub configured, blocked calls include a hosted approval URL with no extra setup:
corsair.ts
export const corsair = createCorsair({
    plugins: [
        github({
            permissions: {
                mode: "cautious",
                overrides: { "repositories.delete": "deny" },
            },
        }),
    ],
    database: db,
    kek: process.env.CORSAIR_KEK!,
    permissions: {
        timeout: "1h",
        onTimeout: "deny",
        mode: "asynchronous",
    },
    hub: {
        projectApiKey: process.env.CORSAIR_API_KEY!,
        signingSecret: process.env.CORSAIR_SIGNING_SECRET!,
    },
});
Compare with manual mode, where you provide the review surface yourself:
corsair.ts
manual: {
    approvalBaseUrl: `${appUrl}/approve`,
    onApprovalRequired: ({ approvalUrl }) =>
        `Approval required. Visit ${approvalUrl} then retry.`,
},
Approvals require the corsair_permissions table in both modes. Hub hosts the UI, not the data. See Permissions for the migration.

Mixing modes

Connect through Hub while keeping approvals manual, or the reverse. The two surfaces are independent: pass both blocks and Hub handles connect while your own page handles approvals.
corsair.ts
export const corsair = createCorsair({
    multiTenancy: false,
    database: db,
    kek: process.env.CORSAIR_KEK!,
    permissions: {
        timeout: "10m",
        onTimeout: "deny",
    },
    // Hub hosts the connect surface
    hub: {
        projectApiKey: process.env.CORSAIR_API_KEY!,
        signingSecret: process.env.CORSAIR_SIGNING_SECRET!,
    },
    // Approvals stay on your own page
    manual: {
        // Corsair appends the token: ${appUrl}/permissions/[token]
        approvalBaseUrl: `${appUrl}/permissions`,
        onApprovalRequired: ({ approvalUrl }) =>
            `Send the user to ${approvalUrl} to approve this permission.`,
    },
    plugins: [github({ authType: "managed" })],
});
approvalBaseUrl is the only manual field you need for this; Corsair turns it into a per-request /[token] URL and surfaces it through onApprovalRequired. Connect still routes through Hub because the hub block is present.

What’s next

Permissions

Policies, modes, overrides, and the full approval lifecycle.

Manual or Hub

What each mode asks you to build.

Hub overview

The relay model and the surfaces Hub hosts.

MCP Adapters

How approvals gate agent tool calls.