All concepts
ConceptCore7 minv2 preview
conceptsconsenttasksasyncgovernance

Mid-flight consent for async tools

Updated July 22, 2026

Mid-flight consent for async tools

Most governance in Hangar is a verdict, not a conversation. Identity is checked, a digest is matched, an egress rule fires – and the call either proceeds or it does not. Nothing waits for a person. That is deliberate: a proxy that pauses to find a human on every hard call is a proxy that stops being fast, and stops being predictable.

There is exactly one place where that changes. An async task can pause mid-flight and ask for something – a confirmation, a missing parameter, a yes-or-no before it touches production – and surface that pause to the client as an input_required status. This is the one spot in the whole stack where governance stops enforcing and starts asking. The mid-flight consent gate is how it asks without ever guessing.

Why this one gate can be interactive

It is worth stating precisely why mid-flight consent is possible here and nowhere else, because the contrast is the whole point.

Synchronous L7 requireApproval fails closed. When the enforcement plane decides a synchronous tools/call needs approval, the call is blocked. There is no guaranteed back-channel at the invocation instant to go find a human, so the verdict is a hard, fail-closed deny – not a hold. It is a gate, not an approval queue: nothing prompts a person and waits for them to click yes. The call stops.

The async consent gate is different, because the task is already paused. A task in input_required is, by definition, suspended and waiting – and the client polling it is right there on a live session that negotiated an elicitation capability. That is a real back-channel. So when tasks/get observes input_required on a 2025-11-25 session, it resolves the input in handler: it elicits the downstream client for consent over ctx.session, and only on an explicit accept does it open the gate and relay the answer upstream. This is the only genuinely interactive consent flow in Hangar. It routes to a real human decision because, uniquely, the context to do so exists. Do not conflate it with the synchronous L7 gate – one asks a person, the other just blocks.

The gate opens on nothing but an explicit yes

The primitive underneath is a presence gate with a strict rule: it is opened only after a confirmed downstream accept. Consent is obtained before the gate opens, so there is no pre-decision race in which the answer relays ahead of the human’s yes. The decision is taken first; the gate is the record that it was.

Every other outcome is terminal and fail-closed. Walk the branches:

  • No elicitation capability negotiated. There is no back-channel to ask, so the decision is a fail-closed denial before any prompt is shown.
  • The client declines or cancels. A real no – the task is failed.
  • The elicitation raises for any reason. Any error at all is caught and treated as a denial. Ambiguity resolves closed, never open.
  • The consent slot was evicted under TTL pressure. The binding fails closed – an expired or missing consent is never read as an implicit yes.

“Fail closed” here means one concrete thing: the task is moved to failed, a best-effort tasks/cancel is relayed upstream, and the now-failed snapshot is returned to the caller. A paused task is never left dangling in input_required waiting on a maybe. There is exactly one exception, and it fails in the safe direction: a transient upstream refusal while relaying an already-accepted answer discards the gate without consuming the single-use consent, so a retry re-elicits and completes rather than burning the task on a blip.

Concurrency, without a double prompt

A task under real load is polled more than once. If two tasks/get calls hit the same input_required state, only one human should be asked. The gate derives a deterministic input_key from the upstream’s pending input request(s) – stable across concurrent polls – so a second in-flight tasks/get maps to the same gate key and returns the current snapshot instead of prompting again. One pause, one question, one answer.

Every decision is a recorded line in the chain

Whichever way it goes, the outcome is written down. Each accept or deny becomes a TaskConsentDecided event on the task’s provenance chain, keyed by task_id, carrying the input_key and attributing the principal_id that was prompted. The gate itself stays deliberately minimal – it decides presence; the ledger owns the event bus and writes the record. Because consent is an additive layer (the relay is correct without it), it could be built and left dormant, then wired in the moment the relay seam went live.

That is the shape of the whole gate: a decision is taken first, the gate opens only on an explicit yes, and every path that isn’t an explicit yes terminates the task in the safe direction and leaves an append-only line in the provenance chain. It is the one place in Hangar where governance stops and asks – and even there, it refuses to guess.


Grounded in ADR-014 (relay-with-governance) and the mid-flight consent gate. Hangar relays and governs the task an upstream owns – it never runs it. Everything is MIT and self-hosted – no SaaS.