Skip to content

A gate's RequireApproval cannot reach a human — route it through the #2676 ApprovalBroker #3288

Description

@macanderson

AuthzGate can return AuthzDecision::RequireApproval, and authz_verdict folds it to GateVerdict::RequireApproval correctly — but GatedToolSet::execute (PR #3281) turns that verdict into a refusal:

`{name}` needs a human's approval ({reason}), and this session has no approval route attached — it cannot be asked from here

That matches the documented headless posture and is safe, but it means a gate cannot actually ask anyone anything. The ask half of the vocabulary is inert.

The route already exists

#2676 shipped ApprovalBroker with emit-before-park, a bounded TTL wait, and approval.granted / denied / expired audit events. stella-core's HookApprovalRoute port is how the shell-hook surface reaches it without stella-core depending on stella-tools, and stella_tools::hook_bridge::BrokerApprovalRoute is the production implementation. A gate should reach the same broker through the same port rather than growing a second approval path.

Definition of done

  • GatedToolSet takes an optional approval route and resolves RequireApproval through it.
  • With no route attached, keep today's refusal wording — an engine that cannot ask must not silently allow, and the message should name the grant path.
  • Reuse HookApprovalRequest / HookApprovalResolution rather than inventing a parallel pair. If the naming is too hook-specific once a second producer exists, rename in the same PR and chase it through — a stale name is a bug in review here.
  • Honour stella-tty's human_can_answer(...) derivation for whether there is anybody to ask, rather than re-deriving presence.

Related, and probably worth doing together

#2793 — MCP and custom tools bypass the blocking policy chains and the #2676 approval flow. GatedToolSet sits above both layers by construction, so it is the natural place to close that bypass for the approval path too. Check whether #3283's wiring already resolves part of it before scoping.

Witness

A gate returning RequireApproval parks and runs the call when a scripted route approves; the same gate and a denying route refuses without executing. Anti-vacuity: with NoAuthz the call runs and no approval is ever requested.

Refs #2716, #2676, #2793, #3281.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageUntyped request — convert by adding bug / feature / epic

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions