Skip to content

Cursor HITL: approval-form elicitation schema + pid-stamped gate telemetry#39

Draft
pragati-agrawal-glean wants to merge 2 commits into
mainfrom
fix/cursor-hitl-elicitation-empty-form
Draft

Cursor HITL: approval-form elicitation schema + pid-stamped gate telemetry#39
pragati-agrawal-glean wants to merge 2 commits into
mainfrom
fix/cursor-hitl-elicitation-empty-form

Conversation

@pragati-agrawal-glean

@pragati-agrawal-glean pragati-agrawal-glean commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

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 empty requestedSchema, so we believed Cursor's form renderer had nothing to draw. This PR branches the schema: Cursor gets a required decision field (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/create from stdio MCP servers:

  • A minimal Glean-free probe server (one tool, flat single-enum-field requestedSchema, 120s timeout) negotiated clientCaps.elicitation={"form":{}} with cursor-vscode/1.0.0 — and timed out with no UI, no JSON-RPC error response, and no client log line.
  • Cursor's own bundle (mcpProcessMain.js) routes elicitations to the workbench via an internal mcp.elicitationRequest command; there are two silent-drop branches (missing executeCommandBridge, or resolveCommandTarget failure) that swallow the request without responding — matching the observed server-side hang until timeout.
  • The workbench UI handler exists ([MCPService] Received elicitation request) but its log line never appears in any session log — the request dies before reaching it.
  • Cancelling the chat run does return 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-error telemetry (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.
  • Spec-correct Approve/Deny form schema for form-capable clients plus isApproved handling (a submitted Deny = 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: runToolAnnotations currently sets readOnlyHint: true under 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 typecheck clean; npx vitest run 196/196.
  • Live Cursor 3.10.20 verification of the telemetry path (gate → elicit-sent → decline-on-cancel all observed in ~/.glean/glean-server.log).

🤖 Generated with Claude Code

pragati-agrawal-glean and others added 2 commits July 19, 2026 14:52
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>
@pragati-agrawal-glean pragati-agrawal-glean changed the title Fix Cursor HITL approval banner not rendering (empty elicitation form) Cursor HITL: approval-form elicitation schema + pid-stamped gate telemetry Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant