@confirm/sdk
v0.1.2
Published
Human-in-the-loop approvals for AI agents. Pause on risky actions, get a human decision, resume.
Maintainers
Readme
@confirm/sdk
Human-in-the-loop approvals for AI agents. Your agent pauses before an irreversible action (refund, delete, deploy, send, spend), a human approves or edits it, and the call resumes with the approved payload. Every decision is logged.
npm i @confirm/sdkGet an API key at confirm.dev and set CONFIRM_API_KEY.
Guard your whole tool layer (recommended)
Don't hand-wrap every risky tool. Wrap the toolset once and let policies decide what needs a human. With default: "require" it's fail-closed: a tool nobody wrote a rule for is still gated, so nothing slips through unwrapped.
import { guard } from "@confirm/sdk";
const tools = guard(agentTools, {
policies: [
{ when: (t) => t.name.startsWith("delete_"), notify: "[email protected]" },
{ when: (t) => t.name === "refund" && t.args.amount > 500, notify: "[email protected]" },
{ when: (t) => t.name === "send_email" && isExternal(t.args.to), notify: "[email protected]" },
{ when: (t) => t.name === "search", decision: "allow" }, // read-only, never gated
],
default: "require", // fail-closed
});
// tools.refund(...) now pauses for a human when the policy matches.Policies are evaluated in order; the first match wins. A matched policy gates the call by default (decision: "require"); use decision: "allow" to exempt safe tools. When a human edits the payload, the tool runs with the edited version.
Why policies, not per-tool wrapping? The model is the thing you are guarding. Deterministic policies are the security floor: they can't be forgotten or prompt-injected away. Use agent skills / MCP on top for long-tail coverage, but the boundary stays deterministic.
Gate a single function
import { withApproval } from "@confirm/sdk";
const safeRefund = withApproval(refund, {
notify: "[email protected]",
summary: (a) => `Refund $${a.amount} to ${a.customerId}`,
reasoning: (a) => a.reason,
});
await safeRefund({ amount: 10000, customerId: "4821", reason: "double charge" });
// pauses -> human approves/edits -> runs with the approved payload
// throws ApprovalRejectedError / ApprovalExpiredError otherwiseBy default the call arguments are the payload the approver sees and edits, so edits flow straight back into the call. Set a custom payload (and applyEdits) if your action shape differs from your function signature.
Low-level client
import { ConfirmClient } from "@confirm/sdk";
const confirm = new ConfirmClient(); // reads CONFIRM_API_KEY
const req = await confirm.requests.create({
summary: "Deploy api@sha 9f2a1c to production",
notify: "group:platform", // escalate to an approver group
payload: { service: "api", sha: "9f2a1c" },
agentState: { threadId, step: 7 }, // rehydrate the agent from the webhook later
ttlMinutes: 60,
});
const resolved = await confirm.requests.wait(req.id, { pollIntervalMs: 3000, timeoutMs: 3_600_000 });
if (resolved.status === "APPROVED") execute(resolved.effectivePayload);Prefer webhooks over polling for long waits: let the agent yield and resume when the signed webhook arrives.
Verify webhooks
import { constructEvent } from "@confirm/sdk";
// In your webhook route, read the RAW body (do not re-serialize).
const event = constructEvent({
payload: rawBody,
signature: req.headers["x-confirm-signature"],
secret: process.env.CONFIRM_WEBHOOK_SECRET,
});
if (event.event === "request.approved") {
execute(event.data.effectivePayload); // the human-edited action
}constructEvent verifies the HMAC-SHA256 signature in constant time and rejects stale timestamps (5-minute tolerance by default), throwing WebhookVerificationError on any mismatch.
API
new ConfirmClient(apiKey | { apiKey, baseUrl, fetch })client.requests.create(input)/.get(id)/.wait(id, opts)/.createAndWait(input, opts)guard(tools, { policies, default, notify, client, wait })withApproval(fn, { notify, summary, payload, applyEdits, ... })constructEvent(opts)/verifyWebhookSignature(opts)- Errors:
ConfirmApiError,ApprovalRejectedError,ApprovalExpiredError,ApprovalTimeoutError,WebhookVerificationError
Requires Node 18+ (global fetch). ESM. Docs: confirm.dev/docs.
