UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

406 lines (405 loc) 16.7 kB
import { s as resolveConfigPath } from "./paths-mvMm5bYV.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { i as isPathInside } from "./path-BlG8lhgR.js"; import { c as isRecord } from "./utils-CCC-BEJH.js"; import "./types.secrets-_0JOMGE5.js"; import { n as replaceFileAtomic } from "./replace-file-BrS02dAb.js"; import "./scan-paths-Bve2UhXh.js"; import { B as applyUnsetPathsForWrite, E as validateConfigObjectWithPlugins, G as createInvalidConfigError, J as maintainConfigBackups, K as formatInvalidConfigDetails, U as resolveManagedUnsetPathsForWrite, b as restoreEnvChangesIfUnchanged, d as readConfigFileSnapshotForWrite, x as writeConfigFile, y as resolveConfigSnapshotHash } from "./io-Gi7-pyU-.js"; import { r as INCLUDE_KEY } from "./includes-Cf57krG9.js"; import { n as assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard-DMwPAosm.js"; import { d as preflightRuntimeSnapshotWrite, h as resolveConfigWriteFollowUp, i as getRuntimeConfigSnapshot, m as resolveConfigWriteAfterWrite, n as createRuntimeConfigWriteNotification, o as getRuntimeConfigSnapshotRefreshHandler, r as finalizeRuntimeSnapshotWrite, s as getRuntimeConfigSourceSnapshot, u as notifyRuntimeConfigWriteListeners } from "./runtime-snapshot-D93_HOsR.js"; import { o as withFileLock } from "./file-lock-BOaqUSu6.js"; import "./file-lock-DQM8YrNh.js"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; import { isDeepStrictEqual } from "node:util"; import { AsyncLocalStorage } from "node:async_hooks"; //#region src/config/mutate.ts const CONFIG_MUTATION_LOCK_OPTIONS = { retries: { retries: 80, factor: 1.2, minTimeout: 25, maxTimeout: 250, randomize: true }, stale: 3e4 }; const DEFAULT_CONFIG_MUTATION_RETRY_ATTEMPTS = 5; const activeConfigMutationLocks = new AsyncLocalStorage(); const configMutationQueueTails = /* @__PURE__ */ new Map(); /** Raised when a config write loses an optimistic hash race. */ var ConfigMutationConflictError = class extends Error { constructor(message, params) { super(message); this.name = "ConfigMutationConflictError"; this.currentHash = params.currentHash; } }; function assertBaseHashMatches(snapshot, expectedHash) { const currentHash = resolveConfigSnapshotHash(snapshot) ?? null; if (expectedHash !== void 0 && expectedHash !== currentHash) throw new ConfigMutationConflictError("config changed since last load", { currentHash }); return currentHash; } async function withConfigMutationLock(params, fn) { if (params.io) return await fn(); const configPath = path.resolve(params.lockPath ?? resolveConfigPath()); const activeLocks = activeConfigMutationLocks.getStore(); if (activeLocks?.has(configPath)) return await fn(); assertConfigWriteAllowedInCurrentMode({ configPath }); await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 448 }); const previousTail = configMutationQueueTails.get(configPath) ?? Promise.resolve(); let releaseQueueSlot; const currentRun = new Promise((resolve) => { releaseQueueSlot = resolve; }); const currentTail = previousTail.catch(() => void 0).then(() => currentRun); configMutationQueueTails.set(configPath, currentTail); await previousTail.catch(() => void 0); try { const nextActiveLocks = new Set(activeLocks ?? []); nextActiveLocks.add(configPath); return await activeConfigMutationLocks.run(nextActiveLocks, async () => await withFileLock(configPath, CONFIG_MUTATION_LOCK_OPTIONS, fn)); } finally { releaseQueueSlot(); if (configMutationQueueTails.get(configPath) === currentTail) configMutationQueueTails.delete(configPath); } } function markActiveConfigMutationPath(configPath) { activeConfigMutationLocks.getStore()?.add(path.resolve(configPath)); } async function readConfigSnapshotForMutation(params) { if (params.io) return await params.io.readConfigFileSnapshotForWrite(); return await readConfigFileSnapshotForWrite({ skipPluginValidation: params.writeOptions?.skipPluginValidation }); } function getChangedTopLevelKeys(base, next) { if (!isRecord(base) || !isRecord(next)) return isDeepStrictEqual(base, next) ? [] : ["<root>"]; return [...new Set([...Object.keys(base), ...Object.keys(next)])].filter((key) => !isDeepStrictEqual(base[key], next[key])); } function getSingleTopLevelIncludeTarget(params) { if (!isRecord(params.snapshot.parsed)) return null; const authoredSection = params.snapshot.parsed[params.key]; if (!isRecord(authoredSection)) return null; const keys = Object.keys(authoredSection); const includeValue = authoredSection[INCLUDE_KEY]; if (keys.length !== 1 || typeof includeValue !== "string") return null; const rootDir = path.dirname(params.snapshot.path); const resolved = path.normalize(path.isAbsolute(includeValue) ? includeValue : path.resolve(rootDir, includeValue)); if (!isPathInside(rootDir, resolved)) return null; return resolved; } function formatJsonFileValue(value) { return `${JSON.stringify(value, null, 2)}\n`; } function hashFileRaw(raw) { const hash = crypto.createHash("sha256"); if (raw === null) hash.update("missing"); else { hash.update("present\0"); hash.update(raw, "utf-8"); } return hash.digest("hex"); } async function readFileRawIfExists(filePath) { try { return await fs.readFile(filePath, "utf-8"); } catch (error) { if (error?.code === "ENOENT") return null; throw error; } } async function rollbackJsonFileWriteIfUnchanged(params) { if (hashFileRaw(await readFileRawIfExists(params.filePath)) !== params.committedHash) return false; if (params.previousRaw !== null) { await replaceFileAtomic({ filePath: params.filePath, content: params.previousRaw, dirMode: 448, mode: 384, tempPrefix: path.basename(params.filePath) }); return true; } try { await fs.unlink(params.filePath); } catch (error) { if (error?.code !== "ENOENT") throw error; } return true; } async function writeJsonFileAtomic(filePath, value) { await replaceFileAtomic({ filePath, content: formatJsonFileValue(value), dirMode: 448, mode: 384, tempPrefix: path.basename(filePath), beforeRename: async () => { await fs.access(filePath).then(async () => await maintainConfigBackups(filePath, fs), () => void 0); } }); } async function tryWriteSingleTopLevelIncludeMutation(params) { const nextConfig = applyUnsetPathsForWrite(params.nextConfig, resolveManagedUnsetPathsForWrite(params.writeOptions?.unsetPaths)); const changedKeys = getChangedTopLevelKeys(params.snapshot.sourceConfig, nextConfig); if (changedKeys.length !== 1 || changedKeys[0] === "<root>") return null; const key = changedKeys[0]; const includePath = getSingleTopLevelIncludeTarget({ snapshot: params.snapshot, key }); if (!includePath || !isRecord(nextConfig) || !(key in nextConfig)) return null; const nextConfigRecord = nextConfig; const validated = validateConfigObjectWithPlugins(nextConfig, params.writeOptions?.skipPluginValidation ? { pluginValidation: "skip" } : void 0); if (!validated.ok) throw createInvalidConfigError(params.snapshot.path, formatInvalidConfigDetails(validated.issues)); const runtimeConfigSnapshot = getRuntimeConfigSnapshot(); const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshot(); const hadRuntimeSnapshot = Boolean(runtimeConfigSnapshot); const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot); const runtimePreflightResult = await preflightRuntimeSnapshotWrite({ nextSourceConfig: nextConfig, refreshOptions: params.writeOptions?.runtimeRefresh, formatRefreshError: (error) => formatErrorMessage(error), createRefreshError: (detail, cause) => new Error(`Config write blocked before committing ${includePath}: active SecretRef resolution failed: ${detail}`, { cause }) }); const previousIncludeRaw = await readFileRawIfExists(includePath); const committedIncludeHash = hashFileRaw(formatJsonFileValue(nextConfigRecord[key])); await writeJsonFileAtomic(includePath, nextConfigRecord[key]); const writeEnv = params.io?.env ?? process.env; const envBeforePostWriteRead = { ...writeEnv }; let envAfterPostWriteRead = envBeforePostWriteRead; try { if (params.writeOptions?.skipRuntimeSnapshotRefresh && !hadRuntimeSnapshot && !getRuntimeConfigSnapshotRefreshHandler()) return { persistedHash: null, persistedConfig: nextConfig }; let refreshed; try { refreshed = await (params.io?.readConfigFileSnapshotForWrite ?? readConfigFileSnapshotForWrite)(params.writeOptions?.skipPluginValidation ? { skipPluginValidation: true } : void 0); } finally { envAfterPostWriteRead = { ...writeEnv }; } const refreshedSnapshot = refreshed.snapshot; const persistedHash = resolveConfigSnapshotHash(refreshedSnapshot); if (!refreshedSnapshot.valid) throw createInvalidConfigError(params.snapshot.path, formatInvalidConfigDetails(refreshedSnapshot.issues)); if (!persistedHash) throw new Error(`Config was written to ${params.snapshot.path}, but no persisted hash was available.`); const notifyCommittedWrite = () => { const currentRuntimeConfig = getRuntimeConfigSnapshot(); if (!currentRuntimeConfig) return; notifyRuntimeConfigWriteListeners(createRuntimeConfigWriteNotification({ configPath: params.snapshot.path, sourceConfig: refreshedSnapshot.sourceConfig, runtimeConfig: currentRuntimeConfig, persistedHash, afterWrite: params.afterWrite ?? params.writeOptions?.afterWrite })); }; await finalizeRuntimeSnapshotWrite({ nextSourceConfig: refreshedSnapshot.sourceConfig, refreshOptions: params.writeOptions?.runtimeRefresh, hadRuntimeSnapshot, hadBothSnapshots, loadFreshConfig: () => refreshedSnapshot.runtimeConfig, notifyCommittedWrite, preflightResult: runtimePreflightResult, formatRefreshError: (error) => formatErrorMessage(error), createRefreshError: (detail, cause) => new Error(`Config was written to ${params.snapshot.path}, but runtime snapshot refresh failed: ${detail}`, { cause }) }); return { persistedHash, persistedConfig: refreshedSnapshot.sourceConfig }; } catch (error) { try { if (await rollbackJsonFileWriteIfUnchanged({ filePath: includePath, previousRaw: previousIncludeRaw, committedHash: committedIncludeHash })) restoreEnvChangesIfUnchanged({ env: writeEnv, before: envBeforePostWriteRead, after: envAfterPostWriteRead }); } catch (rollbackError) { throw new Error(`${formatErrorMessage(error)} Rollback failed: ${formatErrorMessage(rollbackError)}`, { cause: rollbackError }); } throw error; } } function resolveConfigWriteResult(result, fallbackConfig) { if (result) return { persistedHash: result.persistedHash, persistedConfig: result.persistedConfig }; return { persistedHash: null, persistedConfig: fallbackConfig }; } async function replaceConfigFile(params) { return await withConfigMutationLock({ io: params.io, lockPath: params.snapshot?.path }, async () => await replaceConfigFileUnlocked(params)); } async function replaceConfigFileUnlocked(params) { const { snapshot, writeOptions } = params.snapshot && params.writeOptions ? { snapshot: params.snapshot, writeOptions: params.writeOptions } : await readConfigSnapshotForMutation({ io: params.io, writeOptions: params.writeOptions }); assertConfigWriteAllowedInCurrentMode({ configPath: snapshot.path }); markActiveConfigMutationPath(snapshot.path); const previousHash = assertBaseHashMatches(snapshot, params.baseHash); const afterWrite = resolveConfigWriteAfterWrite(params.afterWrite ?? params.writeOptions?.afterWrite); let writeResult = await tryWriteSingleTopLevelIncludeMutation({ snapshot, nextConfig: params.nextConfig, afterWrite, writeOptions: params.writeOptions ?? writeOptions, io: params.io }); if (!writeResult) { const fallbackWriteOptions = { baseSnapshot: snapshot, ...writeOptions, ...params.writeOptions, afterWrite }; const ioPreCommitRuntimePreflight = params.io ? fallbackWriteOptions.preCommitRuntimePreflight : void 0; if (params.io) fallbackWriteOptions.preCommitRuntimePreflight = async (sourceConfig) => { await ioPreCommitRuntimePreflight?.(sourceConfig); await preflightRuntimeSnapshotWrite({ nextSourceConfig: sourceConfig, refreshOptions: fallbackWriteOptions.runtimeRefresh, formatRefreshError: (error) => formatErrorMessage(error), createRefreshError: (detail, cause) => new Error(`Config write blocked before committing ${snapshot.path}: active SecretRef resolution failed: ${detail}`, { cause }) }); }; writeResult = resolveConfigWriteResult(await (params.io?.writeConfigFile ?? writeConfigFile)(params.nextConfig, fallbackWriteOptions), params.nextConfig); } return { path: snapshot.path, previousHash, snapshot, nextConfig: writeResult.persistedConfig, persistedHash: writeResult.persistedHash, afterWrite, followUp: resolveConfigWriteFollowUp(afterWrite) }; } async function commitPreparedConfigMutation(params) { const result = await replaceConfigFileUnlocked({ nextConfig: params.nextConfig, snapshot: params.snapshot, baseHash: params.baseHash, writeOptions: { ...params.writeOptions, afterWrite: params.afterWrite }, io: params.io }); return { config: result.nextConfig, persistedHash: result.persistedHash, afterWrite: result.afterWrite }; } async function transformConfigFileAttempt(params, attempt) { const { snapshot, writeOptions } = await readConfigSnapshotForMutation({ io: params.io, writeOptions: params.writeOptions }); assertConfigWriteAllowedInCurrentMode({ configPath: snapshot.path }); markActiveConfigMutationPath(snapshot.path); const previousHash = assertBaseHashMatches(snapshot, params.baseHash); const baseConfig = params.base === "runtime" ? snapshot.runtimeConfig : snapshot.sourceConfig; const afterWrite = resolveConfigWriteAfterWrite(params.afterWrite ?? params.writeOptions?.afterWrite); const mergedWriteOptions = { ...writeOptions, ...params.writeOptions }; const transformed = await params.transform(baseConfig, { snapshot, previousHash, attempt }); const committed = await (params.commit ?? commitPreparedConfigMutation)({ nextConfig: transformed.nextConfig, snapshot, ...previousHash !== null ? { baseHash: previousHash } : {}, writeOptions: mergedWriteOptions, afterWrite, io: params.io }); const committedAfterWrite = committed.afterWrite ?? afterWrite; return { path: snapshot.path, previousHash, snapshot, nextConfig: committed.config, persistedHash: committed.persistedHash, result: transformed.result, attempts: attempt + 1, afterWrite: committedAfterWrite, followUp: resolveConfigWriteFollowUp(committedAfterWrite) }; } async function transformConfigFile(params) { return await withConfigMutationLock({ io: params.io }, async () => await transformConfigFileAttempt(params, 0)); } async function transformConfigFileWithRetry(params) { const maxAttempts = params.maxAttempts ?? DEFAULT_CONFIG_MUTATION_RETRY_ATTEMPTS; if (!Number.isInteger(maxAttempts) || maxAttempts < 1) throw new Error("Config mutation maxAttempts must be a positive integer."); return await withConfigMutationLock({ io: params.io }, async () => { for (let attempt = 0; attempt < maxAttempts; attempt += 1) try { return await transformConfigFileAttempt(params, attempt); } catch (err) { if (err instanceof ConfigMutationConflictError && attempt < maxAttempts - 1) continue; throw err; } throw new Error("Config mutation retry loop exhausted unexpectedly."); }); } async function mutateConfigFile(params) { return await transformConfigFile({ base: params.base, baseHash: params.baseHash, afterWrite: params.afterWrite, writeOptions: params.writeOptions, io: params.io, transform: async (currentConfig, context) => { const draft = structuredClone(currentConfig); return { nextConfig: draft, result: await params.mutate(draft, context) }; } }); } async function mutateConfigFileWithRetry(params) { return await transformConfigFileWithRetry({ base: params.base, baseHash: params.baseHash, maxAttempts: params.maxAttempts, afterWrite: params.afterWrite, writeOptions: params.writeOptions, io: params.io, transform: async (currentConfig, context) => { const draft = structuredClone(currentConfig); return { nextConfig: draft, result: await params.mutate(draft, context) }; } }); } //#endregion export { transformConfigFile as a, replaceConfigFile as i, mutateConfigFile as n, transformConfigFileWithRetry as o, mutateConfigFileWithRetry as r, ConfigMutationConflictError as t };