UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

711 lines (710 loc) 35.1 kB
import "./src-vebZIeLe.js"; import { t as expectDefined } from "./expect-CyE8FADM.js"; import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { S as createConfigIO, _ as writeConfigFile, l as readConfigFileSnapshotForWrite, v as attachRuntimeConfigWriteApplication, x as getRuntimeConfigWriteApplication, y as copyRuntimeConfigWriteApplication } from "./io.runtime-B9iJRs3w.js"; import { n as isErrno, r as isMissingPathError } from "./errno-CkbDOfLk.js"; import { n as isPathInside } from "./path-safety-Bi0ppMWC.js"; import { A as resolveConfigIncludeWritePath, E as INCLUDE_KEY, O as hashConfigIncludeRaw, S as resolveConfigEnvVars, T as ConfigIncludeError } from "./redact-BtvPPfTi.js"; import { w as root } from "./fs-safe-B6pvPGnf.js"; import "./utils-P__uGsPB.js"; import { t as parseJsonWithJson5Fallback } from "./parse-json-compat-Cc8PAOTi.js"; import { f as resolveConfigPath } from "./paths-D2sRr1a_.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { c as getPublishedConfigRuntimeEnvState, n as cloneEnvWithPlatformSemantics, s as createConfigRuntimeEnvBase, t as applyConfigEnvVars } from "./config-env-vars-DUfQlcAk.js"; import { b as resolveConfigWriteAfterWrite, c as getRuntimeConfigSnapshotRefreshHandler, h as preflightRuntimeSnapshotWrite, i as finalizeRuntimeSnapshotWrite, l as getRuntimeConfigSourceSnapshot, m as preflightManagedRuntimeConfigWrite, o as getRuntimeConfigSnapshot, p as notifyRuntimeConfigWriteListeners, r as createRuntimeConfigWriteNotification, u as hasManagedRuntimeConfigWriteOwner, x as resolveConfigWriteFollowUp } from "./runtime-snapshot-BaQikjTR.js"; import { c as resolveManagedUnsetPathsForWrite, f as warnIfJSON5CommentsWillBeStripped, h as validateConfigObjectWithPlugins, i as resolveWriteEnvSnapshotForPath, o as restoreEnvVarRefs, s as applyUnsetPathsForWrite } from "./io.types-BUCjdS5v.js"; import { S as GATEWAY_CONFIG_SELECTION_ENV_KEYS, h as resolveConfigSnapshotHash, u as rejectConfigNonFiniteNumbers, y as restoreEnvChangesIfUnchanged } from "./io.read-helpers-ZKp-UiGx.js"; import { n as GUARDED_CONFIG_INCLUDE_WRITE_ERROR, t as ConfigMutationConflictError } from "./mutation-conflict-Be0wSyDG.js"; import { n as formatInvalidConfigDetails, t as createInvalidConfigError } from "./io.invalid-config-xEPg4zuq.js"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js"; import { n as assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard-uwcnAyQN.js"; import "./io-bdCzpGWJ.js"; import { s as withFileLock } from "./file-lock-B0wiaenm.js"; import "./file-lock-DPooFrLa.js"; import { n as maintainConfigBackups } from "./backup-rotation-CYn_TZGJ.js"; import { isDeepStrictEqual } from "node:util"; import path from "node:path"; import { AsyncLocalStorage } from "node:async_hooks"; import fs from "node:fs/promises"; //#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 configMutationQueue = new KeyedAsyncQueue(); function resolveManagedRuntimeEnvBaseline() { const published = getPublishedConfigRuntimeEnvState(); return { generation: published.generation, sourceConfig: published.sourceConfig ?? getRuntimeConfigSourceSnapshot() ?? {} }; } function assertManagedRuntimeEnvGeneration(generation) { if (getPublishedConfigRuntimeEnvState().generation !== generation) throw new ConfigMutationConflictError("active config environment changed while preparing write"); } function assertBaseHashMatches(snapshot, expectedHash) { const currentHash = resolveConfigSnapshotHash(snapshot) ?? null; if (expectedHash !== void 0 && expectedHash !== currentHash) throw new ConfigMutationConflictError("config changed since last load"); return currentHash; } function assertExpectedConfigPathMatches(snapshot, expectedConfigPath) { if (expectedConfigPath !== void 0 && expectedConfigPath !== snapshot.path) throw new ConfigMutationConflictError("config path changed since last load", { retryable: false }); } 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 }); const configDir = path.dirname(configPath); await fs.mkdir(configDir, { recursive: true, mode: 448 }); const nextActiveLocks = new Set(activeLocks ?? []); nextActiveLocks.add(configPath); return await configMutationQueue.enqueue(configPath, () => activeConfigMutationLocks.run(nextActiveLocks, async () => await withFileLock(configPath, CONFIG_MUTATION_LOCK_OPTIONS, fn))).catch(async (error) => { if (!await isPermissionErrorInDirectory(error, configDir)) throw error; throw new Error(`OpenClaw cannot write to the config directory ${configDir}. Fix its ownership or permissions, then try again. Underlying error: ${formatErrorMessage(error)}`, { cause: error }); }); } async function isPermissionErrorInDirectory(error, directory) { if (!isErrno(error) || error.code !== "EACCES" && error.code !== "EPERM" && error.code !== "EROFS") return false; const failedPath = error.path; if (typeof failedPath !== "string") return false; const failedDir = path.dirname(path.resolve(failedPath)); if (failedDir === directory) return true; const canonicalDirectory = await fs.realpath(directory).catch(() => void 0); return canonicalDirectory !== void 0 && failedDir === canonicalDirectory; } function markActiveConfigMutationPath(configPath) { activeConfigMutationLocks.getStore()?.add(path.resolve(configPath)); } async function readConfigSnapshotForMutation(params) { const options = params.writeOptions?.skipPluginValidation ? { skipPluginValidation: true } : {}; if (params.io) return await params.io.readConfigFileSnapshotForWrite(options); if (params.ownedConfigPathForWrite) { const ioOptions = { configPath: params.ownedConfigPathForWrite, ...params.writeOptions?.skipPluginValidation ? { pluginValidation: "skip" } : {} }; return await (hasManagedRuntimeConfigWriteOwner(params.ownedConfigPathForWrite) ? createConfigIO({ ...ioOptions, env: createConfigRuntimeEnvBase(resolveManagedRuntimeEnvBaseline().sourceConfig, process.env, { preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS }) }) : createConfigIO(ioOptions)).readConfigFileSnapshotForWrite(); } return await readConfigFileSnapshotForWrite(options); } function mergeConfigMutationWriteOptions(prepared, caller) { const merged = copyRuntimeConfigWriteApplication(caller, { ...prepared, ...caller }); const capturedGuard = prepared.assertConfigPathForWrite; const callerGuard = caller?.assertConfigPathForWrite; if (capturedGuard && callerGuard && capturedGuard !== callerGuard) merged.assertConfigPathForWrite = () => { capturedGuard(); callerGuard(); }; else if (capturedGuard) merged.assertConfigPathForWrite = capturedGuard; return merged; } function createConfigMutationOwnership(prepared, writeOptions) { const mergedWriteOptions = mergeConfigMutationWriteOptions(prepared.writeOptions, writeOptions); return { initialized: true, expectedConfigPath: mergedWriteOptions.expectedConfigPath ?? prepared.snapshot.path, ownedConfigPathForWrite: mergedWriteOptions.ownedConfigPathForWrite, assertConfigPathForWrite: mergedWriteOptions.assertConfigPathForWrite }; } async function withConfigMutationSnapshotLock(params, fn) { let lockPath = path.resolve(params.writeOptions?.ownedConfigPathForWrite ?? resolveConfigPath()); for (let attempt = 0; attempt < 3; attempt += 1) { const outcome = await withConfigMutationLock({ lockPath }, async () => { const prepared = await readConfigSnapshotForMutation({ ...params.writeOptions?.ownedConfigPathForWrite ? { ownedConfigPathForWrite: params.writeOptions.ownedConfigPathForWrite } : {}, writeOptions: params.writeOptions }); const preparedPath = path.resolve(prepared.snapshot.path); if (preparedPath !== lockPath) return { done: false, lockPath: preparedPath }; return { done: true, value: await fn(prepared) }; }); if (outcome.done) return outcome.value; lockPath = outcome.lockPath; } throw new ConfigMutationConflictError("config path changed repeatedly while acquiring lock", { retryable: false }); } /** * Run a multi-phase operation under the canonical cross-process write lock. * Nested mutation helpers are reentrant through activeConfigMutationLocks. */ async function withConfigMutationExclusive(fn) { return await withConfigMutationSnapshotLock({}, async (prepared) => await fn(prepared.snapshot.sourceConfig)); } function getChangedTopLevelKeys(base, next) { if (!isRecord(base) || !isRecord(next)) return isDeepStrictEqual(base, next) ? [] : ["<root>"]; return [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(next)])].filter((key) => !isDeepStrictEqual(base[key], next[key])); } function getSingleTopLevelIncludeTarget(params) { const targetPath = [params.key]; const ownership = params.snapshot.includeProvenance?.findLast((entry) => entry.path.length <= targetPath.length && entry.path.every((segment, index) => segment === targetPath[index])); if (ownership?.path.length === targetPath.length && ownership.kind === "single" && !ownership.hasSiblingOverrides && ownership.targetPath) return path.normalize(ownership.targetPath); if (params.snapshot.includeProvenance !== void 0) return null; 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); return path.normalize(path.isAbsolute(includeValue) ? includeValue : path.resolve(rootDir, includeValue)); } function containsConfigIncludeDirective(value) { if (Array.isArray(value)) return value.some((item) => containsConfigIncludeDirective(item)); if (!isRecord(value)) return false; return Object.hasOwn(value, "$include") || Object.values(value).some((item) => containsConfigIncludeDirective(item)); } function snapshotProvesBrokenInclude(snapshot, includePath) { return !snapshot.valid && snapshot.issues.some((issue) => /Failed to (?:read|parse) include file:/.test(issue.message) && issue.message.includes(includePath)); } function formatJsonFileValue(value) { rejectConfigNonFiniteNumbers(value); return `${JSON.stringify(value, null, 2)}\n`; } function resolveRootBoundRelativePath(target, absolutePath) { const relativePath = path.relative(target.root.rootReal, path.resolve(absolutePath)); const firstSegment = relativePath.split(path.sep)[0]; if (path.isAbsolute(relativePath) || firstSegment === "..") throw new Error(`Config include backup path escaped its approved root: ${absolutePath}`); return relativePath; } async function resolveRootBoundIncludeFile(params) { const absolutePath = resolveConfigIncludeWritePath(params); const candidateRoots = [path.dirname(params.configPath), ...params.allowedRoots]; for (const candidateRoot of candidateRoots) { const rootReal = await fs.realpath(candidateRoot).catch(() => null); if (!rootReal || !isPathInside(rootReal, absolutePath)) continue; const relativePath = path.relative(rootReal, absolutePath); if (!relativePath || path.isAbsolute(relativePath) || relativePath.split(path.sep)[0] === "..") continue; return { absolutePath, relativePath, root: await root(rootReal, { hardlinks: "reject", mkdir: true, mode: 384, symlinks: "reject" }) }; } throw new Error(`Config include write path has no approved existing root: ${absolutePath}`); } async function resolveExpectedRootBoundIncludeFile(params) { let target; try { target = await resolveRootBoundIncludeFile(params); } catch (error) { if (error instanceof ConfigIncludeError || error instanceof Error && error.message.startsWith("Config include write path has no approved existing root:")) throw new ConfigMutationConflictError("included config target changed since last load"); throw error; } if (path.normalize(target.absolutePath) !== path.normalize(params.expectedAbsolutePath)) throw new ConfigMutationConflictError("included config target changed since last load"); return target; } async function readRootBoundFileRawIfExists(target) { try { return await target.root.readText(target.relativePath); } catch (error) { if (isMissingPathError(error)) return null; throw error; } } async function assertRootConfigStillMatchesSnapshot(snapshot) { let currentRaw = null; try { currentRaw = await fs.readFile(snapshot.path, "utf-8"); } catch (error) { if (!isMissingPathError(error)) throw error; } if (hashConfigIncludeRaw(currentRaw) !== hashConfigIncludeRaw(snapshot.exists ? snapshot.raw ?? null : null)) throw new ConfigMutationConflictError("config changed while preparing include write"); } async function rollbackJsonFileWriteIfUnchanged(params) { const currentRaw = await readRootBoundFileRawIfExists(params.target); if (hashConfigIncludeRaw(currentRaw) !== params.committedHash) return false; if (params.previousRaw !== null) { await params.target.root.write(params.target.relativePath, params.previousRaw, { mkdir: true, mode: 384, overwrite: true }); return true; } try { await params.target.root.remove(params.target.relativePath); } catch (error) { if (!isMissingPathError(error)) throw error; } return true; } function createRootBoundBackupFs(target) { return { chmod: async (filePath, mode) => { const opened = await target.root.open(resolveRootBoundRelativePath(target, filePath)); try { await opened.handle.chmod(mode); } finally { await opened[Symbol.asyncDispose](); } }, copyFile: async (from, to) => { const content = await target.root.readBytes(resolveRootBoundRelativePath(target, from)); await target.root.write(resolveRootBoundRelativePath(target, to), content, { mkdir: true, mode: 384, overwrite: true }); }, rename: async (from, to) => { await target.root.move(resolveRootBoundRelativePath(target, from), resolveRootBoundRelativePath(target, to), { overwrite: true }); }, unlink: async (filePath) => { await target.root.remove(resolveRootBoundRelativePath(target, filePath)); } }; } async function writeRootBoundJsonFile(params) { params.assertConfigPathForWrite(); const targetBeforeBackup = await resolveExpectedRootBoundIncludeFile({ configPath: params.configPath, includePath: params.includePath, allowedRoots: params.allowedRoots, expectedAbsolutePath: params.expectedTargetPath }); if (await targetBeforeBackup.root.exists(targetBeforeBackup.relativePath)) await maintainConfigBackups(targetBeforeBackup.absolutePath, createRootBoundBackupFs(targetBeforeBackup)); const targetAtCommit = await resolveExpectedRootBoundIncludeFile({ configPath: params.configPath, includePath: params.includePath, allowedRoots: params.allowedRoots, expectedAbsolutePath: params.expectedTargetPath }); params.assertConfigPathForWrite(); await assertRootConfigStillMatchesSnapshot(params.rootSnapshot); const currentRaw = await readRootBoundFileRawIfExists(targetAtCommit); if (hashConfigIncludeRaw(currentRaw) !== hashConfigIncludeRaw(params.expectedRaw)) throw new ConfigMutationConflictError("included config changed while preparing write"); const content = formatJsonFileValue(params.value); await params.preCommitRuntimePreflight?.(); params.assertConfigPathForWrite(); warnIfJSON5CommentsWillBeStripped({ raw: currentRaw, filePath: targetAtCommit.absolutePath, skipOutputLogs: params.skipOutputLogs }); await targetAtCommit.root.write(targetAtCommit.relativePath, content, { mkdir: true, mode: 384, overwrite: true }); try { params.assertConfigPathForWrite(); } catch (error) { await rollbackJsonFileWriteIfUnchanged({ target: targetAtCommit, previousRaw: currentRaw, committedHash: hashConfigIncludeRaw(content) }); throw error; } } async function tryWriteSingleTopLevelIncludeMutation(params) { const nextConfig = applyUnsetPathsForWrite(params.nextConfig, resolveManagedUnsetPathsForWrite(params.writeOptions?.unsetPaths)); const changedKeys = getChangedTopLevelKeys(params.snapshot.sourceConfig, nextConfig); if (params.writeOptions?.persistCanonicalAgentRoster === true || changedKeys.length !== 1 || changedKeys[0] === "<root>") return null; const key = expectDefined(changedKeys[0], "changed keys entry at 0"); const includePath = getSingleTopLevelIncludeTarget({ snapshot: params.snapshot, key }); if (!includePath || !isRecord(nextConfig) || !(key in nextConfig)) return null; if (params.writeOptions?.beforeCommit) throw new Error(GUARDED_CONFIG_INCLUDE_WRITE_ERROR); const nextConfigRecord = nextConfig; const writeEnv = params.io?.env ?? process.env; const allowedRoots = []; const expectedIncludeTarget = params.writeOptions?.includeFileTargetsForWrite?.[includePath]; if (!expectedIncludeTarget) throw new ConfigMutationConflictError("included config target changed since last load"); const assertConfigPathForWrite = params.writeOptions?.assertConfigPathForWrite; if (!assertConfigPathForWrite) return null; assertConfigPathForWrite(); const configRoot = await fs.realpath(path.dirname(params.snapshot.path)); if (!isPathInside(configRoot, expectedIncludeTarget)) throw new Error(`Config mutation cannot update external $include target ${includePath}; edit the included file directly or move it under the config directory.`); const includeTarget = await resolveExpectedRootBoundIncludeFile({ configPath: params.snapshot.path, includePath, allowedRoots, expectedAbsolutePath: expectedIncludeTarget }); const previousIncludeRaw = await readRootBoundFileRawIfExists(includeTarget); const previousIncludeHash = hashConfigIncludeRaw(previousIncludeRaw); const expectedIncludeHash = params.writeOptions?.includeFileHashesForWrite?.[includePath]; if (expectedIncludeHash !== void 0 && expectedIncludeHash !== previousIncludeHash) throw new ConfigMutationConflictError("included config changed since last load"); const envForRestore = resolveWriteEnvSnapshotForPath({ actualConfigPath: params.snapshot.path, expectedConfigPath: params.writeOptions?.expectedConfigPath, envSnapshotForRestore: params.writeOptions?.envSnapshotForRestore }) ?? params.io?.env ?? process.env; const snapshotHasBrokenInclude = snapshotProvesBrokenInclude(params.snapshot, includePath); if (previousIncludeRaw === null && (!snapshotHasBrokenInclude || expectedIncludeHash === void 0)) throw new ConfigMutationConflictError("included config changed since last load"); let includedValueToWrite = nextConfigRecord[key]; if (previousIncludeRaw !== null) { let authoredIncludeValue; let parsedInclude = false; try { authoredIncludeValue = parseJsonWithJson5Fallback(previousIncludeRaw); parsedInclude = true; } catch { if (!snapshotHasBrokenInclude || expectedIncludeHash === void 0) throw new ConfigMutationConflictError("included config changed since last load"); } if (parsedInclude) { if (containsConfigIncludeDirective(authoredIncludeValue)) return null; const currentIncludedValue = resolveConfigEnvVars(authoredIncludeValue, envForRestore, { onMissing: () => {} }); const snapshotIncludedValue = (params.snapshot.sourceConfigBeforeMigrations ?? params.snapshot.sourceConfig)[key]; if (!isDeepStrictEqual(currentIncludedValue, snapshotIncludedValue)) throw new ConfigMutationConflictError("included config changed since last load"); includedValueToWrite = restoreEnvVarRefs(includedValueToWrite, authoredIncludeValue, envForRestore); } } const deferRuntimeActivation = hasManagedRuntimeConfigWriteOwner(params.snapshot.path); const runtimeEnvBaseline = deferRuntimeActivation ? resolveManagedRuntimeEnvBaseline() : void 0; const runtimeCandidateEnv = runtimeEnvBaseline ? createConfigRuntimeEnvBase(runtimeEnvBaseline.sourceConfig, process.env, { preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS }) : cloneEnvWithPlatformSemantics(writeEnv); const authoredRuntimeCandidate = restoreEnvVarRefs(nextConfig, params.snapshot.parsed, envForRestore); applyConfigEnvVars(authoredRuntimeCandidate, runtimeCandidateEnv); const runtimeConfigToWrite = resolveConfigEnvVars({ ...authoredRuntimeCandidate, [key]: includedValueToWrite }, runtimeCandidateEnv, { onMissing: () => {} }); const validated = validateConfigObjectWithPlugins(runtimeConfigToWrite, 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); let managedPreparedCandidates = /* @__PURE__ */ new Map(); let runtimePreflightResult; if (runtimeEnvBaseline) { managedPreparedCandidates = await preflightManagedRuntimeConfigWrite(params.snapshot.path, runtimeConfigToWrite, params.writeOptions?.runtimeRefresh); assertManagedRuntimeEnvGeneration(runtimeEnvBaseline.generation); } else runtimePreflightResult = await preflightRuntimeSnapshotWrite({ nextSourceConfig: runtimeConfigToWrite, 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 committedIncludeRaw = formatJsonFileValue(includedValueToWrite); const committedIncludeHash = hashConfigIncludeRaw(committedIncludeRaw); const callerPreCommit = params.writeOptions?.preCommitRuntimePreflight; assertConfigPathForWrite(); await assertRootConfigStillMatchesSnapshot(params.snapshot); const includeRawAtCommit = await readRootBoundFileRawIfExists(includeTarget); if (hashConfigIncludeRaw(includeRawAtCommit) !== hashConfigIncludeRaw(previousIncludeRaw)) throw new ConfigMutationConflictError("included config changed while preparing write"); await writeRootBoundJsonFile({ configPath: params.snapshot.path, includePath, allowedRoots, expectedTargetPath: expectedIncludeTarget, value: includedValueToWrite, expectedRaw: includeRawAtCommit, rootSnapshot: params.snapshot, assertConfigPathForWrite, skipOutputLogs: params.writeOptions?.skipOutputLogs, preCommitRuntimePreflight: runtimeEnvBaseline || callerPreCommit ? async () => { if (runtimeEnvBaseline) assertManagedRuntimeEnvGeneration(runtimeEnvBaseline.generation); await callerPreCommit?.(runtimeConfigToWrite); } : void 0 }); const envBeforePostWriteRead = { ...writeEnv }; let envAfterPostWriteRead = envBeforePostWriteRead; try { if (params.writeOptions?.skipRuntimeSnapshotRefresh && !hadRuntimeSnapshot && !getRuntimeConfigSnapshotRefreshHandler()) return { persistedHash: null, persistedConfig: runtimeConfigToWrite }; let refreshed; try { refreshed = await readConfigSnapshotForMutation({ ownedConfigPathForWrite: params.snapshot.path, io: params.io, writeOptions: params.writeOptions }); } finally { envAfterPostWriteRead = { ...writeEnv }; } const refreshedSnapshot = refreshed.snapshot; assertConfigPathForWrite(); assertExpectedConfigPathMatches(refreshedSnapshot, params.snapshot.path); 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(); const notificationRuntimeConfig = deferRuntimeActivation ? refreshedSnapshot.runtimeConfig : currentRuntimeConfig; if (!notificationRuntimeConfig) return; const notificationPreparedCandidates = new Map([...managedPreparedCandidates].map(([ownerId, candidate]) => [ownerId, { ...candidate, runtimeConfig: candidate.reapplyRuntimeOverlays?.(refreshedSnapshot.runtimeConfig) ?? candidate.runtimeConfig, compareConfig: candidate.reapplyCompareOverlays?.(refreshedSnapshot.sourceConfig) ?? candidate.compareConfig }])); notifyRuntimeConfigWriteListeners(attachRuntimeConfigWriteApplication(createRuntimeConfigWriteNotification({ configPath: params.snapshot.path, sourceConfig: refreshedSnapshot.sourceConfig, runtimeConfig: notificationRuntimeConfig, persistedHash, afterWrite: params.afterWrite ?? params.writeOptions?.afterWrite, runtimeRefresh: params.writeOptions?.runtimeRefresh, ...notificationPreparedCandidates.size > 0 ? { preparedCandidatesByOwner: notificationPreparedCandidates } : {} }), getRuntimeConfigWriteApplication(params.writeOptions ?? {}))); }; await finalizeRuntimeSnapshotWrite({ nextSourceConfig: refreshedSnapshot.sourceConfig, refreshOptions: params.writeOptions?.runtimeRefresh, hadRuntimeSnapshot, hadBothSnapshots, loadFreshConfig: () => refreshedSnapshot.runtimeConfig, notifyCommittedWrite, preflightResult: runtimePreflightResult, deferRuntimeActivation, 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({ target: includeTarget, previousRaw: includeRawAtCommit, 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) { if (!params.snapshot && !params.io) return await withConfigMutationSnapshotLock({ writeOptions: params.writeOptions }, async (prepared) => await replaceConfigFileUnlocked({ ...params, snapshot: prepared.snapshot, writeOptions: mergeConfigMutationWriteOptions(prepared.writeOptions, params.writeOptions) })); return await withConfigMutationLock({ io: params.io, lockPath: params.snapshot?.path }, async () => await replaceConfigFileUnlocked(params)); } async function replaceConfigFileUnlocked(params) { const { snapshot, writeOptions } = params.snapshot ? { snapshot: params.snapshot, writeOptions: params.writeOptions ?? {} } : await readConfigSnapshotForMutation({ io: params.io, writeOptions: params.writeOptions }); const mergedWriteOptions = mergeConfigMutationWriteOptions(writeOptions, params.writeOptions); mergedWriteOptions.assertConfigPathForWrite?.(); assertExpectedConfigPathMatches(snapshot, mergedWriteOptions.expectedConfigPath); 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: mergedWriteOptions, io: params.io }); if (!writeResult) { const fallbackWriteOptions = copyRuntimeConfigWriteApplication(mergedWriteOptions, { baseSnapshot: snapshot, ...mergedWriteOptions, afterWrite }); const ioPreCommitRuntimePreflight = params.io ? fallbackWriteOptions.preCommitRuntimePreflight : void 0; if (params.io) fallbackWriteOptions.preCommitRuntimePreflight = async (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 }) }); await ioPreCommitRuntimePreflight?.(sourceConfig); }; 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: copyRuntimeConfigWriteApplication(params.writeOptions, { ...params.writeOptions, afterWrite: params.afterWrite }), io: params.io }); return { config: result.nextConfig, persistedHash: result.persistedHash, afterWrite: result.afterWrite }; } async function transformConfigFileAttempt(params, attempt, ownership, prepared) { ownership?.assertConfigPathForWrite?.(); const { snapshot, writeOptions } = prepared ?? await readConfigSnapshotForMutation({ ...ownership?.ownedConfigPathForWrite ? { ownedConfigPathForWrite: ownership.ownedConfigPathForWrite } : {}, io: params.io, writeOptions: params.writeOptions }); let mergedWriteOptions = mergeConfigMutationWriteOptions(writeOptions, params.writeOptions); if (ownership) { if (!ownership.initialized) { ownership.initialized = true; ownership.expectedConfigPath = mergedWriteOptions.expectedConfigPath ?? snapshot.path; ownership.ownedConfigPathForWrite = mergedWriteOptions.ownedConfigPathForWrite; ownership.assertConfigPathForWrite = mergedWriteOptions.assertConfigPathForWrite; } mergedWriteOptions = copyRuntimeConfigWriteApplication(mergedWriteOptions, { ...mergedWriteOptions, expectedConfigPath: ownership.expectedConfigPath, ...ownership.ownedConfigPathForWrite ? { ownedConfigPathForWrite: ownership.ownedConfigPathForWrite } : {}, ...ownership.assertConfigPathForWrite ? { assertConfigPathForWrite: ownership.assertConfigPathForWrite } : {} }); } mergedWriteOptions.assertConfigPathForWrite?.(); assertExpectedConfigPathMatches(snapshot, mergedWriteOptions.expectedConfigPath); 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 transformed = await params.transform(baseConfig, { snapshot, previousHash, attempt }, { envSnapshotForRestore: writeOptions.envSnapshotForRestore }); 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) { if (!params.io) return await withConfigMutationSnapshotLock({ writeOptions: params.writeOptions }, async (prepared) => await transformConfigFileAttempt(params, 0, createConfigMutationOwnership(prepared, params.writeOptions), prepared)); 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."); const runWithPrepared = async (prepared) => { const ownership = prepared ? createConfigMutationOwnership(prepared, params.writeOptions) : { initialized: false, expectedConfigPath: "" }; for (let attempt = 0; attempt < maxAttempts; attempt += 1) try { return await transformConfigFileAttempt(params, attempt, ownership, attempt === 0 ? prepared : void 0); } catch (err) { if (err instanceof ConfigMutationConflictError && err.retryable && attempt < maxAttempts - 1) continue; throw err; } throw new Error("Config mutation retry loop exhausted unexpectedly."); }; if (!params.io) return await withConfigMutationSnapshotLock({ writeOptions: params.writeOptions }, runWithPrepared); return await withConfigMutationLock({ io: params.io }, async () => await runWithPrepared()); } 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 { transformConfigFileWithRetry as a, transformConfigFile as i, mutateConfigFileWithRetry as n, withConfigMutationExclusive as o, replaceConfigFile as r, mutateConfigFile as t };