fix(message): authorize fallback broadcast provider

This commit is contained in:
joshavant
2026-07-28 23:58:08 -05:00
committed by Josh Avant
parent c4cd5a2bb4
commit 6831f05184
2 changed files with 65 additions and 11 deletions
+45
View File
@@ -404,6 +404,7 @@ function createChannelPlugin(params: {
config?: Partial<ChannelPlugin["config"]>;
message?: ChannelMessageAdapterShape;
messaging?: ChannelPlugin["messaging"];
outbound?: ChannelPlugin["outbound"];
}): ChannelPlugin {
return {
id: params.id as ChannelPlugin["id"],
@@ -423,6 +424,7 @@ function createChannelPlugin(params: {
},
...(params.message ? { message: params.message } : {}),
...(params.messaging ? { messaging: params.messaging } : {}),
...(params.outbound ? { outbound: params.outbound } : {}),
actions: {
describeMessageTool:
params.describeMessageTool ??
@@ -1771,6 +1773,44 @@ describe("message tool secret scoping", () => {
broadcastChannel: "slack",
broadcastTargets: ["slack:channel:one", "slack:channel:two"],
},
{
name: "delegated fallback-resolved current-provider broadcast",
channel: "googlechat",
accountId: "alternate",
requesterAccountId: "current",
trusted: true,
origin: undefined,
rejected: true,
broadcast: true,
broadcastChannel: "last",
broadcastTargets: ["googlechat:spaces/current"],
},
{
name: "delegated fallback-resolved broadcast with matching current account",
channel: "googlechat",
accountId: "current",
requesterAccountId: "current",
trusted: true,
origin: undefined,
rejected: false,
broadcast: true,
broadcastChannel: "last",
broadcastTargets: ["googlechat:spaces/current"],
expectedRunnerChannel: "googlechat",
},
{
name: "direct fallback-resolved broadcast with alternate account",
channel: "googlechat",
accountId: "alternate",
requesterAccountId: undefined,
trusted: false,
origin: "direct-operator" as const,
rejected: false,
broadcast: true,
broadcastChannel: "last",
broadcastTargets: ["googlechat:spaces/current"],
expectedRunnerChannel: "googlechat",
},
{
name: "delegated channel-less broadcast with matching current account",
channel: "googlechat",
@@ -1806,6 +1846,7 @@ describe("message tool secret scoping", () => {
listAccountIds: () => ["current", "alternate"],
resolveAccount: () => ({ enabled: true }),
},
outbound: { deliveryMode: "direct", sendText: vi.fn() as never },
});
const slackPlugin = createChannelPlugin({
id: "slack",
@@ -1817,6 +1858,7 @@ describe("message tool secret scoping", () => {
listAccountIds: () => ["alternate"],
resolveAccount: () => ({ enabled: true }),
},
outbound: { deliveryMode: "direct", sendText: vi.fn() as never },
});
setActivePluginRegistry(
createTestRegistry([
@@ -1905,6 +1947,9 @@ describe("message tool secret scoping", () => {
await expect(invocation).resolves.toBeDefined();
expect(mocks.resolveCommandSecretRefsViaGateway).toHaveBeenCalledOnce();
expect(mocks.runMessageAction).toHaveBeenCalledOnce();
if ("expectedRunnerChannel" in testCase) {
expect(firstRunMessageActionInput()?.params?.channel).toBe(testCase.expectedRunnerChannel);
}
},
);
+20 -11
View File
@@ -51,6 +51,7 @@ import {
import { resolveMessageActionTurnCapability } from "../../gateway/message-action-turn-capability.js";
import { createAbortError } from "../../infra/abort-signal.js";
import { sha256Base64UrlPrefix } from "../../infra/crypto-digest.js";
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
import {
resolveMessageBroadcastAccountPlan,
validateExplicitMessageAccountSelection,
@@ -1589,12 +1590,26 @@ 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,
validateExplicitMessageAccountSelection({
cfg: rawConfig,
accountId: requestedAccountId,
checkResolvedAccount: false,
});
const requestedBroadcastChannel = normalizeOptionalLowercaseString(params.channel);
if (
action === "broadcast" &&
requestedBroadcastChannel &&
requestedBroadcastChannel !== "all"
) {
// Authorize and execute the same canonical provider. Otherwise an unavailable
// hint can fall back to the current provider only after account authorization.
const selection = await resolveMessageChannelSelection({
cfg: rawConfig,
channel: requestedBroadcastChannel,
fallbackChannel: effectiveCurrentChannel.currentChannelProvider,
});
params.channel = selection.channel;
}
const scope = resolveMessageSecretScope({
channel: params.channel,
target: params.target,
@@ -1603,7 +1618,6 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
accountId: requestedAccountId,
fallbackAccountId: agentAccountId,
});
const requestedBroadcastChannel = normalizeOptionalLowercaseString(params.channel);
// Broadcast execution only narrows on an explicit non-all channel. Target
// prefixes cannot authorize fewer providers than the runner will execute.
const unscopedExplicitBroadcast =
@@ -1627,12 +1641,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
explicitAccountId,
selectedChannels: broadcastAccountPlan
? broadcastAccountPlan.candidateChannels
: [
requestedScope.channel ??
(action === "broadcast"
? undefined
: trustedTurnContext?.toolContext?.currentChannelProvider),
],
: [scope.channel],
trustedCurrentChannel: trustedTurnContext?.toolContext?.currentChannelProvider,
trustedRequesterAccountId: trustedTurnContext?.requesterAccountId,
hasTrustedTurnContext: trustedTurnContext !== undefined,