Skip to main content
Human-in-the-loop (HITL) approval gates let an agent ask for permission before running selected MCP tools. They compose in both single-agent and orchestration mode. Use them for operations that need a human decision before execution, such as production changes or destructive actions. Current behavior:
  • Gates compose in both single-agent mode and for orchestration workers. A single-agent run reports scope.kind = "single"; an orchestration worker reports scope.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_requested and aura.approval_completed; conversational also emits aura.approval_pending while 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

The gate is added before the worker’s MCP tools execute. A denied call returns a successful blocked tool result to the worker:
The worker sees that message and can explain the denial to the user. The MCP tool itself is not called.

Webhook request

Aura sends a JSON request to the configured webhook URL. The request uses a flat wire shape with kind tags for scope and origin:
Fields:

Webhook response

Approve the tool call:
Deny the tool call, optionally with a reason:
Response behavior:

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 the AURA_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:
The signed string is the canonical form {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 same X-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.
  1. Set the new key as AURA_HITL_WEBHOOK_SECRET_SECONDARY on 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.
  2. 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.
  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.
A restart can interrupt in-flight approvals. See Current limitations for how the session store affects approvals that are in progress.

SSE lifecycle events

Approval routes emit lifecycle events on streaming responses. These events are emitted even when AURA_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, sends aura.approval_pending over SSE, and waits for a decision on the approval ingress endpoint:
The chat request must set 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:
When signing is enabled, the responder must sign the decision POST with the 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:
Use 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 uses mode = "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_pending can 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.