feat: reuse current plugin metadata for provider discovery

This commit is contained in:
Shakker
2026-04-27 17:18:12 +01:00
parent a478ab3dfa
commit 4e7de4b5c9
10 changed files with 305 additions and 3 deletions

View File

@@ -0,0 +1,114 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
clearCurrentPluginMetadataSnapshot,
getCurrentPluginMetadataSnapshot,
setCurrentPluginMetadataSnapshot,
} from "./current-plugin-metadata-snapshot.js";
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
import { writePersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
function createSnapshot(
params: {
config?: Parameters<typeof resolveInstalledPluginIndexPolicyHash>[0];
workspaceDir?: string;
} = {},
): PluginMetadataSnapshot {
return {
policyHash: resolveInstalledPluginIndexPolicyHash(params.config),
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
index: {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash: resolveInstalledPluginIndexPolicyHash(params.config),
generatedAtMs: 1,
installRecords: {},
plugins: [],
diagnostics: [],
},
registryDiagnostics: [],
manifestRegistry: { plugins: [], diagnostics: [] },
plugins: [],
diagnostics: [],
byPluginId: new Map(),
normalizePluginId: (pluginId) => pluginId,
owners: {
channels: new Map(),
channelConfigs: new Map(),
providers: new Map(),
modelCatalogProviders: new Map(),
cliBackends: new Map(),
setupProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
},
metrics: {
registrySnapshotMs: 0,
manifestRegistryMs: 0,
ownerMapsMs: 0,
totalMs: 0,
indexPluginCount: 0,
manifestPluginCount: 0,
},
};
}
describe("current plugin metadata snapshot", () => {
it("returns the current snapshot only for matching config policy and workspace", () => {
const config = { plugins: { allow: ["demo"] } };
const snapshot = createSnapshot({ config, workspaceDir: "/workspace/a" });
setCurrentPluginMetadataSnapshot(snapshot, { config });
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir: "/workspace/a" })).toBe(
snapshot,
);
expect(getCurrentPluginMetadataSnapshot({ config })).toBe(snapshot);
expect(
getCurrentPluginMetadataSnapshot({
config: { plugins: { allow: ["other"] } },
workspaceDir: "/workspace/a",
}),
).toBeUndefined();
expect(
getCurrentPluginMetadataSnapshot({ config, workspaceDir: "/workspace/b" }),
).toBeUndefined();
});
it("rejects a current snapshot when plugin load paths change", () => {
const config = { plugins: { load: { paths: ["/plugins/one"] } } };
const snapshot = createSnapshot({ config });
setCurrentPluginMetadataSnapshot(snapshot, { config });
expect(getCurrentPluginMetadataSnapshot({ config })).toBe(snapshot);
expect(
getCurrentPluginMetadataSnapshot({
config: { plugins: { load: { paths: ["/plugins/two"] } } },
}),
).toBeUndefined();
});
it("clears the current snapshot", () => {
setCurrentPluginMetadataSnapshot(createSnapshot());
clearCurrentPluginMetadataSnapshot();
expect(getCurrentPluginMetadataSnapshot()).toBeUndefined();
});
it("clears the current snapshot when the persisted installed index changes", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-metadata-"));
try {
setCurrentPluginMetadataSnapshot(createSnapshot());
writePersistedInstalledPluginIndexSync(createSnapshot().index, { stateDir: tempDir });
expect(getCurrentPluginMetadataSnapshot()).toBeUndefined();
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});

View File

@@ -0,0 +1,73 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
let currentPluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
let currentPluginMetadataSnapshotConfigFingerprint: string | undefined;
function normalizeLoadPaths(config: OpenClawConfig | undefined): readonly string[] {
const paths = config?.plugins?.load?.paths;
if (!Array.isArray(paths)) {
return [];
}
return paths.filter((entry) => typeof entry === "string");
}
export function resolvePluginMetadataSnapshotConfigFingerprint(
config: OpenClawConfig | undefined,
): string {
return JSON.stringify({
policyHash: resolveInstalledPluginIndexPolicyHash(config),
pluginLoadPaths: normalizeLoadPaths(config),
});
}
// Single-slot Gateway-owned handoff. Replace or clear it at lifecycle boundaries;
// never accumulate historical metadata snapshots here.
export function setCurrentPluginMetadataSnapshot(
snapshot: PluginMetadataSnapshot | undefined,
options: { config?: OpenClawConfig } = {},
): void {
currentPluginMetadataSnapshot = snapshot;
currentPluginMetadataSnapshotConfigFingerprint = snapshot
? resolvePluginMetadataSnapshotConfigFingerprint(options.config)
: undefined;
}
export function clearCurrentPluginMetadataSnapshot(): void {
currentPluginMetadataSnapshot = undefined;
currentPluginMetadataSnapshotConfigFingerprint = undefined;
}
export function getCurrentPluginMetadataSnapshot(
params: {
config?: OpenClawConfig;
workspaceDir?: string;
} = {},
): PluginMetadataSnapshot | undefined {
const snapshot = currentPluginMetadataSnapshot;
if (!snapshot) {
return undefined;
}
if (
params.config &&
snapshot.policyHash !== resolveInstalledPluginIndexPolicyHash(params.config)
) {
return undefined;
}
if (
params.config &&
currentPluginMetadataSnapshotConfigFingerprint &&
currentPluginMetadataSnapshotConfigFingerprint !==
resolvePluginMetadataSnapshotConfigFingerprint(params.config)
) {
return undefined;
}
if (
params.workspaceDir !== undefined &&
(snapshot.workspaceDir ?? "") !== (params.workspaceDir ?? "")
) {
return undefined;
}
return snapshot;
}

View File

@@ -3,6 +3,7 @@ import { saveJsonFile } from "../infra/json-file.js";
import { readJsonFile, readJsonFileSync, writeJsonAtomic } from "../infra/json-files.js";
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
import { safeParseWithSchema } from "../utils/zod-parse.js";
import { clearCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
import {
resolveInstalledPluginIndexStorePath,
type InstalledPluginIndexStoreOptions,
@@ -171,6 +172,7 @@ export async function writePersistedInstalledPluginIndex(
mode: 0o600,
},
);
clearCurrentPluginMetadataSnapshot();
return filePath;
}
@@ -180,6 +182,7 @@ export function writePersistedInstalledPluginIndexSync(
): string {
const filePath = resolveInstalledPluginIndexStorePath(options);
saveJsonFile(filePath, { ...index, warning: INSTALLED_PLUGIN_INDEX_WARNING });
clearCurrentPluginMetadataSnapshot();
return filePath;
}