aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcaner-akca <94343893+caner-akca@users.noreply.github.com>2026-08-30 01:18:16 +0300
committerGitHub <noreply@github.com>2026-08-30 01:18:16 +0300
commitf807b697353cfa00aa912c7cde4830e863001cf5 (patch)
tree39dd05f851a3d3eef07d289f281a2babcfd32868 /src
parentc2637dc182ddc5425108824d5ed15d24ce38c4e3 (diff)
fix: detect claude mcp elicitation dialogs as blocked (#3383)
refs #3283
Diffstat (limited to 'src')
-rw-r--r--src/detect/manifest/tests.rs24
-rw-r--r--src/detect/manifests/claude.toml23
2 files changed, 45 insertions, 2 deletions
diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs
index 508b9b7e..8eb3b612 100644
--- a/src/detect/manifest/tests.rs
+++ b/src/detect/manifest/tests.rs
@@ -747,6 +747,30 @@ fn claude_blocker_screen_outranks_osc_idle_title() {
}
#[test]
+fn claude_mcp_elicitation_is_blocked() {
+ // Regression for issue #3283: an MCP elicitation dialog has Accept/Decline
+ // controls and an "Esc to cancel" footer but no Enter hint, so no blocked
+ // rule matched and the static OSC title reported idle.
+ // Live capture uses curly quotes around the server name; the issue report
+ // transcribed straight quotes. Both must classify as blocked.
+ for screen in [
+ "MCP server \u{201c}my-server\u{201d} requests your input\n\nGrant temporary access to the demo gateway for 15 minutes?\n\n\u{276f} Accept Decline\n\nEsc to cancel \u{b7} \u{2191}/\u{2193} to navigate\n",
+ "MCP server \"my-server\" requests your input\n\nserver-supplied message\n\n\u{276f} Accept Decline\n\nEsc to cancel \u{b7} \u{2191}/\u{2193} to navigate\n",
+ ] {
+ let result = with_manifest_dirs("claude-mcp-elicitation", || {
+ osc_explain(Agent::Claude, screen, "\u{2733} Claude Code", "")
+ });
+ assert_eq!(result.state, AgentState::Blocked, "{result:#?}");
+ assert!(result.visible_blocker, "{result:#?}");
+ assert_eq!(
+ result.matched_rule.as_ref().map(|r| r.id.as_str()),
+ Some("mcp_elicitation_prompt"),
+ "{result:#?}"
+ );
+ }
+}
+
+#[test]
fn claude_empty_osc_empty_screen_is_idle_fallback() {
// No OSC data, no matching screen rule → fallback idle (unchanged V3 behavior)
let result = osc_explain(Agent::Claude, "", "", "");
diff --git a/src/detect/manifests/claude.toml b/src/detect/manifests/claude.toml
index 696a1978..55adacd6 100644
--- a/src/detect/manifests/claude.toml
+++ b/src/detect/manifests/claude.toml
@@ -1,7 +1,7 @@
id = "claude"
-version = "2026.08.21.1"
+version = "2026.08.29.1"
min_engine_version = 2
-updated_at = "2026-08-21T00:00:00Z"
+updated_at = "2026-08-29T00:00:00Z"
aliases = ["claude-code"]
[[rules]]
@@ -111,6 +111,25 @@ visible_blocker = true
contains = ["run a dynamic workflow?", "esc to cancel"]
[[rules]]
+id = "mcp_elicitation_prompt"
+state = "blocked"
+priority = 980
+region = "whole_recent"
+visible_blocker = true
+# MCP elicitation dialogs (elicitation/create) show Accept/Decline controls
+# with an "Esc to cancel" footer but no Enter hint, so live_blocked_form
+# cannot see them (issue #3283). Gate on the invariant header line, the
+# Accept/Decline control line, and the cancel footer.
+contains = ["esc to cancel"]
+line_regex = ['(?i)^\s*MCP server ["\x{201C}].+["\x{201D}] requests your input\s*$']
+all = [
+ { any = [
+ { line_regex = ['^\s*\x{276F}?\s*Accept\b'] },
+ { line_regex = ['^\s*\x{276F}?\s*Decline\b'] },
+ ] },
+]
+
+[[rules]]
id = "live_prompt_box"
state = "idle"
priority = 950