mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
fix(commands): tighten channel auth contract
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", [
|
||||
|
||||
Reference in New Issue
Block a user