require_confirmation rule can flag a call to it that has no confirmation recorded by your app. confirmToolCall (TypeScript) and confirm_tool_call (Python) are how your app records one.
Record a confirmation
human_in_loop approval span (kind confirmation, outcome approved, actor role user) with the tool name and a hash of the arguments, never the arguments themselves, and puts an entry in a small in-memory ledger. The next tool-call check for that tool sends the matching entries.
The return value is
{ recorded, reason? }. A reason of no_context means the span was recorded but nothing was put in the ledger. This is what a top-level call returns when it runs outside any context (no withZespanContext / with_zespan_context, no session): wrap the confirmation and the model call in the same context (see below), ledger_off that the ledger is switched off, unhashable_args that the arguments could not be hashed (a cycle, deeper than 32 levels, or more than 256 KiB once written out), so the confirmation is tied to the tool only. invalid_input and no_client mean nothing was recorded.
How a confirmation is used
- One confirmation unlocks one call. When the check says a call was confirmed, the entry is used up. Two calls in one model reply need two confirmations, and two checks running at the same time cannot both use one.
- It expires. The rule decides how long (default 10 minutes). The SDK keeps an entry for an hour at most.
- It is tied to the session. The ledger is scoped by the current context’s
sessionId, or its trace id when there is no session. CallconfirmToolCallinside the samewithZespanContext/with_zespan_contextthat sets the session, and set asessionIdwhen the confirmation and the model call happen in different traces. With no context at all, nothing is ledgered, because an entry no session owns could unlock another user’s call. - It lives in this client, in this process. The ledger is in memory, per client instance, and bounded (50 entries per session, 1,000 sessions, 10 entries sent per check). After a fork (a pre-fork server such as gunicorn, or
multiprocessing), child processes inherit a copy of the ledger and of the key: create a new client in each child. - A server that does not know the rule ignores it. An older server, or a project without a
require_confirmationrule, receives two extra fields (ledgerandconfirmations) and ignores them. An older SDK sends neither, and the rule then reports no confirmation data and blocks nothing.
When the confirmation and the tool call are in different processes
The ledger does not cross processes. If a web handler records the confirmation and a queue worker makes the tool call, pass the confirmation to the worker yourself and hand it to the SDK when you run the model call:args (not a hash) so this client hashes them the same way it hashes the call. Each entry works once. ageMs / age_ms (or at) says how old the confirmation is, so the rule’s expiry still applies; without it the entry counts as just recorded. An explicit confirmation is used once per context object, so a job that is retried and rebuilds the same confirmation each time can reuse it on every retry: record one confirmation per attempt, with a new ref. Until you do this, a rule in observe mode reports the call as having no confirmation.
Argument hashes, tool capture and redaction
When tool capture is on, the hash is the SHA-256 of a canonical form of the arguments (keys sorted, numbers and text written the same way in TypeScript and Python), computed from the arguments as your code holds them, before any redaction.confirmationArgsHash(args) / confirmation_args_hash(args) gives the same hash.
When tool capture is off, or PII redaction is on (redactPii / redact_pii), a plain hash of small arguments could be guessed back, so the SDK uses a keyed hash instead (HMAC-SHA256 with a random key made when the client starts, kept private in the process and never sent anywhere). A keyed hash can be compared only inside the same client: it works for confirmations recorded by this client, but never for a hash made in another process (pass args, as above). A rule that sets bindFields cannot be applied to a keyed hash and reports that it could not compare, blocking nothing.
The approval queue
client.awaitApproval() (TypeScript) and client.await_approval() (Python) gate a call on a person’s decision in the dashboard. When the request is approved, the SDK also records it as a human_review entry from a reviewer, tied to the tool and arguments you passed. A require_confirmation rule counts it only if the rule names both human_review and reviewer in accept. By default it does not: a decision by someone on your team is not a confirmation recorded by your app at the point of use, and the two should stay apart.
await_approval needs a project_id (zespan.init(project_id=...); projectId in TypeScript); without one it raises ValueError. Both return when approved, raise ApprovalRejectedError when rejected or expired, and raise ApprovalTimeoutError when no decision arrives in time (default wait 5 minutes, polling every 3 seconds). The Python call blocks; in async code run it with asyncio.to_thread. Both record a human_review approval span for every outcome.
Where the check runs
A call is checked before your code runs the tool only where the SDK sends the tool-call check after the model replies and before your tool code runs: the wrapped clients (wrapOpenAI, wrapAnthropic, wrapGoogle, wrapGoogleGenAI, wrapMistral, wrapCohere, wrapOpenRouter in TypeScript; patch_openai, patch_anthropic, patch_google, patch_google_genai, patch_mistral, patch_cohere, patch_groq, patch_bedrock, patch_litellm in Python) with guardrails turned on. Anywhere else (a framework that runs the tool for you, or an integration that does not send the check), no ledger is sent: the rule reads no confirmation data and blocks nothing.
Language
The confirmation itself is language-independent: your app records it, in any language. What reads text is only the proof Zespan runs when it drafts a rule for you, and that reading is English only: for a conversation it cannot read it says so and does not count the call. It never decides whether a live call runs.Switching it off
SetZESPAN_CONFIRMATION_LEDGER=off in the environment to stop the ledger and the ledger field. The rule then reports no confirmation data and blocks nothing. The approval span from confirmToolCall is still recorded.
Next steps
- Policy file reference:
require_confirmation— the rule, its fields and its reason codes - Guardrails — turning on tool-call checks in a wrapper
- Agent tracing —
recordApprovaland tool roles
Arguments should be plain JSON values (objects, arrays, strings, numbers, booleans, null). A
Date, Map, Set or class instance, which JSON cannot hold, cannot be hashed, so the confirmation is tied to the tool only.
