fix: keep native command auto defaults cold

This commit is contained in:
Shakker
2026-04-26 07:33:58 +01:00
parent aee4c92344
commit 7a7728db13
16 changed files with 345 additions and 14 deletions

View File

@@ -524,6 +524,12 @@ Non-bundled plugins that declare `channels[]` should also declare matching
cold-path config schema, setup, and Control UI surfaces cannot know the
channel-owned option shape until plugin runtime executes.
`channelConfigs.<channel-id>.commands.nativeCommandsAutoEnabled` and
`nativeSkillsAutoEnabled` can declare static `auto` defaults for command config
checks that run before channel runtime loads. Bundled channels can also publish
the same defaults through `package.json#openclaw.channel.commands` alongside
their other package-owned channel catalog metadata.
```json
{
"channelConfigs": {
@@ -543,6 +549,10 @@ channel-owned option shape until plugin runtime executes.
},
"label": "Matrix",
"description": "Matrix homeserver connection",
"commands": {
"nativeCommandsAutoEnabled": true,
"nativeSkillsAutoEnabled": true
},
"preferOver": ["matrix-legacy"]
}
}
@@ -557,6 +567,7 @@ Each channel entry can include:
| `uiHints` | `Record<string, object>` | Optional UI labels/placeholders/sensitive hints for that channel config section. |
| `label` | `string` | Channel label merged into picker and inspect surfaces when runtime metadata is not ready. |
| `description` | `string` | Short channel description for inspect and catalog surfaces. |
| `commands` | `object` | Static native command and native skill auto-defaults for pre-runtime config checks. |
| `preferOver` | `string[]` | Legacy or lower-priority plugin ids this channel should outrank in selection surfaces. |
### Replacing another channel plugin
@@ -792,6 +803,7 @@ Important examples:
| `openclaw.setupEntry` | Lightweight setup-only entrypoint used during onboarding, deferred channel startup, and read-only channel status/SecretRef discovery. Must stay inside the plugin package directory. |
| `openclaw.runtimeSetupEntry` | Declares the built JavaScript setup entrypoint for installed packages. Must stay inside the plugin package directory. |
| `openclaw.channel` | Cheap channel catalog metadata like labels, docs paths, aliases, and selection copy. |
| `openclaw.channel.commands` | Static native command and native skill auto-default metadata used by config, audit, and command-list surfaces before channel runtime loads. |
| `openclaw.channel.configuredState` | Lightweight configured-state checker metadata that can answer "does env-only setup already exist?" without loading the full channel runtime. |
| `openclaw.channel.persistedAuthState` | Lightweight persisted-auth checker metadata that can answer "is anything already signed in?" without loading the full channel runtime. |
| `openclaw.install.npmSpec` / `openclaw.install.localPath` | Install/update hints for bundled and externally published plugins. |

View File

@@ -40,6 +40,10 @@
"blurb": "very well supported right now.",
"systemImage": "bubble.left.and.bubble.right",
"markdownCapable": true,
"commands": {
"nativeCommandsAutoEnabled": true,
"nativeSkillsAutoEnabled": true
},
"configuredState": {
"specifier": "./configured-state",
"exportName": "hasDiscordConfiguredState"

View File

@@ -28,6 +28,10 @@
"blurb": "supported (Socket Mode).",
"systemImage": "number",
"markdownCapable": true,
"commands": {
"nativeCommandsAutoEnabled": false,
"nativeSkillsAutoEnabled": false
},
"configuredState": {
"specifier": "./configured-state",
"exportName": "hasSlackConfiguredState"

View File

@@ -38,6 +38,10 @@
"https://openclaw.ai"
],
"markdownCapable": true,
"commands": {
"nativeCommandsAutoEnabled": true,
"nativeSkillsAutoEnabled": true
},
"configuredState": {
"specifier": "./configured-state",
"exportName": "hasTelegramConfiguredState"

View File

@@ -15,6 +15,7 @@ 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 { listChannelPlugins } from "./registry.js";
@@ -72,6 +73,10 @@ type ReadOnlyChannelPluginResolution = {
missingConfiguredChannelIds: string[];
};
type ManifestChannelConfigRecord = NonNullable<PluginManifestRecord["channelConfigs"]>[string];
type ChannelCommandDefaults = Pick<
NonNullable<ChannelPlugin["commands"]>,
"nativeCommandsAutoEnabled" | "nativeSkillsAutoEnabled"
>;
function addChannelPlugins(
byId: Map<string, ChannelPlugin>,
@@ -125,6 +130,26 @@ function normalizeManifestText(value: string | undefined, fallback: string): str
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,
@@ -258,6 +283,9 @@ function buildManifestChannelPlugin(params: {
channelConfig?.description ?? catalogMeta?.blurb,
params.record.description || "",
);
const commands = normalizeChannelCommandDefaults(
channelConfig?.commands ?? catalogMeta?.commands,
);
return {
id: params.channelId,
meta: {
@@ -273,6 +301,7 @@ function buildManifestChannelPlugin(params: {
: {}),
},
capabilities: { chatTypes: ["direct"] },
...(commands ? { commands } : {}),
...(channelConfig
? {
configSchema: {
@@ -318,6 +347,47 @@ 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;
}
function rebindChannelPluginConfig(
config: ChannelPlugin["config"],
sourceChannelId: string,

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
@@ -100,6 +101,32 @@ describe("resolveNativeSkillsEnabled", () => {
).toBe(false);
});
it("uses package channel metadata for bundled auto defaults before runtime loads", () => {
setActivePluginRegistry(createTestRegistry([]));
const env = {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.resolve("extensions"),
OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY: "1",
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
};
expect(
resolveNativeSkillsEnabled({
providerId: "discord",
globalSetting: "auto",
env,
}),
).toBe(true);
expect(
resolveNativeCommandsEnabled({
providerId: "slack",
globalSetting: "auto",
env,
}),
).toBe(false);
});
it("honors explicit provider settings", () => {
expect(
resolveNativeSkillsEnabled({

View File

@@ -1,30 +1,43 @@
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
import { getLoadedChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js";
import type { ChannelId } from "../channels/plugins/types.public.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import type { NativeCommandsSetting } from "./types.js";
export { isCommandFlagEnabled, isRestartEnabled, type CommandFlagKey } from "./commands.flags.js";
function resolveAutoDefault(
providerId: ChannelId | undefined,
kind: "native" | "nativeSkills",
options?: {
env?: NodeJS.ProcessEnv;
stateDir?: string;
workspaceDir?: string;
autoDefault?: boolean;
},
): boolean {
const id = normalizeChannelId(providerId);
const id = normalizeChannelId(providerId) ?? normalizeOptionalLowercaseString(providerId);
if (!id) {
return false;
}
const plugin = getChannelPlugin(id);
if (!plugin) {
return false;
if (typeof options?.autoDefault === "boolean") {
return options.autoDefault;
}
const commandDefaults =
getLoadedChannelPlugin(id)?.commands ?? resolveReadOnlyChannelCommandDefaults(id, options);
if (kind === "native") {
return plugin.commands?.nativeCommandsAutoEnabled === true;
return commandDefaults?.nativeCommandsAutoEnabled === true;
}
return plugin.commands?.nativeSkillsAutoEnabled === true;
return commandDefaults?.nativeSkillsAutoEnabled === true;
}
export function resolveNativeSkillsEnabled(params: {
providerId: ChannelId;
providerSetting?: NativeCommandsSetting;
globalSetting?: NativeCommandsSetting;
env?: NodeJS.ProcessEnv;
stateDir?: string;
workspaceDir?: string;
autoDefault?: boolean;
}): boolean {
return resolveNativeCommandSetting({ ...params, kind: "nativeSkills" });
}
@@ -33,6 +46,10 @@ export function resolveNativeCommandsEnabled(params: {
providerId: ChannelId;
providerSetting?: NativeCommandsSetting;
globalSetting?: NativeCommandsSetting;
env?: NodeJS.ProcessEnv;
stateDir?: string;
workspaceDir?: string;
autoDefault?: boolean;
}): boolean {
return resolveNativeCommandSetting({ ...params, kind: "native" });
}
@@ -42,8 +59,12 @@ function resolveNativeCommandSetting(params: {
providerSetting?: NativeCommandsSetting;
globalSetting?: NativeCommandsSetting;
kind?: "native" | "nativeSkills";
env?: NodeJS.ProcessEnv;
stateDir?: string;
workspaceDir?: string;
autoDefault?: boolean;
}): boolean {
const { providerId, providerSetting, globalSetting, kind = "native" } = params;
const { providerId, providerSetting, globalSetting, kind = "native", ...options } = params;
const setting = providerSetting === undefined ? globalSetting : providerSetting;
if (setting === true) {
return true;
@@ -51,7 +72,7 @@ function resolveNativeCommandSetting(params: {
if (setting === false) {
return false;
}
return resolveAutoDefault(providerId, kind);
return resolveAutoDefault(providerId, kind, options);
}
export function isNativeCommandsExplicitlyDisabled(params: {

View File

@@ -176,6 +176,9 @@ export function collectBundledChannelConfigs(params: {
: preferOver.length > 0
? { preferOver }
: {}),
...((existing?.commands ?? channelMeta?.commands)
? { commands: existing?.commands ?? channelMeta?.commands }
: {}),
};
}

View File

@@ -1,4 +1,5 @@
import { getChannelPlugin } from "../channels/plugins/index.js";
import { getLoadedChannelPlugin } from "../channels/plugins/index.js";
import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-only.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import type { OpenClawPluginCommandDefinition } from "./types.js";
@@ -90,7 +91,10 @@ export function getPluginCommandSpecs(provider?: string): Array<{
const providerName = normalizeOptionalLowercaseString(provider);
if (
providerName &&
getChannelPlugin(providerName)?.commands?.nativeCommandsAutoEnabled !== true
(
getLoadedChannelPlugin(providerName)?.commands ??
resolveReadOnlyChannelCommandDefaults(providerName)
)?.nativeCommandsAutoEnabled !== true
) {
return [];
}

View File

@@ -143,6 +143,10 @@ function createRichPluginFixture(params: { packageVersion?: string } = {}) {
label: "Demo",
blurb: "Demo channel",
preferOver: ["legacy-demo"],
commands: {
nativeCommandsAutoEnabled: true,
nativeSkillsAutoEnabled: false,
},
},
install: {
npmSpec: "@vendor/demo-plugin@1.2.3",
@@ -195,6 +199,10 @@ describe("installed plugin index", () => {
label: "Demo",
blurb: "Demo channel",
preferOver: ["legacy-demo"],
commands: {
nativeCommandsAutoEnabled: true,
nativeSkillsAutoEnabled: false,
},
},
compat: [
"activation-channel-hint",

View File

@@ -69,7 +69,7 @@ export type InstalledPluginInstallRecordInfo = Pick<
export type InstalledPluginPackageChannelInfo = Pick<
PluginPackageChannel,
"id" | "label" | "blurb" | "preferOver"
"id" | "label" | "blurb" | "preferOver" | "commands"
>;
export type InstalledPluginIndexRecord = {
@@ -317,11 +317,27 @@ function normalizePackageChannel(
const label = normalizeStringField(channel?.label);
const blurb = normalizeStringField(channel?.blurb);
const preferOver = normalizeStringListField(channel?.preferOver);
const commands =
channel?.commands &&
typeof channel.commands === "object" &&
!Array.isArray(channel.commands) &&
(typeof channel.commands.nativeCommandsAutoEnabled === "boolean" ||
typeof channel.commands.nativeSkillsAutoEnabled === "boolean")
? {
...(typeof channel.commands.nativeCommandsAutoEnabled === "boolean"
? { nativeCommandsAutoEnabled: channel.commands.nativeCommandsAutoEnabled }
: {}),
...(typeof channel.commands.nativeSkillsAutoEnabled === "boolean"
? { nativeSkillsAutoEnabled: channel.commands.nativeSkillsAutoEnabled }
: {}),
}
: undefined;
return {
id,
...(label ? { label } : {}),
...(blurb ? { blurb } : {}),
...(preferOver ? { preferOver } : {}),
...(commands ? { commands } : {}),
};
}

View File

@@ -142,6 +142,59 @@ describe("loadPluginManifestRegistryForInstalledIndex", () => {
]);
});
it("hydrates package channel command metadata while reconstructing from an older index", () => {
const rootDir = makeTempDir();
writePlugin(rootDir, "installed", "installed-");
fs.writeFileSync(
path.join(rootDir, "package.json"),
JSON.stringify({
openclaw: {
channel: {
id: "installed",
label: "Installed",
commands: {
nativeCommandsAutoEnabled: true,
nativeSkillsAutoEnabled: false,
},
},
},
}),
"utf8",
);
const index = createIndex(rootDir);
const registry = loadPluginManifestRegistryForInstalledIndex({
index: {
...index,
plugins: [
{
...index.plugins[0],
packageChannel: {
id: "installed",
label: "Installed",
},
packageJson: {
path: "package.json",
hash: "old-index-hash",
},
},
],
},
env: {
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
OPENCLAW_VERSION: "2026.4.25",
VITEST: "true",
},
includeDisabled: true,
});
expect(registry.plugins[0]?.channelCatalogMeta?.commands).toEqual({
nativeCommandsAutoEnabled: true,
nativeSkillsAutoEnabled: false,
});
});
it("round-trips bundle metadata through the persisted index before reconstruction", async () => {
const stateDir = makeTempDir();
const rootDir = makeTempDir();

View File

@@ -5,7 +5,12 @@ import type { PluginCandidate } from "./discovery.js";
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js";
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
import { DEFAULT_PLUGIN_ENTRY_CANDIDATES } from "./manifest.js";
import {
DEFAULT_PLUGIN_ENTRY_CANDIDATES,
getPackageManifestMetadata,
type OpenClawPackageManifest,
type PackageManifest,
} from "./manifest.js";
function resolveInstalledPluginRootDir(record: InstalledPluginIndexRecord): string {
return record.rootDir || path.dirname(record.manifestPath || process.cwd());
@@ -22,8 +27,45 @@ function resolveFallbackPluginSource(record: InstalledPluginIndexRecord): string
return path.join(rootDir, DEFAULT_PLUGIN_ENTRY_CANDIDATES[0]);
}
function resolveInstalledPackageManifest(
record: InstalledPluginIndexRecord,
): OpenClawPackageManifest | undefined {
if (!record.packageChannel) {
return undefined;
}
if (record.packageChannel.commands) {
return { channel: record.packageChannel };
}
const rootDir = resolveInstalledPluginRootDir(record);
const packageJsonPath = record.packageJson?.path
? path.resolve(rootDir, record.packageJson.path)
: undefined;
if (!packageJsonPath) {
return { channel: record.packageChannel };
}
const relative = path.relative(rootDir, packageJsonPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return { channel: record.packageChannel };
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageManifest;
const packageManifest = getPackageManifestMetadata(packageJson);
return {
channel: {
...record.packageChannel,
...(packageManifest?.channel?.commands
? { commands: packageManifest.channel.commands }
: {}),
},
};
} catch {
return { channel: record.packageChannel };
}
}
function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate {
const rootDir = resolveInstalledPluginRootDir(record);
const packageManifest = resolveInstalledPackageManifest(record);
return {
idHint: record.pluginId,
source: record.source ?? resolveFallbackPluginSource(record),
@@ -34,7 +76,7 @@ function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate
...(record.bundleFormat ? { bundleFormat: record.bundleFormat } : {}),
...(record.packageName ? { packageName: record.packageName } : {}),
...(record.packageVersion ? { packageVersion: record.packageVersion } : {}),
...(record.packageChannel ? { packageManifest: { channel: record.packageChannel } } : {}),
...(packageManifest ? { packageManifest } : {}),
packageDir: rootDir,
};
}

View File

@@ -32,6 +32,7 @@ import {
type PluginManifestActivation,
type PluginManifestConfigContracts,
type PluginManifest,
type PluginManifestChannelCommandDefaults,
type PluginManifestChannelConfig,
type PluginManifestContracts,
type PluginManifestMediaUnderstandingProviderMetadata,
@@ -148,6 +149,7 @@ export type PluginManifestRecord = {
label?: string;
blurb?: string;
preferOver?: readonly string[];
commands?: PluginManifestChannelCommandDefaults;
};
};
@@ -220,6 +222,29 @@ function normalizePreferredPluginIds(raw: unknown): string[] | undefined {
return normalizeOptionalTrimmedStringList(raw);
}
function normalizePackageChannelCommands(
commands: unknown,
): PluginManifestChannelCommandDefaults | undefined {
if (!commands || typeof commands !== "object" || Array.isArray(commands)) {
return undefined;
}
const record = commands as Record<string, unknown>;
const nativeCommandsAutoEnabled =
typeof record.nativeCommandsAutoEnabled === "boolean"
? record.nativeCommandsAutoEnabled
: undefined;
const nativeSkillsAutoEnabled =
typeof record.nativeSkillsAutoEnabled === "boolean"
? record.nativeSkillsAutoEnabled
: undefined;
return nativeCommandsAutoEnabled !== undefined || nativeSkillsAutoEnabled !== undefined
? {
...(nativeCommandsAutoEnabled !== undefined ? { nativeCommandsAutoEnabled } : {}),
...(nativeSkillsAutoEnabled !== undefined ? { nativeSkillsAutoEnabled } : {}),
}
: undefined;
}
function mergePackageChannelMetaIntoChannelConfigs(params: {
channelConfigs?: Record<string, PluginManifestChannelConfig>;
packageChannel?: OpenClawPackageManifest["channel"];
@@ -243,6 +268,8 @@ function mergePackageChannelMetaIntoChannelConfigs(params: {
existing.description ?? normalizeOptionalString(params.packageChannel?.blurb) ?? "";
const preferOver =
existing.preferOver ?? normalizePreferredPluginIds(params.packageChannel?.preferOver);
const commands =
existing.commands ?? normalizePackageChannelCommands(params.packageChannel?.commands);
const merged: Record<string, PluginManifestChannelConfig> = Object.create(null);
for (const [key, value] of Object.entries(params.channelConfigs)) {
@@ -255,6 +282,7 @@ function mergePackageChannelMetaIntoChannelConfigs(params: {
...(label ? { label } : {}),
...(description ? { description } : {}),
...(preferOver?.length ? { preferOver } : {}),
...(commands ? { commands } : {}),
};
return merged;
}
@@ -270,6 +298,9 @@ function buildRecord(params: {
channelConfigs: params.manifest.channelConfigs,
packageChannel: params.candidate.packageManifest?.channel,
});
const packageChannelCommands = normalizePackageChannelCommands(
params.candidate.packageManifest?.channel?.commands,
);
return {
id: params.manifest.id,
name: normalizeOptionalString(params.manifest.name) ?? params.candidate.packageName,
@@ -335,6 +366,7 @@ function buildRecord(params: {
...(params.candidate.packageManifest.channel.preferOver
? { preferOver: params.candidate.packageManifest.channel.preferOver }
: {}),
...(packageChannelCommands ? { commands: packageChannelCommands } : {}),
},
}
: {}),

View File

@@ -40,6 +40,12 @@ export type PluginManifestChannelConfig = {
label?: string;
description?: string;
preferOver?: string[];
commands?: PluginManifestChannelCommandDefaults;
};
export type PluginManifestChannelCommandDefaults = {
nativeCommandsAutoEnabled?: boolean;
nativeSkillsAutoEnabled?: boolean;
};
export type PluginManifestModelSupport = {
@@ -820,6 +826,7 @@ function normalizeChannelConfigs(
const label = normalizeOptionalString(rawEntry.label) ?? "";
const description = normalizeOptionalString(rawEntry.description) ?? "";
const preferOver = normalizeTrimmedStringList(rawEntry.preferOver);
const commandDefaults = normalizeManifestChannelCommandDefaults(rawEntry.commands);
normalized[channelId] = {
schema,
...(uiHints ? { uiHints } : {}),
@@ -827,11 +834,32 @@ function normalizeChannelConfigs(
...(label ? { label } : {}),
...(description ? { description } : {}),
...(preferOver.length > 0 ? { preferOver } : {}),
...(commandDefaults ? { commands: commandDefaults } : {}),
};
}
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
function normalizeManifestChannelCommandDefaults(
value: unknown,
): PluginManifestChannelCommandDefaults | undefined {
if (!isRecord(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 resolvePluginManifestPath(rootDir: string): string {
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
const candidate = path.join(rootDir, filename);
@@ -1012,6 +1040,7 @@ export type PluginPackageChannel = {
quickstartAllowFrom?: boolean;
forceAccountBinding?: boolean;
preferSessionLookupForAnnounceTarget?: boolean;
commands?: PluginManifestChannelCommandDefaults;
configuredState?: {
specifier?: string;
exportName?: string;

View File

@@ -342,6 +342,8 @@ export async function collectPluginsTrustFindings(params: {
| boolean
| undefined,
globalSetting: params.cfg.commands?.nativeSkills,
stateDir: params.stateDir,
autoDefault: plugin.commands?.nativeSkillsAutoEnabled === true,
});
}),
)