UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

360 lines (359 loc) 17.5 kB
import { n as isVitestRuntimeEnv } from "./test-runtime-env-DQDRzsLt.js"; import "./env-M3R40TOb.js"; import { j as resolveConfigIncludes, k as readConfigIncludeFileWithGuards } from "./redact-BtvPPfTi.js"; import { v as resolveIncludeRoots } from "./paths-D2sRr1a_.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { t as isVerbose } from "./global-state-BAD7XgmL.js"; import { n as replaceFileAtomic } from "./replace-file-BAJ-TWzD.js"; import { n as createMergePatch, t as applyMergePatch } from "./merge-patch-DCMj2JVh.js"; import { h as preflightRuntimeSnapshotWrite } from "./runtime-snapshot-BaQikjTR.js"; import { B as resolveConfigWriteSuspiciousReasons, F as assertBaseSnapshotStillCurrent, G as recordConfigWriteMetadata, H as stampConfigVersion, I as formatConfigArtifactTimestamp, J as loggedConfigWarningFingerprints, L as resolveConfigSizeBaselineBytes, R as resolveConfigStatMetadata, U as tightenStateDirPermissionsIfNeeded, V as rollbackConfigFileWriteIfUnchanged, X as setBoundedConfigIoWarningEntry, a as restoreEnvRefsFromMap, c as resolveManagedUnsetPathsForWrite, f as warnIfJSON5CommentsWillBeStripped, l as logConfigWarningsOnce, m as validateConfigObjectRawWithPlugins, n as configWritePostCommitRollback, o as restoreEnvVarRefs, r as EnvRefArrayMutationError, s as applyUnsetPathsForWrite, t as ConfigRuntimeRefreshError, z as resolveConfigWriteBlockingReasons } from "./io.types-BUCjdS5v.js"; import { _ as fingerprintConfigSnapshotAuthoredConfig, b as restoreConfigSnapshotAuditRecord, c as createConfigWriteAuditRecordBase, g as configSnapshotAuditRecordMatchesPath, i as appendConfigAuditRecord, l as finalizeConfigWriteAuditRecord, o as capConfigAuditIssues, s as capConfigAuditPaths, u as formatConfigOverwriteLogMessage, x as upsertConfigSnapshotAuditRecord, y as readLatestConfigSnapshotAuditRecord } from "./io.audit-Dor_5i6r.js"; import { a as hasConfigMeta, g as resolveGatewayMode, h as resolveConfigSnapshotHash, l as parseConfigJson5, n as collectEnvRefPaths, o as hashConfigRaw, r as containsConfigIncludeDirective, u as rejectConfigNonFiniteNumbers, v as restoreAuthoredTildePathsForWrite } from "./io.read-helpers-ZKp-UiGx.js"; import { n as formatConfigIssueLines } from "./issue-format-CQvxWNW8.js"; import { n as assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard-uwcnAyQN.js"; import { n as maintainConfigBackups } from "./backup-rotation-CYn_TZGJ.js"; import { f as resolveCronJobsStorePathFromConfig } from "./store-K9I-tbar.js"; import { t as prepareConfigWriteTopology } from "./io.write-topology-BvvklbCP.js"; import { n as resolvePersistCandidateForWrite } from "./io.write-prepare-ijfpLGq7.js"; import { t as collectChangedPaths } from "./config-change-paths-DODizdZf.js"; import { n as prepareCronOwnerWriteRefusal } from "./io.cron-owner-refusal-DXZc1KZo.js"; import { t as createConfigValidationFailedError } from "./io.write-errors-BHsvO_9-.js"; import path from "node:path"; //#region src/config/io.write.ts async function writeConfigFileFromContext(context, cfg, options, readSnapshot) { const { deps, configPath } = context; options.assertConfigPathForWrite?.(); assertConfigWriteAllowedInCurrentMode({ configPath, env: deps.env }); const unsetPaths = resolveManagedUnsetPathsForWrite(options.unsetPaths); const snapshotRead = options.baseSnapshot ? { snapshot: options.baseSnapshot, pluginMetadataSnapshot: options.basePluginMetadataSnapshot } : await readSnapshot(); const snapshot = snapshotRead.snapshot; if (options.baseSnapshot) assertBaseSnapshotStillCurrent(snapshot, configPath, deps.fs); const { nextConfig, explicitSetPaths, explicitSetValueSource, persistCanonicalAgentRoster, preserveLegacyAgentRoster, cronOwner } = prepareConfigWriteTopology({ ...snapshotRead, nextConfig: cfg, options, unsetPaths, env: deps.env, homedir: deps.homedir }); const cronOwnerRefusal = cronOwner ? await prepareCronOwnerWriteRefusal(snapshot.config, { storePath: resolveCronJobsStorePathFromConfig(nextConfig, deps.env), ...cronOwner, env: deps.env }) : void 0; let persistCandidate = nextConfig; let envRefMap = null; const changedPaths = /* @__PURE__ */ new Set(); collectChangedPaths(snapshot.config, nextConfig, "", changedPaths); for (const changedPath of [...explicitSetPaths, ...options.unsetPaths ?? []]) { const normalizedPath = changedPath.filter((segment) => segment.length > 0).join("."); if (normalizedPath) changedPaths.add(normalizedPath); } const identityRestoredPaths = /* @__PURE__ */ new Set(); const hasAuthoredIncludes = containsConfigIncludeDirective(snapshot.parsed); const hasIncludes = hasAuthoredIncludes && !containsConfigIncludeDirective(snapshot.sourceConfig); if (snapshot.valid || snapshot.exists && hasAuthoredIncludes) persistCandidate = resolvePersistCandidateForWrite({ runtimeConfig: snapshot.config, sourceConfig: snapshot.resolved, sourceConfigValid: snapshot.valid, sourceConfigBeforeMigrations: snapshot.sourceConfigBeforeMigrations, nextConfig, rootAuthoredConfig: snapshot.parsed, agentRosterIncludeOwned: snapshot.agentRosterIncludeOwned, unsetPaths, explicitSetPaths, explicitSetValueSource, persistCanonicalAgentRoster, allowedAgentRosterRemovals: options.allowedAgentRosterRemovals, allowIncludeAncestorExplicitSetPaths: options.allowIncludeAncestorExplicitSetPaths, preserveLegacyAgentRoster }); if (snapshot.exists && (snapshot.valid || hasIncludes)) try { const resolvedIncludes = resolveConfigIncludes(snapshot.parsed, configPath, { readFile: (candidate) => deps.fs.readFileSync(candidate, "utf-8"), readFileWithGuards: ({ includePath, resolvedPath, rootRealDir }) => readConfigIncludeFileWithGuards({ includePath, resolvedPath, rootRealDir, ioFs: deps.fs }), parseJson: (raw) => deps.json5.parse(raw) }, { allowedRoots: resolveIncludeRoots(deps.env, deps.homedir) }); const collected = /* @__PURE__ */ new Map(); collectEnvRefPaths(resolvedIncludes, "", collected); if (collected.size > 0) envRefMap = collected; } catch { envRefMap = null; } persistCandidate = applyUnsetPathsForWrite(persistCandidate, unsetPaths); const envForRestore = options.envSnapshotForRestore ?? deps.env; const resolveValidationCandidate = (candidate) => containsConfigIncludeDirective(candidate) ? context.resolveRuntimePreflightSourceConfig(restoreEnvVarRefs(candidate, snapshot.parsed, envForRestore)) : candidate; const validationCandidate = resolveValidationCandidate(persistCandidate); const validateCandidate = (candidate) => { const result = validateConfigObjectRawWithPlugins(candidate, { ...context.pathResolution, pluginValidation: options.skipPluginValidation ? "skip" : "full", semanticValidation: "strict", preservedLegacyRootKeys: options.preservedLegacyRootKeys }); if (!result.ok) throw createConfigValidationFailedError(result.issues); return result; }; validateCandidate(validationCandidate); const materialized = stampConfigVersion(validationCandidate, options.lastTouchedVersionOverride, snapshot.exists ? snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig : null); persistCandidate = applyMergePatch(persistCandidate, createMergePatch(validationCandidate, materialized)); const validated = validateCandidate(resolveValidationCandidate(persistCandidate)); const previousWarningFingerprint = loggedConfigWarningFingerprints.get(configPath); const priorSnapshotAuditRecord = readLatestConfigSnapshotAuditRecord({ env: deps.env, homedir: deps.homedir }); let cfgToWrite = persistCandidate; try { if (deps.fs.existsSync(configPath)) { const currentRaw = await deps.fs.promises.readFile(configPath, "utf-8"); const parsed = parseConfigJson5(currentRaw, deps.json5); if (parsed.ok) { const beforeIdentityRestore = cfgToWrite; cfgToWrite = restoreEnvVarRefs(cfgToWrite, parsed.parsed, envForRestore); collectChangedPaths(beforeIdentityRestore, cfgToWrite, "", identityRestoredPaths); } } } catch (error) { if (error instanceof EnvRefArrayMutationError) throw error; } await deps.fs.promises.mkdir(path.dirname(configPath), { recursive: true, mode: 448 }); await tightenStateDirPermissionsIfNeeded({ configPath, env: deps.env, homedir: deps.homedir, fsModule: deps.fs }); const outputConfigBase = envRefMap ? restoreEnvRefsFromMap(cfgToWrite, "", envRefMap, changedPaths, identityRestoredPaths) : cfgToWrite; const tildeRestoredOutputConfig = restoreAuthoredTildePathsForWrite(outputConfigBase, snapshot.parsed, void 0, deps.homedir()); const outputConfig = applyUnsetPathsForWrite(tildeRestoredOutputConfig, unsetPaths); const stampedOutputConfig = stampConfigVersion(outputConfig, options.lastTouchedVersionOverride); rejectConfigNonFiniteNumbers(stampedOutputConfig); const json = JSON.stringify(stampedOutputConfig, null, 2).trimEnd().concat("\n"); const nextHash = hashConfigRaw(json); const previousHash = resolveConfigSnapshotHash(snapshot); const changedPathCount = changedPaths.size; const previousBytes = typeof snapshot.raw === "string" ? Buffer.byteLength(snapshot.raw, "utf-8") : null; const sizeBaselineBytes = resolveConfigSizeBaselineBytes({ raw: snapshot.raw, json5: deps.json5, lastTouchedVersionOverride: options.lastTouchedVersionOverride }); const nextBytes = Buffer.byteLength(json, "utf-8"); const previousStat = snapshot.exists ? await deps.fs.promises.stat(configPath).catch(() => null) : null; const hasMetaBefore = hasConfigMeta(snapshot.parsed); const hasMetaAfter = hasConfigMeta(stampedOutputConfig); const gatewayModeBefore = resolveGatewayMode(snapshot.resolved); const sourceConfigForPreflight = context.resolveRuntimePreflightSourceConfig(stampedOutputConfig); const gatewayModeAfter = resolveGatewayMode(sourceConfigForPreflight); const suspiciousReasons = resolveConfigWriteSuspiciousReasons({ existsBefore: snapshot.exists, unreadableBefore: snapshot.readError != null, sizeBaselineBytes, nextBytes, hasMetaBefore, gatewayModeBefore, gatewayModeAfter }); const readTestLogFlag = (name) => isVitestRuntimeEnv(deps.env) && deps.env[name] === "1"; const logConfigOverwrite = () => { if (!snapshot.exists || options.skipOutputLogs || isVitestRuntimeEnv(deps.env) && !readTestLogFlag("OPENCLAW_TEST_CONFIG_WRITE_LOG")) return; const testLog = readTestLogFlag("OPENCLAW_TEST_CONFIG_WRITE_LOG"); if (!isVerbose() && deps.env.OPENCLAW_CONFIG_OVERWRITE_LOG !== "1" && !testLog) return; deps.logger.warn(formatConfigOverwriteLogMessage({ configPath, previousHash: previousHash ?? null, nextHash, changedPathCount })); }; const logConfigWriteAnomalies = () => { const testLog = readTestLogFlag("OPENCLAW_TEST_CONFIG_WRITE_LOG"); if (suspiciousReasons.length === 0 || options.skipOutputLogs || isVitestRuntimeEnv(deps.env) && !testLog) return; const visibleReasons = isVerbose() || deps.env.OPENCLAW_CONFIG_WRITE_ANOMALY_LOG === "1" || testLog ? suspiciousReasons : suspiciousReasons.filter((reason) => reason !== "missing-meta-before-write"); if (visibleReasons.length > 0) deps.logger.warn(`Config write anomaly: ${configPath} (${visibleReasons.join(", ")})`); }; const auditRecordBase = createConfigWriteAuditRecordBase({ configPath, env: deps.env, existsBefore: snapshot.exists, previousHash: previousHash ?? null, nextHash, previousBytes, nextBytes, previousMetadata: resolveConfigStatMetadata(previousStat), changedPathCount, changedPaths: [...changedPaths], origin: options.auditOrigin, hasMetaBefore, hasMetaAfter, gatewayModeBefore, gatewayModeAfter, suspicious: suspiciousReasons }); const appendWriteAudit = async (result, error, nextStat) => { await appendConfigAuditRecord({ env: deps.env, homedir: deps.homedir, record: finalizeConfigWriteAuditRecord({ base: auditRecordBase, result, err: error, nextMetadata: resolveConfigStatMetadata(nextStat ?? null) }) }); }; const blockingReasons = resolveConfigWriteBlockingReasons(suspiciousReasons, options); if (blockingReasons.length > 0 && options.allowDestructiveWrite !== true) { const rejectedPath = `${configPath}.rejected.${formatConfigArtifactTimestamp((/* @__PURE__ */ new Date()).toISOString())}`; await deps.fs.promises.writeFile(rejectedPath, json, { encoding: "utf-8", mode: 384, flag: "wx" }).catch(() => {}); const message = `Config write rejected: ${configPath} (${blockingReasons.join(", ")}). Rejected payload saved to ${rejectedPath}.`; const error = Object.assign(new Error(message), { code: "CONFIG_WRITE_REJECTED", rejectedPath, reasons: blockingReasons }); deps.logger.warn(message); await appendWriteAudit("rejected", error); throw error; } await (options.preCommitRuntimePreflight ?? (async (sourceConfig) => { await preflightRuntimeSnapshotWrite({ nextSourceConfig: sourceConfig, refreshOptions: options.runtimeRefresh, formatRefreshError: (error) => formatErrorMessage(error), createRefreshError: (detail, cause) => new ConfigRuntimeRefreshError(`Config write blocked before committing ${configPath}: active SecretRef resolution failed: ${detail}`, { cause }) }); }))(sourceConfigForPreflight); try { const beforeCommit = options.beforeCommit; const result = await replaceFileAtomic({ filePath: configPath, content: json, dirMode: 448, mode: 384, tempPrefix: path.basename(configPath), copyFallbackOnPermissionError: !beforeCommit, fileSystem: beforeCommit ? { promises: { ...deps.fs.promises, rename: async (source, destination) => { await beforeCommit(); options.assertConfigPathForWrite?.(); if (options.baseSnapshot) assertBaseSnapshotStillCurrent(snapshot, configPath, deps.fs); return deps.fs.promises.rename(source, destination); } } } : deps.fs, beforeRename: async () => { options.assertConfigPathForWrite?.(); if (options.baseSnapshot) assertBaseSnapshotStillCurrent(snapshot, configPath, deps.fs); if (deps.fs.existsSync(configPath)) await maintainConfigBackups(configPath, deps.fs.promises); if (options.baseSnapshot) assertBaseSnapshotStillCurrent(snapshot, configPath, deps.fs); options.assertConfigPathForWrite?.(); await cronOwnerRefusal?.recheck(); options.assertConfigPathForWrite?.(); warnIfJSON5CommentsWillBeStripped({ raw: snapshot.raw, filePath: configPath, warn: (message) => deps.logger.warn(message), skipOutputLogs: options.skipOutputLogs }); } }); try { options.assertConfigPathForWrite?.(); } catch (error) { try { await rollbackConfigFileWriteIfUnchanged({ configPath, previousSnapshot: snapshot, committedHash: nextHash, fsModule: deps.fs }); } catch (rollbackError) { throw new ConfigRuntimeRefreshError(`${formatErrorMessage(error)} Rollback failed: ${formatErrorMessage(rollbackError)}`, { cause: error }); } throw error; } try { recordConfigWriteMetadata((/* @__PURE__ */ new Date()).toISOString(), options.lastTouchedVersionOverride); } catch (error) { deps.logger.warn(`Config metadata state update failed: ${formatErrorMessage(error)}`); } logConfigOverwrite(); logConfigWriteAnomalies(); await appendWriteAudit(result.method, void 0, await deps.fs.promises.stat(configPath).catch(() => null)); if (configSnapshotAuditRecordMatchesPath(priorSnapshotAuditRecord, configPath) && priorSnapshotAuditRecord.rawHash !== previousHash) { const offlineChangedPaths = /* @__PURE__ */ new Set(); collectChangedPaths(priorSnapshotAuditRecord.fingerprintedAuthoredConfig, fingerprintConfigSnapshotAuthoredConfig(snapshot.parsed, { env: deps.env, homedir: deps.homedir }), "", offlineChangedPaths); await appendConfigAuditRecord({ env: deps.env, homedir: deps.homedir, record: { ts: (/* @__PURE__ */ new Date()).toISOString(), source: "config-io", event: "config.external", detectedBy: "write", configPath, previousHash: priorSnapshotAuditRecord.rawHash, nextHash: previousHash ?? null, valid: snapshot.valid, ...snapshot.valid ? offlineChangedPaths.size > 0 ? { changedPaths: capConfigAuditPaths([...offlineChangedPaths]) } : { opaqueChange: true } : { issues: capConfigAuditIssues(formatConfigIssueLines(snapshot.issues, "", { normalizeRoot: true })) } } }); } const writtenSnapshotAuditRecord = upsertConfigSnapshotAuditRecord({ env: deps.env, homedir: deps.homedir, configPath, rawHash: nextHash, authoredConfig: stampedOutputConfig, expectedSnapshot: priorSnapshotAuditRecord }); if (!options.skipPluginValidation) logConfigWarningsOnce({ configPath, warnings: validated.warnings, logger: deps.logger }); return { persistedHash: nextHash, persistedConfig: stampedOutputConfig, [configWritePostCommitRollback]: () => { restoreConfigSnapshotAuditRecord({ env: deps.env, homedir: deps.homedir, snapshot: priorSnapshotAuditRecord, expectedSnapshot: writtenSnapshotAuditRecord }); if (previousWarningFingerprint === void 0) loggedConfigWarningFingerprints.delete(configPath); else setBoundedConfigIoWarningEntry(loggedConfigWarningFingerprints, configPath, previousWarningFingerprint); } }; } catch (error) { await appendWriteAudit("failed", error); throw error; } } //#endregion export { writeConfigFileFromContext as t };