fix(commands): validate channel auth override

This commit is contained in:
Marcus Castro
2026-04-14 19:13:28 -03:00
parent 92407d1874
commit 8d64f9fd96
2 changed files with 76 additions and 4 deletions

View File

@@ -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 {

View File

@@ -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([