fix(mcp,hotkeys): harden v1.14–v1.18 surface before reelee-web wiring#51
Merged
Conversation
…b wiring Adversarial pre-wiring review of the v1.14–v1.18 consumer surface (MCP resources+getState, hotkeys keymap, HITL confirmation-gate doc). 12 confirmed defects fixed across two published packages + the gate pattern doc. acture-mcp-server (patch): - errors-as-data no longer broken by a thrown JSON.stringify in callGetState / readResource / formatToolResponse (BigInt / circular / throwing toJSON); a guarded safeStringify returns an unserializable_state payload instead of throwing a TypeError past the tool-call boundary - ok(undefined) serializes as "null" (was a malformed text: undefined field) - createMcpServer fails fast on a getState/command tool-name collision (app.getState -> app_getState) instead of shadowing the command + emitting a duplicate tool that strict hosts reject wholesale acture-hotkeys (patch): - Space key is bindable (tokenFromEvent emits 'Space', not an unmatchable ' ') - a throwing when-clause no longer crashes the key handler / swallows fallbacks - shadow-DOM inputs respected (DEFAULT_IGNORE resolves composedPath()[0]) - resolveKeys de-duplicates -> no phantom self-conflict / double-binding - useHotkeys routes onDispatched/shouldIgnoreEvent through refs (no stale closures) docs (confirmation-gate pattern, not published): - the channel label is the trust boundary: added the load-bearing wiring warning (destructive dispatch runs ungated if channel unset), fixed the internally inconsistent examples + the false "caller-independent regardless of surface" claim (also corrected in the acture-ai-assistant skill) - middleware wrapper forwards dispatch's 4th options arg (internalToken) - preview() computed defensively so a throw can't break errors-as-data - removed a stray NUL byte that made the doc binary to git Full monorepo typecheck + tests green (mcp 38, hotkeys 28); +19 new tests. Separately flagged, NOT changed (out of scope): packages/core/src/when.ts uses a load-bearing NUL byte as the regex pattern|flags separator (lines 180/348) — it makes the file binary to git and is fragile; a proper fix is a structured regex token, which is its own change. Claude-Session: https://claude.ai/code/session_01A1PykLHv2BjQRQhspjvPWy
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.
Why
Pre-wiring adversarial review of the v1.14–v1.18 consumer surface (MCP resources + getState, hotkeys keymap customization, the HITL confirmation-gate pattern) before it's wired into the named consumer
reelee-web. A 6-dimension multi-agent review found 20 candidate issues → 15 confirmed after adversarial verification (4 refuted) → 12 unique defects after dedup. All fixed here; each has a regression test where testable.acture-mcp-server (patch)
JSON.stringify.callGetState,readResource, andformatToolResponseserialize app-supplied values through a guardedsafeStringify. ABigInt/ circular reference / throwingtoJSONin a view value or command result previously threw aTypeErrorpast the tool-call handler (a JSON-RPC protocol crash, not anisErrorresult). Now returns anunserializable_statepayload; anerr(...)with unserializabledetailsstill delivers itscode/message.ok(undefined)no longer emits a malformedtext: undefined(which drops on the wire and fails a strict client'sCallToolResultschema); it serializes as"null".createMcpServer({ getStateTool })fails fast on a name collision. A command that sanitizes to the getState tool name (app.getState→app_getState) would emit a duplicate tool (strict hosts reject the entiretools/list) and silently shadow the command ontools/call. Construction now throws a clear error.acture-hotkeys (patch)
tokenFromEventemitted' ', stripped by tinykeys' chord parser to an unmatchable empty key; now emits'Space'(matched viaevent.code).when-clause no longer crashes the key handler and swallows fallback candidates — the fire loop treats a throw as not-applicable and tries the next command (fail-closed, matchingregistry.dispatch).DEFAULT_IGNOREresolves the real target viaevent.composedPath()[0].resolveKeysde-duplicates, so anaddoverride restating a command's own key no longer double-binds or makesdetectConflictsreport the command against itself.useHotkeysno longer freezes stale callbacks —onDispatched/shouldIgnoreEventrouted through refs.docs — confirmation-gate pattern (not published; copy-paste reference)
channellabel is the trust boundary. The gate only fires forctx.channel === 'assistant', but the doc's recommended backend example (toAITools/createMcpServer) never set it → destructive dispatch ran ungated (fail-open), and the doc was internally inconsistent (frontend example set it, backend didn't). Added a load-bearing wiring warning + a fail-closed hardening note, fixed the examples, and removed the false "caller-independent … regardless of surface" claim (also corrected in theacture-ai-assistantskill).dispatch's 4thoptionsarg (internalToken), which the 3-arity aliases dropped — breaking@internaldispatch once the gate is installed.preview()computed defensively so a throw can't break errors-as-data.Verification
mcp38,hotkeys28; +19 new tests). Both packages build.getRiskis typed total, somap.getundefined is a TS compile error, not a runtime footgun).⚠ Separately flagged — NOT changed (out of scope)
packages/core/src/when.tsuses a load-bearing NUL byte as the regexpattern|flagsseparator (join at line 180, split at line 348). It's self-consistent (tests pass) but makes the file binary to git and is fragile — one reserialization from breaking. A naive NUL→space swap would break regex patterns containing spaces (/foo bar/), so the correct fix is a structured regex token, which belongs in its own change. Happy to file/fix separately.https://claude.ai/code/session_01A1PykLHv2BjQRQhspjvPWy