|
78 | 78 | _CURSOR_MARKERS = ("model", "cursor_version", "conversation_id", "generation_id", "workspace_roots") |
79 | 79 |
|
80 | 80 | #: Copilot CLI's camelCase names, which no other vendor uses -- the name alone identifies |
81 | | -#: these. Derived from EVENT_MAP: a hand-kept list drifted, leaving payloads claimed by |
82 | | -#: the matrix but by no adapter, and so allowed through. |
| 81 | +#: these. Derived from EVENT_MAP: a hand-kept list drifted and left payloads unclaimed. |
83 | 82 | _CLAIMABLE = tuple(name for name in EVENT_MAP if name[:1].islower()) |
84 | 83 |
|
85 | 84 | #: executeHook merges {timestamp, hook_event_name, session_id?, transcript_path?} into |
@@ -115,12 +114,14 @@ def claims(raw): |
115 | 114 | if name in _CLAIMABLE: |
116 | 115 | return True |
117 | 116 | # memory-tool payloads are unmistakable |
118 | | - ti = raw.get("tool_input") or {} |
| 117 | + # The isinstance guard also keeps a non-dict tool_input from raising here. |
| 118 | + ti = raw.get("tool_input") |
119 | 119 | return raw.get("tool_name") in MEMORY_TOOLS and isinstance(ti, dict) and "command" in ti |
120 | 120 |
|
121 | 121 |
|
122 | 122 | def parse(raw): |
123 | | - ti = raw.get("tool_input") or {} |
| 123 | + ti = raw.get("tool_input") |
| 124 | + ti = ti if isinstance(ti, dict) else {} |
124 | 125 | tool = raw.get("tool_name") or raw.get("toolName") |
125 | 126 | path = content = None |
126 | 127 | if tool in MEMORY_TOOLS: |
@@ -158,15 +159,15 @@ def parse(raw): |
158 | 159 |
|
159 | 160 | def is_memory_write(event): |
160 | 161 | """True when this event is a memory-tool content write (VS Code's memory surface).""" |
161 | | - ti = event.raw.get("tool_input") or {} |
| 162 | + ti = event.raw.get("tool_input") |
| 163 | + ti = ti if isinstance(ti, dict) else {} |
162 | 164 | return event.tool in MEMORY_TOOLS and ti.get("command") in MEMORY_WRITE_COMMANDS |
163 | 165 |
|
164 | 166 |
|
165 | | -#: Events whose block verdict is a TOP-LEVEL {decision: "block", reason}. |
166 | | -#: UserPromptSubmitHookOutput declares exactly those two fields and |
167 | | -#: defaultIntentRequestHandler reads `typedOutput.decision` off the root object; |
168 | | -#: executePostToolUseHook reads the same two off a PostToolUse response's root, where a |
169 | | -#: block feeds the reason back to the model instead of the tool result. |
| 167 | +#: Block verdict at the TOP LEVEL: {decision: "block", reason}. |
| 168 | +#: UserPromptSubmitHookOutput declares exactly those two and |
| 169 | +#: defaultIntentRequestHandler reads `typedOutput.decision` off the root; |
| 170 | +#: executePostToolUseHook reads the same two off a PostToolUse response's root. |
170 | 171 | _TOP_LEVEL_BLOCK = (PROMPT_SUBMIT, POST_TOOL) |
171 | 172 |
|
172 | 173 | #: Block verdict NESTED in hookSpecificOutput. executeStopHook requires BOTH decision and |
@@ -253,10 +254,9 @@ def respond(decision, event): |
253 | 254 | #: |
254 | 255 | #: PascalCase, i.e. VS Code's spelling, because CONFIG_PATH is VS Code's file. |
255 | 256 | #: `parseCopilotHooks` resolves each key with `resolveCopilotCliHookType(id) ?? toHookType |
256 | | -#: (id)`, so both spellings work there -- but only one may be written: it does |
257 | | -#: `result.set(hookType, ...)`, an overwrite, so two keys resolving to the same HookType |
258 | | -#: silently drop one. SESSION_END is therefore unwireable: `sessionEnd` is a Copilot CLI |
259 | | -#: name VS Code never fires, claimed for parsing and not offered for installing. |
| 257 | +#: (id)`, so both spellings work -- but only one may be written: it does `result.set(...)`, |
| 258 | +#: an overwrite, so two keys resolving to the same HookType silently drop one. SESSION_END |
| 259 | +#: is therefore unwireable: a Copilot CLI name VS Code never fires. |
260 | 260 | REVERSE_EVENT_MAP = { |
261 | 261 | PRE_TOOL: "PreToolUse", |
262 | 262 | POST_TOOL: "PostToolUse", |
|
0 commit comments