fix: keep channel command defaults read-only

This commit is contained in:
Peter Steinberger
2026-04-26 08:57:38 +01:00
parent 06d409dc27
commit 7cbe271d08
4 changed files with 95 additions and 79 deletions

View File

@@ -0,0 +1,86 @@
import { isBlockedObjectKey } from "../../infra/prototype-keys.js";
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import type { ChannelPlugin } from "./types.plugin.js";
const SAFE_MANIFEST_CHANNEL_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
export type ChannelCommandDefaults = Pick<
NonNullable<ChannelPlugin["commands"]>,
"nativeCommandsAutoEnabled" | "nativeSkillsAutoEnabled"
>;
type ManifestChannelConfigRecord = NonNullable<PluginManifestRecord["channelConfigs"]>[string];
export function isSafeManifestChannelId(channelId: string): boolean {
return SAFE_MANIFEST_CHANNEL_ID_PATTERN.test(channelId) && !isBlockedObjectKey(channelId);
}
export function readOwnRecordValue(record: Record<string, unknown>, key: string): unknown {
if (isBlockedObjectKey(key) || !Object.prototype.hasOwnProperty.call(record, key)) {
return undefined;
}
return record[key];
}
export function normalizeChannelCommandDefaults(
value: ChannelCommandDefaults | undefined,
): ChannelCommandDefaults | undefined {
if (!value) {
return undefined;
}
const nativeCommandsAutoEnabled =
typeof value.nativeCommandsAutoEnabled === "boolean"
? value.nativeCommandsAutoEnabled
: undefined;
const nativeSkillsAutoEnabled =
typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined;
return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
? {
...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
}
: undefined;
}
export function resolveReadOnlyChannelCommandDefaults(
channelId: string,
options: {
env?: NodeJS.ProcessEnv;
stateDir?: string;
workspaceDir?: string;
} = {},
): ChannelCommandDefaults | undefined {
const normalizedChannelId = normalizeOptionalString(channelId) ?? "";
if (!normalizedChannelId || !isSafeManifestChannelId(normalizedChannelId)) {
return undefined;
}
const registry = loadPluginManifestRegistryForPluginRegistry({
stateDir: options.stateDir,
workspaceDir: options.workspaceDir,
env: options.env ?? process.env,
includeDisabled: true,
});
for (const record of registry.plugins) {
if (!record.channels.includes(normalizedChannelId)) {
continue;
}
const channelConfigValue = record.channelConfigs
? readOwnRecordValue(record.channelConfigs as Record<string, unknown>, normalizedChannelId)
: undefined;
const channelConfig =
channelConfigValue &&
typeof channelConfigValue === "object" &&
!Array.isArray(channelConfigValue)
? (channelConfigValue as ManifestChannelConfigRecord)
: undefined;
const commands = normalizeChannelCommandDefaults(
channelConfig?.commands ?? record.channelCatalogMeta?.commands,
);
if (commands) {
return commands;
}
}
return undefined;
}

View File

@@ -15,13 +15,17 @@ import type { loadOpenClawPlugins as loadOpenClawPluginsType } from "../../plugi
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { sanitizeForLog } from "../../terminal/ansi.js";
import { getBundledChannelSetupPlugin } from "./bundled.js";
import {
isSafeManifestChannelId,
normalizeChannelCommandDefaults,
readOwnRecordValue,
resolveReadOnlyChannelCommandDefaults,
} from "./read-only-command-defaults.js";
import { listChannelPlugins } from "./registry.js";
import type { ChannelPlugin } from "./types.plugin.js";
const SAFE_MANIFEST_CHANNEL_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
const LOADER_MODULE_CANDIDATES = [
new URL("../../plugins/loader.js", import.meta.url),
new URL("../../plugins/loader.ts", import.meta.url),
@@ -73,10 +77,6 @@ type ReadOnlyChannelPluginResolution = {
missingConfiguredChannelIds: string[];
};
type ManifestChannelConfigRecord = NonNullable<PluginManifestRecord["channelConfigs"]>[string];
type ChannelCommandDefaults = Pick<
NonNullable<ChannelPlugin["commands"]>,
"nativeCommandsAutoEnabled" | "nativeSkillsAutoEnabled"
>;
function addChannelPlugins(
byId: Map<string, ChannelPlugin>,
@@ -115,41 +115,10 @@ function rebindChannelScopedString(
return value;
}
function isSafeManifestChannelId(channelId: string): boolean {
return SAFE_MANIFEST_CHANNEL_ID_PATTERN.test(channelId) && !isBlockedObjectKey(channelId);
}
function readOwnRecordValue(record: Record<string, unknown>, key: string): unknown {
if (isBlockedObjectKey(key) || !Object.prototype.hasOwnProperty.call(record, key)) {
return undefined;
}
return record[key];
}
function normalizeManifestText(value: string | undefined, fallback: string): string {
return sanitizeForLog(value?.trim() || fallback).trim();
}
function normalizeChannelCommandDefaults(
value: ChannelCommandDefaults | undefined,
): ChannelCommandDefaults | undefined {
if (!value) {
return undefined;
}
const nativeCommandsAutoEnabled =
typeof value.nativeCommandsAutoEnabled === "boolean"
? value.nativeCommandsAutoEnabled
: undefined;
const nativeSkillsAutoEnabled =
typeof value.nativeSkillsAutoEnabled === "boolean" ? value.nativeSkillsAutoEnabled : undefined;
return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
? {
...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
}
: undefined;
}
function rebindChannelConfig(
cfg: OpenClawConfig,
sourceChannelId: string,
@@ -347,46 +316,7 @@ function canUseManifestChannelPlugin(record: PluginManifestRecord, channelId: st
return record.channelCatalogMeta?.id === channelId;
}
export function resolveReadOnlyChannelCommandDefaults(
channelId: string,
options: {
env?: NodeJS.ProcessEnv;
stateDir?: string;
workspaceDir?: string;
} = {},
): ChannelCommandDefaults | undefined {
const normalizedChannelId = normalizeOptionalString(channelId) ?? "";
if (!normalizedChannelId || !isSafeManifestChannelId(normalizedChannelId)) {
return undefined;
}
const registry = loadPluginManifestRegistryForPluginRegistry({
stateDir: options.stateDir,
workspaceDir: options.workspaceDir,
env: options.env ?? process.env,
includeDisabled: true,
});
for (const record of registry.plugins) {
if (!record.channels.includes(normalizedChannelId)) {
continue;
}
const channelConfigValue = record.channelConfigs
? readOwnRecordValue(record.channelConfigs as Record<string, unknown>, normalizedChannelId)
: undefined;
const channelConfig =
channelConfigValue &&
typeof channelConfigValue === "object" &&
!Array.isArray(channelConfigValue)
? (channelConfigValue as ManifestChannelConfigRecord)
: undefined;
const commands = normalizeChannelCommandDefaults(
channelConfig?.commands ?? record.channelCatalogMeta?.commands,
);
if (commands) {
return commands;
}
}
return undefined;
}
export { resolveReadOnlyChannelCommandDefaults };
function rebindChannelPluginConfig(
config: ChannelPlugin["config"],

View File

@@ -1,5 +1,5 @@
import { getLoadedChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js";
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only-command-defaults.js";
import type { ChannelId } from "../channels/plugins/types.public.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import type { NativeCommandsSetting } from "./types.js";

View File

@@ -1,5 +1,5 @@
import { getLoadedChannelPlugin } from "../channels/plugins/index.js";
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js";
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only-command-defaults.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import type { OpenClawPluginCommandDefinition } from "./types.js";