mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:20:44 +00:00
fix: keep read-only channels off setup runtime
This commit is contained in:
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo.
|
||||
- Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd.
|
||||
- Plugins/registry: resolve web provider ownership from the installed plugin index instead of broad manifest scans on secret, tool, and pricing paths. Thanks @shakkernerd.
|
||||
- TTS: strip model-emitted TTS directives from streamed block text before channel
|
||||
delivery, including directives split across adjacent blocks, while preserving
|
||||
|
||||
@@ -838,6 +838,31 @@ describe("listReadOnlyChannelPluginsForConfig", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const plugin = plugins.find((entry) => entry.id === channelId);
|
||||
expect(plugin?.meta.blurb).toBe("bundled setup entry");
|
||||
expect(fs.existsSync(setupMarker)).toBe(false);
|
||||
expect(fs.existsSync(fullMarker)).toBe(false);
|
||||
});
|
||||
|
||||
it("loads bundled setup runtime only when explicitly requested", () => {
|
||||
const { channelId, envVar, fullMarker, pluginId, setupMarker } =
|
||||
writeBundledSetupChannelPlugin();
|
||||
const plugins = listReadOnlyChannelPluginsForConfig(
|
||||
{
|
||||
plugins: {
|
||||
allow: [pluginId],
|
||||
entries: {
|
||||
[pluginId]: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
env: { ...process.env, [envVar]: "configured" },
|
||||
includePersistedAuthState: false,
|
||||
includeSetupRuntimeFallback: true,
|
||||
},
|
||||
);
|
||||
|
||||
const plugin = plugins.find((entry) => entry.id === channelId);
|
||||
expect(plugin?.meta.blurb).toBe("bundled setup entry");
|
||||
expect(fs.existsSync(setupMarker)).toBe(true);
|
||||
|
||||
@@ -188,21 +188,36 @@ function buildManifestChannelPlugin(params: {
|
||||
if (!isSafeManifestChannelId(params.channelId)) {
|
||||
return undefined;
|
||||
}
|
||||
const catalogMeta =
|
||||
params.record.channelCatalogMeta?.id === params.channelId
|
||||
? params.record.channelCatalogMeta
|
||||
: undefined;
|
||||
const channelConfigValue = params.record.channelConfigs
|
||||
? readOwnRecordValue(params.record.channelConfigs as Record<string, unknown>, params.channelId)
|
||||
: undefined;
|
||||
if (
|
||||
!channelConfigValue ||
|
||||
typeof channelConfigValue !== "object" ||
|
||||
Array.isArray(channelConfigValue)
|
||||
!catalogMeta &&
|
||||
(!channelConfigValue ||
|
||||
typeof channelConfigValue !== "object" ||
|
||||
Array.isArray(channelConfigValue))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const channelConfig = channelConfigValue as ManifestChannelConfigRecord;
|
||||
const channelConfig =
|
||||
channelConfigValue &&
|
||||
typeof channelConfigValue === "object" &&
|
||||
!Array.isArray(channelConfigValue)
|
||||
? (channelConfigValue as ManifestChannelConfigRecord)
|
||||
: undefined;
|
||||
const label =
|
||||
normalizeManifestText(channelConfig.label, params.record.name || params.channelId) ||
|
||||
params.channelId;
|
||||
const blurb = normalizeManifestText(channelConfig.description, params.record.description || "");
|
||||
normalizeManifestText(
|
||||
channelConfig?.label ?? catalogMeta?.label,
|
||||
params.record.name || params.channelId,
|
||||
) || params.channelId;
|
||||
const blurb = normalizeManifestText(
|
||||
channelConfig?.description ?? catalogMeta?.blurb,
|
||||
params.record.description || "",
|
||||
);
|
||||
return {
|
||||
id: params.channelId,
|
||||
meta: {
|
||||
@@ -211,14 +226,22 @@ function buildManifestChannelPlugin(params: {
|
||||
selectionLabel: label,
|
||||
docsPath: `/channels/${encodeURIComponent(params.channelId)}`,
|
||||
blurb,
|
||||
...(channelConfig.preferOver?.length ? { preferOver: channelConfig.preferOver } : {}),
|
||||
...(channelConfig?.preferOver?.length
|
||||
? { preferOver: channelConfig.preferOver }
|
||||
: catalogMeta?.preferOver?.length
|
||||
? { preferOver: catalogMeta.preferOver }
|
||||
: {}),
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
configSchema: {
|
||||
schema: channelConfig.schema,
|
||||
...(channelConfig.uiHints ? { uiHints: channelConfig.uiHints } : {}),
|
||||
...(channelConfig.runtime ? { runtime: channelConfig.runtime } : {}),
|
||||
},
|
||||
...(channelConfig
|
||||
? {
|
||||
configSchema: {
|
||||
schema: channelConfig.schema,
|
||||
...(channelConfig.uiHints ? { uiHints: channelConfig.uiHints } : {}),
|
||||
...(channelConfig.runtime ? { runtime: channelConfig.runtime } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
config: {
|
||||
listAccountIds: (cfg) => listManifestChannelAccountIds(cfg, params.channelId),
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
@@ -245,8 +268,14 @@ function buildManifestChannelPlugin(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function canUseManifestChannelPlugin(record: PluginManifestRecord): boolean {
|
||||
return record.setup?.requiresRuntime === false || !record.setupSource;
|
||||
function canUseManifestChannelPlugin(record: PluginManifestRecord, channelId: string): boolean {
|
||||
const hasChannelConfig = Boolean(
|
||||
record.channelConfigs && Object.prototype.hasOwnProperty.call(record.channelConfigs, channelId),
|
||||
);
|
||||
if (hasChannelConfig) {
|
||||
return record.setup?.requiresRuntime === false || !record.setupSource;
|
||||
}
|
||||
return record.channelCatalogMeta?.id === channelId;
|
||||
}
|
||||
|
||||
function rebindChannelPluginConfig(
|
||||
@@ -439,9 +468,6 @@ function addManifestChannelPlugins(
|
||||
if (!options.pluginIds.has(record.id)) {
|
||||
continue;
|
||||
}
|
||||
if (!canUseManifestChannelPlugin(record)) {
|
||||
continue;
|
||||
}
|
||||
for (const channelId of record.channels) {
|
||||
if (!isSafeManifestChannelId(channelId)) {
|
||||
continue;
|
||||
@@ -449,6 +475,9 @@ function addManifestChannelPlugins(
|
||||
if (!channelIds.has(channelId)) {
|
||||
continue;
|
||||
}
|
||||
if (!canUseManifestChannelPlugin(record, channelId)) {
|
||||
continue;
|
||||
}
|
||||
addChannelPlugins(byId, [buildManifestChannelPlugin({ record, channelId })], {
|
||||
onlyIds: channelIds,
|
||||
allowOverwrite: false,
|
||||
@@ -470,6 +499,23 @@ function listExternalChannelManifestRecords(
|
||||
return records.filter((plugin) => plugin.origin !== "bundled" && plugin.channels.length > 0);
|
||||
}
|
||||
|
||||
function listBundledChannelManifestRecords(
|
||||
records: readonly PluginManifestRecord[],
|
||||
): PluginManifestRecord[] {
|
||||
return records.filter((plugin) => plugin.origin === "bundled" && plugin.channels.length > 0);
|
||||
}
|
||||
|
||||
function listPluginIdsForChannels(
|
||||
records: readonly PluginManifestRecord[],
|
||||
channelIds: readonly string[],
|
||||
): string[] {
|
||||
const requestedChannelIds = new Set(channelIds);
|
||||
return records
|
||||
.filter((plugin) => plugin.channels.some((channelId) => requestedChannelIds.has(channelId)))
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolveExternalReadOnlyChannelPluginIds(params: {
|
||||
cfg: OpenClawConfig;
|
||||
activationSourceConfig?: OpenClawConfig;
|
||||
@@ -527,6 +573,7 @@ export function resolveReadOnlyChannelPluginsForConfig(
|
||||
cache: options.cache,
|
||||
includeDisabled: true,
|
||||
}).plugins;
|
||||
const bundledManifestRecords = listBundledChannelManifestRecords(manifestRecords);
|
||||
const externalManifestRecords = listExternalChannelManifestRecords(manifestRecords);
|
||||
const configuredChannelIds = [
|
||||
...new Set(
|
||||
@@ -545,13 +592,25 @@ export function resolveReadOnlyChannelPluginsForConfig(
|
||||
|
||||
addChannelPlugins(byId, listChannelPlugins());
|
||||
|
||||
for (const channelId of configuredChannelIds) {
|
||||
if (byId.has(channelId)) {
|
||||
continue;
|
||||
if (options.includeSetupRuntimeFallback === true) {
|
||||
for (const channelId of configuredChannelIds) {
|
||||
if (byId.has(channelId)) {
|
||||
continue;
|
||||
}
|
||||
addChannelPlugins(byId, [getBundledChannelSetupPlugin(channelId)]);
|
||||
}
|
||||
addChannelPlugins(byId, [getBundledChannelSetupPlugin(channelId)]);
|
||||
}
|
||||
|
||||
const bundledManifestMissingChannelIds = configuredChannelIds.filter(
|
||||
(channelId) => !byId.has(channelId),
|
||||
);
|
||||
addManifestChannelPlugins(byId, bundledManifestRecords, {
|
||||
pluginIds: new Set(
|
||||
listPluginIdsForChannels(bundledManifestRecords, bundledManifestMissingChannelIds),
|
||||
),
|
||||
channelIds: bundledManifestMissingChannelIds,
|
||||
});
|
||||
|
||||
const missingConfiguredChannelIds = configuredChannelIds.filter(
|
||||
(channelId) => !byId.has(channelId),
|
||||
);
|
||||
@@ -566,27 +625,22 @@ export function resolveReadOnlyChannelPluginsForConfig(
|
||||
});
|
||||
if (externalPluginIds.length > 0) {
|
||||
const externalPluginIdSet = new Set(externalPluginIds);
|
||||
addManifestChannelPlugins(byId, externalManifestRecords, {
|
||||
pluginIds: externalPluginIdSet,
|
||||
channelIds: missingConfiguredChannelIds,
|
||||
});
|
||||
|
||||
const setupMissingChannelIds = missingConfiguredChannelIds.filter(
|
||||
(channelId) => !byId.has(channelId),
|
||||
);
|
||||
const missingChannelIdSet = new Set(setupMissingChannelIds);
|
||||
const ownedChannelIdsByPluginId = new Map(
|
||||
externalManifestRecords
|
||||
.filter((record) => externalPluginIdSet.has(record.id))
|
||||
.map((record) => [record.id, record.channels] as const),
|
||||
);
|
||||
const ownedMissingChannelIdsByPluginId = new Map(
|
||||
[...ownedChannelIdsByPluginId].map(
|
||||
([pluginId, channelIds]) =>
|
||||
[pluginId, channelIds.filter((channelId) => missingChannelIdSet.has(channelId))] as const,
|
||||
),
|
||||
);
|
||||
if (setupMissingChannelIds.length > 0 && options.includeSetupRuntimeFallback === true) {
|
||||
if (missingConfiguredChannelIds.length > 0 && options.includeSetupRuntimeFallback === true) {
|
||||
const missingChannelIdSet = new Set(missingConfiguredChannelIds);
|
||||
const ownedMissingChannelIdsByPluginId = new Map(
|
||||
[...ownedChannelIdsByPluginId].map(
|
||||
([pluginId, channelIds]) =>
|
||||
[
|
||||
pluginId,
|
||||
channelIds.filter((channelId) => missingChannelIdSet.has(channelId)),
|
||||
] as const,
|
||||
),
|
||||
);
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: cfg,
|
||||
activationSourceConfig: options.activationSourceConfig ?? cfg,
|
||||
@@ -604,6 +658,13 @@ export function resolveReadOnlyChannelPluginsForConfig(
|
||||
ownedMissingChannelIdsByPluginId,
|
||||
});
|
||||
}
|
||||
const externalManifestMissingChannelIds = missingConfiguredChannelIds.filter(
|
||||
(channelId) => !byId.has(channelId),
|
||||
);
|
||||
addManifestChannelPlugins(byId, externalManifestRecords, {
|
||||
pluginIds: externalPluginIdSet,
|
||||
channelIds: externalManifestMissingChannelIds,
|
||||
});
|
||||
}
|
||||
|
||||
const plugins = [...byId.values()];
|
||||
|
||||
@@ -53,6 +53,7 @@ const InstalledPluginIndexRecordSchema = z
|
||||
installRecord: z.record(z.string(), z.unknown()).optional(),
|
||||
installRecordHash: z.string().optional(),
|
||||
packageInstall: z.unknown().optional(),
|
||||
packageChannel: z.unknown().optional(),
|
||||
manifestPath: z.string(),
|
||||
manifestHash: z.string(),
|
||||
format: z.string().optional(),
|
||||
|
||||
@@ -138,6 +138,12 @@ function createRichPluginFixture(params: { packageVersion?: string } = {}) {
|
||||
packageName: "@vendor/demo-plugin",
|
||||
packageVersion: params.packageVersion ?? "1.2.3",
|
||||
packageManifest: {
|
||||
channel: {
|
||||
id: "demo",
|
||||
label: "Demo",
|
||||
blurb: "Demo channel",
|
||||
preferOver: ["legacy-demo"],
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@vendor/demo-plugin@1.2.3",
|
||||
expectedIntegrity: "sha512-demo",
|
||||
@@ -184,6 +190,12 @@ describe("installed plugin index", () => {
|
||||
},
|
||||
warnings: [],
|
||||
},
|
||||
packageChannel: {
|
||||
id: "demo",
|
||||
label: "Demo",
|
||||
blurb: "Demo channel",
|
||||
preferOver: ["legacy-demo"],
|
||||
},
|
||||
compat: [
|
||||
"activation-channel-hint",
|
||||
"activation-provider-hint",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type PluginManifestRegistry,
|
||||
} from "./manifest-registry.js";
|
||||
import type { PluginDiagnostic } from "./manifest-types.js";
|
||||
import type { PluginPackageChannel } from "./manifest.js";
|
||||
import { safeRealpathSync } from "./path-safety.js";
|
||||
import { hasKind } from "./slots.js";
|
||||
|
||||
@@ -66,6 +67,11 @@ export type InstalledPluginInstallRecordInfo = Pick<
|
||||
| "marketplacePlugin"
|
||||
>;
|
||||
|
||||
export type InstalledPluginPackageChannelInfo = Pick<
|
||||
PluginPackageChannel,
|
||||
"id" | "label" | "blurb" | "preferOver"
|
||||
>;
|
||||
|
||||
export type InstalledPluginIndexRecord = {
|
||||
pluginId: string;
|
||||
packageName?: string;
|
||||
@@ -82,6 +88,7 @@ export type InstalledPluginIndexRecord = {
|
||||
* install intent and must not be treated as the durable install record.
|
||||
*/
|
||||
packageInstall?: PluginInstallSourceInfo;
|
||||
packageChannel?: InstalledPluginPackageChannelInfo;
|
||||
manifestPath: string;
|
||||
manifestHash: string;
|
||||
format?: PluginManifestRecord["format"];
|
||||
@@ -279,6 +286,45 @@ function describePackageInstallSource(
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStringField(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeStringListField(value: unknown): readonly string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value
|
||||
.flatMap((entry) => {
|
||||
const normalizedEntry = normalizeStringField(entry);
|
||||
return normalizedEntry ? [normalizedEntry] : [];
|
||||
})
|
||||
.filter((entry, index, all) => all.indexOf(entry) === index);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizePackageChannel(
|
||||
channel: PluginPackageChannel | undefined,
|
||||
): InstalledPluginPackageChannelInfo | undefined {
|
||||
const id = normalizeStringField(channel?.id);
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
const label = normalizeStringField(channel?.label);
|
||||
const blurb = normalizeStringField(channel?.blurb);
|
||||
const preferOver = normalizeStringListField(channel?.preferOver);
|
||||
return {
|
||||
id,
|
||||
...(label ? { label } : {}),
|
||||
...(blurb ? { blurb } : {}),
|
||||
...(preferOver ? { preferOver } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function setInstallStringField<Key extends keyof Omit<InstalledPluginInstallRecordInfo, "source">>(
|
||||
target: InstalledPluginInstallRecordInfo,
|
||||
key: Key,
|
||||
@@ -491,6 +537,7 @@ function buildInstalledPluginIndex(
|
||||
const packageJsonPath = resolvePackageJsonPath(candidate);
|
||||
const installRecord = installRecords[record.id];
|
||||
const packageInstall = describePackageInstallSource(candidate);
|
||||
const packageChannel = normalizePackageChannel(candidate?.packageManifest?.channel);
|
||||
const manifestHash =
|
||||
safeHashFile({
|
||||
filePath: record.manifestPath,
|
||||
@@ -546,6 +593,9 @@ function buildInstalledPluginIndex(
|
||||
if (packageInstall) {
|
||||
indexRecord.packageInstall = packageInstall;
|
||||
}
|
||||
if (packageChannel) {
|
||||
indexRecord.packageChannel = packageChannel;
|
||||
}
|
||||
if (packageJson) {
|
||||
indexRecord.packageJson = packageJson;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,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 } } : {}),
|
||||
packageDir: rootDir,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user