mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
feat(plugins): plan gateway startup from registry
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Diagnostics/OTEL: align model-call GenAI span attributes with OpenTelemetry stability opt-in semantics, keeping legacy `gen_ai.system` by default while emitting `gen_ai.provider.name` under `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`. Thanks @vincentkoc.
|
||||
- Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc.
|
||||
- Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc.
|
||||
- Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.
|
||||
- Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.
|
||||
- Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna.
|
||||
|
||||
@@ -502,7 +502,7 @@ export function resetPluginsCliTestState() {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 1,
|
||||
migrationVersion: 2,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [],
|
||||
|
||||
@@ -2,7 +2,11 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginCandidate } from "../../../plugins/discovery.js";
|
||||
import { readPersistedInstalledPluginIndex } from "../../../plugins/installed-plugin-index-store.js";
|
||||
import {
|
||||
readPersistedInstalledPluginIndex,
|
||||
writePersistedInstalledPluginIndex,
|
||||
} from "../../../plugins/installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndex } from "../../../plugins/installed-plugin-index.js";
|
||||
import {
|
||||
cleanupTrackedTempDirs,
|
||||
makeTrackedTempDir,
|
||||
@@ -58,12 +62,24 @@ function createCandidate(rootDir: string, id = "demo"): PluginCandidate {
|
||||
};
|
||||
}
|
||||
|
||||
function createCurrentIndex(): InstalledPluginIndex {
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 2,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin registry install migration", () => {
|
||||
it("short-circuits when a registry file already exists", async () => {
|
||||
it("short-circuits when a current registry file already exists", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const filePath = path.join(stateDir, "plugins", "installed-index.json");
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, "{}\n", "utf8");
|
||||
await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir });
|
||||
const readConfig = vi.fn(async () => ({}));
|
||||
|
||||
await expect(
|
||||
@@ -83,6 +99,34 @@ describe("plugin registry install migration", () => {
|
||||
expect(readConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("migrates when an existing registry file is not current", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const filePath = path.join(stateDir, "plugins", "installed-index.json");
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify({ version: 1, migrationVersion: 1 }), "utf8");
|
||||
|
||||
await expect(
|
||||
migratePluginRegistryForInstall({
|
||||
stateDir,
|
||||
candidates: [createCandidate(pluginDir)],
|
||||
readConfig: async () => ({}),
|
||||
env: hermeticEnv(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "migrated",
|
||||
preflight: {
|
||||
action: "migrate",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
migrationVersion: 2,
|
||||
plugins: [expect.objectContaining({ pluginId: "demo" })],
|
||||
});
|
||||
});
|
||||
|
||||
it("persists only plugins enabled by the central config policy", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const enabledDir = path.join(stateDir, "plugins", "enabled-demo");
|
||||
@@ -172,7 +216,7 @@ describe("plugin registry install migration", () => {
|
||||
migrated: true,
|
||||
current: {
|
||||
refreshReason: "migration",
|
||||
migrationVersion: 1,
|
||||
migrationVersion: 2,
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
readPersistedInstalledPluginIndexSync,
|
||||
resolveInstalledPluginIndexStorePath,
|
||||
writePersistedInstalledPluginIndex,
|
||||
type InstalledPluginIndexStoreInspection,
|
||||
@@ -75,12 +76,15 @@ export function preflightPluginRegistryInstallMigration(
|
||||
}
|
||||
const pathExists = params.existsSync ?? fs.existsSync;
|
||||
if (!force && pathExists(filePath)) {
|
||||
return {
|
||||
action: "skip-existing",
|
||||
filePath,
|
||||
force,
|
||||
deprecationWarnings,
|
||||
};
|
||||
const currentRegistry = readPersistedInstalledPluginIndexSync(params);
|
||||
if (currentRegistry) {
|
||||
return {
|
||||
action: "skip-existing",
|
||||
filePath,
|
||||
force,
|
||||
deprecationWarnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
action: "migrate",
|
||||
|
||||
@@ -31,6 +31,14 @@ vi.mock("./manifest-registry.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./installed-plugin-index-store.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./installed-plugin-index-store.js")>();
|
||||
return {
|
||||
...actual,
|
||||
readPersistedInstalledPluginIndexSync: vi.fn(() => null),
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
hasConfiguredChannelsForReadOnlyScope,
|
||||
listConfiguredAnnounceChannelIdsForConfig,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
resolveMemoryDreamingPluginId,
|
||||
} from "../memory-host-sdk/dreaming.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
|
||||
import { hasExplicitChannelConfig } from "./channel-presence-policy.js";
|
||||
import {
|
||||
createPluginActivationSource,
|
||||
@@ -16,8 +15,8 @@ import {
|
||||
normalizePluginsConfig,
|
||||
resolveEffectivePluginActivationState,
|
||||
} from "./config-state.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { hasKind } from "./slots.js";
|
||||
import type { InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
|
||||
|
||||
function listDisabledChannelIds(config: OpenClawConfig): Set<string> {
|
||||
const channels = config.channels;
|
||||
@@ -46,30 +45,12 @@ function listPotentialEnabledChannelIds(config: OpenClawConfig, env: NodeJS.Proc
|
||||
.filter((id) => id && !disabled.has(id));
|
||||
}
|
||||
|
||||
function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
|
||||
return Boolean(
|
||||
plugin.providers.length > 0 ||
|
||||
plugin.cliBackends.length > 0 ||
|
||||
plugin.contracts?.speechProviders?.length ||
|
||||
plugin.contracts?.mediaUnderstandingProviders?.length ||
|
||||
plugin.contracts?.documentExtractors?.length ||
|
||||
plugin.contracts?.imageGenerationProviders?.length ||
|
||||
plugin.contracts?.videoGenerationProviders?.length ||
|
||||
plugin.contracts?.musicGenerationProviders?.length ||
|
||||
plugin.contracts?.webContentExtractors?.length ||
|
||||
plugin.contracts?.webFetchProviders?.length ||
|
||||
plugin.contracts?.webSearchProviders?.length ||
|
||||
plugin.contracts?.memoryEmbeddingProviders?.length ||
|
||||
hasKind(plugin.kind, "memory"),
|
||||
);
|
||||
function isGatewayStartupMemoryPlugin(plugin: InstalledPluginIndexRecord): boolean {
|
||||
return plugin.startup.memory;
|
||||
}
|
||||
|
||||
function isGatewayStartupMemoryPlugin(plugin: PluginManifestRecord): boolean {
|
||||
return hasKind(plugin.kind, "memory");
|
||||
}
|
||||
|
||||
function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean {
|
||||
return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin);
|
||||
function isGatewayStartupSidecar(plugin: InstalledPluginIndexRecord): boolean {
|
||||
return plugin.startup.sidecar;
|
||||
}
|
||||
|
||||
function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set<string> {
|
||||
@@ -92,7 +73,7 @@ function resolveExplicitMemorySlotStartupPluginId(config: OpenClawConfig): strin
|
||||
}
|
||||
|
||||
function shouldConsiderForGatewayStartup(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
startupDreamingPluginIds: ReadonlySet<string>;
|
||||
explicitMemorySlotStartupPluginId?: string;
|
||||
}): boolean {
|
||||
@@ -102,21 +83,23 @@ function shouldConsiderForGatewayStartup(params: {
|
||||
if (!isGatewayStartupMemoryPlugin(params.plugin)) {
|
||||
return false;
|
||||
}
|
||||
if (params.startupDreamingPluginIds.has(params.plugin.id)) {
|
||||
if (params.startupDreamingPluginIds.has(params.plugin.pluginId)) {
|
||||
return true;
|
||||
}
|
||||
return params.explicitMemorySlotStartupPluginId === params.plugin.id;
|
||||
return params.explicitMemorySlotStartupPluginId === params.plugin.pluginId;
|
||||
}
|
||||
|
||||
function hasConfiguredStartupChannel(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
configuredChannelIds: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
return params.plugin.channels.some((channelId) => params.configuredChannelIds.has(channelId));
|
||||
return params.plugin.contributions.channels.some((channelId) =>
|
||||
params.configuredChannelIds.has(channelId),
|
||||
);
|
||||
}
|
||||
|
||||
function canStartConfiguredChannelPlugin(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
config: OpenClawConfig;
|
||||
pluginsConfig: ReturnType<typeof normalizePluginsConfig>;
|
||||
activationSource: ReturnType<typeof createPluginActivationSource>;
|
||||
@@ -124,15 +107,15 @@ function canStartConfiguredChannelPlugin(params: {
|
||||
if (!params.pluginsConfig.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (params.pluginsConfig.deny.includes(params.plugin.id)) {
|
||||
if (params.pluginsConfig.deny.includes(params.plugin.pluginId)) {
|
||||
return false;
|
||||
}
|
||||
if (params.pluginsConfig.entries[params.plugin.id]?.enabled === false) {
|
||||
if (params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
const explicitBundledChannelConfig =
|
||||
params.plugin.origin === "bundled" &&
|
||||
params.plugin.channels.some((channelId) =>
|
||||
params.plugin.contributions.channels.some((channelId) =>
|
||||
hasExplicitChannelConfig({
|
||||
config: params.activationSource.rootConfig ?? params.config,
|
||||
channelId,
|
||||
@@ -140,7 +123,7 @@ function canStartConfiguredChannelPlugin(params: {
|
||||
);
|
||||
if (
|
||||
params.pluginsConfig.allow.length > 0 &&
|
||||
!params.pluginsConfig.allow.includes(params.plugin.id) &&
|
||||
!params.pluginsConfig.allow.includes(params.plugin.pluginId) &&
|
||||
!explicitBundledChannelConfig
|
||||
) {
|
||||
return false;
|
||||
@@ -149,7 +132,7 @@ function canStartConfiguredChannelPlugin(params: {
|
||||
return true;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: params.plugin.id,
|
||||
id: params.plugin.pluginId,
|
||||
origin: params.plugin.origin,
|
||||
config: params.pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
@@ -164,13 +147,14 @@ export function resolveChannelPluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] {
|
||||
return loadPluginManifestRegistry({
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter((plugin) => plugin.channels.length > 0)
|
||||
.map((plugin) => plugin.id);
|
||||
});
|
||||
return index.plugins
|
||||
.filter((plugin) => plugin.contributions.channels.length > 0)
|
||||
.map((plugin) => plugin.pluginId);
|
||||
}
|
||||
|
||||
export function resolveConfiguredDeferredChannelPluginIds(params: {
|
||||
@@ -186,15 +170,16 @@ export function resolveConfiguredDeferredChannelPluginIds(params: {
|
||||
const activationSource = createPluginActivationSource({
|
||||
config: params.config,
|
||||
});
|
||||
return loadPluginManifestRegistry({
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
});
|
||||
return index.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
hasConfiguredStartupChannel({ plugin, configuredChannelIds }) &&
|
||||
plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true &&
|
||||
plugin.startup.deferConfiguredChannelFullLoadUntilAfterListen &&
|
||||
canStartConfiguredChannelPlugin({
|
||||
plugin,
|
||||
config: params.config,
|
||||
@@ -202,7 +187,7 @@ export function resolveConfiguredDeferredChannelPluginIds(params: {
|
||||
activationSource,
|
||||
}),
|
||||
)
|
||||
.map((plugin) => plugin.id);
|
||||
.map((plugin) => plugin.pluginId);
|
||||
}
|
||||
|
||||
export function resolveGatewayStartupPluginIds(params: {
|
||||
@@ -219,33 +204,23 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
const activationSource = createPluginActivationSource({
|
||||
config: params.activationSourceConfig ?? params.config,
|
||||
});
|
||||
const requiredAgentHarnessPluginIds = new Set(
|
||||
const requiredAgentHarnessRuntimes = new Set(
|
||||
collectConfiguredAgentHarnessRuntimes(
|
||||
params.activationSourceConfig ?? params.config,
|
||||
params.env,
|
||||
).flatMap((runtime) =>
|
||||
resolveManifestActivationPluginIds({
|
||||
trigger: {
|
||||
kind: "agentHarness",
|
||||
runtime,
|
||||
},
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
cache: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
|
||||
const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId(
|
||||
params.activationSourceConfig ?? params.config,
|
||||
);
|
||||
return loadPluginManifestRegistry({
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter((plugin) => {
|
||||
});
|
||||
return index.plugins
|
||||
.filter((plugin) => {
|
||||
if (hasConfiguredStartupChannel({ plugin, configuredChannelIds })) {
|
||||
return canStartConfiguredChannelPlugin({
|
||||
plugin,
|
||||
@@ -254,9 +229,11 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
activationSource,
|
||||
});
|
||||
}
|
||||
if (requiredAgentHarnessPluginIds.has(plugin.id)) {
|
||||
if (
|
||||
plugin.startup.agentHarnesses.some((runtime) => requiredAgentHarnessRuntimes.has(runtime))
|
||||
) {
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
id: plugin.pluginId,
|
||||
origin: plugin.origin,
|
||||
config: pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
@@ -275,7 +252,7 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
return false;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: plugin.id,
|
||||
id: plugin.pluginId,
|
||||
origin: plugin.origin,
|
||||
config: pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
@@ -290,5 +267,5 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
}
|
||||
return activationState.source === "explicit" || activationState.source === "default";
|
||||
})
|
||||
.map((plugin) => plugin.id);
|
||||
.map((plugin) => plugin.pluginId);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ function createIndex(overrides: Partial<InstalledPluginIndex> = {}): InstalledPl
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 1,
|
||||
migrationVersion: 2,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [
|
||||
@@ -48,6 +48,12 @@ function createIndex(overrides: Partial<InstalledPluginIndex> = {}): InstalledPl
|
||||
commandAliases: [],
|
||||
contracts: [],
|
||||
},
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -47,6 +47,15 @@ const InstalledPluginIndexContributionsSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const InstalledPluginIndexStartupSchema = z
|
||||
.object({
|
||||
sidecar: z.boolean(),
|
||||
memory: z.boolean(),
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: z.boolean(),
|
||||
agentHarnesses: ContributionArraySchema,
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const InstalledPluginIndexRecordSchema = z
|
||||
.object({
|
||||
pluginId: z.string(),
|
||||
@@ -68,6 +77,7 @@ const InstalledPluginIndexRecordSchema = z
|
||||
enabled: z.boolean(),
|
||||
enabledByDefault: z.boolean().optional(),
|
||||
contributions: InstalledPluginIndexContributionsSchema,
|
||||
startup: InstalledPluginIndexStartupSchema,
|
||||
compat: z.array(z.string()),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("installed plugin index", () => {
|
||||
|
||||
expect(index).toMatchObject({
|
||||
version: 1,
|
||||
migrationVersion: 1,
|
||||
migrationVersion: 2,
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [
|
||||
{
|
||||
@@ -679,7 +679,7 @@ describe("installed plugin index", () => {
|
||||
env: hermeticEnv({ OPENCLAW_VERSION: "2026.4.26" }),
|
||||
}),
|
||||
compatRegistryVersion: "different-compat-registry",
|
||||
migrationVersion: 2 as 1,
|
||||
migrationVersion: 3 as 2,
|
||||
};
|
||||
|
||||
expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([
|
||||
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
type PluginManifestRegistry,
|
||||
} from "./manifest-registry.js";
|
||||
import type { PluginDiagnostic } from "./manifest-types.js";
|
||||
import { hasKind } from "./slots.js";
|
||||
|
||||
export const INSTALLED_PLUGIN_INDEX_VERSION = 1;
|
||||
export const INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION = 1;
|
||||
export const INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION = 2;
|
||||
|
||||
export type InstalledPluginIndexRefreshReason =
|
||||
| "missing"
|
||||
@@ -44,6 +45,13 @@ export type InstalledPluginIndexContributions = {
|
||||
contracts: readonly string[];
|
||||
};
|
||||
|
||||
export type InstalledPluginStartupInfo = {
|
||||
sidecar: boolean;
|
||||
memory: boolean;
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: boolean;
|
||||
agentHarnesses: readonly string[];
|
||||
};
|
||||
|
||||
export type InstalledPluginInstallRecordInfo = Pick<
|
||||
PluginInstallRecord,
|
||||
| "source"
|
||||
@@ -95,6 +103,7 @@ export type InstalledPluginIndexRecord = {
|
||||
enabled: boolean;
|
||||
enabledByDefault?: boolean;
|
||||
contributions: InstalledPluginIndexContributions;
|
||||
startup: InstalledPluginStartupInfo;
|
||||
compat: readonly PluginCompatCode[];
|
||||
};
|
||||
|
||||
@@ -199,6 +208,34 @@ function collectContractKeys(record: PluginManifestRecord): readonly string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function hasRuntimeContractSurface(record: PluginManifestRecord): boolean {
|
||||
return Boolean(
|
||||
record.providers.length > 0 ||
|
||||
record.cliBackends.length > 0 ||
|
||||
record.contracts?.speechProviders?.length ||
|
||||
record.contracts?.mediaUnderstandingProviders?.length ||
|
||||
record.contracts?.documentExtractors?.length ||
|
||||
record.contracts?.imageGenerationProviders?.length ||
|
||||
record.contracts?.videoGenerationProviders?.length ||
|
||||
record.contracts?.musicGenerationProviders?.length ||
|
||||
record.contracts?.webContentExtractors?.length ||
|
||||
record.contracts?.webFetchProviders?.length ||
|
||||
record.contracts?.webSearchProviders?.length ||
|
||||
record.contracts?.memoryEmbeddingProviders?.length ||
|
||||
hasKind(record.kind, "memory"),
|
||||
);
|
||||
}
|
||||
|
||||
function buildStartupInfo(record: PluginManifestRecord): InstalledPluginStartupInfo {
|
||||
return {
|
||||
sidecar: record.channels.length === 0 && !hasRuntimeContractSurface(record),
|
||||
memory: hasKind(record.kind, "memory"),
|
||||
deferConfiguredChannelFullLoadUntilAfterListen:
|
||||
record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
|
||||
agentHarnesses: sortUnique(record.activation?.onAgentHarnesses ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
function collectCompatCodes(record: PluginManifestRecord): readonly PluginCompatCode[] {
|
||||
const codes: PluginCompatCode[] = [];
|
||||
if (record.providerAuthEnvVars && Object.keys(record.providerAuthEnvVars).length > 0) {
|
||||
@@ -463,6 +500,7 @@ function buildInstalledPluginIndex(
|
||||
origin: record.origin,
|
||||
enabled,
|
||||
contributions: buildContributions(record),
|
||||
startup: buildStartupInfo(record),
|
||||
compat: collectCompatCodes(record),
|
||||
};
|
||||
if (record.enabledByDefault === true) {
|
||||
|
||||
@@ -94,7 +94,7 @@ function createIndex(pluginId = "demo"): InstalledPluginIndex {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 1,
|
||||
migrationVersion: 2,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
plugins: [
|
||||
@@ -115,6 +115,12 @@ function createIndex(pluginId = "demo"): InstalledPluginIndex {
|
||||
commandAliases: [],
|
||||
contracts: [],
|
||||
},
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user