openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
360 lines (359 loc) • 17.5 kB
JavaScript
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 };