mirror of https://github.com/openclaw/openclaw.git
fix(gateway): keep reserved channel ids immutable
This commit is contained in:
parent
f616055958
commit
9efa86be99
|
|
@ -4,8 +4,8 @@ import type { SessionEntry } from "../../config/sessions.js";
|
|||
import { normalizeAccountId } from "../../utils/account-id.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
RESERVED_CHANNEL_IDS,
|
||||
isDeliverableMessageChannel,
|
||||
isReservedChannelId,
|
||||
isGatewayMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
type GatewayMessageChannel,
|
||||
|
|
@ -94,7 +94,7 @@ export function resolveAgentDeliveryPlan(params: {
|
|||
// before reaching this planner.
|
||||
if (
|
||||
requestedChannel &&
|
||||
RESERVED_CHANNEL_IDS.has(requestedChannel.toLowerCase()) &&
|
||||
isReservedChannelId(requestedChannel) &&
|
||||
requestedChannel !== INTERNAL_MESSAGE_CHANNEL
|
||||
) {
|
||||
return INTERNAL_MESSAGE_CHANNEL;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
|||
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
|
||||
import * as skillScanner from "../security/skill-scanner.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
import { RESERVED_CHANNEL_IDS } from "../utils/message-channel.js";
|
||||
import { isReservedChannelId } from "../utils/message-channel.js";
|
||||
import {
|
||||
loadPluginManifest,
|
||||
resolvePackageExtensionEntries,
|
||||
|
|
@ -86,6 +86,8 @@ function safeFileName(input: string): string {
|
|||
return safeDirName(input);
|
||||
}
|
||||
|
||||
// Reserved plugin ids are checked through message-channel.ts so plugin install
|
||||
// and runtime registration share one immutable source of truth.
|
||||
function encodePluginInstallDirName(pluginId: string): string {
|
||||
const trimmed = pluginId.trim();
|
||||
if (!trimmed.includes("/")) {
|
||||
|
|
@ -115,12 +117,12 @@ function validatePluginId(pluginId: string): string | null {
|
|||
if (trimmed.startsWith("@")) {
|
||||
return "invalid plugin name: scoped ids must use @scope/name format";
|
||||
}
|
||||
if (RESERVED_CHANNEL_IDS.has(trimmed.toLowerCase())) {
|
||||
if (isReservedChannelId(trimmed)) {
|
||||
return `invalid plugin name: "${pluginId}" is a reserved internal channel id`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (RESERVED_CHANNEL_IDS.has(unscopedPackageName(trimmed).toLowerCase())) {
|
||||
if (isReservedChannelId(unscopedPackageName(trimmed))) {
|
||||
return `invalid plugin name: "${pluginId}" is a reserved internal channel id`;
|
||||
}
|
||||
if (segments.length !== 2) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
||||
import type { HookEntry } from "../hooks/types.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { RESERVED_CHANNEL_IDS } from "../utils/message-channel.js";
|
||||
import { isReservedChannelId } from "../utils/message-channel.js";
|
||||
import { registerPluginCommand } from "./commands.js";
|
||||
import { normalizePluginHttpPath } from "./http-path.js";
|
||||
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
|
||||
|
|
@ -431,7 +431,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (RESERVED_CHANNEL_IDS.has(id.toLowerCase())) {
|
||||
if (isReservedChannelId(id)) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
|
|
@ -444,8 +444,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||
// "inter_session" or "webchat" would cause normalizeMessageChannel to
|
||||
// remap the sentinel into a real deliverable channel, bypassing the guards
|
||||
// in resolveLastChannelRaw / resolveLastToRaw.
|
||||
const reservedAliases =
|
||||
plugin.meta?.aliases?.filter((a) => RESERVED_CHANNEL_IDS.has(a.trim().toLowerCase())) ?? [];
|
||||
const reservedAliases = plugin.meta?.aliases?.filter((a) => isReservedChannelId(a)) ?? [];
|
||||
if (reservedAliases.length > 0) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
|
|
@ -456,9 +455,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||
// Strip reserved aliases rather than blocking the whole registration so
|
||||
// the rest of the channel can still be used normally.
|
||||
if (plugin.meta?.aliases) {
|
||||
plugin.meta.aliases = plugin.meta.aliases.filter(
|
||||
(a) => !RESERVED_CHANNEL_IDS.has(a.trim().toLowerCase()),
|
||||
);
|
||||
plugin.meta.aliases = plugin.meta.aliases.filter((a) => !isReservedChannelId(a));
|
||||
}
|
||||
}
|
||||
const existing = registry.channels.find((entry) => entry.plugin.id === id);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { setActivePluginRegistry } from "../plugins/runtime.js";
|
|||
import { createMSTeamsTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
INTER_SESSION_CHANNEL,
|
||||
listReservedChannelIds,
|
||||
normalizeMessageChannel,
|
||||
resolveGatewayMessageChannel,
|
||||
} from "./message-channel.js";
|
||||
|
|
@ -50,4 +51,14 @@ describe("message-channel", () => {
|
|||
|
||||
expect(normalizeMessageChannel(INTER_SESSION_CHANNEL)).toBe(INTER_SESSION_CHANNEL);
|
||||
});
|
||||
|
||||
it("does not let callers mutate the reserved channel source of truth", () => {
|
||||
const reserved = listReservedChannelIds();
|
||||
|
||||
reserved.length = 0;
|
||||
reserved.push("discord");
|
||||
|
||||
expect(normalizeMessageChannel(INTER_SESSION_CHANNEL)).toBe(INTER_SESSION_CHANNEL);
|
||||
expect(normalizeMessageChannel("webchat")).toBe("webchat");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,11 +33,20 @@ export type InterSessionChannel = typeof INTER_SESSION_CHANNEL;
|
|||
* Checked in both plugin install (validatePluginId) and runtime channel
|
||||
* registration (registerChannel) to cover all registration paths.
|
||||
*/
|
||||
export const RESERVED_CHANNEL_IDS: Set<string> = new Set([
|
||||
const RESERVED_CHANNEL_IDS: ReadonlySet<string> = new Set([
|
||||
INTER_SESSION_CHANNEL,
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
]);
|
||||
|
||||
export function isReservedChannelId(raw?: string | null): boolean {
|
||||
const normalized = raw?.trim().toLowerCase();
|
||||
return Boolean(normalized) && RESERVED_CHANNEL_IDS.has(normalized);
|
||||
}
|
||||
|
||||
export function listReservedChannelIds(): string[] {
|
||||
return Array.from(RESERVED_CHANNEL_IDS);
|
||||
}
|
||||
|
||||
export function isInterSessionChannel(raw?: string | null): boolean {
|
||||
// Guard against collision with real deliverable plugin channels: a plugin
|
||||
// could theoretically register a channel named "inter_session", which must
|
||||
|
|
@ -88,7 +97,7 @@ export function normalizeMessageChannel(raw?: string | null): string | undefined
|
|||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (RESERVED_CHANNEL_IDS.has(normalized)) {
|
||||
if (isReservedChannelId(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
const builtIn = normalizeChatChannelId(normalized);
|
||||
|
|
|
|||
Loading…
Reference in New Issue