Approval

Human oversight for agent actions

Agents can request human approval before running a tool.

Diagram

Approval Flow

approve

deny

🤖 Agent

⚠️ Request

👤 Human

✅ Execute

❌ Block

Quick Start

  1. Simple Usage

    Set approval: true to gate every tool behind an interactive y/n prompt on the terminal.

    typescript
    import { Agent } from 'praisonai';
    
    const agent = new Agent({
      instructions: 'You can execute commands',
      approval: true  // gate every tool call behind a CLI prompt
    });
    
    await agent.chat('Delete the temp files');
    // → 🔐 Tool "delete_files" requires approval.
    //      Input: { "path": "/tmp" }
    //
    //    Approve? (y/n):
  2. With an ApprovalManager

    Pass an ApprovalManager to add auto-approve / auto-deny rules and a custom handler.

    typescript
    import { Agent, ApprovalManager } from 'praisonai';
    
    const approver = new ApprovalManager();
    approver.addAutoApprove(/^(read|list|search)_/);  // safe patterns
    approver.addAutoDeny(/^delete_/);                 // dangerous patterns
    
    const agent = new Agent({
      instructions: 'You can execute commands',
      approval: approver
    });

User Interaction Flow

Diagram
ToolAgentUserToolAgentUser“Delete old files”Detect sensitive action“Approve delete_files?”“Yes”Execute“Deleted 5 files”

How approval: true behaves

approval: true builds an ApprovalManager for you and wires createCLIApprovalPrompt() as its handler, so the default is a blocking y/n prompt on stdin.

Without a handler, requestApproval() waits on respond() — which nobody calls in a plain CLI session — so it would otherwise stall until the 5-minute default-deny timeout. Wiring the prompt is what keeps the default usable.


Correlation with toolInvocationId

The gate passes the LLM’s tool_call.id as toolInvocationId on every approval request. Handlers and out-of-band UIs use it to correlate a decision back to the exact pending call — never positionally. This keeps multiple in-flight tool calls disambiguated.

typescript
approver.onApprovalRequest(async (request) => {
  // request.toolInvocationId === the LLM's tool_call.id
  return await askUser(request.toolName, request.input);
});

Custom Handler

Register handlers with onApprovalRequest. The first handler to resolve true approves; if all resolve false, the call is denied.

typescript
import { Agent, ApprovalManager } from 'praisonai';

const approver = new ApprovalManager({ defaultTimeout: 60_000 });

approver.onApprovalRequest(async (request) => {
  // request: { requestId, toolInvocationId, toolName, input, timestamp, reason? }
  return await askUserViaSlack(request);
});

approver.addAutoApprove(/^(read|list|search)_/);  // auto-approve safe reads
approver.addAutoDeny(/^delete_/);                 // auto-deny deletions

const agent = new Agent({
  instructions: 'You can execute commands',
  approval: approver
});

Auto-deny is checked first (safety), then auto-approve, then handlers.


Configuration Options

ApprovalManager constructor options:

OptionTypeDefaultDescription
defaultTimeoutnumber (ms)300000 (5 min)Time to wait for a decision before default-denying.

ApprovalManager instance methods:

MethodSignatureDescription
onApprovalRequest(handler: (req) => Promise<boolean>) => voidRegister a handler. First to resolve true approves.
addAutoApprove`(toolName: string \RegExp, inputPattern?) => void`Auto-approve matching calls. Checked after auto-deny.
addAutoDeny`(toolName: string \RegExp, inputPattern?) => void`Auto-deny matching calls. Checked first (safety).
requestApproval({ toolInvocationId?, toolName, input, reason?, timeout? }) => Promise<boolean>The gate itself.
respond(response) => voidAnswer a pending request from an out-of-band UI.
getPendingRequests() => ToolApprovalRequest[]List requests awaiting a decision.
cancel(requestId: string) => voidCancel one pending request (resolves denied).
cancelAll() => voidCancel all pending requests.

Advanced

Per-tool wrapper: withApproval

Wrap a single tool so it gates itself. Throws ToolApprovalDeniedError on deny unless you provide onDenied.

typescript
import { withApproval } from 'praisonai';
import { promises as fs } from 'fs';

const deleteFile = withApproval({
  name: 'deleteFile',
  needsApproval: true,
  execute: async (args: { path: string }) => {
    await fs.unlink(args.path);
    return { success: true };
  },
  onDenied: () => ({ success: false })  // optional fallback instead of throwing
});

Global manager: getApprovalManager / setApprovalManager

withApproval uses a process-global manager when none is passed. Configure it once.

typescript
import { getApprovalManager, setApprovalManager, ApprovalManager, createCLIApprovalPrompt } from 'praisonai';

const manager = new ApprovalManager();
manager.onApprovalRequest(createCLIApprovalPrompt());
setApprovalManager(manager);

// later, anywhere in the process
const shared = getApprovalManager();

Dangerous-pattern helpers

Pre-built regexes and checkers flag risky inputs so you can require approval only when needed.

typescript
import { isDangerous, createDangerousPatternChecker, DANGEROUS_PATTERNS } from 'praisonai';

isDangerous('rm -rf /');  // → true

const needsApproval = createDangerousPatternChecker([/\bwipe\b/i]);

DANGEROUS_PATTERNS exposes fileDelete, dbDestructive, shellDangerous, and networkSensitive.

Events

ApprovalManager is an event emitter — subscribe with .on(...) to drive an out-of-band UI (e.g. a web dashboard that answers via respond()).

EventFired when
approval-requestA call awaits a decision.
approval-responserespond() resolves a request.
auto-approvedAn addAutoApprove rule matched.
auto-deniedAn addAutoDeny rule matched.
timeoutNo decision arrived before defaultTimeout.

Errors

ErrorThrown when
ToolApprovalDeniedErrorA withApproval tool is denied and no onDenied is set.
ToolApprovalTimeoutErrorAn approval request exceeds its timeout.

Approval Backends (Python parity)

Swap the default y/n prompt for a richer backend that matches praisonaiagents.approval — auto-approve, a diff-aware console, an LLM reviewer, or a legacy callback.

Diagram

Backend Flow

🤖 Agent

📋 Request

⚙️ Backend

✅ Decision

An Agent with approval: true builds an ApprovalManager; setApprovalBackend answers its requests.

typescript
import { Agent, setApprovalBackend, AutoApproveBackend } from 'praisonai';

// Trusted CI: approve every tool call automatically.
setApprovalBackend(new AutoApproveBackend());

const agent = new Agent({
  instructions: 'You can execute commands',
  approval: true  // creates the ApprovalManager; the backend above answers it
});

await agent.chat('Clean up the temp directory');

Pass a specific ApprovalManager to install the backend on it instead of the process-global one.

typescript
import { Agent, ApprovalManager, setApprovalBackend, ConsoleBackend } from 'praisonai';

const manager = new ApprovalManager();
setApprovalBackend(new ConsoleBackend(), manager);  // diffs + once/session/always

const agent = new Agent({
  instructions: 'Refactor files carefully',
  approval: manager
});

await agent.chat('Rename all .js files to .ts');

Which backend?

Diagram

Trusted CI / bot

Human at a terminal

Let an LLM decide

Have an old callback

Pick a backend

🟢 AutoApproveBackend
approve everything

🔵 ConsoleBackend
diffs + scopes

🟠 AgentApproval
reviewer agent

🟣 CallbackBackend
wrap a function

AutoApproveBackend

Approves every call — use in bots, CI, or trusted unattended runs.

typescript
import { setApprovalBackend, AutoApproveBackend } from 'praisonai';

setApprovalBackend(new AutoApproveBackend());

ConsoleBackend

Prompts on the terminal with a coloured unified diff and once/session/always scopes.

typescript
import { setApprovalBackend, ConsoleBackend } from 'praisonai';

setApprovalBackend(new ConsoleBackend());
// turn colours off for logs / non-TTY:
setApprovalBackend(new ConsoleBackend({ colors: false }));
Diagram
ConsoleBackendApprovalManagerAgentUserConsoleBackendApprovalManagerAgentUseralt[once][this session][always][deny & redirect]“Rename all .js files”requestApproval(edit_file)requestApproval(request + diff)Show diff + [o/s/a/n/d]oapproved (scope=once)sapproved (scope=session)aapproved (scope=always, scopePattern)d → “edit only .ts files”denied (feedback)boolean

What the user sees:

────────────────────────────────────────────────────────────
🔒 Tool Approval Required
────────────────────────────────────────────────────────────
Function: edit_file
Risk Level: HIGH
Agent: refactor-bot
Diff:
--- old
+++ new
@@ ...

────────────────────────────────────────────────────────────
Allow edit_file?  [o] once   [s] this session   [a] always (edit:/path/to/file)   [n] no   [d] deny & redirect
Choice [o/s/a/n/d] (n):
  • y/yes is accepted as once.
  • Empty input defaults to n (deny).
  • d prompts for free-text guidance returned on decision.feedback.
  • Diffs render in ANSI unless NO_COLOR is set or output is not a TTY — override with new ConsoleBackend({ colors: false }).
OptionTypeDefaultDescription
inputNodeJS.ReadableStreamprocess.stdinInput stream for the default async reader.
outputNodeJS.WritableStreamprocess.stdoutOutput stream for prompts.
reader(q: string) => Promise<string>built-in readlineInjected async reader (useful in tests).
syncReader(q: string) => stringblocking fs.readSync on stdinInjected sync reader; required by requestApprovalSync when input is not process.stdin.
colorsbooleanisTTY && !NO_COLORForce ANSI colours on/off.

AgentApproval

Delegates the decision to another AI agent that replies APPROVE or DENY.

typescript
import { Agent, setApprovalBackend, AgentApproval } from 'praisonai';

const reviewer = new Agent({
  name: 'strict-reviewer',
  instructions: 'Only approve file reads. Deny everything else. Reply APPROVE or DENY.'
});

setApprovalBackend(new AgentApproval(reviewer));          // custom reviewer
// or let it create a default reviewer agent lazily:
setApprovalBackend(new AgentApproval(null, 'gpt-4o-mini'));

CallbackBackend

Wraps a legacy (toolName, arguments, riskLevel) => decision | boolean callback.

typescript
import { setApprovalBackend, CallbackBackend } from 'praisonai';

setApprovalBackend(new CallbackBackend((toolName, args, riskLevel) => {
  return toolName.startsWith('read_');  // boolean, decision object, or Promise
}));

Build your own backend

Implement ApprovalBackend — requestApproval (async) and requestApprovalSync. Use the approvalRequest / approvalDecision builders for Python-shaped defaults and suggestScopePattern for a reusable “always” pattern.

typescript
import { setApprovalBackend, approvalDecision, suggestScopePattern } from 'praisonai';
import type { ApprovalBackend, ApprovalBackendRequest } from 'praisonai';

const myBackend: ApprovalBackend = {
  async requestApproval(request: ApprovalBackendRequest) {
    const ok = request.riskLevel !== 'critical';
    return approvalDecision({
      approved: ok,
      approver: 'policy',
      scope: ok ? 'always' : 'once',
      scopePattern: ok ? suggestScopePattern(request) : null
    });
  },
  requestApprovalSync(request: ApprovalBackendRequest) {
    return approvalDecision({ approved: request.riskLevel !== 'critical', approver: 'policy' });
  }
};

setApprovalBackend(myBackend);

Wiring functions

setApprovalBackend installs a backend; getApprovalBackend reads it back.

typescript
import { setApprovalBackend, getApprovalBackend, AutoApproveBackend, ApprovalManager } from 'praisonai';

const manager = new ApprovalManager();
setApprovalBackend(new AutoApproveBackend(), manager, { riskLevel: 'high', agentName: 'ci-bot' });

getApprovalBackend(manager);  // → the AutoApproveBackend instance

Adapt a backend into a plain ApprovalHandler, or convert a manager request into the Python shape, directly.

typescript
import { approvalHandlerFromBackend, toApprovalBackendRequest, ConsoleBackend, ApprovalManager } from 'praisonai';

const manager = new ApprovalManager();
manager.onApprovalRequest(approvalHandlerFromBackend(new ConsoleBackend()));

// inside a handler, reshape a manager request:
manager.onApprovalRequest(async (req) => {
  const backendRequest = toApprovalBackendRequest(req, { riskLevel: 'high' });
  return backendRequest.riskLevel !== 'critical';
});

approvalRequest and approvalDecision build the Python-shaped objects with dataclass defaults (approvalId auto-fills like uuid4().hex).

typescript
import { approvalRequest, approvalDecision } from 'praisonai';

const request = approvalRequest({ toolName: 'edit_file', arguments: { path: 'a.ts' }, riskLevel: 'high' });
const decision = approvalDecision({ approved: true, scope: 'session' });

Permission-target helpers

Turn a tool call into a reusable permission target or glob for session/always scopes.

HelperInputOutput
buildPermissionTarget('execute_command', { command: 'ls -la' })shell toolbash:ls -la
buildPermissionTarget('edit_file', { path: '/x.ts' })file tooledit:/x.ts
derivePattern('bash:git status -s')shell targetbash:git status *
commandPrefix(['git', 'status', '-s'])tokensgit status
suggestScopePattern(request)a requestbest-effort always pattern

ARITY maps commands (git, npm, docker, …) to the number of leading tokens commandPrefix keeps.

typescript
import { buildPermissionTarget, derivePattern, commandPrefix, suggestScopePattern, ARITY, approvalRequest } from 'praisonai';

buildPermissionTarget('execute_command', { command: 'ls -la' });  // 'bash:ls -la'
derivePattern('bash:git status -s');                              // 'bash:git status *'
commandPrefix(['npm', 'run', 'build']);                           // 'npm run'
suggestScopePattern(approvalRequest({ toolName: 'execute_command', arguments: { command: 'git push' }, riskLevel: 'high' }));
ARITY.git;  // 2

API surface

Everything exported from the approval module — all import flat from praisonai.

KindExports
ClassesAutoApproveBackend, ConsoleBackend, AgentApproval, CallbackBackend
FunctionssetApprovalBackend, getApprovalBackend, approvalHandlerFromBackend, toApprovalBackendRequest, approvalRequest, approvalDecision, buildPermissionTarget, derivePattern, commandPrefix, suggestScopePattern
ConstantsARITY
TypesApprovalBackend, ApprovalBackendRequest, ApprovalBackendDecision, ApprovalRequestInit, ApprovalDecisionInit, ApprovalScope, ConsoleBackendOptions, ConsolePromptResult, BackendHandlerOptions, ApproverAgentLike, LegacyApprovalCallback

ApprovalBackendRequest — what a custom backend receives:

FieldTypeDescription
toolNamestringTool requesting approval.
argumentsRecord<string, unknown>Arguments the tool will be called with.
riskLevelstringcritical / high / medium / low.
agentName`string \null`Triggering agent.
sessionId`string \null`Session tracker.
contextRecord<string, unknown>Arbitrary context — e.g. a rendered diff.
approvalIdstringStable correlation id (auto uuid4().hex via approvalRequest).
authorizedReviewers`string[] \null`Reviewer identities allowed to resolve.
liveness`(() => boolean) \null`Predicate that returns true while the originating turn is live.

ApprovalBackendDecision — what a backend returns:

FieldTypeDefaultDescription
approvedboolean—Final verdict.
reasonstring""Human-readable justification.
modifiedArgsRecord<string, unknown>{}Optional replacement arguments.
approver`string \null`nullWho/what approved (user, system, console, agent, …).
metadataRecord<string, unknown>{}Backend-specific metadata.
scope`’once’ \‘session’ \‘always’`'once'How long to remember the decision.
scopePattern`string \null`nullReusable target/pattern for session/always scopes.
feedback`string \null`nullFree-text guidance when denying with redirect.

BackendHandlerOptions — passed to setApprovalBackend / approvalHandlerFromBackend:

OptionTypeDefaultDescription
riskLevelstring"medium"Risk stamped on requests from the manager (which carries none).
agentName`string \null`nullAgent name stamped on requests.

Best Practices

Auto-approve safe actions

Reading data is usually safe. approver.addAutoApprove(/^(read|list|search)_/) skips the prompt for read-only tools.

Auto-deny dangerous patterns

Auto-deny is checked first. Block deletions and destructive commands with approver.addAutoDeny(/^delete_/).

Set reasonable timeouts

The default is 5 minutes and default-denies on timeout. Lower it for interactive flows: new ApprovalManager({ defaultTimeout: 60_000 }).

Correlate with toolInvocationId

In UIs with concurrent calls, always route decisions by request.toolInvocationId, never by order — positional matching authorises the wrong call.



Related