mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix: preserve plugin install records without manifests
This commit is contained in:
@@ -204,6 +204,7 @@ Docs: https://docs.openclaw.ai
|
||||
a parent `$HOME/node_modules` tree. Fixes #71730.
|
||||
- Plugins/install: pass onboarding plugin config into plugin index writes so local plugin installs outside default discovery roots keep their install records. Thanks @shakkernerd.
|
||||
- Plugins/install: migrate shipped `plugins.installs` config records into the plugin index while stripping them from runtime config and future writes. Thanks @shakkernerd.
|
||||
- Plugins/install: keep migrated plugin install records in the plugin index even when the plugin manifest is missing or invalid, so update, uninstall, inspect, and audit can still recover broken installs. Thanks @shakkernerd.
|
||||
- Plugins/security: keep plugin audit JSON check ids stable while reporting plugin index install-record findings with updated wording. Thanks @shakkernerd.
|
||||
- CLI/config: reject direct `plugins.installs` edits with guidance to use `openclaw plugins install`, `openclaw plugins update`, or `openclaw plugins uninstall` instead. Thanks @shakkernerd.
|
||||
- Live tests/voice: accept common STT variants for OpenClaw and ElevenLabs
|
||||
|
||||
@@ -238,8 +238,11 @@ the managed plugin index while keeping the default behavior unpinned.
|
||||
|
||||
Plugin install metadata is machine-managed state, not user config. Installs
|
||||
and updates write it to `plugins/installs.json` under the active OpenClaw state
|
||||
directory. The file includes a do-not-edit warning and is used by
|
||||
`openclaw plugins update`, uninstall, diagnostics, and the cold plugin registry.
|
||||
directory. Its top-level `installRecords` map is the durable source of install
|
||||
metadata, including records for broken or missing plugin manifests. The
|
||||
`plugins` array is the manifest-derived cold registry cache. The file includes a
|
||||
do-not-edit warning and is used by `openclaw plugins update`, uninstall,
|
||||
diagnostics, and the cold plugin registry.
|
||||
|
||||
### Uninstall
|
||||
|
||||
|
||||
@@ -921,6 +921,8 @@ paths into long-lived config. This keeps local development installs visible to
|
||||
source-plane diagnostics without adding a second raw filesystem-path disclosure
|
||||
surface. The persisted `plugins/installs.json` plugin index is the install
|
||||
source of truth and can be refreshed without loading plugin runtime modules.
|
||||
Its `installRecords` map is durable even when a plugin manifest is missing or
|
||||
invalid; its `plugins` array is a rebuildable manifest/cache view.
|
||||
|
||||
## Context engine plugins
|
||||
|
||||
|
||||
@@ -305,8 +305,10 @@ immediately loadable after restart.
|
||||
OpenClaw keeps a persisted local plugin registry as the cold read model for
|
||||
plugin inventory, contribution ownership, and startup planning. Install, update,
|
||||
uninstall, enable, and disable flows refresh that registry after changing plugin
|
||||
state. If the registry is missing, stale, or invalid, `openclaw plugins registry
|
||||
--refresh` rebuilds it from the durable plugin index, config policy, and
|
||||
state. The same `plugins/installs.json` file keeps durable install metadata in
|
||||
top-level `installRecords` and rebuildable manifest metadata in `plugins`. If
|
||||
the registry is missing, stale, or invalid, `openclaw plugins registry
|
||||
--refresh` rebuilds its manifest view from install records, config policy, and
|
||||
manifest/package metadata without loading plugin runtime modules.
|
||||
`openclaw plugins update <id-or-npm-spec>` applies to tracked installs. Passing
|
||||
an npm package spec with a dist-tag or exact version resolves the package name
|
||||
|
||||
@@ -70,6 +70,7 @@ function createCurrentIndex(): InstalledPluginIndex {
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {},
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
@@ -75,6 +75,7 @@ function createCurrentIndex(): InstalledPluginIndex {
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {},
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
@@ -268,33 +269,91 @@ describe("plugin registry install migration", () => {
|
||||
).resolves.toMatchObject({
|
||||
status: "migrated",
|
||||
current: {
|
||||
installRecords: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "demo@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
source: "npm",
|
||||
spec: "demo@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
installRecords: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "demo@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
source: "npm",
|
||||
spec: "demo@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves shipped install records when the plugin manifest cannot be discovered", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "missing");
|
||||
|
||||
await expect(
|
||||
migratePluginRegistryForInstall({
|
||||
stateDir,
|
||||
candidates: [],
|
||||
readConfig: async () => ({
|
||||
plugins: {
|
||||
entries: {
|
||||
missing: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
installs: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: hermeticEnv(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "migrated",
|
||||
current: {
|
||||
installRecords: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
installRecords: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("marks force migration env as deprecated break-glass", () => {
|
||||
expect(
|
||||
preflightPluginRegistryInstallMigration({
|
||||
|
||||
@@ -165,14 +165,17 @@ describe("config io write", () => {
|
||||
stateDir: path.join(home, ".openclaw"),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
installRecords: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "demo@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
source: "npm",
|
||||
spec: "demo@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -185,6 +188,53 @@ describe("config io write", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates shipped plugin install config records even when the manifest is missing", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
const pluginDir = path.join(home, ".openclaw", "plugins", "missing");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
plugins: {
|
||||
entries: { missing: { enabled: true } },
|
||||
installs: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const io = createFastConfigIO(home);
|
||||
const cfg = io.loadConfig();
|
||||
|
||||
expect(cfg.plugins?.installs).toBeUndefined();
|
||||
await expect(
|
||||
readPersistedInstalledPluginIndex({
|
||||
stateDir: path.join(home, ".openclaw"),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
installRecords: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: pluginDir,
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps shipped plugin install config records when index migration fails", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
|
||||
@@ -15,12 +15,30 @@ function cloneInstallRecords(
|
||||
return structuredClone(records ?? {});
|
||||
}
|
||||
|
||||
function readRecordMap(value: unknown): Record<string, PluginInstallRecord> | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const records: Record<string, PluginInstallRecord> = {};
|
||||
for (const [pluginId, record] of Object.entries(value).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
if (isRecord(record) && typeof record.source === "string") {
|
||||
records[pluginId] = structuredClone(record) as PluginInstallRecord;
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
export function extractPluginInstallRecordsFromPersistedInstalledPluginIndex(
|
||||
index: unknown,
|
||||
): Record<string, PluginInstallRecord> | null {
|
||||
if (!isRecord(index) || !Array.isArray(index.plugins)) {
|
||||
return null;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(index, "installRecords")) {
|
||||
return readRecordMap(index.installRecords) ?? {};
|
||||
}
|
||||
const records: Record<string, PluginInstallRecord> = {};
|
||||
for (const entry of index.plugins) {
|
||||
if (!isRecord(entry) || typeof entry.pluginId !== "string" || !isRecord(entry.installRecord)) {
|
||||
|
||||
@@ -75,14 +75,17 @@ describe("plugin index install records store", () => {
|
||||
expect(JSON.parse(fs.readFileSync(indexPath, "utf8"))).toMatchObject({
|
||||
version: 1,
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {
|
||||
twitch: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/plugin-twitch@1.0.0",
|
||||
installPath: "plugins/npm/@openclaw/plugin-twitch",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
pluginId: "twitch",
|
||||
installRecord: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/plugin-twitch@1.0.0",
|
||||
installPath: "plugins/npm/@openclaw/plugin-twitch",
|
||||
},
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -95,6 +98,47 @@ describe("plugin index install records store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves install records for plugins without a discovered manifest", async () => {
|
||||
const stateDir = makeStateDir();
|
||||
|
||||
await writePersistedInstalledPluginIndexInstallRecords(
|
||||
{
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
stateDir,
|
||||
candidates: [],
|
||||
now: () => new Date(1777118400000),
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
JSON.parse(
|
||||
fs.readFileSync(resolveInstalledPluginIndexRecordsStorePath({ stateDir }), "utf8"),
|
||||
),
|
||||
).toMatchObject({
|
||||
installRecords: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
});
|
||||
await expect(loadInstalledPluginIndexInstallRecords({ stateDir })).resolves.toEqual({
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reads persisted records from the plugin index", async () => {
|
||||
const stateDir = makeStateDir();
|
||||
const candidate = createPluginCandidate(stateDir, "persisted");
|
||||
|
||||
@@ -30,6 +30,7 @@ function createIndex(overrides: Partial<InstalledPluginIndex> = {}): InstalledPl
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {},
|
||||
plugins: [
|
||||
{
|
||||
pluginId: "demo",
|
||||
|
||||
@@ -84,6 +84,8 @@ const InstalledPluginIndexRecordSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const InstalledPluginInstallRecordSchema = z.record(z.string(), z.unknown());
|
||||
|
||||
const PluginDiagnosticSchema = z
|
||||
.object({
|
||||
level: z.union([z.literal("warn"), z.literal("error")]),
|
||||
@@ -103,13 +105,27 @@ const InstalledPluginIndexSchema = z
|
||||
policyHash: z.string(),
|
||||
generatedAtMs: z.number(),
|
||||
refreshReason: z.string().optional(),
|
||||
installRecords: z.record(z.string(), InstalledPluginInstallRecordSchema).optional(),
|
||||
plugins: z.array(InstalledPluginIndexRecordSchema),
|
||||
diagnostics: z.array(PluginDiagnosticSchema),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
function parseInstalledPluginIndex(value: unknown): InstalledPluginIndex | null {
|
||||
return safeParseWithSchema(InstalledPluginIndexSchema, value) as InstalledPluginIndex | null;
|
||||
const parsed = safeParseWithSchema(InstalledPluginIndexSchema, value) as
|
||||
| (Omit<InstalledPluginIndex, "installRecords"> & {
|
||||
installRecords?: InstalledPluginIndex["installRecords"];
|
||||
})
|
||||
| null;
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...parsed,
|
||||
installRecords:
|
||||
parsed.installRecords ??
|
||||
extractPluginInstallRecordsFromInstalledPluginIndex(parsed as InstalledPluginIndex),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readPersistedInstalledPluginIndex(
|
||||
|
||||
@@ -372,8 +372,8 @@ describe("installed plugin index", () => {
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
installRecord: {
|
||||
expect(index.installRecords).toMatchObject({
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@latest",
|
||||
installPath: "plugins/demo",
|
||||
@@ -385,6 +385,8 @@ describe("installed plugin index", () => {
|
||||
resolvedAt: "2026-04-25T11:00:00.000Z",
|
||||
installedAt: "2026-04-25T11:01:00.000Z",
|
||||
},
|
||||
});
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
packageInstall: {
|
||||
npm: {
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
@@ -393,6 +395,7 @@ describe("installed plugin index", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(index.plugins[0]?.installRecord).toBeUndefined();
|
||||
expect(index.plugins[0]?.installRecordHash).toMatch(/^[a-f0-9]{64}$/u);
|
||||
});
|
||||
|
||||
@@ -425,7 +428,10 @@ describe("installed plugin index", () => {
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
});
|
||||
expect(index.installRecords).toMatchObject({
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@latest",
|
||||
installPath: fixture.rootDir,
|
||||
@@ -467,7 +473,10 @@ describe("installed plugin index", () => {
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
});
|
||||
expect(index.installRecords).toMatchObject({
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
installPath: fixture.rootDir,
|
||||
@@ -500,7 +509,10 @@ describe("installed plugin index", () => {
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
pluginId: "demo",
|
||||
installRecord: {
|
||||
installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
|
||||
});
|
||||
expect(index.installRecords).toMatchObject({
|
||||
demo: {
|
||||
source: "path",
|
||||
sourcePath: "./plugins/demo",
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
|
||||
@@ -83,10 +83,11 @@ export type InstalledPluginIndexRecord = {
|
||||
packageName?: string;
|
||||
packageVersion?: string;
|
||||
/**
|
||||
* Actual install record recorded by OpenClaw in the persisted plugin index.
|
||||
* Legacy embedded install record accepted when reading earlier index files.
|
||||
* New index writes keep install records in InstalledPluginIndex.installRecords.
|
||||
*/
|
||||
installRecord?: InstalledPluginInstallRecordInfo;
|
||||
/** Hash of installRecord; used to detect source-changed invalidation. */
|
||||
/** Hash of the top-level installRecords entry; used to detect source-changed invalidation. */
|
||||
installRecordHash?: string;
|
||||
/**
|
||||
* Package-authored openclaw.install metadata. This describes catalog/package
|
||||
@@ -117,6 +118,7 @@ export type InstalledPluginIndex = {
|
||||
policyHash: string;
|
||||
generatedAtMs: number;
|
||||
refreshReason?: InstalledPluginIndexRefreshReason;
|
||||
installRecords: Readonly<Record<string, InstalledPluginInstallRecordInfo>>;
|
||||
plugins: readonly InstalledPluginIndexRecord[];
|
||||
diagnostics: readonly PluginDiagnostic[];
|
||||
};
|
||||
@@ -384,9 +386,42 @@ function restoreInstallRecord(
|
||||
return structuredClone(record) as PluginInstallRecord;
|
||||
}
|
||||
|
||||
function normalizeInstallRecordMap(
|
||||
records: Record<string, PluginInstallRecord> | undefined,
|
||||
): Record<string, InstalledPluginInstallRecordInfo> {
|
||||
const normalized: Record<string, InstalledPluginInstallRecordInfo> = {};
|
||||
for (const [pluginId, record] of Object.entries(records ?? {}).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
const installRecord = normalizeInstallRecord(record);
|
||||
if (installRecord) {
|
||||
normalized[pluginId] = installRecord;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function restoreInstallRecordMap(
|
||||
records: Readonly<Record<string, InstalledPluginInstallRecordInfo>> | undefined,
|
||||
): Record<string, PluginInstallRecord> {
|
||||
const restored: Record<string, PluginInstallRecord> = {};
|
||||
for (const [pluginId, record] of Object.entries(records ?? {}).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
const installRecord = restoreInstallRecord(record);
|
||||
if (installRecord) {
|
||||
restored[pluginId] = installRecord;
|
||||
}
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
export function extractPluginInstallRecordsFromInstalledPluginIndex(
|
||||
index: InstalledPluginIndex | null | undefined,
|
||||
): Record<string, PluginInstallRecord> {
|
||||
if (index && Object.prototype.hasOwnProperty.call(index, "installRecords")) {
|
||||
return restoreInstallRecordMap(index.installRecords);
|
||||
}
|
||||
const records: Record<string, PluginInstallRecord> = {};
|
||||
for (const plugin of index?.plugins ?? []) {
|
||||
const record = restoreInstallRecord(plugin.installRecord);
|
||||
@@ -501,11 +536,11 @@ function buildInstalledPluginIndex(
|
||||
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
|
||||
const diagnostics: PluginDiagnostic[] = [...registry.diagnostics];
|
||||
const generatedAtMs = (params.now?.() ?? new Date()).getTime();
|
||||
const installRecords = structuredClone(params.installRecords ?? {});
|
||||
const installRecords = normalizeInstallRecordMap(params.installRecords);
|
||||
const plugins = registry.plugins.map((record): InstalledPluginIndexRecord => {
|
||||
const candidate = candidateByRootDir.get(record.rootDir);
|
||||
const packageJsonPath = resolvePackageJsonPath(candidate);
|
||||
const installRecord = normalizeInstallRecord(installRecords[record.id]);
|
||||
const installRecord = installRecords[record.id];
|
||||
const packageInstall = describePackageInstallSource(candidate);
|
||||
const manifestHash =
|
||||
safeHashFile({
|
||||
@@ -548,7 +583,6 @@ function buildInstalledPluginIndex(
|
||||
indexRecord.packageVersion = candidate.packageVersion;
|
||||
}
|
||||
if (installRecord) {
|
||||
indexRecord.installRecord = installRecord;
|
||||
indexRecord.installRecordHash = hashJson(installRecord);
|
||||
}
|
||||
if (packageInstall) {
|
||||
@@ -569,6 +603,7 @@ function buildInstalledPluginIndex(
|
||||
policyHash: resolveInstalledPluginIndexPolicyHash(params.config),
|
||||
generatedAtMs,
|
||||
...(params.refreshReason ? { refreshReason: params.refreshReason } : {}),
|
||||
installRecords,
|
||||
plugins,
|
||||
diagnostics,
|
||||
};
|
||||
@@ -773,6 +808,9 @@ export function diffInstalledPluginIndexInvalidationReasons(
|
||||
if (previous.policyHash !== current.policyHash) {
|
||||
reasons.add("policy-changed");
|
||||
}
|
||||
if (hashJson(previous.installRecords ?? {}) !== hashJson(current.installRecords ?? {})) {
|
||||
reasons.add("source-changed");
|
||||
}
|
||||
|
||||
const previousByPluginId = new Map(previous.plugins.map((plugin) => [plugin.pluginId, plugin]));
|
||||
const currentByPluginId = new Map(current.plugins.map((plugin) => [plugin.pluginId, plugin]));
|
||||
|
||||
@@ -105,6 +105,7 @@ function createIndex(
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {},
|
||||
plugins: [
|
||||
{
|
||||
pluginId,
|
||||
|
||||
@@ -114,9 +114,9 @@ describe("security audit install metadata findings", () => {
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy",
|
||||
generatedAtMs: Date.now(),
|
||||
plugins: Object.entries(records).map(([pluginId, installRecord]) => ({
|
||||
installRecords: records,
|
||||
plugins: Object.keys(records).map((pluginId) => ({
|
||||
pluginId,
|
||||
installRecord,
|
||||
manifestPath: path.join(stateDir, "extensions", pluginId, "openclaw.plugin.json"),
|
||||
manifestHash: "manifest",
|
||||
rootDir: path.join(stateDir, "extensions", pluginId),
|
||||
|
||||
Reference in New Issue
Block a user