From 66aa4b2ba71bdb74ba94b37ba5f011f06b9bf0bb Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:52:12 -0500 Subject: [PATCH] fix: preserve broadcast account metadata checks --- src/agents/tools/message-tool.test.ts | 8 +++++++- src/agents/tools/message-tool.ts | 14 +++++++++++--- src/commands/message.test.ts | 5 ++++- src/infra/outbound/message-account-selection.ts | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 12537ba10667..b08b11e39ac3 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -1666,7 +1666,10 @@ describe("message tool secret scoping", () => { actions: ["send"], config: { listAccountIds: () => ["shared"], - resolveAccount: () => ({ enabled: true }), + inspectAccount: () => ({ enabled: true }), + resolveAccount: () => { + throw new Error("unresolved Slack SecretRef"); + }, }, }); const telegramPlugin = createChannelPlugin({ @@ -1677,6 +1680,7 @@ describe("message tool secret scoping", () => { actions: ["send"], config: { listAccountIds: () => ["shared"], + isEnabled: () => false, resolveAccount: () => ({ enabled: false }), }, }); @@ -1707,6 +1711,8 @@ describe("message tool secret scoping", () => { mockSendResult({ channel: "slack", to: "channel:ops" }); const tool = createMessageTool({ config: rawConfig as never, + currentChannelProvider: "telegram", + currentChannelId: "channel:current", getScopedChannelsCommandSecretTargets: mocks.getScopedChannelsCommandSecretTargets as never, resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway as never, runMessageAction: mocks.runMessageAction as never, diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index a84dad432b33..a7c4773857d7 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -1560,6 +1560,12 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { const gatewayOpts = readGatewayCallOptions(params); const rawConfig = options?.config ?? loadConfigForTool(); const requestedAccountId = readStringParam(params, "accountId"); + const requestedScope = resolveMessageSecretScope({ + channel: params.channel, + target: params.target, + targets: params.targets, + accountId: requestedAccountId, + }); const scope = resolveMessageSecretScope({ channel: params.channel, target: params.target, @@ -1568,9 +1574,11 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { accountId: requestedAccountId, fallbackAccountId: agentAccountId, }); + const unscopedExplicitBroadcast = + action === "broadcast" && !requestedScope.channel && requestedAccountId !== undefined; const explicitAccountId = validateExplicitMessageAccountSelection({ cfg: rawConfig, - channel: scope.channel, + channel: unscopedExplicitBroadcast ? undefined : scope.channel, accountId: requestedAccountId, checkResolvedAccount: false, }); @@ -1579,7 +1587,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { params.accountId = explicitAccountId; } const broadcastAccountPlan = - action === "broadcast" && !scope.channel && explicitAccountId + unscopedExplicitBroadcast && explicitAccountId ? resolveMessageBroadcastAccountPlan({ cfg: rawConfig, accountId: explicitAccountId, @@ -1587,7 +1595,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { : undefined; const scopedTargets = getScopedSecretTargetsForTool({ config: rawConfig, - channel: scope.channel, + channel: broadcastAccountPlan ? undefined : scope.channel, ...(broadcastAccountPlan ? { channels: broadcastAccountPlan.secretChannels } : {}), accountId: scope.accountId, }); diff --git a/src/commands/message.test.ts b/src/commands/message.test.ts index 10cfd04a1273..0aa5195668a0 100644 --- a/src/commands/message.test.ts +++ b/src/commands/message.test.ts @@ -164,7 +164,10 @@ function createAccountPlugin(id: "slack" | "telegram", accountIds: string[]): Ch capabilities: { chatTypes: ["direct", "group"], media: true }, config: { listAccountIds: () => accountIds, - resolveAccount: () => ({ enabled: true }), + inspectAccount: () => ({ enabled: true }), + resolveAccount: () => { + throw new Error("raw account credentials must not resolve during planning"); + }, }, }; } diff --git a/src/infra/outbound/message-account-selection.ts b/src/infra/outbound/message-account-selection.ts index 584ba7142b40..39d271bc93b9 100644 --- a/src/infra/outbound/message-account-selection.ts +++ b/src/infra/outbound/message-account-selection.ts @@ -154,7 +154,23 @@ export function resolveMessageBroadcastAccountPlan(params: { channel: plugin.id, accountId, plugin, + checkResolvedAccount: false, }); + // Prefer the SecretRef-safe metadata view. Legacy plugins without it keep + // their existing resolver contract; a resolver that cannot read refs fails closed. + const accountForEnablement = + plugin.config.inspectAccount?.(params.cfg, accountId) ?? + plugin.config.resolveAccount(params.cfg, accountId); + if ( + accountForEnablement === undefined || + !resolveChannelAccountEnabled({ + plugin, + account: accountForEnablement, + cfg: params.cfg, + }) + ) { + throw new Error(`Account "${accountId}" for channel ${plugin.id} is disabled.`); + } return [plugin.id]; } catch { return [];