Approvals
A treasury wire that needs a human signoff cannot be solved by holding an HTTP connection open for two hours. Veridex makes approvals a first-class runtime mechanic that integrates with the policy engine (Policy) and the checkpoint manager (Checkpoints).
Routes
| Route | Behaviour |
|---|---|
auto | Proceed without approval |
human_required | Single approver (any with permission) |
dual_approval | Two independent approvers; proposer cannot be one of them |
policy_pack | Delegate to a control-plane workflow (multi-tier chains, SLAs, on-call rotation) |
Set per-tool, per-rule, or per-tenant:
const transfer = tool({
name: 'transfer',
safetyClass: 'financial',
approval: { route: 'human_required', timeoutMs: 24 * 3600_000 },
// ...
});Or via a policy rule (overrides the tool default):
policyRule({
id: 'large-transfer-dual',
priority: 10,
evaluate(ctx) {
if (ctx.proposal.kind === 'tool'
&& ctx.proposal.contract.safetyClass === 'financial'
&& BigInt(ctx.proposal.arguments.amount) >= 10_000_00n) {
return { kind: 'escalate', route: 'dual_approval' };
}
return { kind: 'allow' };
},
});The suspend/resume flow
const run = await agent.run("Send $50,000 to acme.com");
if (run.status === 'suspended') {
console.log('Approval needed:', run.approvalId);
// Run is checkpointed. Process is free.
}
// …minutes/hours/days later, in any process:
const final = await agent.resume(run.runId, run.approvalId, {
decision: 'allow',
approver: 'cfo@example.com',
reason: 'Counterparty verified; quarterly invoice.',
});Internally:
- The policy rule emits
escalate→policy_decisionevent. ApprovalManager.requestcreates anApprovalRequestwith the content-hashed proposal envelope and emitsapproval_requested.CheckpointManager.savesnapshots the run (working memory, pending proposal, event-log position).- The run returns
status: "suspended"to the caller. - Approver decides via React inbox, REST API, CLI, or webhook.
agent.resumeloads the checkpoint, verifies the proposal hash still matches, emitsapproval_resolved, and continues.
Hash mismatch on resume aborts with proposal_mutation_detected and emits a security event — a hostile mutation between request and resume is impossible to hide.
Timeouts and escalation chains
approval: {
route: 'policy_pack',
pack: 'treasury-high-value',
// pack defines: tier1 (15 min) → tier2 (1 hr) → tier3 (4 hr) → auto-deny
},Control-plane SLA breach alerts notify approvers; metrics surface chronic timeouts.
React inbox
import { useApprovals } from '@veridex/agents-react';
function Inbox() {
const { pending, decide } = useApprovals({ scope: 'mine' });
return pending.map(req => (
<ApprovalCard key={req.id} request={req}
onAllow={(reason) => decide(req.id, { decision: 'allow', reason })}
onDeny={(reason) => decide(req.id, { decision: 'deny', reason })}
/>
));
}Each card shows the full proposal, the policy rule that triggered, the agent's reasoning chain (filtered), and the trace events leading up to the request.
End-to-end: treasury agent with HITL
import { createAgent } from '@veridex/agents';
import { treasuryPolicyPack, createTreasuryKit } from '@veridex/agents-treasury';
const kit = createTreasuryKit({
ceilings: inMemoryCeilings,
idempotency: inMemoryIdempotency,
timeLock,
sanctions: sanctionsScreener,
reputation: sbtProvider,
evidenceSigner,
portal: portalTelemetry,
dualApprovalAbove: { amountUsdMicro: 10_000_000_000n },
});
const agent = createAgent({
name: 'treasury-bot',
instructions: '...',
tools: kit.tools,
policies: kit.policyRules,
}, { modelProviders: { default: provider }, plugins: [kit.plugin] });
const run = await agent.run("Pay $50k to acme.com for invoice INV-1234");
// run.status === 'suspended' — dual approval neededTwo approvers later, agent.resume(...) runs the transfer through:
- Idempotency reservation (impossible to double-execute).
- Sanctions screening (denylist + allowlist).
- Time-lock (zero-delay here; configurable per-route).
- Executor (signs, broadcasts, waits for receipt).
- Ceiling commit (the spend is recorded).
- Evidence Bundle finalised — every event, verdict, approval, and chain receipt is canonicalised, content-hashed, and signed (Ed25519 or HMAC).
The bundle is submitted to the developer-portal trace store via the relayer; any auditor can verify it offline.