@ekaone/approval
v0.0.2
Published
Human-in-the-loop policy engine and surface adapters
Maintainers
Readme
@ekaone/approval
What is it?
@ekaone/approval is a human-in-the-loop policy engine for autonomous agents. It lets you declare rules that intercept agent tasks before they execute and route them to a human (or automated) decision surface — terminal prompt, webhook, or a custom adapter.
Why it exists
Fully autonomous agents that touch real systems (file writes, API calls, database mutations) have no natural checkpoint for human authority. This package fills that gap. You define which tasks require approval, where the approval request is sent, and what happens if no response arrives in time. Everything else is handled for you.
Installation
As a library
npm install @ekaone/approval
# or
pnpm add @ekaone/approval
# or
yarn add @ekaone/approvalRequires Node >= 18 and TypeScript >= 5 (strict mode supported).
As a global CLI
npm install -g @ekaone/approval
# or
pnpm add -g @ekaone/approvalOr run without installing:
npx @ekaone/approval --helpCore concepts
| Concept | Description |
|---|---|
| Task | The unit of work an agent wants to execute. Has an id, optional tags, description, and payload. |
| Policy | A predicate (task) => boolean that decides which tasks need approval. |
| Surface | Where the approval request is sent — CLI terminal, webhook URL, or a custom adapter. |
| Fallback | The automatic decision (approve / reject / escalate) taken when the surface does not respond within timeout ms. |
API
createApproval(config)
Creates and returns an approval instance.
import { createApproval } from '@ekaone/approval'
const approval = createApproval(config)config options
| Option | Type | Default | Description |
|---|---|---|---|
| surface | 'cli' \| 'webhook' \| 'noop' \| ApprovalSurface | 'cli' | Where approval requests are sent. |
| timeout | number | 30_000 | Milliseconds before fallback fires. |
| fallback | 'approve' \| 'reject' \| 'escalate' | 'reject' | Decision taken on timeout. |
| policy | (task) => boolean \| ApprovalPolicyRule[] | () => true | Single predicate or ordered rule list. |
| webhookUrl | string | — | Required when surface is 'webhook'. |
| webhookSecret | string | — | Sent as X-Approval-Secret header. |
Returns: ApprovalInstance
| Method | Signature | Description |
|---|---|---|
| request | (task: Task) => Promise<ApprovalResponse> | Runs the policy check and, if matched, waits for a decision. |
| addPolicy | (rule: ApprovalPolicyRule) => void | Appends a rule to the end of the policy list at runtime. |
ApprovalResponse
type ApprovalResponse = {
decision: 'approve' | 'reject' | 'escalate'
reason?: string
approvedBy?: string
timestamp: number
}Built-in surfaces
'cli' (default)
Prints task details to stdout and reads a decision from stdin. Good for local development and scripts.
--- Approval Required ---
Task ID : deploy-001
Description: Deploy v2.3.1 to production
Tags : destructive, deploy
Decision [approve/reject/escalate] (default: reject):'noop'
Auto-resolves with a preset decision. Intended for tests and CI — requires an explicit opt-in call.
import { createNoopSurface } from '@ekaone/approval'
const surface = createNoopSurface('approve') // or 'reject' | 'escalate''webhook'
POSTs the pending task as JSON to a URL and reads the decision from the synchronous HTTP response.
createApproval({
surface: 'webhook',
webhookUrl: 'https://your-service/approve',
webhookSecret: process.env.APPROVAL_SECRET,
})Expected response body from your endpoint:
{
"decision": "approve",
"approvedBy": "[email protected]",
"reason": "looks good"
}Custom surface
Implement ApprovalSurface directly for Telegram bots, Slack, email, or any other channel:
import type { ApprovalSurface } from '@ekaone/approval'
const slackSurface: ApprovalSurface = {
async request(task) {
// send a Slack message, await user reaction...
return { decision: 'approve', approvedBy: 'bob', timestamp: Date.now() }
},
}ApprovalPolicyRule
Used when you need multiple rules with different surfaces or timeouts.
type ApprovalPolicyRule = {
match: (task: Task) => boolean
surface: ApprovalSurface
timeout: number
fallback: 'approve' | 'reject' | 'escalate'
}Rules are evaluated in order — the first match wins. Tasks that match no rule are auto-approved without interruption.
CLI
The package ships a standalone approval binary for sending approval requests directly from the terminal — useful for shell scripts, CI pipelines, or manual smoke-testing.
Syntax
approval [options]Options
| Flag | Type | Default | Description |
|---|---|---|---|
| --id | string | task-<timestamp> | Task ID |
| --tags | string | — | Comma-separated tags |
| --description | string | — | Human-readable task description |
| --timeout | number | 30000 | Milliseconds before fallback fires |
| --fallback | approve \| reject \| escalate | reject | Decision taken on timeout |
| -h, --help | — | — | Print help and exit |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Decision was approve |
| 1 | Decision was reject or escalate |
Output
The command always prints the full ApprovalResponse as JSON to stdout before exiting, regardless of the decision.
CLI examples
Gate a deploy with a 60-second window
approval \
--id deploy-prod-v2 \
--tags destructive,deploy \
--description "Deploy v2.3.1 to production" \
--timeout 60000 \
--fallback rejectTerminal prompt:
--- Approval Required ---
Task ID : deploy-prod-v2
Description: Deploy v2.3.1 to production
Tags : destructive, deploy
Decision [approve/reject/escalate] (default: reject): approveOutput on approve:
{
"decision": "approve",
"approvedBy": "cli-operator",
"timestamp": 1718000000000
}Exit code: 0
Use in a shell script
Integrate with any deployment script using the exit code:
#!/usr/bin/env bash
set -euo pipefail
echo "Requesting approval before migration..."
if approval \
--id "db-migrate-$(date +%s)" \
--tags high-risk,database \
--description "Run migration 042 on the users table" \
--timeout 120000 \
--fallback reject; then
echo "Approved — running migration"
pnpm db:migrate
else
echo "Rejected — migration cancelled"
exit 1
fiQuick smoke-test (no real prompt needed)
Use --timeout 1 with --fallback approve to confirm the binary is wired up without waiting for input:
approval --id smoke-test --timeout 1 --fallback approveOutput:
{
"decision": "approve",
"reason": "timeout",
"approvedBy": "system",
"timestamp": 1718000000001
}Examples
Example 1 — Gate destructive tasks via the CLI
An agent runs file operations. Any task tagged destructive stops and waits for a human to approve it in the terminal. Everything else runs uninterrupted. If no answer arrives within 30 seconds, the task is rejected automatically.
import { createApproval } from '@ekaone/approval'
const approval = createApproval({
surface: 'cli',
timeout: 30_000,
fallback: 'reject',
policy: (task) => task.tags?.includes('destructive') ?? false,
})
// Safe operation — no approval needed, runs immediately
const readResult = await approval.request({
id: 'read-config',
tags: ['read-only'],
description: 'Read application config',
})
console.log(readResult.decision) // 'approve' (no matching policy)
// Destructive operation — pauses for human input
const deployResult = await approval.request({
id: 'deploy-001',
tags: ['destructive', 'deploy'],
description: 'Deploy v2.3.1 to production',
payload: { version: '2.3.1', environment: 'production' },
})
if (deployResult.decision === 'approve') {
// proceed with deploy
} else {
console.log('Deploy rejected:', deployResult.reason)
}Example 2 — Multi-tier policy (ordered rules)
A production agent applies different approval surfaces depending on task severity. Low-risk tasks auto-approve, medium-risk go to a webhook, and high-risk tasks go to a stricter webhook with a shorter timeout.
import { createApproval, createNoopSurface, createWebhookSurface } from '@ekaone/approval'
const approval = createApproval({
policy: [
// High-risk: short window, strict webhook
{
match: (task) => task.tags?.includes('high-risk') ?? false,
surface: createWebhookSurface({
url: 'https://ops.internal/approve/critical',
secret: process.env.OPS_SECRET,
}),
timeout: 10_000,
fallback: 'reject',
},
// Medium-risk: standard webhook, 60s window
{
match: (task) => task.tags?.includes('medium-risk') ?? false,
surface: createWebhookSurface({
url: 'https://ops.internal/approve/standard',
secret: process.env.OPS_SECRET,
}),
timeout: 60_000,
fallback: 'reject',
},
// Low-risk: auto-approve, no human needed
{
match: (task) => task.tags?.includes('low-risk') ?? false,
surface: createNoopSurface('approve'),
timeout: 1_000,
fallback: 'approve',
},
// No match → tasks without a risk tag are blocked by default
],
})
const result = await approval.request({
id: 'db-migrate-042',
tags: ['high-risk', 'database'],
description: 'Run migration 042 on the users table',
})
console.log(result.decision) // 'approve' | 'reject' | 'escalate'
console.log(result.approvedBy) // who or what approved itExample 3 — Dynamic policy at runtime
Start with a conservative policy and unlock task types as the agent earns trust.
import { createApproval, createNoopSurface } from '@ekaone/approval'
const approval = createApproval({
surface: 'cli',
timeout: 30_000,
fallback: 'reject',
policy: (task) => task.tags?.includes('write') ?? false, // gate all writes
})
// Later, once a category is trusted, auto-approve it without restarting:
approval.addPolicy({
match: (task) => task.tags?.includes('write') && task.tags?.includes('trusted-source') || false,
surface: createNoopSurface('approve'),
timeout: 1_000,
fallback: 'approve',
})Types
type Task = {
id: string
tags?: string[]
description?: string
payload?: unknown
}
type ApprovalDecision = 'approve' | 'reject' | 'escalate'
type ApprovalResponse = {
decision: ApprovalDecision
reason?: string
approvedBy?: string
timestamp: number
}
type ApprovalSurface = {
request(task: PendingTask): Promise<ApprovalResponse>
}
type ApprovalPolicyRule = {
match: (task: Task) => boolean
surface: ApprovalSurface
timeout: number
fallback: ApprovalDecision
}
type ApprovalConfig = {
surface?: 'cli' | 'webhook' | 'noop' | ApprovalSurface
timeout?: number
fallback?: ApprovalDecision
policy?: ((task: Task) => boolean) | ApprovalPolicyRule[]
webhookUrl?: string
webhookSecret?: string
}License
MIT © Eka Prasetia
