mirror of https://github.com/openclaw/openclaw.git
Plugins: add loader finalization policy
This commit is contained in:
parent
9fbfd18c76
commit
fd7488e10a
|
|
@ -43,6 +43,7 @@ This is an implementation checklist, not a future-design spec.
|
|||
| 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 activation policy outcomes | open-coded in `src/plugins/loader.ts` and `src/extension-host/loader-flow.ts` | `src/extension-host/loader-activation-policy.ts` | `partial` | Duplicate precedence, config enablement, and early memory-slot gating now resolve through explicit host-owned activation-policy outcomes instead of remaining as inline loader decisions. |
|
||||
| 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 finalization policy results | mixed inside `src/plugins/loader.ts`, `src/extension-host/loader-policy.ts`, and `src/extension-host/loader-finalize.ts` | `src/extension-host/loader-finalization-policy.ts` | `partial` | Memory-slot finalization warnings and provenance-based untracked-extension warnings now resolve through explicit host-owned finalization-policy results before the finalizer applies them. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
|
@ -89,14 +90,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, explicit activation-policy outcomes, 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
|
||||
- loader compatibility, cache control, initial candidate planning, entry-path import, explicit activation-policy outcomes, runtime decisions, post-import register flow, per-candidate orchestration, top-level load orchestration, session-owned activation state, explicit loader lifecycle transitions, explicit finalization-policy results, 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, session-owned activation state, and activation-policy outcomes into broader activation-state and policy ownership in `src/extension-host/*`.
|
||||
2. Extend the new loader lifecycle state machine, session-owned activation state, activation-policy outcomes, and finalization-policy results 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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { resolveExtensionHostFinalizationPolicy } from "./loader-finalization-policy.js";
|
||||
|
||||
function createRegistry(): PluginRegistry {
|
||||
return {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("extension host loader finalization policy", () => {
|
||||
it("emits memory-slot diagnostics when no selected memory plugin matched", () => {
|
||||
const result = resolveExtensionHostFinalizationPolicy({
|
||||
registry: createRegistry(),
|
||||
memorySlot: "memory-a",
|
||||
memorySlotMatched: false,
|
||||
provenance: {
|
||||
loadPathMatcher: { exact: new Set(), dirs: [] },
|
||||
installRules: new Map(),
|
||||
},
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(result.diagnostics).toContainEqual({
|
||||
level: "warn",
|
||||
message: "memory slot plugin not found or not marked as memory: memory-a",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits provenance warnings for untracked non-bundled plugins", () => {
|
||||
const registry = createRegistry();
|
||||
registry.plugins.push({
|
||||
id: "demo",
|
||||
name: "demo",
|
||||
source: "/tmp/demo/index.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
lifecycleState: "ready",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
});
|
||||
|
||||
const result = resolveExtensionHostFinalizationPolicy({
|
||||
registry,
|
||||
memorySlotMatched: true,
|
||||
provenance: {
|
||||
loadPathMatcher: { exact: new Set(), dirs: [] },
|
||||
installRules: new Map(),
|
||||
},
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(result.diagnostics).toContainEqual({
|
||||
level: "warn",
|
||||
pluginId: "demo",
|
||||
source: "/tmp/demo/index.js",
|
||||
message:
|
||||
"loaded without install/load-path provenance; treat as untracked local code and pin trust via plugins.allow or install records",
|
||||
});
|
||||
expect(result.warningMessages[0]).toContain("[plugins] demo:");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginDiagnostic } from "../plugins/types.js";
|
||||
import type { ExtensionHostProvenanceIndex } from "./loader-policy.js";
|
||||
|
||||
function safeRealpathOrResolve(value: string): string {
|
||||
try {
|
||||
return fs.realpathSync(value);
|
||||
} catch {
|
||||
return path.resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
function matchesPathMatcher(
|
||||
matcher: { exact: Set<string>; dirs: string[] },
|
||||
sourcePath: string,
|
||||
): boolean {
|
||||
if (matcher.exact.has(sourcePath)) {
|
||||
return true;
|
||||
}
|
||||
return matcher.dirs.some(
|
||||
(dirPath) => sourcePath === dirPath || sourcePath.startsWith(`${dirPath}/`),
|
||||
);
|
||||
}
|
||||
|
||||
function isTrackedByProvenance(params: {
|
||||
pluginId: string;
|
||||
source: string;
|
||||
index: ExtensionHostProvenanceIndex;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const sourcePath = params.source.startsWith("~")
|
||||
? `${params.env.HOME ?? ""}${params.source.slice(1)}`
|
||||
: params.source;
|
||||
const installRule = params.index.installRules.get(params.pluginId);
|
||||
if (installRule) {
|
||||
if (installRule.trackedWithoutPaths) {
|
||||
return true;
|
||||
}
|
||||
if (matchesPathMatcher(installRule.matcher, sourcePath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return matchesPathMatcher(params.index.loadPathMatcher, sourcePath);
|
||||
}
|
||||
|
||||
export function resolveExtensionHostFinalizationPolicy(params: {
|
||||
registry: PluginRegistry;
|
||||
memorySlot?: string | null;
|
||||
memorySlotMatched: boolean;
|
||||
provenance: ExtensionHostProvenanceIndex;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): {
|
||||
diagnostics: PluginDiagnostic[];
|
||||
warningMessages: string[];
|
||||
} {
|
||||
const diagnostics: PluginDiagnostic[] = [];
|
||||
const warningMessages: string[] = [];
|
||||
|
||||
if (typeof params.memorySlot === "string" && !params.memorySlotMatched) {
|
||||
diagnostics.push({
|
||||
level: "warn",
|
||||
message: `memory slot plugin not found or not marked as memory: ${params.memorySlot}`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const plugin of params.registry.plugins) {
|
||||
if (plugin.status !== "loaded" || plugin.origin === "bundled") {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
isTrackedByProvenance({
|
||||
pluginId: plugin.id,
|
||||
source: plugin.source,
|
||||
index: params.provenance,
|
||||
env: params.env,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const message =
|
||||
"loaded without install/load-path provenance; treat as untracked local code and pin trust via plugins.allow or install records";
|
||||
diagnostics.push({
|
||||
level: "warn",
|
||||
pluginId: plugin.id,
|
||||
source: plugin.source,
|
||||
message,
|
||||
});
|
||||
warningMessages.push(
|
||||
`[plugins] ${plugin.id}: ${message} (${safeRealpathOrResolve(plugin.source)})`,
|
||||
);
|
||||
}
|
||||
|
||||
return { diagnostics, warningMessages };
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import { resolveExtensionHostFinalizationPolicy } from "./loader-finalization-policy.js";
|
||||
import type { ExtensionHostProvenanceIndex } from "./loader-policy.js";
|
||||
import { warnAboutUntrackedLoadedExtensions } from "./loader-policy.js";
|
||||
import { markExtensionHostRegistryPluginsReady } from "./loader-state.js";
|
||||
|
||||
export function finalizeExtensionHostRegistryLoad(params: {
|
||||
|
|
@ -16,19 +16,17 @@ export function finalizeExtensionHostRegistryLoad(params: {
|
|||
setCachedRegistry: (cacheKey: string, registry: PluginRegistry) => void;
|
||||
activateRegistry: (registry: PluginRegistry, cacheKey: string) => void;
|
||||
}): PluginRegistry {
|
||||
if (typeof params.memorySlot === "string" && !params.memorySlotMatched) {
|
||||
params.registry.diagnostics.push({
|
||||
level: "warn",
|
||||
message: `memory slot plugin not found or not marked as memory: ${params.memorySlot}`,
|
||||
});
|
||||
}
|
||||
|
||||
warnAboutUntrackedLoadedExtensions({
|
||||
const finalizationPolicy = resolveExtensionHostFinalizationPolicy({
|
||||
registry: params.registry,
|
||||
memorySlot: params.memorySlot,
|
||||
memorySlotMatched: params.memorySlotMatched,
|
||||
provenance: params.provenance,
|
||||
logger: params.logger,
|
||||
env: params.env,
|
||||
});
|
||||
params.registry.diagnostics.push(...finalizationPolicy.diagnostics);
|
||||
for (const warning of finalizationPolicy.warningMessages) {
|
||||
params.logger.warn(warning);
|
||||
}
|
||||
|
||||
if (params.cacheEnabled) {
|
||||
params.setCachedRegistry(params.cacheKey, params.registry);
|
||||
|
|
|
|||
Loading…
Reference in New Issue