- Gates compose in both single-agent mode and for orchestration workers. A
single-agent run reports
scope.kind = "single"; an orchestration worker reportsscope.kind = "worker". - Webhook routing works for unattended approvals.
- Conversational routing works for attended approvals over an open SSE stream. The AURA CLI in HTTP mode is the first attended client.
- Matching tool calls are blocked until the configured route approves them.
- Human denials are returned to the model as normal tool feedback, so the agent can explain the denial without treating it as a transport failure.
- Timeouts, cancellation, and webhook channel failures still fail closed as tool errors.
- Conversational HITL requires
stream=true; non-streaming requests are rejected because approval prompts are delivered over SSE. - Approval lifecycle events emit on streaming responses. Webhook emits
aura.approval_requestedandaura.approval_completed; conversational also emitsaura.approval_pendingwhile the tool call is parked.
Configure a webhook gate
Add a top-level[hitl] table and a required [hitl.route] table:
require_approval is a list of glob patterns matched against MCP tool names.
When an agent calls a matching tool, Aura requests approval through the
configured route before the MCP tool runs.
A tool is gated if it matches any pattern in the list, so pattern order does
not affect whether a tool is gated. To leave a tool ungated, do not list a
pattern that matches it. When more than one pattern matches, the first in config
order is reported as origin.matched_pattern in the webhook payload and SSE
events; that is the only effect of ordering.
The request_approval tool is never matched by these globs. It is excluded from
the gate so the agent can ask for approval without triggering the gate itself.
timeout_secs defaults to 300 for webhooks. If the webhook does not return a
decision before the timeout, the tool does not run.
Orchestration example
Webhook request
Aura sends a JSON request to the configured webhook URL. The request uses a flat wire shape withkind tags for scope and origin:
Webhook response
Approve the tool call:Sign and Verify Approval Webhooks
Signing is an opt-in HMAC-SHA256 (hash-based message authentication code) “root of trust” for the webhook exchange. It verifies that the party returning a decision (the responder) is the same party Aura sent the request to. This is the same pattern Stripe and GitHub use to verify webhook signatures. The same signing also applies to the conversational route’s decision submission, so both approval paths share one root of trust. Signing is off by default. When no secret is set, behavior matches the pre-signing default and Aura logs a warning that verification is disabled. Signing is configured entirely by environment variables, read once at startup.Enable Signing
To enable signing, set theAURA_HITL_WEBHOOK_SECRET environment variable. The
secret’s raw UTF-8 bytes are used directly as the HMAC key, so the secret must be
at least 32 bytes long. The recommended way to generate one is
openssl rand -hex 32, which produces a 64-character value used as-is with no
decoding step.
A present-but-empty or whitespace-only secret is a hard startup error, so Aura
fails loudly rather than silently disabling signing. When a secret is set
together with an http:// (unencrypted) webhook URL, Aura refuses to start.
HTTPS is required when signing is on. Secrets are read once at startup, so
changing any of them requires a restart. At startup, Aura logs whether webhook
signing is enabled, so you can confirm the secret was picked up after a restart.
The off state logs a warning that verification is disabled.
Setting AURA_HITL_WEBHOOK_SECRET_SECONDARY without a primary is a startup
error, consistent with the empty or short primary case.
Request Signature Aura Sends
The next two subsections are the contract for whoever builds the webhook receiver. Aura’s outbound approval-request POST carries two headers:{unix_timestamp}.{context}.{raw_request_body}. Aura computes HMAC-SHA256 over
that string with the secret and hex-encodes the result in lowercase. The approval
request uses the context approval-request:{decision_id}.
raw_request_body is the exact bytes of the request body as sent on the wire.
Compute and verify the signature over those raw bytes before parsing the JSON,
because parsing and re-encoding the body can change the bytes and break the signature.
The context label ties each signature to a specific decision and direction. As a
result, a captured signature can’t be replayed against a different decision or in
the other direction.
A receiver verifies Aura’s request, and signs its decision, with the same
computation:
Responder Obligation
When signing is enabled, the party that returns a decision (the responder) must also sign it. Use the same secret, the sameX-Aura-Signature-256 and
X-Aura-Timestamp headers, and the context approval-decision:{decision_id}.
This applies to both the webhook route’s HTTP response body and the
conversational route’s POST /v1/approvals/{decision_id} request.
The responder signs its decision with the same canonical string
{unix_timestamp}.{context}.{raw_response_body}, using the context
approval-decision:{decision_id} and the same lowercase-hex sha256=<hex>
format. The responder is a separate service. Give it the same secret value out
of band. It does not read Aura’s environment variables. When the responder
cannot verify Aura’s request signature, it should reject the request with a
non-2xx response, which Aura treats as an approval channel error.
Aura rejects an invalid, stale, or missing signature. A timestamp outside the
AURA_HITL_WEBHOOK_TOLERANCE_SECS window counts as stale.
Signature direction and context labels at a glance:
Rotate the Signing Key
Rotate the key with a three-restart procedure. Steps 1 and 3 are rolling restarts. Each must finish on every Aura instance before you move to the next step.- Set the new key as
AURA_HITL_WEBHOOK_SECRET_SECONDARYon every Aura instance and restart, so Aura accepts both the old and new keys when it verifies decisions. Complete this on all instances before continuing. - Update each responder to sign its decisions with the new key. During this window, keep each responder able to verify Aura’s requests with both the old and new keys, because Aura still signs its outbound requests with the old key until step 3.
- Promote the new key to
AURA_HITL_WEBHOOK_SECRET, clear the secondary, and restart every Aura instance. After this, Aura signs and verifies with the new key only, so each responder must also be verifying Aura’s requests with the new key.
SSE lifecycle events
Approval routes emit lifecycle events on streaming responses. These events are emitted even whenAURA_CUSTOM_EVENTS=false because clients may need to react to
approval state.
aura.approval_requested includes decision_id, tool_name, origin, and
scope. aura.approval_pending is emitted only by the conversational route and
contains the attended prompt payload that an Aura-aware client renders before
posting a decision. aura.approval_completed includes decision_id, terminal
outcome, duration_ms, and scope. Outcome kinds are approved, denied,
timed_out, cancelled, and errored; errored means the approval channel
failed before a human decision was obtained.
Conversational route
Use conversational routing when the approver is present on the chat stream. The server parks the worker tool call, sendsaura.approval_pending over SSE, and
waits for a decision on the approval ingress endpoint:
stream=true. Aura rejects non-streaming requests for
conversational HITL because there is no channel for the pending approval prompt.
An attended client resolves a pending approval by POSTing the same decision shape
as a webhook response:
approval-decision:{decision_id} context. See
Sign and Verify Approval Webhooks for the
signing contract.
The AURA CLI supports this flow in HTTP mode. It renders
aura.approval_pending, prompts for approve/deny, and POSTs the decision back to
the server. One-shot CLI mode fails loud instead of prompting because it has no
interactive approval surface.
Webhook manual smoke test
Start a webhook service that accepts the request shape above and returns an approval response. Then run Aura with an orchestration config that uses:mock_tool (the tool name from the bundled math orchestration example config)
so the glob matches a tool the worker actually calls. Put the webhook on a
different port than the mock MCP server (9999) to avoid a collision.
Ask an orchestration worker to use the gated tool. An approval should let the
tool run. A denial with a custom reason should produce a successful blocked tool
result containing that reason.
Conversational manual smoke test
Run Aura with an orchestration config that usesmode = "conversational", a
route timeout shorter than [orchestration.timeouts].per_call_timeout_secs, and
at least one gated worker tool. Connect with the AURA CLI in HTTP mode and send a
query that forces the worker to call the gated tool.
Expected behavior:
- The CLI renders an approval prompt from
aura.approval_pending. - Approving the prompt POSTs to
/v1/approvals/{decision_id}and lets the tool run. - Denying the prompt POSTs the denial and returns blocked-action feedback to the worker.
Current limitations
- The webhook route is synchronous. Aura waits for the webhook response during the tool call.
- With the default in-memory session store, conversational approvals are
single-instance, so only the server process that emitted
aura.approval_pendingcan resolve them. - You can resume a parked approval on a different pod by configuring the optional
Redis or Valkey session store. A
POST /v1/approvals/{id}request that lands on any instance then resolves an approval parked on another. See Session Store. - Without a signing secret configured, the webhook and conversational approval traffic is unauthenticated. See Sign and Verify Approval Webhooks to add an HMAC-SHA256 root of trust.

