diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index 60239e7c608..3243f7db97e 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -55,7 +55,11 @@ export function resolveEffectiveCommandAuthorized(params: { commandAuthorized: boolean; channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization; }): boolean { - return params.channelResolvedCommandAuthorization?.isAuthorizedSender ?? params.commandAuthorized; + const provided = resolveProvidedChannelCommandAuthorization({ + channelResolvedCommandAuthorization: params.channelResolvedCommandAuthorization, + warnOnMalformed: false, + }); + return provided?.isAuthorizedSender ?? params.commandAuthorized; } export function resolveCommandProviderIdFromContext(params: { @@ -67,21 +71,27 @@ export function resolveCommandProviderIdFromContext(params: { function resolveProvidedChannelCommandAuthorization(params: { channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization; + warnOnMalformed?: boolean; }): ChannelResolvedCommandAuthorization | undefined { const { channelResolvedCommandAuthorization: provided } = params; + const maybeWarn = (reason: string) => { + if (params.warnOnMalformed !== false) { + warnMalformedChannelResolvedCommandAuthorization(reason); + } + }; if (!provided || typeof provided !== "object") { return undefined; } if (!Array.isArray(provided.ownerList)) { - warnMalformedChannelResolvedCommandAuthorization("ownerList must be an array"); + maybeWarn("ownerList must be an array"); return undefined; } if (typeof provided.senderIsOwner !== "boolean") { - warnMalformedChannelResolvedCommandAuthorization("senderIsOwner must be a boolean"); + maybeWarn("senderIsOwner must be a boolean"); return undefined; } if (typeof provided.isAuthorizedSender !== "boolean") { - warnMalformedChannelResolvedCommandAuthorization("isAuthorizedSender must be a boolean"); + maybeWarn("isAuthorizedSender must be a boolean"); return undefined; } return { diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 056c786bc52..26029be74f9 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -1505,6 +1505,68 @@ describe("dispatchReplyFromConfig", () => { ); }); + it("does not trust malformed channel auth for plugin inbound-claim events", async () => { + setNoAbort(); + hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({ + status: "handled", + result: { handled: true }, + }); + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "binding-auth-invalid-1", + targetSessionKey: "plugin-binding:codex:auth-invalid-1", + targetKind: "session", + conversation: { + channel: "telegram", + accountId: "default", + conversationId: "chat:trusted", + }, + status: "active", + boundAt: 1710000000000, + metadata: { + pluginBindingOwner: "plugin", + pluginId: "openclaw-codex-app-server", + }, + } satisfies SessionBindingRecord); + const dispatcher = createDispatcher(); + const ctx = buildTestCtx({ + Provider: "telegram", + Surface: "telegram", + OriginatingChannel: "telegram", + OriginatingTo: "telegram:chat:trusted", + To: "telegram:chat:trusted", + AccountId: "default", + SenderId: "trusted-user", + CommandAuthorized: false, + WasMentioned: false, + CommandBody: "/status", + RawBody: "/status", + Body: "/status", + MessageSid: "msg-claim-auth-invalid-1", + SessionKey: "agent:main:telegram:chat:trusted", + }); + const channelResolvedCommandAuthorization = { + ownerList: "trusted-user", + senderIsOwner: true, + isAuthorizedSender: true, + } as unknown as ChannelResolvedCommandAuthorization; + + await dispatchReplyFromConfig({ + ctx, + cfg: emptyConfig, + dispatcher, + replyResolver: vi.fn(async () => ({ text: "unused" }) as ReplyPayload), + replyOptions: { channelResolvedCommandAuthorization }, + }); + + expect(hookMocks.runner.runInboundClaimForPluginOutcome).toHaveBeenCalledWith( + "openclaw-codex-app-server", + expect.objectContaining({ + commandAuthorized: false, + }), + expect.anything(), + ); + }); + it("routes ACP sessions through the runtime branch and streams block replies", async () => { setNoAbort(); const runtime = createAcpRuntime([