Plugins: add loader activation session
This commit is contained in:
parent
5de733fa80
commit
5b26ce7252
@ -40,6 +40,7 @@ This is an implementation checklist, not a future-design spec.
|
||||
| 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 top-level load orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-orchestrator.ts` | `partial` | Cache hits, runtime creation, discovery, manifest loading, candidate ordering, candidate processing, and finalization now route through a host-owned loader orchestrator while `src/plugins/loader.ts` remains the compatibility facade. |
|
||||
| Loader mutable activation state session | local variables in `src/plugins/loader.ts` and `src/extension-host/loader-orchestrator.ts` | `src/extension-host/loader-session.ts` | `partial` | Seen-id tracking, memory-slot selection state, and finalization inputs now live in a host-owned loader session instead of being spread across top-level loader variables. |
|
||||
| Loader record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | The loader now enforces an explicit lifecycle transition model (`prepared -> imported -> validated -> registered -> ready`, plus terminal `disabled` and `error`) while still mapping back to compatibility `PluginRecord.status` values. |
|
||||
| Loader final cache, warning, and activation finalization | `src/plugins/loader.ts` | `src/extension-host/loader-finalize.ts` | `partial` | Cache writes, untracked-extension warnings, final memory-slot warnings, readiness promotion, and registry activation now delegate through a host-owned loader-finalize helper; broader host lifecycle and policy semantics are still pending. |
|
||||
| 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. |
|
||||
@ -87,14 +88,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, cache control, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, top-level load orchestration, explicit loader lifecycle transitions, and final cache plus activation finalization
|
||||
- loader compatibility, cache control, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, top-level load orchestration, session-owned activation state, explicit loader lifecycle transitions, and final cache plus activation finalization
|
||||
|
||||
## 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. Extend the new loader lifecycle state machine into broader activation-state and policy ownership in `src/extension-host/*`.
|
||||
2. Extend the new loader lifecycle state machine and session-owned activation state into broader activation-state and policy ownership in `src/extension-host/*`.
|
||||
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.
|
||||
|
||||
@ -7,8 +7,6 @@ import {
|
||||
getCachedExtensionHostRegistry,
|
||||
setCachedExtensionHostRegistry,
|
||||
} from "../extension-host/loader-cache.js";
|
||||
import { finalizeExtensionHostRegistryLoad } from "../extension-host/loader-finalize.js";
|
||||
import { processExtensionHostPluginCandidate } from "../extension-host/loader-flow.js";
|
||||
import {
|
||||
buildExtensionHostProvenanceIndex,
|
||||
compareExtensionHostDuplicateCandidateOrder,
|
||||
@ -21,15 +19,16 @@ import { clearPluginCommands } from "../plugins/commands.js";
|
||||
import { applyTestPluginDefaults, normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { discoverOpenClawPlugins } from "../plugins/discovery.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
createPluginRegistry,
|
||||
type PluginRecord,
|
||||
type PluginRegistry,
|
||||
} from "../plugins/registry.js";
|
||||
import { createPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
|
||||
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "../plugins/runtime/index.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import type { OpenClawPluginModule, PluginLogger } from "../plugins/types.js";
|
||||
import { resolvePluginSdkAlias, resolvePluginSdkScopedAliasMap } from "./loader-compat.js";
|
||||
import {
|
||||
createExtensionHostLoaderSession,
|
||||
finalizeExtensionHostLoaderSession,
|
||||
processExtensionHostLoaderSessionCandidate,
|
||||
} from "./loader-session.js";
|
||||
|
||||
export type ExtensionHostPluginLoadOptions = {
|
||||
config?: OpenClawConfig;
|
||||
@ -187,43 +186,34 @@ export function loadExtensionHostPluginRegistry(
|
||||
});
|
||||
});
|
||||
|
||||
const seenIds = new Map<string, PluginRecord["origin"]>();
|
||||
const memorySlot = normalized.slots.memory;
|
||||
let selectedMemoryPluginId: string | null = null;
|
||||
let memorySlotMatched = false;
|
||||
const session = createExtensionHostLoaderSession({
|
||||
registry,
|
||||
logger,
|
||||
env,
|
||||
provenance,
|
||||
cacheEnabled,
|
||||
cacheKey,
|
||||
memorySlot: normalized.slots.memory,
|
||||
setCachedRegistry: setCachedExtensionHostRegistry,
|
||||
activateRegistry: activateExtensionHostRegistry,
|
||||
});
|
||||
|
||||
for (const candidate of orderedCandidates) {
|
||||
const manifestRecord = manifestByRoot.get(candidate.rootDir);
|
||||
if (!manifestRecord) {
|
||||
continue;
|
||||
}
|
||||
const processed = processExtensionHostPluginCandidate({
|
||||
processExtensionHostLoaderSessionCandidate({
|
||||
session,
|
||||
candidate,
|
||||
manifestRecord,
|
||||
normalizedConfig: normalized,
|
||||
rootConfig: cfg,
|
||||
validateOnly,
|
||||
logger,
|
||||
registry,
|
||||
seenIds,
|
||||
selectedMemoryPluginId,
|
||||
createApi,
|
||||
loadModule: (safeSource) => getJiti()(safeSource) as OpenClawPluginModule,
|
||||
});
|
||||
selectedMemoryPluginId = processed.selectedMemoryPluginId;
|
||||
memorySlotMatched ||= processed.memorySlotMatched;
|
||||
}
|
||||
|
||||
return finalizeExtensionHostRegistryLoad({
|
||||
registry,
|
||||
memorySlot,
|
||||
memorySlotMatched,
|
||||
provenance,
|
||||
logger,
|
||||
env,
|
||||
cacheEnabled,
|
||||
cacheKey,
|
||||
setCachedRegistry: setCachedExtensionHostRegistry,
|
||||
activateRegistry: activateExtensionHostRegistry,
|
||||
});
|
||||
return finalizeExtensionHostLoaderSession(session);
|
||||
}
|
||||
|
||||
164
src/extension-host/loader-session.test.ts
Normal file
164
src/extension-host/loader-session.test.ts
Normal file
@ -0,0 +1,164 @@
|
||||
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 { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import {
|
||||
createExtensionHostLoaderSession,
|
||||
finalizeExtensionHostLoaderSession,
|
||||
processExtensionHostLoaderSessionCandidate,
|
||||
} from "./loader-session.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-session-"));
|
||||
tempDirs.push(rootDir);
|
||||
const entryPath = path.join(rootDir, "index.js");
|
||||
fs.writeFileSync(entryPath, "export default { id: 'demo', register() {} }");
|
||||
return { rootDir, entryPath };
|
||||
}
|
||||
|
||||
function createManifestRecord(rootDir: string, entryPath: string): PluginManifestRecord {
|
||||
return {
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
version: "1.0.0",
|
||||
kind: "memory",
|
||||
channels: [],
|
||||
providers: [],
|
||||
skills: [],
|
||||
origin: "bundled",
|
||||
rootDir,
|
||||
source: entryPath,
|
||||
manifestPath: path.join(rootDir, "openclaw.plugin.json"),
|
||||
schemaCacheKey: "demo-schema",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
resolvedExtension: {
|
||||
id: "demo",
|
||||
source: entryPath,
|
||||
origin: "bundled",
|
||||
rootDir,
|
||||
static: {
|
||||
package: {},
|
||||
config: {},
|
||||
setup: {},
|
||||
},
|
||||
runtime: {
|
||||
kind: "memory",
|
||||
contributions: [],
|
||||
},
|
||||
policy: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createRegistry(): PluginRegistry {
|
||||
return {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("extension host loader session", () => {
|
||||
it("owns mutable activation state for memory-slot selection", () => {
|
||||
const { rootDir, entryPath } = createTempPluginFixture();
|
||||
const session = createExtensionHostLoaderSession({
|
||||
registry: createRegistry(),
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
env: process.env,
|
||||
provenance: {
|
||||
loadPathMatcher: { exact: new Set(), dirs: [] },
|
||||
installRules: new Map(),
|
||||
},
|
||||
cacheEnabled: false,
|
||||
cacheKey: "cache-key",
|
||||
memorySlot: "demo",
|
||||
setCachedRegistry: () => {},
|
||||
activateRegistry: () => {},
|
||||
});
|
||||
|
||||
processExtensionHostLoaderSessionCandidate({
|
||||
session,
|
||||
candidate: {
|
||||
source: entryPath,
|
||||
rootDir,
|
||||
packageDir: rootDir,
|
||||
origin: "bundled",
|
||||
},
|
||||
manifestRecord: createManifestRecord(rootDir, entryPath),
|
||||
normalizedConfig: normalizePluginsConfig({
|
||||
slots: {
|
||||
memory: "demo",
|
||||
},
|
||||
}),
|
||||
rootConfig: {},
|
||||
validateOnly: true,
|
||||
createApi: () => ({}) as never,
|
||||
loadModule: () =>
|
||||
({
|
||||
default: {
|
||||
id: "demo",
|
||||
register: () => {},
|
||||
},
|
||||
}) as never,
|
||||
});
|
||||
|
||||
expect(session.selectedMemoryPluginId).toBe("demo");
|
||||
expect(session.memorySlotMatched).toBe(true);
|
||||
expect(session.registry.plugins[0]?.lifecycleState).toBe("validated");
|
||||
});
|
||||
|
||||
it("finalizes the session through the shared finalizer", () => {
|
||||
const session = createExtensionHostLoaderSession({
|
||||
registry: createRegistry(),
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
env: process.env,
|
||||
provenance: {
|
||||
loadPathMatcher: { exact: new Set(), dirs: [] },
|
||||
installRules: new Map(),
|
||||
},
|
||||
cacheEnabled: false,
|
||||
cacheKey: "cache-key",
|
||||
setCachedRegistry: () => {},
|
||||
activateRegistry: () => {},
|
||||
});
|
||||
|
||||
const result = finalizeExtensionHostLoaderSession(session);
|
||||
|
||||
expect(result).toBe(session.registry);
|
||||
});
|
||||
});
|
||||
115
src/extension-host/loader-session.ts
Normal file
115
src/extension-host/loader-session.ts
Normal file
@ -0,0 +1,115 @@
|
||||
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 { finalizeExtensionHostRegistryLoad } from "./loader-finalize.js";
|
||||
import { processExtensionHostPluginCandidate } from "./loader-flow.js";
|
||||
import type { ExtensionHostProvenanceIndex } from "./loader-policy.js";
|
||||
|
||||
export type ExtensionHostLoaderSession = {
|
||||
registry: PluginRegistry;
|
||||
logger: PluginLogger;
|
||||
env: NodeJS.ProcessEnv;
|
||||
provenance: ExtensionHostProvenanceIndex;
|
||||
cacheEnabled: boolean;
|
||||
cacheKey: string;
|
||||
memorySlot?: string | null;
|
||||
seenIds: Map<string, PluginRecord["origin"]>;
|
||||
selectedMemoryPluginId: string | null;
|
||||
memorySlotMatched: boolean;
|
||||
setCachedRegistry: (cacheKey: string, registry: PluginRegistry) => void;
|
||||
activateRegistry: (registry: PluginRegistry, cacheKey: string) => void;
|
||||
};
|
||||
|
||||
export function createExtensionHostLoaderSession(params: {
|
||||
registry: PluginRegistry;
|
||||
logger: PluginLogger;
|
||||
env: NodeJS.ProcessEnv;
|
||||
provenance: ExtensionHostProvenanceIndex;
|
||||
cacheEnabled: boolean;
|
||||
cacheKey: string;
|
||||
memorySlot?: string | null;
|
||||
setCachedRegistry: (cacheKey: string, registry: PluginRegistry) => void;
|
||||
activateRegistry: (registry: PluginRegistry, cacheKey: string) => void;
|
||||
}): ExtensionHostLoaderSession {
|
||||
return {
|
||||
registry: params.registry,
|
||||
logger: params.logger,
|
||||
env: params.env,
|
||||
provenance: params.provenance,
|
||||
cacheEnabled: params.cacheEnabled,
|
||||
cacheKey: params.cacheKey,
|
||||
memorySlot: params.memorySlot,
|
||||
seenIds: new Map(),
|
||||
selectedMemoryPluginId: null,
|
||||
memorySlotMatched: false,
|
||||
setCachedRegistry: params.setCachedRegistry,
|
||||
activateRegistry: params.activateRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
export function processExtensionHostLoaderSessionCandidate(params: {
|
||||
session: ExtensionHostLoaderSession;
|
||||
candidate: PluginCandidate;
|
||||
manifestRecord: PluginManifestRecord;
|
||||
normalizedConfig: {
|
||||
entries: Record<
|
||||
string,
|
||||
{
|
||||
enabled?: boolean;
|
||||
hooks?: {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
config?: unknown;
|
||||
}
|
||||
>;
|
||||
slots: {
|
||||
memory?: string | null;
|
||||
};
|
||||
};
|
||||
rootConfig: OpenClawConfig;
|
||||
validateOnly: boolean;
|
||||
createApi: (
|
||||
record: PluginRecord,
|
||||
options: {
|
||||
config: OpenClawConfig;
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
hookPolicy?: { allowPromptInjection?: boolean };
|
||||
},
|
||||
) => OpenClawPluginApi;
|
||||
loadModule: (safeSource: string) => OpenClawPluginModule;
|
||||
}): void {
|
||||
const processed = processExtensionHostPluginCandidate({
|
||||
candidate: params.candidate,
|
||||
manifestRecord: params.manifestRecord,
|
||||
normalizedConfig: params.normalizedConfig,
|
||||
rootConfig: params.rootConfig,
|
||||
validateOnly: params.validateOnly,
|
||||
logger: params.session.logger,
|
||||
registry: params.session.registry,
|
||||
seenIds: params.session.seenIds,
|
||||
selectedMemoryPluginId: params.session.selectedMemoryPluginId,
|
||||
createApi: params.createApi,
|
||||
loadModule: params.loadModule,
|
||||
});
|
||||
params.session.selectedMemoryPluginId = processed.selectedMemoryPluginId;
|
||||
params.session.memorySlotMatched ||= processed.memorySlotMatched;
|
||||
}
|
||||
|
||||
export function finalizeExtensionHostLoaderSession(
|
||||
session: ExtensionHostLoaderSession,
|
||||
): PluginRegistry {
|
||||
return finalizeExtensionHostRegistryLoad({
|
||||
registry: session.registry,
|
||||
memorySlot: session.memorySlot,
|
||||
memorySlotMatched: session.memorySlotMatched,
|
||||
provenance: session.provenance,
|
||||
logger: session.logger,
|
||||
env: session.env,
|
||||
cacheEnabled: session.cacheEnabled,
|
||||
cacheKey: session.cacheKey,
|
||||
setCachedRegistry: session.setCachedRegistry,
|
||||
activateRegistry: session.activateRegistry,
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user