UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

368 lines (362 loc) 14.6 kB
import { p as resolveUserPath } from "./utils-CCC-BEJH.js"; import { d as executeSqliteQuerySync, f as executeSqliteQueryTakeFirstSync, l as runSqliteImmediateTransactionSync, o as runOpenClawStateWriteTransaction, p as getNodeSqliteKysely, s as resolveOpenClawStateSqliteDir, t as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS, u as clearNodeSqliteKyselyCacheForDatabase } from "./openclaw-state-db-ULtH_b7x.js"; import { n as requireNodeSqlite, t as configureSqliteWalMaintenance } from "./sqlite-wal-CP9DVd__.js"; import { u as normalizeAgentId } from "./session-key-B_NoIfpX.js"; import { s as resolveDefaultAgentDir, u as resolveRegisteredAgentIdForDir } from "./agent-scope-config-CgCYpZfK.js"; import fs, { chmodSync, existsSync, mkdirSync, statSync } from "node:fs"; import path from "node:path"; import { createHash } from "node:crypto"; //#region src/state/openclaw-agent-db.paths.ts /** Resolve the SQLite file for one normalized agent id. */ function resolveOpenClawAgentSqlitePath(options) { const agentId = normalizeAgentId(options.agentId); return options.path ?? path.join(path.dirname(resolveOpenClawStateSqliteDir(options.env ?? process.env)), "agents", agentId, "agent", "openclaw-agent.sqlite"); } //#endregion //#region src/state/openclaw-agent-schema.generated.ts /** * This file was generated from the SQLite schema source. * Please do not edit it manually. */ const OPENCLAW_AGENT_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS schema_meta ( meta_key TEXT NOT NULL PRIMARY KEY, role TEXT NOT NULL, schema_version INTEGER NOT NULL, agent_id TEXT, app_version TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS cache_entries ( scope TEXT NOT NULL, key TEXT NOT NULL, value_json TEXT, blob BLOB, expires_at INTEGER, updated_at INTEGER NOT NULL, PRIMARY KEY (scope, key) ); CREATE INDEX IF NOT EXISTS idx_agent_cache_expiry ON cache_entries(scope, expires_at, key) WHERE expires_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_agent_cache_updated ON cache_entries(scope, updated_at DESC, key); CREATE TABLE IF NOT EXISTS auth_profile_store ( store_key TEXT NOT NULL PRIMARY KEY, store_json TEXT NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS auth_profile_state ( state_key TEXT NOT NULL PRIMARY KEY, state_json TEXT NOT NULL, updated_at INTEGER NOT NULL );\n`; //#endregion //#region src/state/openclaw-agent-db.ts /** * Per-agent SQLite database lifecycle and shared-state registration. * * Each opened agent database is schema-owned by one normalized agent id, cached * per pathname, protected with private file modes, and registered in the shared * OpenClaw state database for discovery and maintenance. */ const OPENCLAW_AGENT_SCHEMA_VERSION = 1; const OPENCLAW_AGENT_DB_DIR_MODE = 448; const OPENCLAW_AGENT_DB_FILE_MODE = 384; const OPENCLAW_AGENT_DB_SIDECAR_SUFFIXES = [ "", "-shm", "-wal" ]; const cachedDatabases = /* @__PURE__ */ new Map(); function readSqliteUserVersion(db) { const row = db.prepare("PRAGMA user_version").get(); return Number(row?.user_version ?? 0); } function assertSupportedAgentSchemaVersion(db, pathname) { const userVersion = readSqliteUserVersion(db); if (userVersion > OPENCLAW_AGENT_SCHEMA_VERSION) throw new Error(`OpenClaw agent database ${pathname} uses newer schema version ${userVersion}; this OpenClaw build supports ${OPENCLAW_AGENT_SCHEMA_VERSION}.`); } function ensureOpenClawAgentDatabasePermissions(pathname, options) { const dir = path.dirname(pathname); const defaultPath = resolveOpenClawAgentSqlitePath({ agentId: options.agentId, env: options.env }); const isDefaultAgentDatabase = path.resolve(pathname) === path.resolve(defaultPath); const dirExisted = existsSync(dir); mkdirSync(dir, { recursive: true, mode: OPENCLAW_AGENT_DB_DIR_MODE }); if (isDefaultAgentDatabase || !dirExisted) chmodSync(dir, OPENCLAW_AGENT_DB_DIR_MODE); for (const suffix of OPENCLAW_AGENT_DB_SIDECAR_SUFFIXES) { const candidate = `${pathname}${suffix}`; if (existsSync(candidate)) chmodSync(candidate, OPENCLAW_AGENT_DB_FILE_MODE); } } function readExistingSchemaMeta(db) { if (!db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_meta'").get()) return null; const row = db.prepare("SELECT role, agent_id FROM schema_meta WHERE meta_key = 'primary'").get(); if (!row) return null; return { agentId: typeof row.agent_id === "string" ? row.agent_id : null, role: typeof row.role === "string" ? row.role : null }; } function assertExistingSchemaOwner(existing, agentId, pathname) { if (!existing) return; if (existing.role !== "agent") throw new Error(`OpenClaw agent database ${pathname} has schema role ${existing.role ?? "unknown"}; expected agent.`); if (!existing.agentId) throw new Error(`OpenClaw agent database ${pathname} has no agent owner.`); if (normalizeAgentId(existing.agentId) !== agentId) throw new Error(`OpenClaw agent database ${pathname} belongs to agent ${existing.agentId}; requested agent ${agentId}.`); } function ensureAgentSchema(db, agentId, pathname) { assertSupportedAgentSchemaVersion(db, pathname); assertExistingSchemaOwner(readExistingSchemaMeta(db), agentId, pathname); db.exec(OPENCLAW_AGENT_SCHEMA_SQL); const kysely = getNodeSqliteKysely(db); db.exec(`PRAGMA user_version = ${OPENCLAW_AGENT_SCHEMA_VERSION};`); const now = Date.now(); executeSqliteQuerySync(db, kysely.insertInto("schema_meta").values({ meta_key: "primary", role: "agent", schema_version: OPENCLAW_AGENT_SCHEMA_VERSION, agent_id: agentId, app_version: null, created_at: now, updated_at: now }).onConflict((conflict) => conflict.column("meta_key").doUpdateSet({ role: "agent", schema_version: OPENCLAW_AGENT_SCHEMA_VERSION, agent_id: agentId, app_version: null, updated_at: now }))); } function registerAgentDatabase(params) { let sizeBytes = null; try { sizeBytes = statSync(params.path).size; } catch { sizeBytes = null; } const lastSeenAt = Date.now(); runOpenClawStateWriteTransaction((database) => { const db = getNodeSqliteKysely(database.db); executeSqliteQuerySync(database.db, db.insertInto("agent_databases").values({ agent_id: params.agentId, path: params.path, schema_version: OPENCLAW_AGENT_SCHEMA_VERSION, last_seen_at: lastSeenAt, size_bytes: sizeBytes }).onConflict((conflict) => conflict.columns(["agent_id", "path"]).doUpdateSet({ schema_version: OPENCLAW_AGENT_SCHEMA_VERSION, last_seen_at: lastSeenAt, size_bytes: sizeBytes }))); }, { env: params.env }); } /** Open or return a cached per-agent database after schema and owner validation. */ function openOpenClawAgentDatabase(options) { const agentId = normalizeAgentId(options.agentId); const databaseOptions = { ...options, agentId }; const pathname = resolveOpenClawAgentSqlitePath(databaseOptions); const cached = cachedDatabases.get(pathname); if (cached?.db.isOpen) { if (cached.agentId !== agentId) throw new Error(`OpenClaw agent database ${pathname} is already open for agent ${cached.agentId}; requested agent ${agentId}.`); registerAgentDatabase({ agentId, path: pathname, env: options.env }); return cached; } if (cached) { cached.walMaintenance.close(); clearNodeSqliteKyselyCacheForDatabase(cached.db); cachedDatabases.delete(pathname); } ensureOpenClawAgentDatabasePermissions(pathname, databaseOptions); const db = new (requireNodeSqlite()).DatabaseSync(pathname); const walMaintenance = configureSqliteWalMaintenance(db, { databaseLabel: `openclaw-agent:${agentId}`, databasePath: pathname }); db.exec("PRAGMA synchronous = NORMAL;"); db.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); db.exec("PRAGMA foreign_keys = ON;"); try { ensureAgentSchema(db, agentId, pathname); } catch (err) { walMaintenance.close(); db.close(); throw err; } ensureOpenClawAgentDatabasePermissions(pathname, databaseOptions); const database = { agentId, db, path: pathname, walMaintenance }; cachedDatabases.set(pathname, database); registerAgentDatabase({ agentId, path: pathname, env: options.env }); return database; } /** Run a synchronous immediate transaction against an agent database. */ function runOpenClawAgentWriteTransaction(operation, options) { const database = openOpenClawAgentDatabase(options); const result = runSqliteImmediateTransactionSync(database.db, () => operation(database)); ensureOpenClawAgentDatabasePermissions(database.path, options); return result; } //#endregion //#region src/agents/auth-profiles/sqlite.ts /** * SQLite persistence adapter for auth profile secrets and runtime state. * The public helpers expose raw JSON payloads so normalization stays in the * store/state layers that own compatibility rules. */ const PRIMARY_ROW_KEY = "primary"; function resolveAgentDir(agentDir) { return resolveUserPath(agentDir ?? resolveDefaultAgentDir({})); } function inferAgentIdFromDir(agentDir) { const normalized = path.normalize(agentDir); if (path.basename(normalized) === "agent") { const parent = path.basename(path.dirname(normalized)); if (parent) return parent; } return `custom-${createHash("sha256").update(normalized).digest("hex").slice(0, 12)}`; } function resolveAuthProfileDatabaseOptions(agentDir) { const dir = resolveAgentDir(agentDir); return { agentId: resolveRegisteredAgentIdForDir(dir) ?? inferAgentIdFromDir(dir), path: path.join(dir, "openclaw-agent.sqlite") }; } /** Resolves the SQLite database path that stores auth profiles for an agent dir. */ function resolveAuthProfileDatabasePath(agentDir) { return resolveAuthProfileDatabaseOptions(agentDir).path; } /** Resolves the SQLite database and sidecar paths used by auth profiles. */ function resolveAuthProfileDatabaseFilePaths(agentDir) { const databasePath = resolveAuthProfileDatabasePath(agentDir); return [ databasePath, `${databasePath}-wal`, `${databasePath}-shm` ]; } function parseJsonCell(raw) { if (!raw) return null; try { return JSON.parse(raw); } catch { return null; } } function getAuthProfileKysely(db) { return getNodeSqliteKysely(db); } function readAuthProfileJsonCellReadOnly(pathname, target) { const db = new (requireNodeSqlite()).DatabaseSync(pathname, { readOnly: true }); try { const kysely = getAuthProfileKysely(db); if (target === "store") return parseJsonCell(executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("auth_profile_store").select("store_json").where("store_key", "=", PRIMARY_ROW_KEY))?.store_json); return parseJsonCell(executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("auth_profile_state").select("state_json").where("state_key", "=", PRIMARY_ROW_KEY))?.state_json); } catch { return null; } finally { clearNodeSqliteKyselyCacheForDatabase(db); db.close(); } } /** Reads the raw persisted secrets-store payload without coercing the schema. */ function readPersistedAuthProfileStoreRaw(agentDir, database) { if (database) { const db = getAuthProfileKysely(database.db); return parseJsonCell(executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("auth_profile_store").select("store_json").where("store_key", "=", PRIMARY_ROW_KEY))?.store_json); } const databasePath = resolveAuthProfileDatabasePath(agentDir); if (!fs.existsSync(databasePath)) return null; return readAuthProfileJsonCellReadOnly(databasePath, "store"); } /** Reads the raw persisted runtime-state payload without coercing the schema. */ function readPersistedAuthProfileStateRaw(agentDir, database) { if (database) { const db = getAuthProfileKysely(database.db); return parseJsonCell(executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("auth_profile_state").select("state_json").where("state_key", "=", PRIMARY_ROW_KEY))?.state_json); } const databasePath = resolveAuthProfileDatabasePath(agentDir); if (!fs.existsSync(databasePath)) return null; return readAuthProfileJsonCellReadOnly(databasePath, "state"); } /** Writes the raw persisted secrets-store payload inside the auth database. */ function writePersistedAuthProfileStoreRaw(payload, agentDir, database) { const write = (target) => { const db = getAuthProfileKysely(target.db); executeSqliteQuerySync(target.db, db.insertInto("auth_profile_store").values({ store_key: PRIMARY_ROW_KEY, store_json: JSON.stringify(payload), updated_at: Date.now() }).onConflict((conflict) => conflict.column("store_key").doUpdateSet({ store_json: JSON.stringify(payload), updated_at: Date.now() }))); }; if (database) { write(database); return; } runOpenClawAgentWriteTransaction(write, resolveAuthProfileDatabaseOptions(agentDir)); } /** Deletes the persisted secrets-store row while leaving runtime state intact. */ function deletePersistedAuthProfileStoreRaw(agentDir, database) { const remove = (target) => { const db = getAuthProfileKysely(target.db); executeSqliteQuerySync(target.db, db.deleteFrom("auth_profile_store").where("store_key", "=", PRIMARY_ROW_KEY)); }; if (database) { remove(database); return; } runOpenClawAgentWriteTransaction(remove, resolveAuthProfileDatabaseOptions(agentDir)); } /** Writes or deletes the persisted runtime-state payload. */ function writePersistedAuthProfileStateRaw(payload, agentDir, database) { const write = (target) => { const db = getAuthProfileKysely(target.db); if (!payload) { executeSqliteQuerySync(target.db, db.deleteFrom("auth_profile_state").where("state_key", "=", PRIMARY_ROW_KEY)); return; } executeSqliteQuerySync(target.db, db.insertInto("auth_profile_state").values({ state_key: PRIMARY_ROW_KEY, state_json: JSON.stringify(payload), updated_at: Date.now() }).onConflict((conflict) => conflict.column("state_key").doUpdateSet({ state_json: JSON.stringify(payload), updated_at: Date.now() }))); }; if (database) { write(database); return; } runOpenClawAgentWriteTransaction(write, resolveAuthProfileDatabaseOptions(agentDir)); } /** Runs an auth-profile database write transaction for store/state updates. */ function runAuthProfileWriteTransaction(agentDir, operation) { return runOpenClawAgentWriteTransaction(operation, resolveAuthProfileDatabaseOptions(agentDir)); } //#endregion export { resolveAuthProfileDatabasePath as a, writePersistedAuthProfileStoreRaw as c, resolveAuthProfileDatabaseFilePaths as i, readPersistedAuthProfileStateRaw as n, runAuthProfileWriteTransaction as o, readPersistedAuthProfileStoreRaw as r, writePersistedAuthProfileStateRaw as s, deletePersistedAuthProfileStoreRaw as t };