fix(hitl): elicitation fallback for managed clients when the approval socket is unreachable#296
Merged
Conversation
… socket is unreachable The flagship per-call HITL had a hole in the dominant deployment. Managed clients (the Claude family) skip MCP elicitation to avoid double prompts, and the socket channel denies when unreachable — so a default `npx airmcp` setup (no menubar app, hitl level destructive-only) hard-denied every gated tool out of the box. Issue #28's reporter resolved it by setting hitl level "off": the safety feature was being disabled by its own UX. WWDC26 session 347 now tells every agentic developer to ship per-call confirmation — ours has to actually work where agents run. Channel order now adapts to what is available, with per-call granularity unchanged in every path (this changes the approval channel, never the granularity — CLAUDE.md escalation reviewed): non-managed: elicitation → socket → deny (unchanged) managed: socket (if reachable) → elicitation → deny with an actionable hint (menubar app / elicitation-capable client / hitl.whitelist / hitl.level) Implementation: HitlClient.isReachable() probes the socket without sending a request (a successful probe leaves the connection open for the approval call); the guard uses a NOT_HANDLED sentinel so a tool legitimately returning undefined can never double-execute; telemetry gains an "unavailable" channel value for the no-channel deny. When the menubar app IS running, managed clients still use the socket exclusively — no double prompt. Tests: 6 new behavior tests — managed+unreachable approves/denies via elicitation (socket never asked), no-channel deny carries the actionable message and never runs the tool, reachable socket keeps today's exact behavior; isReachable false on dead path / true with a live listener whose connection is reused. Suite: 1935 pass. RFC 0008 amended (Phase 1.5).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The flagship per-call HITL had a hole in the dominant deployment. Managed
clients (the Claude family) skip MCP elicitation to avoid double prompts, and
the socket channel denies when unreachable — so a default
npx airmcpsetup(no menubar app, hitl level destructive-only) hard-denied every gated tool out
of the box. Issue #28's reporter resolved it by setting hitl level "off": the
safety feature was being disabled by its own UX. WWDC26 session 347 now tells
every agentic developer to ship per-call confirmation — ours has to actually
work where agents run.
Channel order now adapts to what is available, with per-call granularity
unchanged in every path (this changes the approval channel, never the
granularity — CLAUDE.md escalation reviewed):
non-managed: elicitation → socket → deny (unchanged)
managed: socket (if reachable) → elicitation → deny
with an actionable hint (menubar app / elicitation-capable
client / hitl.whitelist / hitl.level)
Implementation: HitlClient.isReachable() probes the socket without sending a
request (a successful probe leaves the connection open for the approval call);
the guard uses a NOT_HANDLED sentinel so a tool legitimately returning
undefined can never double-execute; telemetry gains an "unavailable" channel
value for the no-channel deny. When the menubar app IS running, managed
clients still use the socket exclusively — no double prompt.
Tests: 6 new behavior tests — managed+unreachable approves/denies via
elicitation (socket never asked), no-channel deny carries the actionable
message and never runs the tool, reachable socket keeps today's exact
behavior; isReachable false on dead path / true with a live listener whose
connection is reused. Suite: 1935 pass. RFC 0008 amended (Phase 1.5).