SDK tutorial · 8 of 10

Handle approvals

Approval is a product policy boundary, not merely a confirmation dialog. Heddle routes gated calls and records the decision; your host knows the user, tenant, trust model, and real side effects.

Customization depth

Level 4 · Host-owned policy

Hosting depth

In-process callback or hosted approval lifecycle

Documentation status

Supported SDK boundary

Assumptions

  • At least one tool or policy may request a decision before execution.
  • The host can identify an authenticated approver and the product resource being changed.
  • The product has an explicit deny behavior when no decision surface is available.
Heddle owns
  • Evaluate the configured policy chain and route requested decisions to the host callback.
  • Record approval-requested and approval-resolved activity in the turn lifecycle and trace.
  • Deny an approval-gated call when no handler is configured.
Your product owns
  • Authenticate the approver, authorize the target action, and render the decision context.
  • Define allow, deny, and request policies appropriate to the product's data and trust model.
  • Keep capabilities unavailable when the user or tenant should never be able to invoke them.

Mark a tool that requires a decision

TypeScript
import type { ToolDefinition } from '@roackb2/heddle'

const publishRecord: ToolDefinition = {
  name: 'publish_record',
  description: 'Publish a reviewed product record.',
  requiresApproval: true,
  parameters: {
    type: 'object',
    properties: {
      recordId: { type: 'string' },
    },
    required: ['recordId'],
    additionalProperties: false,
  },
  execute: async (input) => publishAuthorizedRecord(input),
}

requiresApproval asks the default policy chain to request a host decision. It does not replace authorization inside publishAuthorizedRecord.

The example intentionally omits an application-defined capability tag. If you add one, make the host policy interpret it explicitly; built-in read-only profiles only recognize Heddle's documented capability vocabulary.

Provide the approval surface

TypeScript
import type { ConversationEngineHost } from '@roackb2/heddle'

const host: ConversationEngineHost = {
  approvals: {
    async requestToolApproval(request) {
      const allowed = await productPolicy.canApprove({
        authenticatedUser,
        tool: request.call.tool,
        input: request.call.input,
      })

      if (!allowed) {
        return {
          approved: false,
          reason: 'The current user cannot approve this action.',
        }
      }

      return {
        approved: true,
        reason: 'Approved through product policy.',
      }
    },
  },
}

const result = await engine.turns.submit({
  sessionId,
  prompt,
  host,
})

The callback receives the requested call and the registered tool definition. Return a boolean decision and an operator-readable reason. Avoid exposing credentials or sensitive internal policy diagnostics in that reason.

Understand policy resolution

A ToolApprovalPolicy may allow, deny, request a human decision, or abstain. Policies run in declaration order. The first decisive allow or deny settles the call. A request is remembered while later policies are evaluated; it reaches the configured approval surface only if no later policy makes a decisive decision. If a request remains but no handler exists, Heddle denies the call. When every policy abstains, execution proceeds.

Use tool profiles and capability filters to remove tools that should never be visible. Use approval policy for context-dependent decisions. Enforce product authorization again inside the domain operation.

Remote approval flows

The callback above can await an in-process product decision. When a browser may disconnect while an approval is pending, use the hosted run service's pending-approval lifecycle instead of storing an unrelated promise or event bus in the route layer.

Verify both paths

Test approval and denial with deterministic policy inputs. Confirm that denied calls do not execute, both decisions appear in trace/activity, and a missing handler fails closed for a gated call.

Canonical sources