Skip to main content

What approval flows solve

Some agent actions are too sensitive to run without a human saying yes first. File deletions, financial transfers, permission escalations, these benefit from a checkpoint before execution. KavachOS supports this with a permission constraint called requireApproval. When the permission engine sees it, authorization is denied with the reason "This action requires human approval before execution". Your application catches that signal, creates an approval request, notifies a human, and retries the action after a response comes back. The flow is async. The agent does not block waiting. The human can respond minutes or hours later, within the request’s TTL.
KavachOS creates the approval request and persists it. Delivering the notification to the human, email, Slack, push notification, is your application’s job. Use webhookUrl or onApprovalNeeded to hook into your existing notification stack.

How the flow works

1
Agent triggers approvalThe agent calls an action protected by requireApproval: true. Authorization is denied. Your application detects the denial reason and calls kavach.approval.request().
2
Human gets notifiedKavachOS fires your webhookUrl or onApprovalNeeded handler with the request details. Your app sends an email, opens a Slack DM, or surfaces a notification in your dashboard.
3
Human approves or deniesThe human clicks a button in your UI. Your UI calls your backend, which calls kavach.approval.approve(requestId) or kavach.approval.deny(requestId).
4
Agent retriesOnce approved, your application retries the original action. For maximum simplicity, pass the request ID back to the agent so it can poll or be woken up when a decision arrives.

ApprovalRequest fields

id
string
Unique identifier prefixed apr_.
agentId
string
The agent requesting approval.
userId
string
The user who owns the agent and should receive the notification.
action
string
The action the agent wants to perform.
resource
string
The resource the action targets.
arguments
The arguments the agent passed at the time of the call.
status
'pending' | 'approved' | 'denied' | 'expired'
Current state. Transitions are one-way.
expiresAt
Date
When the request expires if no response is received. Default TTL is 5 minutes.
respondedAt
Date | undefined
When the human responded.
respondedBy
string | undefined
Identifier of the person who approved or denied.
createdAt
Date
When the request was created.

Configuration

Pass approval config to createKavach:
const kavach = await createKavach({
  database: { provider: 'sqlite', url: 'kavach.db' },
  approval: {
    ttl: 600,                                          // 10-minute window (seconds)
    webhookUrl: 'https://your-app.com/webhooks/approval',
  },
});
Or use a custom handler for full control over delivery:
const kavach = await createKavach({
  database: { provider: 'sqlite', url: 'kavach.db' },
  approval: {
    ttl: 300,
    onApprovalNeeded: async (request) => {
      await sendSlackDm(request.userId, {
        text: `Agent ${request.agentId} wants to ${request.action} on ${request.resource}.`,
        approveUrl: `https://your-app.com/approvals/${request.id}/approve`,
        denyUrl: `https://your-app.com/approvals/${request.id}/deny`,
      });
    },
  },
});
Both webhookUrl and onApprovalNeeded fire asynchronously so the request() call is not delayed by notification latency.

How webhooks work

When webhookUrl is set, KavachOS sends a POST to that URL with a JSON body:
{
  "event": "approval_needed",
  "request": {
    "id": "apr_...",
    "agentId": "agt_...",
    "userId": "user-123",
    "action": "delete",
    "resource": "file:prod-data/*",
    "arguments": { "path": "/prod/dataset.csv" },
    "status": "pending",
    "expiresAt": "2026-03-21T10:15:00.000Z",
    "createdAt": "2026-03-21T10:10:00.000Z"
  }
}
Webhook delivery failures are non-fatal. The request is already persisted, your app can poll listPending() as a fallback.

Code examples

Set up a permission that requires approval

const agent = await kavach.agent.create({
  ownerId: 'user-123',
  name: 'file-manager',
  type: 'autonomous',
  permissions: [
    {
      resource: 'file:prod-data/*',
      actions: ['read'],
    },
    {
      resource: 'file:prod-data/*',
      actions: ['delete'],
      constraints: { requireApproval: true },
    },
  ],
});

Catch the denial and create a request

const result = await kavach.authorize(agent.id, {
  action: 'delete',
  resource: 'file:prod-data/dataset.csv',
  arguments: { path: '/prod/dataset.csv' },
});

if (!result.allowed && result.reason?.includes('requires human approval')) {
  const approvalRequest = await kavach.approval.request({
    agentId: agent.id,
    userId: agent.ownerId,
    action: 'delete',
    resource: 'file:prod-data/dataset.csv',
    arguments: { path: '/prod/dataset.csv' },
  });

  return { pending: true, approvalId: approvalRequest.id };
}

Approve or deny from your UI handler

// In your API route handler
app.post('/approvals/:id/approve', async (req, res) => {
  const updated = await kavach.approval.approve(req.params.id, req.user.email);
  res.json({ status: updated.status });
});

app.post('/approvals/:id/deny', async (req, res) => {
  const updated = await kavach.approval.deny(req.params.id, req.user.email);
  res.json({ status: updated.status });
});

List pending requests for a user

// All pending approvals across all users
const all = await kavach.approval.listPending();

// Pending approvals for a specific user
const forUser = await kavach.approval.listPending('user-123');

console.log(`${forUser.length} approvals waiting on user-123`);

Expire stale requests

Requests that exceed their TTL are still stored with status: 'pending' until you run cleanup. Call this from a cron job:
const result = await kavach.approval.cleanup();
console.log(`Expired ${result.expired} stale approval requests`);

Check the status before retrying

const request = await kavach.approval.get('apr_...');

if (request?.status === 'approved') {
  // Safe to retry the action
  await deleteFile(agent.id, '/prod/dataset.csv');
} else if (request?.status === 'denied') {
  console.log('Human denied the request.');
} else if (request?.status === 'expired') {
  console.log('Request expired without a response.');
}

Next steps

Permissions

Add requireApproval constraints to individual permissions.

Trust scoring

Use trust levels to decide which agents need approval.

Audit trail

Every approval decision is linked to an audit entry.