Appearance
Approvals
Status: Implemented in v0.1 — v0.1 concepts and normative approval semantics. APIs shown are the as-built v0.1 surface.
An approval is a trusted decision required before a capability executes. It is a lifecycle, not a boolean: requested, pending, decided, expiring, consumed — each transition attributable and audited.
Two invariants define the design (ADR-006):
- Models cannot approve themselves (SI-4). A decision must originate from a trusted human, deterministic policy, or an external authorized system. Any flow where model output constitutes the approval is theater.
- Approval binds the exact operation (SI-5). The record ties capability id + requesting actor + a hash of the canonical validated input. Change anything → new approval. Execute once → consumed.
Lifecycle
text
requireApproval() / meta.approval.required
|
v
+----------------- PENDING ------------------+
| (expiresAt running) |
v v v
REJECTED APPROVED EXPIRED
| |
| v runtime.resume(id, { context })
| integrity checks: approved? unexpired?
| unconsumed? hash match? approver != requester?
| |
| v
| CONSUMED --> execution proceeds (stages 9-15)
|
+--> terminal; a new invocation creates a NEW request
(CANCELLED: application withdrew the request before decision)Distinct acts, deliberately separated:
- Approving an operation (coordinator
decide) records a decision. It executes nothing. - Executing an approved operation (
runtime.resume) re-enters the pipeline with integrity checks and fresh execution-phase policies. - Retrying an approved operation: runtime-managed retries inside the same execution are covered by the consumed record; a new invocation — even with identical input — requires a new approval.
- Changing input after approval is impossible by construction: the stored input is what executes, and its hash must match.
- Expiration bounds the window in which a stale "yes" can act.
Requesting approval
Two routes converge into identical requests at pipeline stage 8:
ts
// Static: every invocation gates
meta: { agent: { approval: { required: true, type: "human-confirmation" }, ... } }
// Conditional: policy-driven
requireApproval({ reason: `Refund of $${amount} exceeds $500`, approvalType: "manager", expiresInMs: 3_600_000 })The runtime creates the record, emits capability.approval_requested, and returns:
ts
const result = await runtime.invoke("orders.refund", input, { actor, context, surface: "aiSdk" });
// result.status === "approval-required"; result.approval.id, .reasons, .expiresAtWhat the requester (and the model, via adapters) sees is the pending record — never a way to decide it.
Deciding
Your application surfaces pending approvals to authorized approvers (dashboard, Slack, CLI — your choice) and records decisions through the coordinator:
ts
await runtime.approvals.decide(approvalId, {
status: "approved",
approver: { id: "u_manager_7", kind: "user", displayName: "Priya" },
comment: "Verified with customer",
});Who may approve is your application's authorization question — gate the decide endpoint like any privileged operation. The runtime enforces the structural rules: pending-only transitions, expiry, and approver ≠ requester (APPROVAL_SELF_APPROVAL; disable only via rejectSelfApproval: false with a documented reason).
Approval UIs display redact.approvalInput(input) when defined — approvers see what they need; the hash still binds the full input.
Resuming
ts
const final = await runtime.resume(approvalId, { context: freshContext });Resume runs as the original actor (identity is bound in the record; no re-attribution), with the approver recorded in context.agent.approval. The caller supplies fresh application context — live handles are not serializable, and stale context is exactly what execution-phase policies exist to catch.
Failure modes are precise: APPROVAL_PENDING, APPROVAL_REJECTED, APPROVAL_EXPIRED, APPROVAL_CONSUMED, APPROVAL_INPUT_MISMATCH, APPROVAL_SELF_APPROVAL (reference/errors.md).
Two operating modes
Inline handler — synchronous decision inside the same call. For CLI confirmation prompts, auto-approval rules, or delegating to an external system that responds within the request:
ts
approvals: {
handler: async (req) =>
req.types.includes("manager") && (await slackApprovalWithTimeout(req))
? { status: "approved", approver: SLACK_BRIDGE_ACTOR }
: { status: "rejected", approver: SLACK_BRIDGE_ACTOR },
}Coordinator + resume — the durable shape: invoke returns approval-required; the app persists, notifies, collects the decision, and later calls resume. v0.1 ships createInMemoryApprovalCoordinator (dev/test only — records die with the process); production implements ApprovalCoordinator over your database. The interface is the durability seam (ADR-007); workflow-engine-backed coordinators are the planned path for long-lived, restart-surviving approvals.
Input integrity mechanics
- Canonical serialization: JSON with lexicographically sorted object keys, UTF-8, no insignificant whitespace; SHA-256 over the bytes.
- Hashed value: the validated input (post parsing/defaults), the same value that will execute (SI-6).
- Approval-gated capabilities therefore require JSON-serializable validated input; otherwise the invocation fails with
APPROVAL_UNSERIALIZABLE_INPUTrather than gating on an unverifiable value. - On resume the stored input is re-hashed and compared (tamper check on the store), then re-validated against the current schema (deploys happen between request and decision).
Related
- Guide: human-approval.md (end-to-end wiring) · Reference: runtime.md
- Security: security-model.md · Example: customer-support-agent.md