mirror of https://github.com/openclaw/openclaw.git
Plugins: extract loader candidate orchestration
This commit is contained in:
parent
d9a0fea97e
commit
e1b207f4cf
|
|
@ -37,6 +37,7 @@ This is an implementation checklist, not a future-design spec.
|
|||
| Loader entry-path opening and module import | `src/plugins/loader.ts` | `src/extension-host/loader-import.ts` | `partial` | Boundary-checked entry opening and module import now delegate through host-owned loader-import helpers while preserving the current trusted in-process loading model. |
|
||||
| Loader module-export, config-validation, and memory-slot decisions | `src/plugins/loader.ts` | `src/extension-host/loader-runtime.ts` | `partial` | Module export resolution, export-metadata application, config validation, and early or final memory-slot decisions now delegate through host-owned loader-runtime helpers. |
|
||||
| Loader post-import planning and register execution | `src/plugins/loader.ts` | `src/extension-host/loader-register.ts` | `partial` | Definition application, post-import validation planning, and `register(...)` execution now delegate through host-owned loader-register helpers while preserving current plugin behavior. |
|
||||
| Loader per-candidate orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-flow.ts` | `partial` | The per-candidate load flow now runs through a host-owned orchestrator that composes planning, import, runtime validation, register execution, and record-state helpers. |
|
||||
| Loader record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | Disabled, error, and appended plugin-record state transitions now delegate through host-owned loader-state helpers; a real lifecycle state machine still does not exist. |
|
||||
| Channel lookup | `src/channels/plugins/index.ts`, `src/channels/plugins/registry-loader.ts`, `src/channels/registry.ts` | extension-host-backed registries plus kernel channel contracts | `partial` | Readers now consume the host-owned active registry, but writes still originate from plugin registration. |
|
||||
| Dock lookup | `src/channels/dock.ts` | host-owned static descriptors | `partial` | Runtime lookup now uses the host boundary; dock ownership itself has not moved yet. |
|
||||
|
|
@ -83,14 +84,14 @@ That pattern has been used for:
|
|||
- active registry ownership
|
||||
- normalized extension schema and resolved-extension records
|
||||
- static consumers such as skills, validation, auto-enable, and config baseline generation
|
||||
- loader compatibility, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, and record-state transitions
|
||||
- loader compatibility, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, and record-state transitions
|
||||
|
||||
## Immediate Next Targets
|
||||
|
||||
These are the next lowest-risk cutover steps:
|
||||
|
||||
1. Replace remaining static-only manifest-registry injections with resolved-extension registry inputs where practical.
|
||||
2. Move the remaining loader orchestration into `src/extension-host/*`, especially cache wiring, final registry append flow, enablement completion, and lifecycle-state transitions.
|
||||
2. Move the remaining loader orchestration into `src/extension-host/*`, especially cache wiring, final registry finalization, and lifecycle-state transitions.
|
||||
3. Introduce explicit host-owned registration surfaces for runtime writes, starting with the least-coupled registries.
|
||||
4. Move minimal SDK compatibility and loader normalization into `src/extension-host/*` without breaking current `openclaw/plugin-sdk/*` loading.
|
||||
5. Start the first pilot on `extensions/thread-ownership` only after the host-side registry and lifecycle seams are explicit.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import type { PluginCandidate } from "../plugins/discovery.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { processExtensionHostPluginCandidate } from "./loader-flow.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function createTempPluginFixture() {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-loader-flow-"));
|
||||
tempDirs.push(rootDir);
|
||||
const entryPath = path.join(rootDir, "index.js");
|
||||
fs.writeFileSync(entryPath, "export default {}");
|
||||
return { rootDir, entryPath };
|
||||
}
|
||||
|
||||
function createCandidate(
|
||||
rootDir: string,
|
||||
entryPath: string,
|
||||
overrides: Partial<PluginCandidate> = {},
|
||||
): PluginCandidate {
|
||||
return {
|
||||
source: entryPath,
|
||||
rootDir,
|
||||
packageDir: rootDir,
|
||||
origin: "workspace",
|
||||
workspaceDir: "/workspace",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createManifestRecord(
|
||||
rootDir: string,
|
||||
entryPath: string,
|
||||
overrides: Partial<PluginManifestRecord> = {},
|
||||
): PluginManifestRecord {
|
||||
return {
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
version: "1.0.0",
|
||||
kind: "context-engine",
|
||||
channels: [],
|
||||
providers: [],
|
||||
skills: [],
|
||||
origin: "workspace",
|
||||
workspaceDir: "/workspace",
|
||||
rootDir,
|
||||
source: entryPath,
|
||||
manifestPath: path.join(rootDir, "openclaw.plugin.json"),
|
||||
schemaCacheKey: "demo-schema",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
resolvedExtension: {
|
||||
id: "demo",
|
||||
source: "/plugins/demo/index.ts",
|
||||
origin: "workspace",
|
||||
rootDir: "/plugins/demo",
|
||||
workspaceDir: "/workspace",
|
||||
static: {
|
||||
package: {},
|
||||
config: {},
|
||||
setup: {},
|
||||
},
|
||||
runtime: {
|
||||
kind: "context-engine",
|
||||
contributions: [],
|
||||
},
|
||||
policy: {},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createRegistry(): PluginRegistry {
|
||||
return {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("extension host loader flow", () => {
|
||||
it("handles validate-only candidates through the host orchestrator", () => {
|
||||
const { rootDir, entryPath } = createTempPluginFixture();
|
||||
const registry = createRegistry();
|
||||
|
||||
const result = processExtensionHostPluginCandidate({
|
||||
candidate: createCandidate(rootDir, entryPath),
|
||||
manifestRecord: createManifestRecord(rootDir, entryPath),
|
||||
normalizedConfig: normalizePluginsConfig({
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
config: { enabled: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
rootConfig: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
config: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateOnly: true,
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
registry,
|
||||
seenIds: new Map(),
|
||||
selectedMemoryPluginId: null,
|
||||
createApi: () => ({}) as never,
|
||||
loadModule: () =>
|
||||
({
|
||||
default: {
|
||||
id: "demo",
|
||||
register: () => {},
|
||||
},
|
||||
}) as never,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
selectedMemoryPluginId: null,
|
||||
memorySlotMatched: false,
|
||||
});
|
||||
expect(registry.plugins).toHaveLength(1);
|
||||
expect(registry.plugins[0]?.id).toBe("demo");
|
||||
expect(registry.plugins[0]?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("records import failures through the existing plugin error path", () => {
|
||||
const { rootDir, entryPath } = createTempPluginFixture();
|
||||
const registry = createRegistry();
|
||||
|
||||
processExtensionHostPluginCandidate({
|
||||
candidate: createCandidate(rootDir, entryPath),
|
||||
manifestRecord: createManifestRecord(rootDir, entryPath),
|
||||
normalizedConfig: normalizePluginsConfig({
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
rootConfig: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateOnly: false,
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
registry,
|
||||
seenIds: new Map(),
|
||||
selectedMemoryPluginId: null,
|
||||
createApi: () => ({}) as never,
|
||||
loadModule: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.plugins).toHaveLength(1);
|
||||
expect(registry.plugins[0]?.status).toBe("error");
|
||||
expect(registry.diagnostics[0]?.message).toContain("failed to load plugin");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { PluginCandidate } from "../plugins/discovery.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginRecord, PluginRegistry } from "../plugins/registry.js";
|
||||
import type { OpenClawPluginApi, OpenClawPluginModule, PluginLogger } from "../plugins/types.js";
|
||||
import { importExtensionHostPluginModule } from "./loader-import.js";
|
||||
import { recordExtensionHostPluginError } from "./loader-policy.js";
|
||||
import { prepareExtensionHostPluginCandidate } from "./loader-records.js";
|
||||
import {
|
||||
planExtensionHostLoadedPlugin,
|
||||
runExtensionHostPluginRegister,
|
||||
} from "./loader-register.js";
|
||||
import {
|
||||
resolveExtensionHostEarlyMemoryDecision,
|
||||
resolveExtensionHostModuleExport,
|
||||
} from "./loader-runtime.js";
|
||||
import {
|
||||
appendExtensionHostPluginRecord,
|
||||
setExtensionHostPluginRecordDisabled,
|
||||
setExtensionHostPluginRecordError,
|
||||
} from "./loader-state.js";
|
||||
|
||||
export function processExtensionHostPluginCandidate(params: {
|
||||
candidate: PluginCandidate;
|
||||
manifestRecord: PluginManifestRecord;
|
||||
normalizedConfig: {
|
||||
entries: Record<
|
||||
string,
|
||||
{
|
||||
enabled?: boolean;
|
||||
hooks?: {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
config?: unknown;
|
||||
}
|
||||
>;
|
||||
slots: {
|
||||
memory?: string | null;
|
||||
};
|
||||
};
|
||||
rootConfig: OpenClawConfig;
|
||||
validateOnly: boolean;
|
||||
logger: PluginLogger;
|
||||
registry: PluginRegistry;
|
||||
seenIds: Map<string, PluginRecord["origin"]>;
|
||||
selectedMemoryPluginId: string | null;
|
||||
createApi: (
|
||||
record: PluginRecord,
|
||||
options: {
|
||||
config: OpenClawConfig;
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
hookPolicy?: { allowPromptInjection?: boolean };
|
||||
},
|
||||
) => OpenClawPluginApi;
|
||||
loadModule: (safeSource: string) => OpenClawPluginModule;
|
||||
}): { selectedMemoryPluginId: string | null; memorySlotMatched: boolean } {
|
||||
const { candidate, manifestRecord } = params;
|
||||
const pluginId = manifestRecord.id;
|
||||
const preparedCandidate = prepareExtensionHostPluginCandidate({
|
||||
candidate,
|
||||
manifestRecord,
|
||||
normalizedConfig: params.normalizedConfig,
|
||||
rootConfig: params.rootConfig,
|
||||
seenIds: params.seenIds,
|
||||
});
|
||||
if (preparedCandidate.kind === "duplicate") {
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record: preparedCandidate.record,
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
memorySlotMatched: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { record, entry, enableState } = preparedCandidate;
|
||||
const pushPluginLoadError = (message: string) => {
|
||||
setExtensionHostPluginRecordError(record, message);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
params.registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
};
|
||||
|
||||
if (!enableState.enabled) {
|
||||
setExtensionHostPluginRecordDisabled(record, enableState.reason);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
memorySlotMatched: false,
|
||||
};
|
||||
}
|
||||
|
||||
const earlyMemoryDecision = resolveExtensionHostEarlyMemoryDecision({
|
||||
origin: candidate.origin,
|
||||
manifestKind: manifestRecord.kind,
|
||||
recordId: record.id,
|
||||
memorySlot: params.normalizedConfig.slots.memory,
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
});
|
||||
if (!earlyMemoryDecision.enabled) {
|
||||
setExtensionHostPluginRecordDisabled(record, earlyMemoryDecision.reason);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
memorySlotMatched: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!manifestRecord.configSchema) {
|
||||
pushPluginLoadError("missing config schema");
|
||||
return {
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
memorySlotMatched: false,
|
||||
};
|
||||
}
|
||||
|
||||
const moduleImport = importExtensionHostPluginModule({
|
||||
rootDir: candidate.rootDir,
|
||||
source: candidate.source,
|
||||
origin: candidate.origin,
|
||||
loadModule: params.loadModule,
|
||||
});
|
||||
if (!moduleImport.ok) {
|
||||
if (moduleImport.message !== "failed to load plugin") {
|
||||
pushPluginLoadError(moduleImport.message);
|
||||
return {
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
memorySlotMatched: false,
|
||||
};
|
||||
}
|
||||
recordExtensionHostPluginError({
|
||||
logger: params.logger,
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
error: moduleImport.error,
|
||||
logPrefix: `[plugins] ${record.id} failed to load from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "failed to load plugin: ",
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
memorySlotMatched: false,
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = resolveExtensionHostModuleExport(moduleImport.module);
|
||||
const loadedPlan = planExtensionHostLoadedPlugin({
|
||||
record,
|
||||
manifestRecord,
|
||||
definition: resolved.definition,
|
||||
register: resolved.register,
|
||||
diagnostics: params.registry.diagnostics,
|
||||
memorySlot: params.normalizedConfig.slots.memory,
|
||||
selectedMemoryPluginId: params.selectedMemoryPluginId,
|
||||
entryConfig: entry?.config,
|
||||
validateOnly: params.validateOnly,
|
||||
});
|
||||
|
||||
if (loadedPlan.kind === "error") {
|
||||
pushPluginLoadError(loadedPlan.message);
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "disabled") {
|
||||
setExtensionHostPluginRecordDisabled(record, loadedPlan.reason);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "invalid-config") {
|
||||
params.logger.error(`[plugins] ${record.id} ${loadedPlan.message}`);
|
||||
pushPluginLoadError(loadedPlan.message);
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "validate-only") {
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "missing-register") {
|
||||
params.logger.error(`[plugins] ${record.id} missing register/activate export`);
|
||||
pushPluginLoadError(loadedPlan.message);
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
||||
const registerResult = runExtensionHostPluginRegister({
|
||||
register: loadedPlan.register,
|
||||
createApi: params.createApi,
|
||||
record,
|
||||
config: params.rootConfig,
|
||||
pluginConfig: loadedPlan.pluginConfig,
|
||||
hookPolicy: entry?.hooks,
|
||||
diagnostics: params.registry.diagnostics,
|
||||
});
|
||||
if (!registerResult.ok) {
|
||||
recordExtensionHostPluginError({
|
||||
logger: params.logger,
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
error: registerResult.error,
|
||||
logPrefix: `[plugins] ${record.id} failed during register from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "plugin failed during register: ",
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
seenIds: params.seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
return {
|
||||
selectedMemoryPluginId: loadedPlan.selectedMemoryPluginId,
|
||||
memorySlotMatched: loadedPlan.memorySlotMatched,
|
||||
};
|
||||
}
|
||||
|
|
@ -10,29 +10,14 @@ import {
|
|||
resolvePluginSdkAliasFile,
|
||||
resolvePluginSdkScopedAliasMap,
|
||||
} from "../extension-host/loader-compat.js";
|
||||
import { importExtensionHostPluginModule } from "../extension-host/loader-import.js";
|
||||
import { processExtensionHostPluginCandidate } from "../extension-host/loader-flow.js";
|
||||
import {
|
||||
buildExtensionHostProvenanceIndex,
|
||||
compareExtensionHostDuplicateCandidateOrder,
|
||||
pushExtensionHostDiagnostics,
|
||||
recordExtensionHostPluginError,
|
||||
warnAboutUntrackedLoadedExtensions,
|
||||
warnWhenExtensionAllowlistIsOpen,
|
||||
} from "../extension-host/loader-policy.js";
|
||||
import { prepareExtensionHostPluginCandidate } from "../extension-host/loader-records.js";
|
||||
import {
|
||||
planExtensionHostLoadedPlugin,
|
||||
runExtensionHostPluginRegister,
|
||||
} from "../extension-host/loader-register.js";
|
||||
import {
|
||||
resolveExtensionHostEarlyMemoryDecision,
|
||||
resolveExtensionHostModuleExport,
|
||||
} from "../extension-host/loader-runtime.js";
|
||||
import {
|
||||
appendExtensionHostPluginRecord,
|
||||
setExtensionHostPluginRecordDisabled,
|
||||
setExtensionHostPluginRecordError,
|
||||
} from "../extension-host/loader-state.js";
|
||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
|
@ -287,190 +272,21 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||
if (!manifestRecord) {
|
||||
continue;
|
||||
}
|
||||
const pluginId = manifestRecord.id;
|
||||
const preparedCandidate = prepareExtensionHostPluginCandidate({
|
||||
const processed = processExtensionHostPluginCandidate({
|
||||
candidate,
|
||||
manifestRecord,
|
||||
normalizedConfig: normalized,
|
||||
rootConfig: cfg,
|
||||
seenIds,
|
||||
});
|
||||
if (preparedCandidate.kind === "duplicate") {
|
||||
const { record } = preparedCandidate;
|
||||
appendExtensionHostPluginRecord({ registry, record });
|
||||
continue;
|
||||
}
|
||||
const { record, entry, enableState } = preparedCandidate;
|
||||
const pushPluginLoadError = (message: string) => {
|
||||
setExtensionHostPluginRecordError(record, message);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: record.error,
|
||||
});
|
||||
};
|
||||
|
||||
if (!enableState.enabled) {
|
||||
setExtensionHostPluginRecordDisabled(record, enableState.reason);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fast-path bundled memory plugins that are guaranteed disabled by slot policy.
|
||||
// This avoids opening/importing heavy memory plugin modules that will never register.
|
||||
const earlyMemoryDecision = resolveExtensionHostEarlyMemoryDecision({
|
||||
origin: candidate.origin,
|
||||
manifestKind: manifestRecord.kind,
|
||||
recordId: record.id,
|
||||
memorySlot,
|
||||
selectedMemoryPluginId,
|
||||
});
|
||||
if (!earlyMemoryDecision.enabled) {
|
||||
setExtensionHostPluginRecordDisabled(record, earlyMemoryDecision.reason);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!manifestRecord.configSchema) {
|
||||
pushPluginLoadError("missing config schema");
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleImport = importExtensionHostPluginModule({
|
||||
rootDir: candidate.rootDir,
|
||||
source: candidate.source,
|
||||
origin: candidate.origin,
|
||||
loadModule: (safeSource) => getJiti()(safeSource),
|
||||
});
|
||||
if (!moduleImport.ok) {
|
||||
if (moduleImport.message !== "failed to load plugin") {
|
||||
pushPluginLoadError(moduleImport.message);
|
||||
continue;
|
||||
}
|
||||
recordExtensionHostPluginError({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
error: moduleImport.error,
|
||||
logPrefix: `[plugins] ${record.id} failed to load from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "failed to load plugin: ",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolved = resolveExtensionHostModuleExport(moduleImport.module as OpenClawPluginModule);
|
||||
const definition = resolved.definition;
|
||||
const register = resolved.register;
|
||||
|
||||
const loadedPlan = planExtensionHostLoadedPlugin({
|
||||
record,
|
||||
manifestRecord,
|
||||
definition,
|
||||
register,
|
||||
diagnostics: registry.diagnostics,
|
||||
memorySlot,
|
||||
selectedMemoryPluginId,
|
||||
entryConfig: entry?.config,
|
||||
validateOnly,
|
||||
});
|
||||
if (loadedPlan.memorySlotMatched) {
|
||||
memorySlotMatched = true;
|
||||
}
|
||||
selectedMemoryPluginId = loadedPlan.selectedMemoryPluginId;
|
||||
|
||||
if (loadedPlan.kind === "error") {
|
||||
pushPluginLoadError(loadedPlan.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "disabled") {
|
||||
setExtensionHostPluginRecordDisabled(record, loadedPlan.reason);
|
||||
appendExtensionHostPluginRecord({
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "invalid-config") {
|
||||
logger.error(`[plugins] ${record.id} ${loadedPlan.message}`);
|
||||
pushPluginLoadError(loadedPlan.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "validate-only") {
|
||||
appendExtensionHostPluginRecord({
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loadedPlan.kind === "missing-register") {
|
||||
logger.error(`[plugins] ${record.id} missing register/activate export`);
|
||||
pushPluginLoadError(loadedPlan.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
const registerResult = runExtensionHostPluginRegister({
|
||||
register: loadedPlan.register,
|
||||
createApi,
|
||||
record,
|
||||
config: cfg,
|
||||
pluginConfig: loadedPlan.pluginConfig,
|
||||
hookPolicy: entry?.hooks,
|
||||
diagnostics: registry.diagnostics,
|
||||
});
|
||||
if (!registerResult.ok) {
|
||||
recordExtensionHostPluginError({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
error: registerResult.error,
|
||||
logPrefix: `[plugins] ${record.id} failed during register from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "plugin failed during register: ",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
appendExtensionHostPluginRecord({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
selectedMemoryPluginId,
|
||||
createApi,
|
||||
loadModule: (safeSource) => getJiti()(safeSource) as OpenClawPluginModule,
|
||||
});
|
||||
selectedMemoryPluginId = processed.selectedMemoryPluginId;
|
||||
memorySlotMatched ||= processed.memorySlotMatched;
|
||||
}
|
||||
|
||||
if (typeof memorySlot === "string" && !memorySlotMatched) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue