Cursor HITL: approval-form elicitation schema + pid-stamped gate telemetry#39
Draft
pragati-agrawal-glean wants to merge 2 commits into
Draft
Cursor HITL: approval-form elicitation schema + pid-stamped gate telemetry#39pragati-agrawal-glean wants to merge 2 commits into
pragati-agrawal-glean wants to merge 2 commits into
Conversation
Cursor advertises a form-based elicitation capability (`elicitation:
{form:{}}`) and builds the approval banner from the elicitation schema's
`properties`. The HITL gate sent `requestedSchema: {type:"object",
properties:{}}` to every client, so Cursor had no fields to render and
showed no visible approval banner — the action ran ungated. Claude Code
renders the message text with Accept/Decline regardless of schema, so it
was unaffected.
Branch the schema by client: Cursor gets a single required Approve/Deny
field (guarantees the form renders and forces an explicit choice); the
non-Cursor path keeps the empty schema so Claude Code behavior is
unchanged. A submitted "Deny" (action "accept" + decision "Deny") is
treated as a rejection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server log had no line between "run_tool was called" and an elicitation error, so "gate never reached" (routing, missing tool JSON, requires_approval false) was indistinguishable from "elicitation sent but the client rendered nothing" — the exact ambiguity that made the Cursor no-banner bug hard to localize. Log the gate decision, the elicitation send (with which schema variant), the result, and errors. Lines are pid-stamped because multiple plugin instances (marketplace install next to a local checkout) can share one log file. Tool names and structural facts only — never argument values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Status: hypothesis disproven by live test — kept for telemetry + forward-compat; root cause is a Cursor client bug
Original hypothesis (disproven)
Cursor advertises form-based elicitation (
elicitation:{form:{}}) and the HITL gate sent an emptyrequestedSchema, so we believed Cursor's form renderer had nothing to draw. This PR branches the schema: Cursor gets a requireddecisionfield (enum: ["Approve","Deny"]); other clients keep the empty schema (Claude Code behavior unchanged).Live testing on Cursor 3.10.20 disproved the hypothesis: the banner still does not render with the non-empty form schema.
Actual root cause (verified 2026-07-20)
Cursor 3.10.20 silently drops
elicitation/createfrom stdio MCP servers:requestedSchema, 120s timeout) negotiatedclientCaps.elicitation={"form":{}}withcursor-vscode/1.0.0— and timed out with no UI, no JSON-RPC error response, and no client log line.mcpProcessMain.js) routes elicitations to the workbench via an internalmcp.elicitationRequestcommand; there are two silent-drop branches (missingexecuteCommandBridge, orresolveCommandTargetfailure) that swallow the request without responding — matching the observed server-side hang until timeout.[MCPService] Received elicitation request) but its log line never appears in any session log — the request dies before reaching it.action:"decline"to the server, so the response plumbing works; only the UI surfacing is broken.A Cursor bug report with the probe repro is the follow-up.
What this PR still delivers
hitl.gate/hitl.elicit-sent/hitl.elicit-result/hitl.elicit-errortelemetry (pid-stamped, no argument values): before this, "gate never reached" was indistinguishable from "elicitation sent but not rendered" — this ambiguity cost the whole investigation. Worth keeping regardless.isApprovedhandling (a submittedDeny= rejection): correct behavior the moment Cursor fixes rendering, harmless meanwhile (fail-closed timeout is unchanged).Open question for review (not implemented here)
Interim UX for Cursor:
runToolAnnotationscurrently setsreadOnlyHint: trueunder HITL to avoid a double prompt — but on Cursor this suppresses the native tool-approval prompt while the elicitation gate can never render, producing a guaranteed 5-minute silent hang and then a fail-closed error. Until Cursor fixes elicitation, consider not claiming read-only for Cursor clients (let Cursor's native approval gate the call) and skipping the plugin's elicitation gate for Cursor.Testing
npm run typecheckclean;npx vitest run196/196.~/.glean/glean-server.log).🤖 Generated with Claude Code