fix(commands): tighten channel auth contract

This commit is contained in:
Marcus Castro
2026-04-14 18:48:32 -03:00
parent cf81c6de34
commit d0a2b4d103
18 changed files with 183 additions and 105 deletions

View File

@@ -1,2 +1,2 @@
c0a29a6e8d8502c7692074df047572ef5c04b745e895eadf1bde5915af0205af plugin-sdk-api-baseline.json
b3bf862306dae859eb3858366c496b10a4f595507c7b11642e50923b72a4fbff plugin-sdk-api-baseline.jsonl
441a818ee54496e33987431bd3ddc40779b78449873fed107ac4f44167899485 plugin-sdk-api-baseline.json
43a2440d74a47ef88bc16c6e414218853544caa1183c5802b6187b40e66ca9ed plugin-sdk-api-baseline.jsonl

View File

@@ -17,7 +17,10 @@ import {
isInternalMessageChannel,
normalizeMessageChannel,
} from "../utils/message-channel.js";
import type { CommandAuthorization, ResolvedCommandAuthorization } from "./command-auth.types.js";
import type {
ChannelResolvedCommandAuthorization,
CommandAuthorization,
} from "./command-auth.types.js";
import type { MsgContext } from "./templating.js";
type InferredProviderCandidate = {
@@ -44,56 +47,44 @@ type OwnerAuthorizationState = {
ownerList: string[];
};
function warnMalformedResolvedCommandAuthorization(reason: string) {
console.warn(`[command-auth] ignoring malformed ResolvedCommandAuthorization: ${reason}`);
function warnMalformedChannelResolvedCommandAuthorization(reason: string) {
console.warn(`[command-auth] ignoring malformed ChannelResolvedCommandAuthorization: ${reason}`);
}
export function resolveEffectiveCommandAuthorized(params: {
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): boolean {
return params.resolvedCommandAuthorization?.isAuthorizedSender ?? params.commandAuthorized;
return params.channelResolvedCommandAuthorization?.isAuthorizedSender ?? params.commandAuthorized;
}
function resolveDirectProviderIdFromContext(ctx: MsgContext): ChannelId | undefined {
const explicitMessageChannels = [ctx.Surface, ctx.OriginatingChannel, ctx.Provider]
.map((value) => normalizeMessageChannel(value))
.filter((value): value is string => Boolean(value));
const explicitMessageChannel = explicitMessageChannels.find(
(value) => value !== INTERNAL_MESSAGE_CHANNEL,
);
return (
normalizeAnyChannelId(explicitMessageChannel ?? undefined) ??
(explicitMessageChannel as ChannelId | undefined) ??
normalizeAnyChannelId(ctx.Provider) ??
normalizeAnyChannelId(ctx.Surface) ??
normalizeAnyChannelId(ctx.OriginatingChannel) ??
undefined
);
}
function resolveProvidedCommandAuthorization(params: {
export function resolveCommandProviderIdFromContext(params: {
ctx: MsgContext;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
}): ResolvedCommandAuthorization | undefined {
const { ctx, resolvedCommandAuthorization: provided } = params;
cfg: OpenClawConfig;
}): ChannelId | undefined {
return resolveProviderFromContext(params.ctx, params.cfg).providerId;
}
function resolveProvidedChannelCommandAuthorization(params: {
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): ChannelResolvedCommandAuthorization | undefined {
const { channelResolvedCommandAuthorization: provided } = params;
if (!provided || typeof provided !== "object") {
return undefined;
}
if (!Array.isArray(provided.ownerList)) {
warnMalformedResolvedCommandAuthorization("ownerList must be an array");
warnMalformedChannelResolvedCommandAuthorization("ownerList must be an array");
return undefined;
}
if (typeof provided.senderIsOwner !== "boolean") {
warnMalformedResolvedCommandAuthorization("senderIsOwner must be a boolean");
warnMalformedChannelResolvedCommandAuthorization("senderIsOwner must be a boolean");
return undefined;
}
if (typeof provided.isAuthorizedSender !== "boolean") {
warnMalformedResolvedCommandAuthorization("isAuthorizedSender must be a boolean");
warnMalformedChannelResolvedCommandAuthorization("isAuthorizedSender must be a boolean");
return undefined;
}
return {
providerId: provided.providerId ?? resolveDirectProviderIdFromContext(ctx),
ownerList: normalizeStringEntries(provided.ownerList),
senderIsOwner: provided.senderIsOwner,
isAuthorizedSender: provided.isAuthorizedSender,
@@ -674,16 +665,15 @@ export function resolveCommandAuthorization(params: {
ctx: MsgContext;
cfg: OpenClawConfig;
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): CommandAuthorization {
const { ctx, cfg, commandAuthorized, resolvedCommandAuthorization } = params;
const provided = resolveProvidedCommandAuthorization({
ctx,
resolvedCommandAuthorization,
const { ctx, cfg, commandAuthorized, channelResolvedCommandAuthorization } = params;
const provided = resolveProvidedChannelCommandAuthorization({
channelResolvedCommandAuthorization,
});
const { providerId, hadResolutionError: providerResolutionError } = provided
? {
providerId: provided.providerId ?? resolveDirectProviderIdFromContext(ctx),
providerId: resolveCommandProviderIdFromContext({ ctx, cfg }),
hadResolutionError: false,
}
: resolveProviderFromContext(ctx, cfg);
@@ -704,6 +694,8 @@ export function resolveCommandAuthorization(params: {
});
if (provided) {
// Channel-provided command auth outcomes are authoritative, but core still
// derives sender identity from inbound context for non-authorization uses.
const matchedSender = provided.ownerList.length
? senderCandidates.find((candidate) => provided.ownerList.includes(candidate))
: undefined;

View File

@@ -1,7 +1,11 @@
import type { ChannelId } from "../channels/plugins/channel-id.types.js";
export type ResolvedCommandAuthorization = {
providerId?: ChannelId;
/**
* Trusted channel-to-core command auth facts for a single inbound turn.
* When present, these outcomes are authoritative for command authorization only.
* Core still derives generic sender identity and message context from MsgContext.
*/
export type ChannelResolvedCommandAuthorization = {
ownerList: string[];
senderIsOwner: boolean;
isAuthorizedSender: boolean;

View File

@@ -3,7 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
import { resolveCommandAuthorization } from "./command-auth.js";
import type { ResolvedCommandAuthorization } from "./command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "./command-auth.types.js";
import { hasControlCommand, hasInlineCommandTokens } from "./command-detection.js";
import { listChatCommands } from "./commands-registry.js";
import { parseActivationCommand } from "./group-activation.js";
@@ -388,8 +388,7 @@ describe("resolveCommandAuthorization", () => {
ctx: makeWhatsAppContext("otheruser"),
cfg: commandsAllowFromConfig,
commandAuthorized: false,
resolvedCommandAuthorization: {
providerId: "whatsapp",
channelResolvedCommandAuthorization: {
ownerList: ["+19995550123"],
senderIsOwner: true,
isAuthorizedSender: true,
@@ -410,16 +409,15 @@ describe("resolveCommandAuthorization", () => {
ctx: makeWhatsAppContext("otheruser"),
cfg: commandsAllowFromConfig,
commandAuthorized: false,
resolvedCommandAuthorization: {
providerId: "whatsapp",
channelResolvedCommandAuthorization: {
ownerList: "not-an-array",
senderIsOwner: true,
isAuthorizedSender: true,
} as unknown as ResolvedCommandAuthorization,
} as unknown as ChannelResolvedCommandAuthorization,
});
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("malformed ResolvedCommandAuthorization"),
expect.stringContaining("malformed ChannelResolvedCommandAuthorization"),
);
expect(auth.providerId).toBe("whatsapp");
expect(auth.isAuthorizedSender).toBe(false);
@@ -428,12 +426,12 @@ describe("resolveCommandAuthorization", () => {
}
});
it("backfills providerId from context when channel-resolved authorization omits it", () => {
it("keeps provider inference in core when channel-resolved authorization is provided", () => {
const auth = resolveCommandAuthorization({
ctx: makeWhatsAppContext("otheruser"),
cfg: commandsAllowFromConfig,
commandAuthorized: false,
resolvedCommandAuthorization: {
channelResolvedCommandAuthorization: {
ownerList: ["123"],
senderIsOwner: false,
isAuthorizedSender: false,
@@ -443,6 +441,27 @@ describe("resolveCommandAuthorization", () => {
expect(auth.providerId).toBe("whatsapp");
});
it("preserves fallback provider inference when channel-resolved authorization is provided", () => {
const auth = resolveCommandAuthorization({
ctx: {
From: "telegram:123",
} as MsgContext,
cfg: {
channels: {
telegram: {},
},
} as OpenClawConfig,
commandAuthorized: false,
channelResolvedCommandAuthorization: {
ownerList: ["123"],
senderIsOwner: true,
isAuthorizedSender: true,
},
});
expect(auth.providerId).toBe("telegram");
});
it("uses commands.allowFrom provider-specific list over global", () => {
const cfg = {
commands: {

View File

@@ -1,6 +1,6 @@
import type { ImageContent } from "@mariozechner/pi-ai";
import type { PromptImageOrderEntry } from "../media/prompt-image-order.js";
import type { ResolvedCommandAuthorization } from "./command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "./command-auth.types.js";
import type { ReplyPayload } from "./reply-payload.js";
import type { TypingController } from "./reply/typing.js";
@@ -31,8 +31,8 @@ export type ReplyThreadingPolicy = {
};
export type GetReplyOptions = {
/** Trusted channel-resolved command auth snapshot for this inbound turn. */
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
/** Trusted channel command auth outcomes for this inbound turn. Core still derives sender/context data. */
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
/** Override run id for agent events (defaults to random UUID). */
runId?: string;
/** Abort signal for the underlying agent run. */

View File

@@ -1,5 +1,5 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import type { FinalizedMsgContext } from "../templating.js";
export type FastAbortResult = {
@@ -11,7 +11,7 @@ export type FastAbortResult = {
export type TryFastAbortFromMessage = (params: {
ctx: FinalizedMsgContext;
cfg: OpenClawConfig;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}) => Promise<FastAbortResult>;
export type FormatAbortReplyText = (stoppedSubagents?: number) => string;

View File

@@ -4,7 +4,7 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import {
__testing as abortTesting,
getAbortMemory,
@@ -112,7 +112,7 @@ describe("abort detection", () => {
from: string;
to: string;
commandAuthorized?: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
targetSessionKey?: string;
messageSid?: string;
timestamp?: number;
@@ -132,7 +132,7 @@ describe("abort detection", () => {
...(typeof params.timestamp === "number" ? { Timestamp: params.timestamp } : {}),
}),
cfg: params.cfg,
resolvedCommandAuthorization: params.resolvedCommandAuthorization,
channelResolvedCommandAuthorization: params.channelResolvedCommandAuthorization,
});
}
@@ -406,8 +406,7 @@ describe("abort detection", () => {
from: "telegram:123",
to: "telegram:123",
commandAuthorized: false,
resolvedCommandAuthorization: {
providerId: "telegram",
channelResolvedCommandAuthorization: {
ownerList: ["telegram:123"],
senderIsOwner: true,
isAuthorizedSender: true,

View File

@@ -27,7 +27,7 @@ import {
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { resolveCommandAuthorization } from "../command-auth.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import type { FinalizedMsgContext } from "../templating.js";
import {
applyAbortCutoffToSessionEntry,
@@ -226,9 +226,9 @@ export function stopSubagentsForRequester(params: {
export async function tryFastAbortFromMessage(params: {
ctx: FinalizedMsgContext;
cfg: OpenClawConfig;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): Promise<{ handled: boolean; aborted: boolean; stoppedSubagents?: number }> {
const { ctx, cfg, resolvedCommandAuthorization } = params;
const { ctx, cfg, channelResolvedCommandAuthorization } = params;
const targetKey =
normalizeOptionalString(ctx.CommandTargetSessionKey) ?? normalizeOptionalString(ctx.SessionKey);
// Use RawBody/CommandBody for abort detection (clean message without structural context).
@@ -255,7 +255,7 @@ export async function tryFastAbortFromMessage(params: {
ctx,
cfg,
commandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
if (!auth.isAuthorizedSender) {
return { handled: false, aborted: false };

View File

@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { resolveCommandAuthorization } from "../command-auth.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import { normalizeCommandBody } from "../commands-registry-normalize.js";
import type { MsgContext } from "../templating.js";
import type { CommandContext } from "./commands-types.js";
@@ -15,14 +15,14 @@ export function buildCommandContext(params: {
isGroup: boolean;
triggerBodyNormalized: string;
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): CommandContext {
const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } = params;
const auth = resolveCommandAuthorization({
ctx,
cfg,
commandAuthorized: params.commandAuthorized,
resolvedCommandAuthorization: params.resolvedCommandAuthorization,
channelResolvedCommandAuthorization: params.channelResolvedCommandAuthorization,
});
const surface = normalizeLowercaseStringOrEmpty(ctx.Surface ?? ctx.Provider);
const channel = normalizeLowercaseStringOrEmpty(ctx.Provider ?? surface);

View File

@@ -19,7 +19,7 @@ import {
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import type { MsgContext } from "../templating.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import type { ReplyDispatcher } from "./reply-dispatcher.js";
@@ -1395,7 +1395,7 @@ describe("dispatchReplyFromConfig", () => {
});
});
it("derives CommandAuthorized from resolved command auth before hook and reply dispatch", async () => {
it("derives CommandAuthorized from channel auth before hook and reply dispatch", async () => {
setNoAbort();
hookMocks.runner.hasHooks.mockImplementation(
(hookName?: string) => hookName === "reply_dispatch",
@@ -1409,12 +1409,11 @@ describe("dispatchReplyFromConfig", () => {
CommandBody: "/status",
CommandAuthorized: false,
});
const resolvedCommandAuthorization = {
providerId: "telegram",
const channelResolvedCommandAuthorization = {
ownerList: ["telegram:123"],
senderIsOwner: true,
isAuthorizedSender: true,
} satisfies ResolvedCommandAuthorization;
} satisfies ChannelResolvedCommandAuthorization;
const replyResolver = vi.fn(async (_ctx: MsgContext) => ({ text: "hi" }) as ReplyPayload);
await dispatchReplyFromConfig({
@@ -1422,13 +1421,13 @@ describe("dispatchReplyFromConfig", () => {
cfg: emptyConfig,
dispatcher,
replyResolver,
replyOptions: { resolvedCommandAuthorization },
replyOptions: { channelResolvedCommandAuthorization },
});
expect(mocks.tryFastAbortFromMessage).toHaveBeenCalledWith(
expect.objectContaining({
ctx: expect.objectContaining({ CommandAuthorized: true }),
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
}),
);
expect(hookMocks.runner.runReplyDispatch).toHaveBeenCalledWith(
@@ -1439,11 +1438,73 @@ describe("dispatchReplyFromConfig", () => {
);
expect(replyResolver).toHaveBeenCalledWith(
expect.objectContaining({ CommandAuthorized: true }),
expect.objectContaining({ resolvedCommandAuthorization }),
expect.objectContaining({ channelResolvedCommandAuthorization }),
undefined,
);
});
it("derives CommandAuthorized from channel auth before plugin inbound-claim events", async () => {
setNoAbort();
hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({
status: "handled",
result: { handled: true },
});
sessionBindingMocks.resolveByConversation.mockReturnValue({
bindingId: "binding-auth-1",
targetSessionKey: "plugin-binding:codex:auth123",
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-1",
SessionKey: "agent:main:telegram:chat:trusted",
});
const channelResolvedCommandAuthorization = {
ownerList: ["trusted-user"],
senderIsOwner: true,
isAuthorizedSender: true,
} satisfies 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: true,
}),
expect.anything(),
);
});
it("routes ACP sessions through the runtime branch and streams block replies", async () => {
setNoAbort();
const runtime = createAcpRuntime([

View File

@@ -203,10 +203,11 @@ export async function dispatchReplyFromConfig(
params: DispatchFromConfigParams,
): Promise<DispatchFromConfigResult> {
const { ctx, cfg, dispatcher } = params;
const resolvedCommandAuthorization = params.replyOptions?.resolvedCommandAuthorization;
const channelResolvedCommandAuthorization =
params.replyOptions?.channelResolvedCommandAuthorization;
const effectiveCommandAuthorized = resolveEffectiveCommandAuthorized({
commandAuthorized: ctx.CommandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
const dispatchCtx =
effectiveCommandAuthorized === ctx.CommandAuthorized
@@ -304,13 +305,16 @@ export async function dispatchReplyFromConfig(
typeof ctx.Timestamp === "number" && Number.isFinite(ctx.Timestamp) ? ctx.Timestamp : undefined;
const messageIdForHook =
ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast;
const hookContext = deriveInboundMessageHookContext(ctx, { messageId: messageIdForHook });
const hookContext = deriveInboundMessageHookContext(dispatchCtx, { messageId: messageIdForHook });
const { isGroup, groupId } = hookContext;
const inboundClaimContext = toPluginInboundClaimContext(hookContext);
const inboundClaimEvent = toPluginInboundClaimEvent(hookContext, {
commandAuthorized:
typeof ctx.CommandAuthorized === "boolean" ? ctx.CommandAuthorized : undefined,
wasMentioned: typeof ctx.WasMentioned === "boolean" ? ctx.WasMentioned : undefined,
typeof dispatchCtx.CommandAuthorized === "boolean"
? dispatchCtx.CommandAuthorized
: undefined,
wasMentioned:
typeof dispatchCtx.WasMentioned === "boolean" ? dispatchCtx.WasMentioned : undefined,
});
// Check if we should route replies to originating channel instead of dispatcher.
@@ -579,7 +583,7 @@ export async function dispatchReplyFromConfig(
const fastAbort = await fastAbortResolver({
ctx: dispatchCtx,
cfg,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
if (fastAbort.handled) {
let queuedFinal = false;

View File

@@ -11,7 +11,7 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import { shouldHandleTextCommands } from "../commands-text-routing.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
@@ -139,7 +139,7 @@ export async function resolveReplyDirectives(params: {
isGroup: boolean;
triggerBodyNormalized: string;
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
defaultProvider: string;
defaultModel: string;
aliasIndex: ModelAliasIndex;
@@ -197,7 +197,7 @@ export async function resolveReplyDirectives(params: {
isGroup,
triggerBodyNormalized,
commandAuthorized,
resolvedCommandAuthorization: params.resolvedCommandAuthorization,
channelResolvedCommandAuthorization: params.channelResolvedCommandAuthorization,
});
const allowTextCommands = shouldHandleTextCommands({
cfg,

View File

@@ -11,7 +11,8 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import { resolveCommandProviderIdFromContext } from "../command-auth.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import { normalizeCommandBody } from "../commands-registry.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import type { CommandContext } from "./commands-types.js";
@@ -160,7 +161,7 @@ export function buildFastReplyCommandContext(params: {
isGroup: boolean;
triggerBodyNormalized: string;
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): CommandContext {
const {
ctx,
@@ -170,21 +171,19 @@ export function buildFastReplyCommandContext(params: {
isGroup,
triggerBodyNormalized,
commandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
} = params;
const surface = normalizeOptionalLowercaseString(ctx.Surface ?? ctx.Provider) ?? "";
const channel = normalizeOptionalLowercaseString(ctx.Provider ?? surface) ?? "";
const from = normalizeOptionalString(ctx.From);
const to = normalizeOptionalString(ctx.To);
const auth = resolvedCommandAuthorization;
const auth = channelResolvedCommandAuthorization;
const providerId = resolveCommandProviderIdFromContext({ ctx, cfg });
return {
surface,
channel,
channelId:
auth?.providerId ??
normalizeAnyChannelId(channel) ??
normalizeAnyChannelId(surface) ??
undefined,
providerId ?? normalizeAnyChannelId(channel) ?? normalizeAnyChannelId(surface) ?? undefined,
ownerList: auth?.ownerList ?? [],
senderIsOwner: auth?.senderIsOwner ?? false,
isAuthorizedSender: auth?.isAuthorizedSender ?? commandAuthorized,

View File

@@ -178,7 +178,7 @@ export async function getReplyFromConfig(
);
const resolvedOpts =
mergedSkillFilter !== undefined ? { ...opts, skillFilter: mergedSkillFilter } : opts;
const resolvedCommandAuthorization = resolvedOpts?.resolvedCommandAuthorization;
const channelResolvedCommandAuthorization = resolvedOpts?.channelResolvedCommandAuthorization;
const agentCfg = cfg.agents?.defaults;
const sessionCfg = cfg.session;
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
@@ -235,7 +235,7 @@ export async function getReplyFromConfig(
const finalized = finalizeInboundContext(ctx);
const commandAuthorized = resolveEffectiveCommandAuthorized({
commandAuthorized: finalized.CommandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
const finalizedCtx =
commandAuthorized === finalized.CommandAuthorized
@@ -272,7 +272,7 @@ export async function getReplyFromConfig(
ctx: finalizedCtx,
cfg,
commandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
let {
sessionCtx,
@@ -371,7 +371,7 @@ export async function getReplyFromConfig(
isGroup,
triggerBodyNormalized,
commandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
return runPreparedReply({
ctx,
@@ -447,7 +447,7 @@ export async function getReplyFromConfig(
isGroup,
triggerBodyNormalized,
commandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
defaultProvider,
defaultModel,
aliasIndex,

View File

@@ -45,7 +45,7 @@ import {
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.shared.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import { resolveCommandAuthorization } from "../command-auth.js";
import type { ResolvedCommandAuthorization } from "../command-auth.types.js";
import type { ChannelResolvedCommandAuthorization } from "../command-auth.types.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import { resolveConversationBindingContextFromMessage } from "./conversation-binding-input.js";
import { normalizeInboundTextNewlines } from "./inbound-text.js";
@@ -159,7 +159,7 @@ function isResetAuthorizedForContext(params: {
ctx: MsgContext;
cfg: OpenClawConfig;
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): boolean {
const auth = resolveCommandAuthorization(params);
if (!params.commandAuthorized && !auth.isAuthorizedSender) {
@@ -239,9 +239,9 @@ export async function initSessionState(params: {
ctx: MsgContext;
cfg: OpenClawConfig;
commandAuthorized: boolean;
resolvedCommandAuthorization?: ResolvedCommandAuthorization;
channelResolvedCommandAuthorization?: ChannelResolvedCommandAuthorization;
}): Promise<SessionInitResult> {
const { ctx, cfg, commandAuthorized, resolvedCommandAuthorization } = params;
const { ctx, cfg, commandAuthorized, channelResolvedCommandAuthorization } = params;
const conversationBindingContext = resolveSessionConversationBindingContext(cfg, ctx);
// Native slash commands (Telegram/Discord/Slack) are delivered on a separate
// "slash session" key, but should mutate the target chat session.
@@ -341,7 +341,7 @@ export async function initSessionState(params: {
ctx,
cfg,
commandAuthorized,
resolvedCommandAuthorization,
channelResolvedCommandAuthorization,
});
// Timestamp/message prefixes (e.g. "[Dec 4 17:35] ") are added by the
// web inbox before we get here. They prevented reset triggers like "/new"

View File

@@ -14,6 +14,6 @@ export {
export { resolveNativeCommandSessionTargets } from "../channels/native-command-session-targets.js";
export { resolveCommandAuthorization } from "../auto-reply/command-auth.js";
export type {
ChannelResolvedCommandAuthorization,
CommandAuthorization,
ResolvedCommandAuthorization,
} from "../auto-reply/command-auth.types.js";

View File

@@ -68,8 +68,8 @@ export {
} from "../channels/native-command-session-targets.js";
export { resolveCommandAuthorization } from "../auto-reply/command-auth.js";
export type {
ChannelResolvedCommandAuthorization,
CommandAuthorization,
ResolvedCommandAuthorization,
} from "../auto-reply/command-auth.types.js";
export {
listReservedChatSlashCommandNames,

View File

@@ -874,8 +874,8 @@ describe("plugin-sdk subpath exports", () => {
"buildCommandsMessage",
"buildCommandsMessagePaginated",
"buildCommandsPaginationKeyboard",
"ChannelResolvedCommandAuthorization",
"CommandAuthorization",
"ResolvedCommandAuthorization",
"buildHelpMessage",
"buildModelsProviderData",
"hasControlCommand",
@@ -894,8 +894,8 @@ describe("plugin-sdk subpath exports", () => {
"shouldHandleTextCommands",
]);
expectSourceMentions("command-auth-native", [
"ChannelResolvedCommandAuthorization",
"CommandAuthorization",
"ResolvedCommandAuthorization",
"resolveCommandAuthorization",
]);
expectSourceMentions("command-status", [