From 56f3a2de2536a23c0ad901e490fabf60f4283b5c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 08:27:52 +0000 Subject: [PATCH] fix(security): default-deny command execution --- extensions/zalo/src/monitor.ts | 32 +++++++++++-- extensions/zalouser/src/monitor.ts | 32 +++++++++++-- src/auto-reply/command-detection.test.ts | 10 +++- src/auto-reply/command-detection.ts | 13 ++++++ ...ccepts-thinking-xhigh-codex-models.test.ts | 15 ++++-- ...ng-mixed-messages-acks-immediately.test.ts | 8 ++-- ...nk-low-reasoning-capable-models-no.test.ts | 4 +- ...ists-allowlisted-models-model-list.test.ts | 12 ++--- ...tches-fuzzy-selection-is-ambiguous.test.ts | 8 ++-- ...er-agent-allowlist-addition-global.test.ts | 5 ++ ...atus-alongside-directive-only-acks.test.ts | 9 ++-- ...urrent-elevated-level-as-off-after.test.ts | 6 +++ ...rrent-verbose-level-verbose-has-no.test.ts | 6 ++- ...uzzy-model-matches-model-directive.test.ts | 10 ++-- ...l-verbose-during-flight-run-toggle.test.ts | 6 +-- src/auto-reply/reply.raw-body.test.ts | 5 ++ ...s-activation-from-allowfrom-groups.test.ts | 2 + ...proved-sender-toggle-elevated-mode.test.ts | 1 + ...levated-off-groups-without-mention.test.ts | 3 ++ ...age-summary-current-model-provider.test.ts | 6 +++ ...ne-commands-strips-it-before-agent.test.ts | 2 + ...evated-directive-unapproved-sender.test.ts | 1 + ...-error-cause-embedded-agent-throws.test.ts | 1 + ...inline-status-unauthorized-senders.test.ts | 2 + ...ve-auth-profile-key-snippet-status.test.ts | 3 ++ ...ling.runs-compact-as-gated-command.test.ts | 1 + ...ng.runs-greeting-prompt-bare-reset.test.ts | 2 + ...efault-model-status-not-configured.test.ts | 6 +++ ...uick-model-picker-grouped-by-model.test.ts | 7 +++ src/auto-reply/reply/abort.test.ts | 3 ++ src/auto-reply/reply/abort.ts | 2 +- src/auto-reply/reply/get-reply.ts | 2 +- src/gateway/ws-log.test.ts | 8 ++++ src/gateway/ws-log.ts | 8 +++- src/security/audit.test.ts | 46 +++++++++++++++++++ src/web/auto-reply/monitor/process-message.ts | 6 ++- 36 files changed, 247 insertions(+), 46 deletions(-) diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index 1d581e22f..b43fbc546 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -1,7 +1,12 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { ResolvedZaloAccount } from "./accounts.js"; +import { + hasInlineCommandTokens, + isControlCommandMessage, +} from "../../../src/auto-reply/command-detection.js"; import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; +import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js"; import { ZaloApiError, deleteWebhook, @@ -437,6 +442,22 @@ async function processMessageWithPipeline(params: { const dmPolicy = account.config.dmPolicy ?? "pairing"; const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v)); + const rawBody = text?.trim() || (mediaPath ? "" : ""); + const shouldComputeCommandAuthorized = + isControlCommandMessage(rawBody, config) || hasInlineCommandTokens(rawBody); + const storeAllowFrom = + !isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized) + ? await deps.readChannelAllowFromStore("zalo").catch(() => []) + : []; + const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; + const useAccessGroups = config.commands?.useAccessGroups !== false; + const senderAllowedForCommands = isSenderAllowed(senderId, effectiveAllowFrom); + const commandAuthorized = shouldComputeCommandAuthorized + ? resolveCommandAuthorizedFromAuthorizers({ + useAccessGroups, + authorizers: [{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }], + }) + : undefined; if (!isGroup) { if (dmPolicy === "disabled") { @@ -445,9 +466,7 @@ async function processMessageWithPipeline(params: { } if (dmPolicy !== "open") { - const storeAllowFrom = await deps.readChannelAllowFromStore("zalo").catch(() => []); - const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; - const allowed = isSenderAllowed(senderId, effectiveAllowFrom); + const allowed = senderAllowedForCommands; if (!allowed) { if (dmPolicy === "pairing") { @@ -496,7 +515,11 @@ async function processMessageWithPipeline(params: { }, }); - const rawBody = text?.trim() || (mediaPath ? "" : ""); + if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) { + logVerbose(deps, `zalo: drop control command from unauthorized sender ${senderId}`); + return; + } + const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`; @@ -519,6 +542,7 @@ async function processMessageWithPipeline(params: { ConversationLabel: fromLabel, SenderName: senderName || undefined, SenderId: senderId, + CommandAuthorized: commandAuthorized, Provider: "zalo", Surface: "zalo", MessageSid: message_id, diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index ddf1fd221..b3273505d 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -1,7 +1,12 @@ import type { ChildProcess } from "node:child_process"; import type { RuntimeEnv } from "../../../src/runtime.js"; +import { + hasInlineCommandTokens, + isControlCommandMessage, +} from "../../../src/auto-reply/command-detection.js"; import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; +import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js"; import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js"; import { sendMessageZalouser } from "./send.js"; import type { CoreConfig, ResolvedZalouserAccount, ZcaMessage } from "./types.js"; @@ -105,6 +110,22 @@ async function processMessage( const dmPolicy = account.config.dmPolicy ?? "pairing"; const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v)); + const rawBody = content.trim(); + const shouldComputeCommandAuthorized = + isControlCommandMessage(rawBody, config) || hasInlineCommandTokens(rawBody); + const storeAllowFrom = + !isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized) + ? await deps.readChannelAllowFromStore("zalouser").catch(() => []) + : []; + const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; + const useAccessGroups = config.commands?.useAccessGroups !== false; + const senderAllowedForCommands = isSenderAllowed(senderId, effectiveAllowFrom); + const commandAuthorized = shouldComputeCommandAuthorized + ? resolveCommandAuthorizedFromAuthorizers({ + useAccessGroups, + authorizers: [{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }], + }) + : undefined; if (!isGroup) { if (dmPolicy === "disabled") { @@ -113,9 +134,7 @@ async function processMessage( } if (dmPolicy !== "open") { - const storeAllowFrom = await deps.readChannelAllowFromStore("zalouser").catch(() => []); - const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; - const allowed = isSenderAllowed(senderId, effectiveAllowFrom); + const allowed = senderAllowedForCommands; if (!allowed) { if (dmPolicy === "pairing") { @@ -158,6 +177,11 @@ async function processMessage( } } + if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) { + logVerbose(deps, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`); + return; + } + const peer = isGroup ? { kind: "group" as const, id: chatId } : { kind: "group" as const, id: senderId }; const route = deps.resolveAgentRoute({ @@ -171,7 +195,6 @@ async function processMessage( }, }); - const rawBody = content.trim(); const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`; @@ -194,6 +217,7 @@ async function processMessage( ConversationLabel: fromLabel, SenderName: senderName || undefined, SenderId: senderId, + CommandAuthorized: commandAuthorized, Provider: "zalouser", Surface: "zalouser", MessageSid: message.msgId ?? `${timestamp}`, diff --git a/src/auto-reply/command-detection.test.ts b/src/auto-reply/command-detection.test.ts index f55df8101..fafa82432 100644 --- a/src/auto-reply/command-detection.test.ts +++ b/src/auto-reply/command-detection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { hasControlCommand } from "./command-detection.js"; +import { hasControlCommand, hasInlineCommandTokens } from "./command-detection.js"; import { listChatCommands } from "./commands-registry.js"; import { parseActivationCommand } from "./group-activation.js"; import { parseSendPolicyCommand } from "./send-policy.js"; @@ -72,6 +72,14 @@ describe("control command parsing", () => { expect(hasControlCommand("/send on")).toBe(true); }); + it("detects inline command tokens", () => { + expect(hasInlineCommandTokens("hello /status")).toBe(true); + expect(hasInlineCommandTokens("hey /think high")).toBe(true); + expect(hasInlineCommandTokens("plain text")).toBe(false); + expect(hasInlineCommandTokens("http://example.com/path")).toBe(false); + expect(hasInlineCommandTokens("stop")).toBe(false); + }); + it("ignores telegram commands addressed to other bots", () => { expect( hasControlCommand("/help@otherbot", undefined, { diff --git a/src/auto-reply/command-detection.ts b/src/auto-reply/command-detection.ts index 40cfe7150..226d82d54 100644 --- a/src/auto-reply/command-detection.ts +++ b/src/auto-reply/command-detection.ts @@ -45,3 +45,16 @@ export function isControlCommandMessage( const normalized = normalizeCommandBody(trimmed, options).trim().toLowerCase(); return isAbortTrigger(normalized); } + +/** + * Coarse detection for inline directives/shortcuts (e.g. "hey /status") so channel monitors + * can decide whether to compute CommandAuthorized for a message. + * + * This intentionally errs on the side of false positives; CommandAuthorized only gates + * command/directive execution, not normal chat replies. + */ +export function hasInlineCommandTokens(text?: string): boolean { + const body = text ?? ""; + if (!body.trim()) return false; + return /(?:^|\s)[/!][a-z]/i.test(body); +} diff --git a/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts b/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts index b627f4142..5d4ba9d30 100644 --- a/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts @@ -81,6 +81,7 @@ describe("directive behavior", () => { Body: "/thinking xhigh", From: "+1004", To: "+2000", + CommandAuthorized: true, }, {}, { @@ -90,7 +91,7 @@ describe("directive behavior", () => { workspace: path.join(home, "clawd"), }, }, - whatsapp: { allowFrom: ["*"] }, + channels: { whatsapp: { allowFrom: ["*"] } }, session: { store: storePath }, }, ); @@ -108,6 +109,7 @@ describe("directive behavior", () => { Body: "/thinking xhigh", From: "+1004", To: "+2000", + CommandAuthorized: true, }, {}, { @@ -117,7 +119,7 @@ describe("directive behavior", () => { workspace: path.join(home, "clawd"), }, }, - whatsapp: { allowFrom: ["*"] }, + channels: { whatsapp: { allowFrom: ["*"] } }, session: { store: storePath }, }, ); @@ -135,6 +137,7 @@ describe("directive behavior", () => { Body: "/thinking xhigh", From: "+1004", To: "+2000", + CommandAuthorized: true, }, {}, { @@ -144,7 +147,7 @@ describe("directive behavior", () => { workspace: path.join(home, "clawd"), }, }, - whatsapp: { allowFrom: ["*"] }, + channels: { whatsapp: { allowFrom: ["*"] } }, session: { store: storePath }, }, ); @@ -164,6 +167,7 @@ describe("directive behavior", () => { Body: "/help", From: "+1222", To: "+1222", + CommandAuthorized: true, }, {}, { @@ -201,6 +205,7 @@ describe("directive behavior", () => { Body: "/demo_skill", From: "+1222", To: "+1222", + CommandAuthorized: true, }, {}, { @@ -232,6 +237,7 @@ describe("directive behavior", () => { Body: "/queue collect debounce:bogus cap:zero drop:maybe", From: "+1222", To: "+1222", + CommandAuthorized: true, }, {}, { @@ -263,6 +269,7 @@ describe("directive behavior", () => { From: "+1222", To: "+1222", Provider: "whatsapp", + CommandAuthorized: true, }, {}, { @@ -300,7 +307,7 @@ describe("directive behavior", () => { vi.mocked(runEmbeddedPiAgent).mockReset(); const res = await getReplyFromConfig( - { Body: "/think", From: "+1222", To: "+1222" }, + { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts b/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts index 0a96e53a6..babadf224 100644 --- a/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts @@ -173,7 +173,7 @@ describe("directive behavior", () => { vi.mocked(runEmbeddedPiAgent).mockReset(); const res = await getReplyFromConfig( - { Body: "/verbose on", From: "+1222", To: "+1222" }, + { Body: "/verbose on", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -197,7 +197,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/verbose off", From: "+1222", To: "+1222" }, + { Body: "/verbose off", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -223,7 +223,7 @@ describe("directive behavior", () => { vi.mocked(runEmbeddedPiAgent).mockReset(); const res = await getReplyFromConfig( - { Body: "/think", From: "+1222", To: "+1222" }, + { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -248,7 +248,7 @@ describe("directive behavior", () => { vi.mocked(runEmbeddedPiAgent).mockReset(); const res = await getReplyFromConfig( - { Body: "/think", From: "+1222", To: "+1222" }, + { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts index 953e5e4d8..e1b3098a0 100644 --- a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts @@ -73,7 +73,7 @@ describe("directive behavior", () => { ]); const res = await getReplyFromConfig( - { Body: "/think", From: "+1222", To: "+1222" }, + { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -105,7 +105,7 @@ describe("directive behavior", () => { ]); const res = await getReplyFromConfig( - { Body: "/think", From: "+1222", To: "+1222" }, + { Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts index fd77248f1..4578fb0fb 100644 --- a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts @@ -66,7 +66,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/model list", From: "+1222", To: "+1222" }, + { Body: "/model list", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -97,7 +97,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/model", From: "+1222", To: "+1222" }, + { Body: "/model", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -137,7 +137,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/model list", From: "+1222", To: "+1222" }, + { Body: "/model list", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -178,7 +178,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/model list", From: "+1222", To: "+1222" }, + { Body: "/model list", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -205,7 +205,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" }, + { Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -235,7 +235,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model Opus", From: "+1222", To: "+1222" }, + { Body: "/model Opus", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts index a2e226485..e42ced2db 100644 --- a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts @@ -68,7 +68,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model ki", From: "+1222", To: "+1222" }, + { Body: "/model ki", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -135,7 +135,7 @@ describe("directive behavior", () => { ); const res = await getReplyFromConfig( - { Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" }, + { Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -167,7 +167,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model Opus", From: "+1222", To: "+1222" }, + { Body: "/model Opus", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -200,6 +200,7 @@ describe("directive behavior", () => { From: "+1222", To: "+1222", Provider: "whatsapp", + CommandAuthorized: true, }, {}, { @@ -230,6 +231,7 @@ describe("directive behavior", () => { From: "+1222", To: "+1222", Provider: "whatsapp", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts b/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts index 3e5699204..bf0ac3df2 100644 --- a/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts @@ -72,6 +72,7 @@ describe("directive behavior", () => { Provider: "whatsapp", SenderE164: "+1222", SessionKey: "agent:work:main", + CommandAuthorized: true, }, {}, { @@ -118,6 +119,7 @@ describe("directive behavior", () => { Provider: "whatsapp", SenderE164: "+1333", SessionKey: "agent:work:main", + CommandAuthorized: true, }, {}, { @@ -163,6 +165,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { @@ -200,6 +203,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { @@ -235,6 +239,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts b/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts index 3209a80c3..b9391eb61 100644 --- a/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts @@ -72,6 +72,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { @@ -115,6 +116,7 @@ describe("directive behavior", () => { Provider: "whatsapp", SenderE164: "+1222", SessionKey: "agent:restricted:main", + CommandAuthorized: true, }, {}, { @@ -153,7 +155,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/queue interrupt", From: "+1222", To: "+1222" }, + { Body: "/queue interrupt", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -185,6 +187,7 @@ describe("directive behavior", () => { Body: "/queue collect debounce:2s cap:5 drop:old", From: "+1222", To: "+1222", + CommandAuthorized: true, }, {}, { @@ -219,7 +222,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/queue interrupt", From: "+1222", To: "+1222" }, + { Body: "/queue interrupt", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -234,7 +237,7 @@ describe("directive behavior", () => { ); const res = await getReplyFromConfig( - { Body: "/queue reset", From: "+1222", To: "+1222" }, + { Body: "/queue reset", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts index 554ed71d2..71f7d1556 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts @@ -72,6 +72,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { @@ -99,6 +100,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { @@ -153,6 +155,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, cfg, @@ -164,6 +167,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, cfg, @@ -176,6 +180,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, cfg, @@ -203,6 +208,7 @@ describe("directive behavior", () => { Provider: "whatsapp", SenderE164: "+1222", SessionKey: "agent:restricted:main", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index b623837a1..51e9fd5f1 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -65,7 +65,7 @@ describe("directive behavior", () => { vi.mocked(runEmbeddedPiAgent).mockReset(); const res = await getReplyFromConfig( - { Body: "/verbose", From: "+1222", To: "+1222" }, + { Body: "/verbose", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -90,7 +90,7 @@ describe("directive behavior", () => { vi.mocked(runEmbeddedPiAgent).mockReset(); const res = await getReplyFromConfig( - { Body: "/reasoning", From: "+1222", To: "+1222" }, + { Body: "/reasoning", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -120,6 +120,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { @@ -158,6 +159,7 @@ describe("directive behavior", () => { To: "+1222", Provider: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts b/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts index cfb6d8e80..28191da4a 100644 --- a/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts @@ -66,7 +66,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model kimi", From: "+1222", To: "+1222" }, + { Body: "/model kimi", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -107,7 +107,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model kimi-k2-0905-preview", From: "+1222", To: "+1222" }, + { Body: "/model kimi-k2-0905-preview", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -148,7 +148,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model moonshot/kimi", From: "+1222", To: "+1222" }, + { Body: "/model moonshot/kimi", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -189,7 +189,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model minimax", From: "+1222", To: "+1222" }, + { Body: "/model minimax", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -234,7 +234,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); await getReplyFromConfig( - { Body: "/model minimax/m2.1", From: "+1222", To: "+1222" }, + { Body: "/model minimax/m2.1", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts b/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts index c97a07434..8ed532b0d 100644 --- a/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts @@ -153,7 +153,7 @@ describe("directive behavior", () => { }); await getReplyFromConfig( - { Body: "/verbose on", From: ctx.From, To: ctx.To }, + { Body: "/verbose on", From: ctx.From, To: ctx.To, CommandAuthorized: true }, {}, { agents: { @@ -193,7 +193,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/model", From: "+1222", To: "+1222" }, + { Body: "/model", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { @@ -224,7 +224,7 @@ describe("directive behavior", () => { const storePath = path.join(home, "sessions.json"); const res = await getReplyFromConfig( - { Body: "/model status", From: "+1222", To: "+1222" }, + { Body: "/model status", From: "+1222", To: "+1222", CommandAuthorized: true }, {}, { agents: { diff --git a/src/auto-reply/reply.raw-body.test.ts b/src/auto-reply/reply.raw-body.test.ts index cebe771b5..4f8060d9e 100644 --- a/src/auto-reply/reply.raw-body.test.ts +++ b/src/auto-reply/reply.raw-body.test.ts @@ -54,6 +54,7 @@ describe("RawBody directive parsing", () => { From: "+1222", To: "+1222", ChatType: "group", + CommandAuthorized: true, }; const res = await getReplyFromConfig( @@ -87,6 +88,7 @@ describe("RawBody directive parsing", () => { From: "+1222", To: "+1222", ChatType: "group", + CommandAuthorized: true, }; const res = await getReplyFromConfig( @@ -123,6 +125,7 @@ describe("RawBody directive parsing", () => { From: "+1222", To: "+1222", ChatType: "group", + CommandAuthorized: true, }; const res = await getReplyFromConfig( @@ -160,6 +163,7 @@ describe("RawBody directive parsing", () => { Provider: "whatsapp", Surface: "whatsapp", SenderE164: "+1222", + CommandAuthorized: true, }; const res = await getReplyFromConfig( @@ -207,6 +211,7 @@ describe("RawBody directive parsing", () => { From: "+1222", To: "+1222", ChatType: "group", + CommandAuthorized: true, }; const res = await getReplyFromConfig( diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts index c01a06339..886e19d58 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts @@ -105,6 +105,7 @@ describe("trigger handling", () => { ChatType: "group", Provider: "whatsapp", SenderE164: "+999", + CommandAuthorized: true, }, {}, cfg, @@ -179,6 +180,7 @@ describe("trigger handling", () => { Body: "/new", From: "+1003", To: "+2000", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts index f4bbc5cbb..972c61262 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts @@ -123,6 +123,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, }, {}, cfg, diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts index 692ad89d2..7df801092 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts @@ -132,6 +132,7 @@ describe("trigger handling", () => { To: "whatsapp:+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, ChatType: "group", WasMentioned: false, }, @@ -175,6 +176,7 @@ describe("trigger handling", () => { To: "whatsapp:+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, ChatType: "group", WasMentioned: true, }, @@ -218,6 +220,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, }, {}, cfg, diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts index fd43f3a0c..01e4449c8 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts @@ -116,6 +116,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, }, {}, makeCfg(home), @@ -138,6 +139,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { @@ -162,6 +164,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { @@ -193,6 +196,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1002", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { @@ -217,6 +221,7 @@ describe("trigger handling", () => { Body: "[Dec 5 10:00] stop", From: "+1000", To: "+2000", + CommandAuthorized: true, }, {}, makeCfg(home), @@ -233,6 +238,7 @@ describe("trigger handling", () => { Body: "/stop", From: "+1003", To: "+2000", + CommandAuthorized: true, }, {}, makeCfg(home), diff --git a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts index a5f91ff1e..40ee71275 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts @@ -108,6 +108,7 @@ describe("trigger handling", () => { Body: "please /commands now", From: "+1002", To: "+2000", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { @@ -141,6 +142,7 @@ describe("trigger handling", () => { From: "+1002", To: "+2000", SenderId: "12345", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { diff --git a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts index 586102035..0d6a0b303 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts @@ -161,6 +161,7 @@ describe("trigger handling", () => { SenderName: "Peter Steinberger", SenderUsername: "steipete", SenderTag: "steipete", + CommandAuthorized: true, }, {}, cfg, diff --git a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts index 2ee12491e..9d8debd38 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts @@ -209,6 +209,7 @@ describe("trigger handling", () => { ChatType: "group", Provider: "whatsapp", SenderE164: "+2000", + CommandAuthorized: true, }, {}, cfg, diff --git a/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts b/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts index 2233b8366..0ec644b97 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts @@ -184,6 +184,7 @@ describe("trigger handling", () => { Body: "/help", From: "+1002", To: "+2000", + CommandAuthorized: true, }, {}, makeCfg(home), @@ -218,6 +219,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1000", + CommandAuthorized: true, }, {}, cfg, diff --git a/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts b/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts index f9e690186..2294780c4 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts @@ -146,6 +146,7 @@ describe("trigger handling", () => { To: "+2000", Provider: "whatsapp", SenderE164: "+1002", + CommandAuthorized: true, }, {}, cfg, @@ -176,6 +177,7 @@ describe("trigger handling", () => { Provider: "whatsapp", Surface: "whatsapp", SenderE164: "+1002", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { @@ -208,6 +210,7 @@ describe("trigger handling", () => { Body: "please /help now", From: "+1002", To: "+2000", + CommandAuthorized: true, }, { onBlockReply: async (payload) => { diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts index 7ea06c523..bcc465bce 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts @@ -117,6 +117,7 @@ describe("trigger handling", () => { Body: "/compact focus on decisions", From: "+1003", To: "+2000", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts index f9c5d2392..e8e9da82f 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts @@ -109,6 +109,7 @@ describe("trigger handling", () => { Body: "/reset", From: "+1003", To: "+2000", + CommandAuthorized: true, }, {}, { @@ -173,6 +174,7 @@ describe("trigger handling", () => { Body: "/reset", From: "+1003", To: "+2000", + CommandAuthorized: true, }, {}, { diff --git a/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts b/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts index c028d5c05..2c2461dcd 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts @@ -106,6 +106,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: "telegram:slash:111", + CommandAuthorized: true, }, {}, cfg, @@ -137,6 +138,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: "telegram:slash:111", + CommandAuthorized: true, }, {}, cfg, @@ -156,6 +158,7 @@ describe("trigger handling", () => { Body: " [Dec 5] /restart", From: "+1001", To: "+2000", + CommandAuthorized: true, }, {}, makeCfg(home), @@ -173,6 +176,7 @@ describe("trigger handling", () => { Body: "/restart", From: "+1001", To: "+2000", + CommandAuthorized: true, }, {}, cfg, @@ -189,6 +193,7 @@ describe("trigger handling", () => { Body: "/status", From: "+1002", To: "+2000", + CommandAuthorized: true, }, {}, makeCfg(home), @@ -205,6 +210,7 @@ describe("trigger handling", () => { Body: "/usage", From: "+1002", To: "+2000", + CommandAuthorized: true, }, {}, makeCfg(home), diff --git a/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts b/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts index 1fd21f2f2..ae1ddd8bc 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts @@ -107,6 +107,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: "telegram:slash:111", + CommandAuthorized: true, }, {}, cfg, @@ -137,6 +138,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: "telegram:slash:111", + CommandAuthorized: true, }, {}, cfg, @@ -169,6 +171,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: sessionKey, + CommandAuthorized: true, }, {}, cfg, @@ -197,6 +200,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: sessionKey, + CommandAuthorized: true, }, {}, cfg, @@ -226,6 +230,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: sessionKey, + CommandAuthorized: true, }, {}, cfg, @@ -256,6 +261,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: sessionKey, + CommandAuthorized: true, }, {}, cfg, @@ -285,6 +291,7 @@ describe("trigger handling", () => { Provider: "telegram", Surface: "telegram", SessionKey: sessionKey, + CommandAuthorized: true, }, {}, cfg, diff --git a/src/auto-reply/reply/abort.test.ts b/src/auto-reply/reply/abort.test.ts index 31d8efae5..4406a5a80 100644 --- a/src/auto-reply/reply/abort.test.ts +++ b/src/auto-reply/reply/abort.test.ts @@ -70,6 +70,7 @@ describe("abort detection", () => { ctx: { CommandBody: "/stop", RawBody: "/stop", + CommandAuthorized: true, SessionKey: "telegram:123", Provider: "telegram", Surface: "telegram", @@ -132,6 +133,7 @@ describe("abort detection", () => { ctx: { CommandBody: "/stop", RawBody: "/stop", + CommandAuthorized: true, SessionKey: sessionKey, Provider: "telegram", Surface: "telegram", @@ -188,6 +190,7 @@ describe("abort detection", () => { ctx: { CommandBody: "/stop", RawBody: "/stop", + CommandAuthorized: true, SessionKey: sessionKey, Provider: "telegram", Surface: "telegram", diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 087d7ea78..aec0f8a0e 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -132,7 +132,7 @@ export async function tryFastAbortFromMessage(params: { const abortRequested = normalized === "/stop" || isAbortTrigger(stripped); if (!abortRequested) return { handled: false, aborted: false }; - const commandAuthorized = ctx.CommandAuthorized ?? true; + const commandAuthorized = ctx.CommandAuthorized ?? false; const auth = resolveCommandAuthorization({ ctx, cfg, diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index 80bbea0cb..d14194c6c 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -84,7 +84,7 @@ export async function getReplyFromConfig( activeModel: { provider, model }, }); - const commandAuthorized = ctx.CommandAuthorized ?? true; + const commandAuthorized = ctx.CommandAuthorized ?? false; resolveCommandAuthorization({ ctx, cfg, diff --git a/src/gateway/ws-log.test.ts b/src/gateway/ws-log.test.ts index b9734e36b..037ccf782 100644 --- a/src/gateway/ws-log.test.ts +++ b/src/gateway/ws-log.test.ts @@ -18,6 +18,14 @@ describe("gateway ws log helpers", () => { expect(formatForLog(obj)).toBe("Oops: failed: code=E1"); }); + test("formatForLog redacts obvious secrets", () => { + const token = "sk-abcdefghijklmnopqrstuvwxyz123456"; + const out = formatForLog({ token }); + expect(out).toContain("token"); + expect(out).not.toContain(token); + expect(out).toContain("…"); + }); + test("summarizeAgentEventForWsLog extracts useful fields", () => { const summary = summarizeAgentEventForWsLog({ runId: "12345678-1234-1234-1234-123456789abc", diff --git a/src/gateway/ws-log.ts b/src/gateway/ws-log.ts index ffff8675d..31f88584a 100644 --- a/src/gateway/ws-log.ts +++ b/src/gateway/ws-log.ts @@ -1,10 +1,15 @@ import chalk from "chalk"; import { isVerbose } from "../globals.js"; +import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact.js"; import { shouldLogSubsystemToConsole } from "../logging.js"; import { DEFAULT_WS_SLOW_MS, getGatewayWsLogStyle } from "./ws-logging.js"; const LOG_VALUE_LIMIT = 240; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const WS_LOG_REDACT_OPTIONS = { + mode: "tools" as const, + patterns: getDefaultRedactPatterns(), +}; type WsInflightEntry = { ts: number; @@ -61,7 +66,8 @@ export function formatForLog(value: unknown): string { ? String(value) : JSON.stringify(value); if (!str) return ""; - return str.length > LOG_VALUE_LIMIT ? `${str.slice(0, LOG_VALUE_LIMIT)}...` : str; + const redacted = redactSensitiveText(str, WS_LOG_REDACT_OPTIONS); + return redacted.length > LOG_VALUE_LIMIT ? `${redacted.slice(0, LOG_VALUE_LIMIT)}...` : redacted; } catch { return String(value); } diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 85701403d..377bdeaae 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -269,6 +269,52 @@ describe("security audit", () => { } }); + it("does not flag Discord slash commands when dm.allowFrom includes a Discord snowflake id", async () => { + const prevStateDir = process.env.CLAWDBOT_STATE_DIR; + const tmp = await fs.mkdtemp( + path.join(os.tmpdir(), "clawdbot-security-audit-discord-allowfrom-snowflake-"), + ); + process.env.CLAWDBOT_STATE_DIR = tmp; + await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); + try { + const cfg: ClawdbotConfig = { + channels: { + discord: { + enabled: true, + token: "t", + dm: { allowFrom: ["387380367612706819"] }, + groupPolicy: "allowlist", + guilds: { + "123": { + channels: { + general: { allow: true }, + }, + }, + }, + }, + }, + }; + + const res = await runSecurityAudit({ + config: cfg, + includeFilesystem: false, + includeChannelSecurity: true, + plugins: [discordPlugin], + }); + + expect(res.findings).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + checkId: "channels.discord.commands.native.no_allowlists", + }), + ]), + ); + } finally { + if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR; + else process.env.CLAWDBOT_STATE_DIR = prevStateDir; + } + }); + it("flags Discord slash commands when access-group enforcement is disabled and no users allowlist exists", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-audit-discord-open-")); diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 5b00a957b..f5d7ee198 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -16,7 +16,7 @@ import { import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js"; import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; import type { ReplyPayload } from "../../../auto-reply/types.js"; -import { isControlCommandMessage } from "../../../auto-reply/command-detection.js"; +import { hasInlineCommandTokens, isControlCommandMessage } from "../../../auto-reply/command-detection.js"; import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js"; import { toLocationContext } from "../../../channels/location.js"; import type { loadConfig } from "../../../config/config.js"; @@ -229,7 +229,9 @@ export async function processMessage(params: { const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp"); let didLogHeartbeatStrip = false; let didSendReply = false; - const commandAuthorized = isControlCommandMessage(params.msg.body, params.cfg) + const shouldComputeCommandAuthorized = + isControlCommandMessage(params.msg.body, params.cfg) || hasInlineCommandTokens(params.msg.body); + const commandAuthorized = shouldComputeCommandAuthorized ? await resolveWhatsAppCommandAuthorized({ cfg: params.cfg, msg: params.msg }) : undefined; const configuredResponsePrefix = params.cfg.messages?.responsePrefix;