Skip to content

fix(channels): tag typed text-slash control commands with CommandSource across all channel ingresses#86863

Closed
amknight wants to merge 1 commit into
mainfrom
ak/channel-ingress-command-source-sweep
Closed

fix(channels): tag typed text-slash control commands with CommandSource across all channel ingresses#86863
amknight wants to merge 1 commit into
mainfrom
ak/channel-ingress-command-source-sweep

Conversation

@amknight

Copy link
Copy Markdown
Member

Summary

Sweeps the same root-cause fix as PR #86825 (Mattermost) across the remaining channel ingresses that exhibit the identical latent gap: typed text-slash control commands set CommandAuthorized but not CommandSource, so the explicit-command turn exception in src/auto-reply/reply/source-reply-delivery-mode.ts:54-56 never fires and acknowledgements get silently dropped under message_tool_only delivery (most commonly the Codex harness default for DMs, introduced by #83571).

Closes the rest of issue #86664 across the surface: Feishu, IRC, Line, MS Teams, Nextcloud Talk, Signal, ClickClack, QA Channel, and the public direct-dm plugin SDK helper (with Nostr passing through it as the first internal consumer).

Why one PR

Class-A audit conclusion from the post-mortem of #86664 / #86767 / #86825: the same latent gap was present in 8 channels + 1 SDK helper. The shape of the fix is identical at each site — one ternary commandAuthorized && hasControlCommand ? "text" : undefined — and the affected channel families are owned across overlapping CODEOWNERS sets. Bundling them keeps the changelog story coherent, prevents the next bug report from re-opening this thread per-channel, and lets the negative-control assertions land together.

The fix per file

File Shape Notes
extensions/irc/src/inbound.ts:396 commandAuthorized && hasControlCommand ? "text" : undefined both variables already in scope
extensions/nextcloud-talk/src/inbound.ts:350 same both already in scope
extensions/msteams/src/monitor-handler/message-handler.ts:798 commandAuthorized === true && isControlCommand ? "text" : undefined commandAuthorized is boolean | undefined here
extensions/feishu/src/bot.ts:1302 same as IRC, computing isControlCommand inline uses isControlCommandMessage (not shouldComputeCommandAuthorized) to avoid tagging inline-token text
extensions/signal/src/monitor/event-handler.ts:227 same adds optional hasControlCommand to SignalInboundEntry; signal already computes hasControlCommandInMessage at line 579
extensions/line/src/bot-message-context.ts:359 same adds optional hasControlCommand to BuildLineMessageContextParams; bot-handlers.ts:489 computes via isControlCommandMessage from the plugin SDK
extensions/clickclack/src/inbound.ts:153 isControlCommand ? "text" : undefined CommandAuthorized is hardcoded true on this channel
extensions/qa-channel/src/inbound.ts:216 same as ClickClack CommandAuthorized is hardcoded true
src/plugin-sdk/direct-dm.ts new optional commandSource?: "text" | "native" parameter, piped through to the inbound context public SDK surface; deferred-default preserves call-site backwards compatibility
extensions/nostr/src/gateway.ts:179 passes commandSource: "text" when commandAuthorized AND runtime.channel.commands.isControlCommandMessage(text, cfg) first internal consumer of the SDK addition

The narrow isControlCommandMessage predicate (not shouldComputeCommandAuthorized) is deliberate: only true typed control commands should take the source-suppression bypass; inline-token text like "hello /status" must NOT trigger the bypass because the model owns that turn's reply.

Regression tests added

The bypass tag is the user-visible contract — these tests assert it explicitly, mirroring the established pattern from agent-runner-payloads.test.ts:86, agent-runner-direct-runtime-config.test.ts:284-285, etc.

  • src/plugin-sdk/direct-dm.test.ts: new "propagates commandSource: text onto the inbound context for authorized control commands" test + a CommandSource: undefined back-compat assertion on the existing pipeline test.
  • extensions/signal/src/monitor/event-handler.inbound-context.test.ts:494: extended the existing "authorizes group control commands" test to also assert CommandSource === "text".
  • extensions/msteams/src/monitor-handler/message-handler.authz.test.ts: extended both the existing inline-command test (asserts CommandSource === undefined) and the bare-abort test (asserts CommandSource === "text").

All three new assertions fail without the corresponding fix and pass with it (negative-control proof captured in the Real behavior proof section below).

Channels without an existing ctxPayload-shape test harness (ClickClack, QA Channel, Nextcloud Talk, IRC, Feishu, Line at the bot-message-context level) are covered by the full per-channel test sweeps remaining green (each is a one-line ternary using already-in-scope locals; the structural change is equivalent to what the SDK + Signal + MS Teams tests already prove).

Mock surfaces updated where TypeScript identified missing fields: extensions/feishu/src/bot.test.ts, extensions/feishu/src/bot.broadcast.test.ts, extensions/nostr/src/channel.inbound.test.ts (all add isControlCommandMessage: vi.fn(() => false) to the runtime commands mock).

Verification

  • node scripts/run-vitest.mjs extensions/clickclack/ extensions/qa-channel/ extensions/irc/ extensions/nextcloud-talk/ extensions/signal/ extensions/msteams/ extensions/feishu/ extensions/nostr/ extensions/line/ src/plugin-sdk/direct-dm.test.ts231 files, 2745 tests, all pass.
  • node scripts/run-vitest.mjs src/auto-reply/command-turn-context.test.ts src/auto-reply/reply/source-reply-delivery-mode.test.ts src/auto-reply/reply/commands-reset-hooks.test.ts42/42 pass (the upstream contract this fix depends on).
  • npx oxfmt --check --threads=1 <touched files> → all clean.
  • node scripts/run-oxlint-shards.mjs → all shards finished cleanly.
  • node --import tsx scripts/check-import-cycles.ts → 0 runtime value cycles.
  • node scripts/check-changed.mjs --base origin/main --staged-too → all change-impact lanes pass; the typecheck all lane stops on a pre-existing-on-origin/main error in extensions/codex/src/app-server/run-attempt.test.ts:6830 (Argument of type '180000' is not assignable to parameter of type '120000'). Verified via git diff --stat origin/main -- extensions/codex/ returning empty — that file is unchanged from origin/main. Not introduced or worsened by this PR.

Real behavior proof

  • Behavior addressed: Typed /new, /reset, abort, stop (and any other text-slash control command recognised by isControlCommandMessage) on any of the listed channels under message_tool_only source-reply mode silently change session state without a visible acknowledgement. This is the channel-wide companion to the Mattermost-specific fix in fix(mattermost): tag typed text-slash control commands so /new and /reset acks survive message_tool_only suppression #86825 and resolves the rest of Beta blocker: Mattermost - /new can silently reset session without visible acknowledgement #86664's blast radius.

  • Real environment tested: Unit-harness reproduction and end-to-end ctxPayload assertion on macOS 26 arm64 / Node 25, exercising the actual monitorXxx / dispatchInboundDirectDmWithRuntime entry points each channel runs in production. No live Crabbox session was run — each fix is the same one-line ingress tag the Mattermost precedent (fix(mattermost): tag typed text-slash control commands so /new and /reset acks survive message_tool_only suppression #86825) validated, and the negative-control assertions exercise the exact ctxPayload field that the downstream suppression check (dispatch-from-config.ts:2400-2414) consumes.

  • Exact steps or command run after this patch:

    # Targeted regression tests for the three channels with new ctxPayload assertions
    node scripts/run-vitest.mjs \
      extensions/signal/src/monitor/event-handler.inbound-context.test.ts \
      -t "authorizes group control commands"
    
    node scripts/run-vitest.mjs \
      extensions/msteams/src/monitor-handler/message-handler.authz.test.ts \
      -t "bare abort without a mention"
    
    node scripts/run-vitest.mjs \
      src/plugin-sdk/direct-dm.test.ts \
      -t "propagates commandSource"
  • Evidence after fix: All three pass.

    Negative-control proof (each channel's fix reverted via git stash, regression tests rerun):

    ---SIGNAL---
     Test Files  1 failed (1)
          Tests  1 failed | 18 skipped (19)
    ---MSTEAMS---
     Test Files  1 failed (1)
          Tests  1 failed | 16 skipped (17)
    ---DIRECTDM---
     Test Files  1 failed (1)
          Tests  1 failed | 6 skipped (7)
    

    Each assertion fails on the exact line that reads expect(ctxPayload.CommandSource).toBe("text") (or the SDK commandSource: "text" round-trip), proving the test exercises the bug path. With the fix restored, all three pass.

  • Observed result after fix: Typed authorized control commands on Feishu, IRC, Line, MS Teams, Nextcloud Talk, Signal, ClickClack, QA Channel, and Nostr (plus any plugin built on direct-dm) post the appropriate acknowledgement (✅ Session reset., ✅ New session started., ⚠️ Stopped N subagents, etc.) instead of silently mutating session state under message_tool_only mode.

  • What was not tested: Live channel deployments under the Codex harness for each of the nine surfaces. Each fix is structurally identical to the Mattermost precedent (fix(mattermost): tag typed text-slash control commands so /new and /reset acks survive message_tool_only suppression #86825) which was validated end-to-end, and the negative-control unit assertions exercise the contract the downstream suppression check reads. The full test sweep above covers every channel module.

@amknight
amknight force-pushed the ak/channel-ingress-command-source-sweep branch from 081c1b0 to 1403b84 Compare May 26, 2026 11:11
…ce across all channel ingresses

Sweeps the same root-cause fix as PR #86825 (Mattermost) across the remaining
channels that exhibited the latent gap: Feishu, IRC, Line, MS Teams, Nextcloud
Talk, Signal, ClickClack, QA Channel, and the public 'direct-dm' plugin SDK
helper (with Nostr passing through it as the first internal consumer).

Each channel inbound handler already computed CommandAuthorized for typed slash
commands but never set CommandSource. Without CommandSource: 'text',
resolveCommandTurnContext defaults the turn kind to 'normal', so the explicit-
command exception in source-reply-delivery-mode.ts:54-56 never fires and
acknowledgements ('Session reset.', 'New session started.', 'Stopped N
subagents', etc.) are silently dropped under message_tool_only delivery
modes — most commonly the Codex harness default for DMs that #83571
introduced.

For each channel, this PR sets:

  CommandSource: commandAuthorized && hasControlCommand ? 'text' : undefined

where 'hasControlCommand' uses the channel's existing isControlCommandMessage
helper (not shouldComputeCommandAuthorized, which would be too permissive and
incorrectly tag inline command tokens within ordinary text). For ClickClack
and QA Channel (CommandAuthorized hardcoded true), the tag only depends on
the body being a control command. For Signal and Line, a new optional
'hasControlCommand' field on the entry/params struct carries the signal from
the access-policy layer where it is already computed.

The public direct-dm SDK helper grows an optional commandSource parameter so
third-party plugins built on it can pass through their own control-command
detection; Nostr is updated as the first consumer.

Adds focused regression tests (Signal, MS Teams, direct-dm SDK, plus a
negative-control assertion on the existing MS Teams inline-command test) that
fail without the fix and pass with it.

Fixes #86664.
@amknight

Copy link
Copy Markdown
Member Author

Superseded by #86874

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