Skip to content

fix(mcp,hotkeys): harden v1.14–v1.18 surface before reelee-web wiring#51

Merged
thorwhalen merged 1 commit into
mainfrom
fix/pre-wiring-hardening
Jul 10, 2026
Merged

fix(mcp,hotkeys): harden v1.14–v1.18 surface before reelee-web wiring#51
thorwhalen merged 1 commit into
mainfrom
fix/pre-wiring-hardening

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

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)

  • Errors-as-data is no longer broken by a thrown JSON.stringify. callGetState, readResource, and formatToolResponse serialize app-supplied values through a guarded safeStringify. A BigInt / circular reference / throwing toJSON in a view value or command result previously threw a TypeError past the tool-call handler (a JSON-RPC protocol crash, not an isError result). Now returns an unserializable_state payload; an err(...) with unserializable details still delivers its code/message.
  • ok(undefined) no longer emits a malformed text: undefined (which drops on the wire and fails a strict client's CallToolResult schema); it serializes as "null".
  • createMcpServer({ getStateTool }) fails fast on a name collision. A command that sanitizes to the getState tool name (app.getStateapp_getState) would emit a duplicate tool (strict hosts reject the entire tools/list) and silently shadow the command on tools/call. Construction now throws a clear error.

acture-hotkeys (patch)

  • Spacebar is bindabletokenFromEvent emitted ' ', stripped by tinykeys' chord parser to an unmatchable empty key; now emits 'Space' (matched via event.code).
  • A throwing 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, matching registry.dispatch).
  • Shadow-DOM inputs respectedDEFAULT_IGNORE resolves the real target via event.composedPath()[0].
  • No phantom self-conflictsresolveKeys de-duplicates, so an add override restating a command's own key no longer double-binds or makes detectConflicts report the command against itself.
  • useHotkeys no longer freezes stale callbacksonDispatched/shouldIgnoreEvent routed through refs.

docs — confirmation-gate pattern (not published; copy-paste reference)

  • The channel label is the trust boundary. The gate only fires for ctx.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 the acture-ai-assistant skill).
  • Middleware wrapper forwards dispatch's 4th options arg (internalToken), which the 3-arity aliases dropped — breaking @internal dispatch once the gate is installed.
  • preview() computed defensively so a throw can't break errors-as-data.
  • Removed a stray NUL byte that made the doc binary to git (GitHub couldn't render its diff/blame).

Verification

  • Full monorepo typecheck green (27 packages) and tests green (mcp 38, hotkeys 28; +19 new tests). Both packages build.
  • The 4 adversarially-refuted findings are documented as correctly dropped (e.g. getRisk is typed total, so map.get undefined is a TS compile error, not a runtime footgun).

⚠ Separately flagged — NOT changed (out of scope)

packages/core/src/when.ts uses a load-bearing NUL byte as the regex pattern|flags separator (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

…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
@thorwhalen thorwhalen merged commit 7bdca33 into main Jul 10, 2026
1 check passed
@thorwhalen thorwhalen deleted the fix/pre-wiring-hardening branch July 10, 2026 10:50
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