mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
231 lines
7.6 KiB
TypeScript
231 lines
7.6 KiB
TypeScript
/**
|
|
* Persist ClawX-managed plugin install records into OpenClaw's SQLite
|
|
* installed_plugin_index store (openclaw.sqlite).
|
|
*
|
|
* OpenClaw 2026.6+ reads trusted install metadata from SQLite at runtime,
|
|
* not from transient plugins.installs in openclaw.json.
|
|
*/
|
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { logger } from './logger';
|
|
|
|
const INSTALLED_PLUGIN_INDEX_KEY = 'installed-plugin-index';
|
|
const INSTALLED_PLUGIN_INDEX_WARNING = 'DO NOT EDIT. This file is generated by OpenClaw from plugin manifests, install records, and config policy. Use `openclaw plugins registry --refresh`, `openclaw plugins install/update/uninstall`, or `openclaw plugins enable/disable` instead.';
|
|
|
|
const INSTALLED_PLUGIN_INDEX_TABLE_SQL = `
|
|
CREATE TABLE IF NOT EXISTS installed_plugin_index (
|
|
index_key TEXT NOT NULL PRIMARY KEY,
|
|
version INTEGER NOT NULL,
|
|
host_contract_version TEXT NOT NULL,
|
|
compat_registry_version TEXT NOT NULL,
|
|
migration_version INTEGER NOT NULL,
|
|
policy_hash TEXT NOT NULL,
|
|
generated_at_ms INTEGER NOT NULL,
|
|
refresh_reason TEXT,
|
|
install_records_json TEXT NOT NULL,
|
|
plugins_json TEXT NOT NULL,
|
|
diagnostics_json TEXT NOT NULL,
|
|
warning TEXT,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
`;
|
|
|
|
function resolveOpenClawStateDir(): string {
|
|
return process.env.OPENCLAW_STATE_DIR?.trim() || join(homedir(), '.openclaw');
|
|
}
|
|
|
|
function resolveOpenClawStateSqlitePath(): string {
|
|
// OpenClaw 2026.7.1 moved the shared state database under state/.
|
|
// Writing the legacy root-level database leaves Gateway migrations reading
|
|
// stale plugin records from the canonical database.
|
|
return join(resolveOpenClawStateDir(), 'state', 'openclaw.sqlite');
|
|
}
|
|
|
|
function parseInstallRecordsJson(raw: unknown): Record<string, Record<string, unknown>> {
|
|
if (!raw || typeof raw !== 'string') {
|
|
return {};
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
return {};
|
|
}
|
|
const records: Record<string, Record<string, unknown>> = {};
|
|
for (const [pluginId, record] of Object.entries(parsed as Record<string, unknown>)) {
|
|
if (record && typeof record === 'object' && !Array.isArray(record)) {
|
|
records[pluginId] = record as Record<string, unknown>;
|
|
}
|
|
}
|
|
return records;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function installRecordsMatch(
|
|
left: Record<string, unknown>,
|
|
right: Record<string, unknown>,
|
|
): boolean {
|
|
const keys = ['source', 'spec', 'installPath', 'version', 'resolvedName', 'resolvedVersion', 'resolvedSpec'] as const;
|
|
return keys.every((key) => left[key] === right[key]);
|
|
}
|
|
|
|
function openStateDatabase(sqlitePath: string): DatabaseSync {
|
|
const db = new DatabaseSync(sqlitePath);
|
|
db.exec(INSTALLED_PLUGIN_INDEX_TABLE_SQL);
|
|
return db;
|
|
}
|
|
|
|
/**
|
|
* Upsert trusted install records into openclaw.sqlite.
|
|
* ClawX-authored records win over stale SQLite entries for the same plugin id.
|
|
*/
|
|
export function upsertPluginInstallRecordsIntoSqlite(
|
|
records: Record<string, Record<string, unknown>>,
|
|
): boolean {
|
|
if (Object.keys(records).length === 0) {
|
|
return false;
|
|
}
|
|
|
|
ensureOpenClawStateDirExists();
|
|
const sqlitePath = resolveOpenClawStateSqlitePath();
|
|
mkdirSync(join(resolveOpenClawStateDir(), 'state'), { recursive: true });
|
|
|
|
let db: DatabaseSync | null = null;
|
|
try {
|
|
db = openStateDatabase(sqlitePath);
|
|
const row = db.prepare(`
|
|
SELECT install_records_json
|
|
FROM installed_plugin_index
|
|
WHERE index_key = ?
|
|
`).get(INSTALLED_PLUGIN_INDEX_KEY) as { install_records_json?: string } | undefined;
|
|
|
|
const now = Date.now();
|
|
let merged: Record<string, Record<string, unknown>>;
|
|
let changed = false;
|
|
|
|
if (row) {
|
|
const existing = parseInstallRecordsJson(row.install_records_json);
|
|
merged = { ...existing };
|
|
for (const [pluginId, record] of Object.entries(records)) {
|
|
const current = merged[pluginId];
|
|
if (current && installRecordsMatch(current, record)) {
|
|
continue;
|
|
}
|
|
merged[pluginId] = record;
|
|
changed = true;
|
|
}
|
|
if (!changed) {
|
|
return false;
|
|
}
|
|
db.prepare(`
|
|
UPDATE installed_plugin_index
|
|
SET install_records_json = ?,
|
|
updated_at_ms = ?,
|
|
generated_at_ms = ?
|
|
WHERE index_key = ?
|
|
`).run(JSON.stringify(merged), now, now, INSTALLED_PLUGIN_INDEX_KEY);
|
|
} else {
|
|
merged = { ...records };
|
|
changed = true;
|
|
db.prepare(`
|
|
INSERT INTO installed_plugin_index (
|
|
index_key, version, host_contract_version, compat_registry_version,
|
|
migration_version, policy_hash, generated_at_ms, refresh_reason,
|
|
install_records_json, plugins_json, diagnostics_json, warning, updated_at_ms
|
|
) VALUES (
|
|
?, 1, 'clawx-managed', 'clawx-managed',
|
|
1, 'clawx-managed', ?, 'source-changed',
|
|
?, '[]', '[]', ?, ?
|
|
)
|
|
`).run(
|
|
INSTALLED_PLUGIN_INDEX_KEY,
|
|
now,
|
|
JSON.stringify(merged),
|
|
INSTALLED_PLUGIN_INDEX_WARNING,
|
|
now,
|
|
);
|
|
}
|
|
|
|
if (changed) {
|
|
logger.info(`[plugin] Persisted trusted install metadata to SQLite for: ${Object.keys(records).join(', ')}`);
|
|
}
|
|
return changed;
|
|
} catch (error) {
|
|
logger.warn('[plugin] Failed to persist trusted install metadata to SQLite:', error);
|
|
return false;
|
|
} finally {
|
|
db?.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove install records that must remain ClawX-managed rather than updated
|
|
* from their raw upstream npm package. Also clean the legacy root-level DB
|
|
* previously written by ClawX before OpenClaw 2026.7.1 moved state to state/.
|
|
*/
|
|
export function removePluginInstallRecordsFromSqlite(pluginIds: string[]): boolean {
|
|
if (pluginIds.length === 0) return false;
|
|
|
|
const stateDir = resolveOpenClawStateDir();
|
|
const sqlitePaths = [
|
|
resolveOpenClawStateSqlitePath(),
|
|
join(stateDir, 'openclaw.sqlite'),
|
|
];
|
|
let changed = false;
|
|
|
|
for (const sqlitePath of sqlitePaths) {
|
|
if (!existsSync(sqlitePath)) continue;
|
|
|
|
let db: DatabaseSync | null = null;
|
|
try {
|
|
db = openStateDatabase(sqlitePath);
|
|
const row = db.prepare(`
|
|
SELECT install_records_json
|
|
FROM installed_plugin_index
|
|
WHERE index_key = ?
|
|
`).get(INSTALLED_PLUGIN_INDEX_KEY) as { install_records_json?: string } | undefined;
|
|
if (!row) continue;
|
|
|
|
const records = parseInstallRecordsJson(row.install_records_json);
|
|
let databaseChanged = false;
|
|
for (const pluginId of pluginIds) {
|
|
if (Object.hasOwn(records, pluginId)) {
|
|
delete records[pluginId];
|
|
databaseChanged = true;
|
|
}
|
|
}
|
|
if (!databaseChanged) continue;
|
|
|
|
const now = Date.now();
|
|
db.prepare(`
|
|
UPDATE installed_plugin_index
|
|
SET install_records_json = ?,
|
|
updated_at_ms = ?,
|
|
generated_at_ms = ?
|
|
WHERE index_key = ?
|
|
`).run(JSON.stringify(records), now, now, INSTALLED_PLUGIN_INDEX_KEY);
|
|
changed = true;
|
|
} catch (error) {
|
|
logger.warn(`[plugin] Failed to remove install metadata from ${sqlitePath}:`, error);
|
|
} finally {
|
|
db?.close();
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
logger.info(`[plugin] Removed managed install metadata from SQLite for: ${pluginIds.join(', ')}`);
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/** Ensure ~/.openclaw exists before first config write in fresh installs. */
|
|
export function ensureOpenClawStateDirExists(): void {
|
|
const stateDir = resolveOpenClawStateDir();
|
|
if (!existsSync(stateDir)) {
|
|
mkdirSync(stateDir, { recursive: true });
|
|
}
|
|
}
|