Skip to content

feat: Human-in-the-Loop approval workflows #12

@iliassjabali

Description

@iliassjabali

Summary

Full implementation of the humanInTheLoop schema section. When approvalRequired: before-destructive-tool, the SDK intercepts tool calls and sends approval requests via webhook before proceeding.

The schema already declares this feature — webhookUrl, timeoutSeconds, timeoutAction are all in manifest.schema.ts. Users reading the docs expect it to work.

Schema (already exists, needs runtime implementation)

humanInTheLoop:
  enabled: true
  approvalRequired:
    - before-destructive-tool
    - before-external-call
  webhookUrl: $secret:APPROVAL_WEBHOOK_URL
  timeoutSeconds: 300
  timeoutAction: reject   # reject | proceed | escalate

Proposed implementation

AgentSpecReporter.approvalGate(toolName, input)

New public method on the reporter. Agents call this before executing any destructive tool:

const decision = await reporter.approvalGate('delete_files', { paths: ['/data'] })
if (decision === 'rejected') throw new Error('Action rejected by approver')

Internally it:

  1. Checks whether toolName matches any approvalRequired trigger (e.g. destructiveHint: truebefore-destructive-tool)
  2. POSTs to the configured webhookUrl
  3. Awaits callback or times out → applies timeoutAction

New module: packages/sdk/src/agent/approval/

File Purpose
index.ts approvalGate() orchestrator — dispatches to backend, awaits result
webhook.ts POST to spec.humanInTheLoop.webhookUrl, polls for signed callback token
console.ts Logs to stdout (dev/test mode — prompts for manual input)
types.ts ApprovalRequest, ApprovalDecision, ApprovalBackend interface

The webhook contract is intentionally generic — callers can wire it to Slack, email, PagerDuty, or anything else on their end. The SDK stays dependency-free.

Webhook contract

Outbound POST to webhookUrl:

{
  "requestId": "uuid",
  "toolName": "delete_files",
  "input": { "paths": ["/data"] },
  "timeoutAt": "2025-01-01T00:05:00Z"
}

Expected callback (POST back to SDK-provided callbackUrl):

{
  "requestId": "uuid",
  "decision": "approved" | "rejected",
  "token": "<signed>"
}

Timeout handling

After timeoutSeconds:

  • reject (default) — throws, tool is blocked
  • proceed — allows the tool to run, logs the timeout
  • escalate — posts to a secondary escalation webhook

Files to modify

  • packages/sdk/src/agent/reporter.ts — add approvalGate() public method
  • packages/sdk/src/agent/approval/ — new directory

Acceptance criteria

  • reporter.approvalGate(toolName, input) resolves 'approved' | 'rejected'
  • Webhook backend POSTs request and polls for signed callback token
  • Console backend works for local dev (no external deps)
  • Timeout fires after timeoutSeconds and applies timeoutAction
  • All backends are zero-cost when humanInTheLoop.enabled: false
  • Unit tests with mocked HTTP for the webhook backend
  • Docs updated with usage example and sample webhook server

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions