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: Each object in items describes the tool call awaiting approval:
  • tool_name — the gated MCP tool being called.
  • arguments — the tool call arguments.
  • tool_call_intentOptional. A short natural-language rationale the agent may supply for why the tool was called, giving the human approver more context. Aura does not act on it automatically. Because it is agent-generated free text, a webhook receiver that displays it to a human approver should treat it as untrusted and escape or sanitize it like any other model output.
  • Omitted entirely from the payload (not null, not an empty string) when the agent supplies no rationale, so its absence does not change the protocol version.
tool_call_intent appears only in the webhook payload’s items[]. It is not included in the aura.approval_requested, aura.approval_pending, or aura.approval_completed SSE events, or in the conversational route’s pending prompt.

Webhook response

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

Forward headers to the webhook

The webhook route accepts two optional [hitl.route] keys (valid only when mode = "webhook") that add headers to every approval POST:
This shows the two common patterns: use static headers (with {{ env.VAR }} interpolation) to authenticate Aura to your own approval service; use headers_from_request to forward a header from the incoming API request (for example a request or correlation id) onto the approval POST.
Forwarding authorization or other sensitive headers sends them to your third-party approval webhook. This is operator-controlled and intentional, but there is no allowlist or header-classification guard, so be deliberate about which headers you forward. Put auth, signing, or network controls in front of the webhook service (see Current limitations).
  • headers — a static map; values are sent verbatim on every approval POST.
  • headers_from_request — maps outgoing header name → incoming request header name; the incoming lookup is case-insensitive.
  • Precedence: when the mapped incoming request header is present, the forwarded value overrides any static headers value for the same (case-normalized) name.
  • When the incoming header is absent, the static headers value remains as a fallback.
  • Aura sends all resolved outgoing header names in lowercase.
  • Both keys are optional; when both are absent, the approval POST is unchanged.
  • Invalid header names or values are skipped with a warning and never abort the request.
  • CLI-context caveat: outside the web-server chat endpoint there is no inbound HTTP request. In CLI standalone mode, headers_from_request resolves only against headers you supply via the AURA_EXTRA_HEADERS environment variable (empty by default, so nothing is forwarded unless you set it). Static headers always apply.

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. These approval events do not carry tool_call_intent; that field is part of the webhook payload only. Aura also stamps this decision_id on the execute_tool span for every gated call, regardless of outcome. This lets you correlate an approval with the authorized action directly in the trace. See Tracing & Span Layout for the full list of span attributes, including decision_id.

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:
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.
  • Webhook egress has no built-in authentication layer. Put auth, signing, or network controls in front of the webhook service.