Skip to content

Commit 5443baa

Browse files
authored
Persist plugin install index in SQLite (#88794)
* refactor: persist plugin install index in sqlite * fix: merge legacy plugin index records into sqlite * test: update plugin index sqlite fixtures * fix: migrate custom plugin install indexes * test: update plugin index sentinel * fix: exclude migrated plugin index archives * fix: read post-upgrade plugin index from sqlite * fix: migrate legacy plugin index before agent runs * fix: respect disabled persisted plugin registry reads * test: type plugin install record fixtures * fix: simplify plugin index record reader type * test: fix sqlite plugin index CI fallout * test: mock provider normalization in agent command tests # Conflicts: # src/commands/agent-command.test-mocks.ts * build: remove unused ui three dependency
1 parent b475de8 commit 5443baa

57 files changed

Lines changed: 1711 additions & 629 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/cli/plugins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in
336336

337337
### Plugin index
338338

339-
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. 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.
339+
Plugin install metadata is machine-managed state, not user config. Installs and updates write it to the shared SQLite state database under the active OpenClaw state directory. The `installed_plugin_index` row stores durable `installRecords` metadata, including records for broken or missing plugin manifests, plus a manifest-derived cold registry cache used by `openclaw plugins update`, uninstall, diagnostics, and the cold plugin registry.
340340

341341
When OpenClaw sees shipped legacy `plugins.installs` records in config, runtime reads treat them as compatibility input without rewriting `openclaw.json`. Explicit plugin writes and `openclaw doctor --fix` move those records into the plugin index and remove the config key when config writes are allowed; if either write fails, the config records are kept so the install metadata is not lost.
342342

docs/plugins/architecture-internals.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,10 +1021,10 @@ plugin index entry with `source: "path"` and a workspace-relative
10211021
`plugins.load.paths`; the install record avoids duplicating local workstation
10221022
paths into long-lived config. This keeps local development installs visible to
10231023
source-plane diagnostics without adding a second raw filesystem-path disclosure
1024-
surface. The persisted `plugins/installs.json` plugin index is the install
1024+
surface. The persisted `installed_plugin_index` SQLite row is the install
10251025
source of truth and can be refreshed without loading plugin runtime modules.
10261026
Its `installRecords` map is durable even when a plugin manifest is missing or
1027-
invalid; its `plugins` array is a rebuildable manifest view.
1027+
invalid; its `plugins` payload is a rebuildable manifest view.
10281028

10291029
## Context engine plugins
10301030

pnpm-lock.yaml

Lines changed: 0 additions & 53 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/check-kysely-guardrails.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const rawSqliteAllowPathGroups = {
5454
"src/infra/outbound/current-conversation-bindings.ts",
5555
"src/media/store.ts",
5656
"src/plugin-sdk/memory-core-host-engine-storage.ts",
57+
"src/plugins/installed-plugin-index-record-reader.ts",
58+
"src/plugins/installed-plugin-index-store.ts",
5759
"src/plugin-state/plugin-state-store.sqlite.ts",
5860
"src/proxy-capture/store.sqlite.ts",
5961
"src/tasks/task-flow-registry.store.sqlite.ts",

scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
22
import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
5+
import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs";
56

67
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
78
const normalizePathForProbe = (value) => String(value ?? "").replace(/\\/g, "/");
@@ -174,10 +175,8 @@ async function selectedManifestEntries() {
174175
function assertInstalled(pluginId, pluginDir, requiresConfig) {
175176
const stateDir = resolveStateDir();
176177
const configPath = path.join(stateDir, "openclaw.json");
177-
const indexPath = path.join(stateDir, "plugins", "installs.json");
178178
const config = readJson(configPath);
179-
const index = readJson(indexPath);
180-
const records = index.installRecords ?? index.records ?? {};
179+
const records = readPluginInstallRecords({ stateDir, configPath });
181180
const record = records[pluginId];
182181
if (!record) {
183182
throw new Error(`missing install record for ${pluginId}`);
@@ -220,10 +219,8 @@ function assertInstalled(pluginId, pluginDir, requiresConfig) {
220219
function assertUninstalled(pluginId, pluginDir) {
221220
const stateDir = resolveStateDir();
222221
const configPath = path.join(stateDir, "openclaw.json");
223-
const indexPath = path.join(stateDir, "plugins", "installs.json");
224222
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
225-
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
226-
const records = index.installRecords ?? index.records ?? {};
223+
const records = readPluginInstallRecords({ stateDir, configPath });
227224
if (records[pluginId]) {
228225
throw new Error(`install record still present after uninstall for ${pluginId}`);
229226
}

scripts/e2e/lib/codex-install-utils.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { readJson } from "./fixtures/common.mjs";
4+
import { readPluginInstallRecords } from "./plugin-index-sqlite.mjs";
45

56
export { readJson };
67

@@ -34,9 +35,7 @@ export function assertPathInside(parentPath, childPath, label) {
3435
}
3536

3637
export function readInstallRecords(fallbackRecords = {}) {
37-
const indexPath = path.join(stateDir(), "plugins", "installs.json");
38-
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
39-
return index.installRecords || index.records || fallbackRecords || {};
38+
return readPluginInstallRecords({ fallbackRecords });
4039
}
4140

4241
export function npmProjectRootForInstalledPackage(installPath, packageName) {

scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
4+
import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs";
45

56
const command = process.argv[2];
67
const scratchRoot = process.env.KITCHEN_SINK_TMP_DIR || os.tmpdir();
@@ -329,9 +330,7 @@ function assertCutoverPreinstalled() {
329330
throw new Error(`invalid kitchen-sink cutover preinstall spec: ${preinstallSpec}`);
330331
}
331332

332-
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
333-
const index = readJson(indexPath);
334-
const record = (index.installRecords ?? index.records ?? {})[pluginId];
333+
const record = readPluginInstallRecords()[pluginId];
335334
if (!record) {
336335
throw new Error(`missing kitchen-sink cutover preinstall record for ${pluginId}`);
337336
}
@@ -456,9 +455,7 @@ function assertInstalled() {
456455
}
457456
assertExpectedDiagnostics(surfaceMode, errorMessages);
458457

459-
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
460-
const index = readJson(indexPath);
461-
const record = (index.installRecords ?? index.records ?? {})[pluginId];
458+
const record = readPluginInstallRecords()[pluginId];
462459
if (!record) {
463460
throw new Error(`missing kitchen-sink install record for ${pluginId}`);
464461
}
@@ -513,9 +510,7 @@ function assertRemoved() {
513510
throw new Error(`kitchen-sink plugin still listed after uninstall: ${pluginId}`);
514511
}
515512

516-
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
517-
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
518-
const records = index.installRecords ?? index.records ?? {};
513+
const records = readPluginInstallRecords();
519514
if (records[pluginId]) {
520515
throw new Error(`kitchen-sink install record still present after uninstall: ${pluginId}`);
521516
}

scripts/e2e/lib/live-plugin-tool/assertions.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from "node:fs";
22
import path from "node:path";
3+
import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs";
34
import { readTextFileTail, tailText } from "../text-file-utils.mjs";
45

56
const command = process.argv[2];
@@ -153,10 +154,12 @@ function writeJson(file, value) {
153154
}
154155

155156
function installRecords() {
156-
const indexPath = path.join(stateDir(), "plugins", "installs.json");
157-
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
158157
const cfg = fs.existsSync(configPath()) ? readJson(configPath()) : {};
159-
return index.installRecords || index.records || cfg.plugins?.installs || {};
158+
return readPluginInstallRecords({
159+
stateDir: stateDir(),
160+
configPath: configPath(),
161+
fallbackRecords: cfg.plugins?.installs ?? {},
162+
});
160163
}
161164

162165
function pluginInstallPath() {

0 commit comments

Comments
 (0)