openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
457 lines (456 loc) • 21.6 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { g as runOpenClawAgentWriteTransaction } from "./openclaw-agent-db-CWtDoRbC.js";
import { h as resolveAuthProfileDatabasePath, m as resolveAuthProfileDatabaseOwnerId } from "./sqlite-MN_7y26V.js";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js";
import { linkSync, readFileSync, readdirSync, renameSync, unlinkSync } from "node:fs";
import path from "node:path";
import { randomUUID } from "node:crypto";
//#region src/agents/plugin-model-catalog-repair.ts
/** Pure repair rules for OpenClaw-generated plugin model catalogs. */
const PLUGIN_MODEL_CATALOG_GENERATED_BY = "openclaw-plugin-model-catalog-v1";
function hasCatalogApi(value) {
return typeof value === "string" && value.length > 0;
}
function isGeneratedPluginModelCatalog$1(value) {
return isRecord(value) && value.generatedBy === "openclaw-plugin-model-catalog-v1";
}
/** Removes model rows whose transport API cannot be derived without inventing semantics. */
function repairPluginModelCatalogTransportMetadata(contents) {
let parsed;
try {
parsed = JSON.parse(contents);
} catch {
return {
contents,
removedModelCount: 0
};
}
if (!isGeneratedPluginModelCatalog$1(parsed) || !isRecord(parsed.providers)) return {
contents,
removedModelCount: 0
};
let removedModelCount = 0;
const providers = {};
for (const [providerId, provider] of Object.entries(parsed.providers)) {
if (!isRecord(provider) || !Array.isArray(provider.models) || hasCatalogApi(provider.api)) {
providers[providerId] = provider;
continue;
}
const models = provider.models.filter((model) => isRecord(model) && hasCatalogApi(model.api));
removedModelCount += provider.models.length - models.length;
providers[providerId] = models.length === provider.models.length ? provider : {
...provider,
models
};
}
if (removedModelCount === 0) return {
contents,
removedModelCount
};
const trailingNewline = contents.endsWith("\n") ? "\n" : "";
return {
contents: `${JSON.stringify({
...parsed,
providers
}, null, 2)}${trailingNewline}`,
removedModelCount
};
}
//#endregion
//#region src/agents/plugin-model-catalog.ts
/**
* Generated plugin model catalog discovery.
*
* The existing agent SQLite cache lets provider discovery reuse plugin-owned
* catalogs without loading runtimes or creating parallel state files.
*/
const PLUGIN_MODEL_CATALOG_FILE = "catalog.json";
const PLUGIN_MODEL_CATALOG_CACHE_SCOPE = "plugin-model-catalog-v1";
const PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE = "plugin-model-catalog-migration-v1";
const log = createSubsystemLogger("agents/plugin-model-catalog");
/** Recognizes canonical catalogs and recoverable atomic migration claims. */
function isPluginModelCatalogMigrationFile(filename) {
return filename === PLUGIN_MODEL_CATALOG_FILE || filename.startsWith(`${PLUGIN_MODEL_CATALOG_FILE}.doctor-importing-`);
}
function pluginModelCatalogDatabaseOptions(agentDir) {
return {
agentId: resolveAuthProfileDatabaseOwnerId(agentDir),
path: resolveAuthProfileDatabasePath(agentDir)
};
}
function readPersistedPluginModelCatalogEntries(agentDir, scope) {
const result = withOpenClawAgentDatabaseReadOnly((database) => {
const kysely = getNodeSqliteKysely(database.db);
return executeSqliteQuerySync(database.db, kysely.selectFrom("cache_entries").select(["key", "value_json"]).where("scope", "=", scope).orderBy("key")).rows.flatMap((row) => row.value_json === null ? [] : [{
pluginId: row.key,
contents: row.value_json
}]);
}, pluginModelCatalogDatabaseOptions(agentDir));
return result.found ? result.value : [];
}
function readPersistedPluginModelCatalogs(agentDir) {
return readPersistedPluginModelCatalogEntries(agentDir, PLUGIN_MODEL_CATALOG_CACHE_SCOPE);
}
/**
* Reads an exact plugin-catalog generation without migration or repair writes.
* Lifecycle preparation uses this for configured providers before atomic publication.
*/
function loadPersistedPluginModelCatalogsReadOnly(agentDir, pluginIds) {
if (pluginIds?.length === 0) return [];
const catalogs = readPersistedPluginModelCatalogs(agentDir);
if (!pluginIds) return catalogs;
const allowed = new Set(pluginIds);
return catalogs.filter(({ pluginId }) => allowed.has(pluginId));
}
function repairPersistedPluginModelCatalogs(params) {
const repairs = params.catalogs.flatMap((catalog) => {
const repaired = repairPluginModelCatalogTransportMetadata(catalog.contents);
return repaired.removedModelCount > 0 ? [{
...catalog,
repairedContents: repaired.contents,
removedModelCount: repaired.removedModelCount
}] : [];
});
if (repairs.length === 0) return false;
const updatedAt = Date.now();
const applied = runOpenClawAgentWriteTransaction((database) => {
const kysely = getNodeSqliteKysely(database.db);
return repairs.flatMap((repair) => {
return executeSqliteQuerySync(database.db, kysely.updateTable("cache_entries").set({
value_json: repair.repairedContents,
updated_at: updatedAt
}).where("scope", "=", PLUGIN_MODEL_CATALOG_CACHE_SCOPE).where("key", "=", repair.pluginId).where("value_json", "=", repair.contents)).numAffectedRows === 1n ? [repair] : [];
});
}, pluginModelCatalogDatabaseOptions(params.agentDir), { operationLabel: "plugin-model-catalog.repair" });
for (const repair of applied) log.warn(`Repaired generated model catalog for plugin ${repair.pluginId}: removed ${repair.removedModelCount} model row(s) without provider or model api metadata.`);
return true;
}
function readPersistedPluginModelCatalogMigrationPayloads(agentDir) {
return new Map(readPersistedPluginModelCatalogEntries(agentDir, PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE).map((catalog) => [catalog.pluginId, catalog.contents]));
}
function replacePersistedPluginModelCatalogEntries(params) {
if (params.planned.size === 0 && (params.deleteMissing === false || readPersistedPluginModelCatalogs(params.agentDir).length === 0)) return false;
const updatedAt = Date.now();
return runOpenClawAgentWriteTransaction((database) => {
const kysely = getNodeSqliteKysely(database.db);
const existing = executeSqliteQuerySync(database.db, kysely.selectFrom("cache_entries").select(["key", "value_json"]).where("scope", "=", PLUGIN_MODEL_CATALOG_CACHE_SCOPE)).rows;
const existingByPluginId = new Map(existing.map((row) => [row.key, row.value_json]));
const existingMigrationPayloads = params.migrationPayloads ? new Map(executeSqliteQuerySync(database.db, kysely.selectFrom("cache_entries").select(["key", "value_json"]).where("scope", "=", PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE)).rows.map((row) => [row.key, row.value_json])) : void 0;
const upsertCacheEntry = (scope, pluginId, contents) => {
executeSqliteQuerySync(database.db, kysely.insertInto("cache_entries").values({
scope,
key: pluginId,
value_json: contents,
blob: null,
expires_at: null,
updated_at: updatedAt
}).onConflict((conflict) => conflict.columns(["scope", "key"]).doUpdateSet({
value_json: contents,
blob: null,
expires_at: null,
updated_at: updatedAt
})));
};
let changed = false;
for (const [pluginId, contents] of params.planned) {
const migrationPayload = params.migrationPayloads?.get(pluginId);
if (migrationPayload && existingMigrationPayloads?.get(pluginId) === migrationPayload) continue;
if (existingByPluginId.get(pluginId) !== contents) {
upsertCacheEntry(PLUGIN_MODEL_CATALOG_CACHE_SCOPE, pluginId, contents);
changed = true;
}
if (migrationPayload) {
upsertCacheEntry(PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE, pluginId, migrationPayload);
changed = true;
}
}
if (params.deleteMissing !== false) for (const pluginId of existingByPluginId.keys()) {
if (params.planned.has(pluginId)) continue;
executeSqliteQuerySync(database.db, kysely.deleteFrom("cache_entries").where("scope", "=", PLUGIN_MODEL_CATALOG_CACHE_SCOPE).where("key", "=", pluginId));
changed = true;
}
return changed;
}, pluginModelCatalogDatabaseOptions(params.agentDir), { operationLabel: params.deleteMissing === false ? "plugin-model-catalog.migrate" : "plugin-model-catalog.replace" });
}
function readLegacyPluginModelCatalog(pathname) {
try {
return readFileSync(pathname, "utf8");
} catch (error) {
if (error.code === "ENOENT") return null;
throw error;
}
}
function hasCommittedExpectedPluginModelCatalogs(agentDir, expectedContents) {
const committed = new Map(readPersistedPluginModelCatalogs(agentDir).map((catalog) => [catalog.pluginId, catalog.contents]));
const migrationPayloads = readPersistedPluginModelCatalogMigrationPayloads(agentDir);
for (const [pluginId, contents] of expectedContents) if (committed.get(pluginId) !== contents && migrationPayloads.get(pluginId) !== contents) return false;
return true;
}
function hasCommittedMigratedPluginModelCatalog(agentDir, pluginId, contents) {
return readPersistedPluginModelCatalogs(agentDir).find((catalog) => catalog.pluginId === pluginId)?.contents === contents && readPersistedPluginModelCatalogMigrationPayloads(agentDir).get(pluginId) === contents;
}
function retireCommittedPluginModelCatalogMigration(params) {
return runOpenClawAgentWriteTransaction((database) => {
const kysely = getNodeSqliteKysely(database.db);
const committed = executeSqliteQuerySync(database.db, kysely.selectFrom("cache_entries").select(["scope", "value_json"]).where("key", "=", params.pluginId).where("scope", "in", [PLUGIN_MODEL_CATALOG_CACHE_SCOPE, PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE])).rows;
const contentsByScope = new Map(committed.map((row) => [row.scope, row.value_json]));
if (contentsByScope.get(PLUGIN_MODEL_CATALOG_CACHE_SCOPE) !== params.contents || contentsByScope.get(PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE) !== params.contents) return false;
executeSqliteQuerySync(database.db, kysely.deleteFrom("cache_entries").where("scope", "=", PLUGIN_MODEL_CATALOG_MIGRATION_SCOPE).where("key", "=", params.pluginId));
return true;
}, pluginModelCatalogDatabaseOptions(params.agentDir), { operationLabel: "plugin-model-catalog.retire-migration" });
}
function retireOrphanedPluginModelCatalogMigrations(params) {
for (const [pluginId, contents] of readPersistedPluginModelCatalogMigrationPayloads(params.agentDir)) {
if (params.protectedPluginIds?.has(pluginId)) continue;
retireCommittedPluginModelCatalogMigration({
agentDir: params.agentDir,
pluginId,
contents
});
}
}
/** Migrates released sidecars before runtime can read or replace agent SQLite state. */
function migrateLegacyPluginModelCatalogs(params) {
const agentDir = path.resolve(params.agentDir);
const pluginsDir = path.join(agentDir, "plugins");
const warnings = [];
let pluginDirs;
try {
pluginDirs = readdirSync(pluginsDir, { withFileTypes: true });
} catch (error) {
if (error.code !== "ENOENT") {
if (params.expectedContents && !hasCommittedExpectedPluginModelCatalogs(agentDir, params.expectedContents)) throw new Error("Could not inspect expected legacy provider catalogs", { cause: error });
return {
detected: 0,
migrated: 0,
warnings: [`Could not inspect legacy provider catalogs: ${pluginsDir}`]
};
}
if (params.expectedContents && params.expectedContents.size > 0 && !hasCommittedExpectedPluginModelCatalogs(agentDir, params.expectedContents)) throw new Error("Legacy provider catalogs disappeared before migration", { cause: error });
retireOrphanedPluginModelCatalogMigrations({ agentDir });
return {
detected: 0,
migrated: 0,
warnings: []
};
}
const legacyCatalogs = [];
const protectedMigrationPluginIds = /* @__PURE__ */ new Set();
for (const pluginDir of pluginDirs) {
if (!pluginDir.isDirectory()) continue;
const pluginId = decodePluginModelCatalogRelativePathPluginId(path.join("plugins", pluginDir.name, PLUGIN_MODEL_CATALOG_FILE));
if (!pluginId) continue;
const pluginPath = path.join(agentDir, "plugins", pluginDir.name);
let catalogFiles;
try {
catalogFiles = readdirSync(pluginPath, { withFileTypes: true });
} catch {
protectedMigrationPluginIds.add(pluginId);
warnings.push(`Could not inspect legacy provider catalogs: ${pluginPath}`);
continue;
}
const sourceFiles = catalogFiles.filter((entry) => entry.isFile() && isPluginModelCatalogMigrationFile(entry.name)).toSorted((left, right) => {
if (left.name === PLUGIN_MODEL_CATALOG_FILE) return 1;
if (right.name === PLUGIN_MODEL_CATALOG_FILE) return -1;
return left.name.localeCompare(right.name);
});
if (sourceFiles.length > 0) protectedMigrationPluginIds.add(pluginId);
const pluginLegacyCatalogs = [];
let hasUnreadableCatalog = false;
for (const sourceFile of sourceFiles) {
const pathname = path.join(pluginPath, sourceFile.name);
let contents;
try {
contents = readLegacyPluginModelCatalog(pathname);
} catch {
hasUnreadableCatalog = true;
warnings.push(`Could not read legacy provider catalog: ${pathname}`);
continue;
}
if (contents === null) {
warnings.push(`Legacy provider catalog disappeared before migration: ${pathname}`);
continue;
}
let parsed;
try {
parsed = JSON.parse(contents);
} catch {
continue;
}
if (isGeneratedPluginModelCatalog(parsed)) pluginLegacyCatalogs.push({
pluginId,
pathname,
contents
});
}
if (hasUnreadableCatalog) continue;
if (!pluginLegacyCatalogs.some((catalog) => path.basename(catalog.pathname) === PLUGIN_MODEL_CATALOG_FILE) && new Set(pluginLegacyCatalogs.map((catalog) => catalog.contents)).size > 1) {
warnings.push(`Conflicting retained legacy provider catalogs: ${pluginPath}`);
continue;
}
legacyCatalogs.push(...pluginLegacyCatalogs);
}
retireOrphanedPluginModelCatalogMigrations({
agentDir,
protectedPluginIds: protectedMigrationPluginIds
});
if (params.expectedContents) {
const observed = new Map(legacyCatalogs.map((catalog) => [catalog.pluginId, catalog.contents]));
for (const [pluginId, contents] of params.expectedContents) {
const observedContents = observed.get(pluginId);
if (observedContents === contents) continue;
if (observedContents === void 0 && hasCommittedExpectedPluginModelCatalogs(agentDir, /* @__PURE__ */ new Map([[pluginId, contents]]))) continue;
if (observedContents !== contents) throw new Error(`Legacy provider catalog changed before migration: ${pluginId}`);
}
}
if (legacyCatalogs.length === 0) return {
detected: 0,
migrated: 0,
warnings
};
let migrated = 0;
for (const catalog of legacyCatalogs) {
if (readPersistedPluginModelCatalogMigrationPayloads(agentDir).get(catalog.pluginId) === catalog.contents && !hasCommittedMigratedPluginModelCatalog(agentDir, catalog.pluginId, catalog.contents)) {
warnings.push(`Left superseded legacy provider catalog in place: ${catalog.pathname}`);
continue;
}
params.beforeLegacyCatalogClaim?.(catalog.pathname);
const claimPath = `${catalog.pathname}.doctor-importing-${process.pid}-${randomUUID()}`;
try {
renameSync(catalog.pathname, claimPath);
} catch (error) {
if (error.code === "ENOENT") {
if (hasCommittedExpectedPluginModelCatalogs(agentDir, /* @__PURE__ */ new Map([[catalog.pluginId, catalog.contents]]))) migrated += 1;
else warnings.push(`Legacy provider catalog was claimed before its migration was committed: ${catalog.pathname}`);
} else {
try {
if (readLegacyPluginModelCatalog(catalog.pathname) === catalog.contents) {
const migrationPayloads = /* @__PURE__ */ new Map([[catalog.pluginId, catalog.contents]]);
replacePersistedPluginModelCatalogEntries({
agentDir,
planned: migrationPayloads,
migrationPayloads,
deleteMissing: false
});
}
} catch {}
warnings.push(`Could not remove migrated legacy provider catalog: ${catalog.pathname}`);
}
continue;
}
try {
if (readLegacyPluginModelCatalog(claimPath) !== catalog.contents) throw new Error("legacy provider catalog changed before migration could claim it");
const migrationPayloads = /* @__PURE__ */ new Map([[catalog.pluginId, catalog.contents]]);
replacePersistedPluginModelCatalogEntries({
agentDir,
planned: migrationPayloads,
migrationPayloads,
deleteMissing: false
});
if (!hasCommittedMigratedPluginModelCatalog(agentDir, catalog.pluginId, catalog.contents)) throw new Error("committed provider catalog changed before migration could remove it");
unlinkSync(claimPath);
} catch {
let retainedPath = claimPath;
try {
linkSync(claimPath, catalog.pathname);
retainedPath = catalog.pathname;
unlinkSync(claimPath);
} catch {}
warnings.push(`Left changed legacy provider catalog in place: ${retainedPath}`);
continue;
}
retireCommittedPluginModelCatalogMigration({
agentDir,
pluginId: catalog.pluginId,
contents: catalog.contents
});
migrated += 1;
}
return {
detected: legacyCatalogs.length,
migrated,
warnings
};
}
/** Reads available provider catalogs without discarding legacy migration diagnostics. */
function loadPersistedPluginModelCatalogs(agentDir) {
const migration = migrateLegacyPluginModelCatalogs({ agentDir });
let catalogs = readPersistedPluginModelCatalogs(agentDir);
if (migration.warnings.length === 0 && repairPersistedPluginModelCatalogs({
agentDir,
catalogs
})) catalogs = readPersistedPluginModelCatalogs(agentDir);
return {
catalogs,
warnings: migration.warnings
};
}
/** Replaces rebuildable provider catalogs in the existing per-agent SQLite cache. */
function replacePersistedPluginModelCatalogs(params) {
const planned = /* @__PURE__ */ new Map();
for (const [relativePath, contents] of Object.entries(params.pluginCatalogWrites)) {
const pluginId = decodePluginModelCatalogRelativePathPluginId(relativePath);
if (!pluginId) throw new Error(`Invalid generated plugin model catalog key: ${relativePath}`);
planned.set(pluginId, repairPluginModelCatalogTransportMetadata(contents).contents);
}
return replacePersistedPluginModelCatalogEntries({
agentDir: params.agentDir,
planned
});
}
/** Encodes the profile-relative path for a plugin-owned generated model catalog. */
function encodePluginModelCatalogRelativePath(pluginId) {
return `plugins/${encodeURIComponent(pluginId)}/${PLUGIN_MODEL_CATALOG_FILE}`;
}
/** Returns true only for canonical profile-relative generated catalog paths. */
function isPluginModelCatalogRelativePath(relativePath) {
const parts = relativePath.split(/[\\/]/);
return !path.isAbsolute(relativePath) && parts.length === 3 && parts[0] === "plugins" && parts[1] !== "" && parts[1] !== "." && parts[1] !== ".." && parts[2] === PLUGIN_MODEL_CATALOG_FILE;
}
/** Decodes the plugin id from a canonical generated catalog path. */
function decodePluginModelCatalogRelativePathPluginId(relativePath) {
if (!isPluginModelCatalogRelativePath(relativePath)) return;
const encodedPluginId = relativePath.split(/[\\/]/)[1];
if (!encodedPluginId) return;
try {
return decodeURIComponent(encodedPluginId);
} catch {
return;
}
}
/** Detects model catalogs generated by OpenClaw rather than user-authored JSON. */
function isGeneratedPluginModelCatalog(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && value.generatedBy === "openclaw-plugin-model-catalog-v1";
}
/** Resolves the sole enabled plugin that owns a provider's model catalog. */
function resolvePluginModelCatalogOwnerPluginId(params) {
const snapshot = params.pluginMetadataSnapshot;
const owners = snapshot?.owners;
if (!owners) return;
const providerId = normalizeProviderId(params.providerId);
const candidates = [
owners.modelCatalogProviders.get(providerId),
owners.providers.get(providerId),
owners.setupProviders.get(providerId)
].find((entry) => Array.isArray(entry) && entry.length > 0);
const pluginId = candidates?.length === 1 ? candidates[0] : void 0;
if (!pluginId) return;
if (!snapshot?.index) return pluginId;
const normalizedPluginId = snapshot.normalizePluginId?.(pluginId) ?? pluginId;
return snapshot.index.plugins.some((plugin) => plugin.pluginId === normalizedPluginId && plugin.enabled) ? normalizedPluginId : void 0;
}
/** Keeps generated catalog providers only when the catalog plugin still owns them. */
function filterGeneratedPluginModelCatalogProviders(params) {
if (!params.catalogPluginId || !params.pluginMetadataSnapshot || params.parsedCatalog !== void 0 && !isGeneratedPluginModelCatalog(params.parsedCatalog)) return {};
return Object.fromEntries(Object.entries(params.providers).filter(([providerId]) => {
return resolvePluginModelCatalogOwnerPluginId({
providerId,
pluginMetadataSnapshot: params.pluginMetadataSnapshot
}) === params.catalogPluginId;
}));
}
//#endregion
export { isPluginModelCatalogMigrationFile as a, migrateLegacyPluginModelCatalogs as c, PLUGIN_MODEL_CATALOG_GENERATED_BY as d, repairPluginModelCatalogTransportMetadata as f, isGeneratedPluginModelCatalog as i, replacePersistedPluginModelCatalogs as l, encodePluginModelCatalogRelativePath as n, loadPersistedPluginModelCatalogs as o, filterGeneratedPluginModelCatalogProviders as r, loadPersistedPluginModelCatalogsReadOnly as s, decodePluginModelCatalogRelativePathPluginId as t, resolvePluginModelCatalogOwnerPluginId as u };