Files
openclaw/src/plugins/installed-plugin-index-record-state.ts
T
Vincent Koc 1471881af7 fix(plugins): preserve install records during recovery (#121045)
* fix(plugins): canonicalize install record persistence

Preserve canonical fields, passthrough metadata, untouched record identity, and deterministic serialized ordering across SQLite and post-core handoff.

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* fix(doctor): block invalid plugin record mutations

Preflight persisted and legacy install-record state before registry repair, package cleanup, generation retirement, archival, or state-directory moves.

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* fix(plugins): harden install record map ownership

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* fix(update): preserve special plugin ids in payload checks

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* test(doctor): assert canonical install record maps

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* test(doctor): preserve invalid plugin index fixtures

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

---------

Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>
2026-08-09 20:24:40 +08:00

56 lines
1.8 KiB
TypeScript

import fs from "node:fs";
import { safeParseJson } from "@openclaw/normalization-core";
import {
inspectPluginInstallRecordMap,
type PluginInstallRecordMapState,
} from "../config/plugin-install-record-map.js";
import { withOpenClawStateDatabaseReadOnly } from "../state/openclaw-state-db-readonly.js";
import {
resolveInstalledPluginIndexStateDatabaseOptions,
resolveInstalledPluginIndexStorePath,
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store-path.js";
type InstalledPluginIndexRecordRow = {
install_records_json: string;
};
export function inspectPersistedInstalledPluginIndexInstallRecordsSync(
options: InstalledPluginIndexStoreOptions = {},
): PluginInstallRecordMapState {
const storePath = resolveInstalledPluginIndexStorePath(options);
if (!fs.existsSync(storePath) || options.filePath?.endsWith(".json")) {
return { status: "missing" };
}
try {
return withOpenClawStateDatabaseReadOnly(({ db }) => {
const hasTable = db
.prepare(
`SELECT 1
FROM sqlite_master
WHERE type = 'table' AND name = 'installed_plugin_index'`,
)
.get();
if (!hasTable) {
return { status: "missing" };
}
const row = db
.prepare(
`
SELECT install_records_json
FROM installed_plugin_index
WHERE index_key = ?
`,
)
.get("installed-plugin-index") as InstalledPluginIndexRecordRow | undefined;
if (!row) {
return { status: "missing" };
}
const parsed = safeParseJson(row.install_records_json);
return parsed === undefined ? { status: "invalid" } : inspectPluginInstallRecordMap(parsed);
}, resolveInstalledPluginIndexStateDatabaseOptions(options));
} catch {
return { status: "invalid" };
}
}