Files
openclaw/src/config/io.load.ts
Peter Steinberger 290836cafd refactor(config): split config I/O responsibilities (#107983)
* refactor(config): split config I/O

* fix(config): align split modules with guardrails

* fix(config): remove dead write metadata helper

* style(config): format write safety imports

* fix(config): preserve comment-loss warning on split writes
2026-07-14 22:11:22 -07:00

202 lines
6.7 KiB
TypeScript

import {
loadShellEnvFallback,
resolveShellEnvFallbackTimeoutMs,
shouldDeferShellEnvFallback,
shouldEnableShellEnvFallback,
} from "../infra/shell-env.js";
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
import type { ConfigIoContext } from "./io.context.js";
import { materializeConfigForLoad } from "./io.context.js";
import { throwInvalidConfig } from "./io.invalid-config.js";
import { maybeRecoverSuspiciousConfigReadSync } from "./io.observe-recovery.js";
import {
coerceConfig,
containsConfigIncludeDirective,
hashConfigRaw,
maybeLoadDotEnvForConfig,
resolveConfigForRead,
resolveConfigIncludesForRead,
restoreEnvChangesIfUnchanged,
snapshotEnv,
} from "./io.read-helpers.js";
import { createConfigFileSnapshot } from "./io.snapshot-shared.js";
import { loggedConfigWarningFingerprints, loggedInvalidConfigs } from "./io.state.js";
import {
logConfigWarningsOnce,
warnIfConfigFromFuture,
warnOnConfigMiskeys,
} from "./io.warnings.js";
import { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
import type { OpenClawConfig } from "./types.js";
import { validateConfigObjectWithPlugins } from "./validation.js";
export function loadConfigFromContext(
context: ConfigIoContext,
options: { skipSuspiciousRecovery?: boolean } = {},
): OpenClawConfig {
const { deps, configPath } = context;
try {
maybeLoadDotEnvForConfig(deps.env);
const envBeforeRead = snapshotEnv(deps.env);
if (!deps.fs.existsSync(configPath)) {
loggedConfigWarningFingerprints.delete(configPath);
if (
context.options.shellEnvFallback !== "defer" &&
shouldEnableShellEnvFallback(deps.env) &&
!shouldDeferShellEnvFallback(deps.env)
) {
loadShellEnvFallback({
enabled: true,
env: deps.env,
expectedKeys: resolveShellEnvExpectedKeys(deps.env),
logger: deps.logger,
timeoutMs: resolveShellEnvFallbackTimeoutMs(deps.env),
});
}
return {};
}
const raw = deps.fs.readFileSync(configPath, "utf-8");
const parsed = deps.json5.parse(raw);
const readResolution = resolveConfigForRead(
resolveConfigIncludesForRead(parsed, configPath, deps),
deps.env,
deps.lowerPrecedenceEnv,
);
const migration = context.migrateAndStripShippedPluginInstallConfigRecords(
readResolution.resolvedConfigRaw,
{ persist: false, rootConfigRaw: parsed },
);
const effectiveConfigRaw = migration.config;
const validationConfigRaw = migration.validationConfig ?? effectiveConfigRaw;
const snapshotRaw = migration.persistedRootRaw ?? raw;
const snapshotParsed = migration.persistedRootParsed ?? parsed;
const hash = hashConfigRaw(snapshotRaw);
for (const warning of readResolution.envWarnings) {
deps.logger.warn(
`Config (${configPath}): missing env var "${warning.varName}" at ${warning.configPath} - feature using this value will be unavailable`,
);
}
warnOnConfigMiskeys(validationConfigRaw, deps.logger);
if (typeof validationConfigRaw !== "object" || validationConfigRaw === null) {
loggedConfigWarningFingerprints.delete(configPath);
context.observeLoadConfigSnapshot(
createConfigFileSnapshot({
path: configPath,
exists: true,
raw: snapshotRaw,
parsed: snapshotParsed,
sourceConfig: {},
valid: true,
runtimeConfig: {},
hash,
issues: [],
warnings: [],
legacyIssues: [],
}),
);
return {};
}
const duplicates = findDuplicateAgentDirs(validationConfigRaw as OpenClawConfig, {
env: deps.env,
homedir: deps.homedir,
});
if (duplicates.length > 0) {
throw new DuplicateAgentDirError(duplicates);
}
const pluginMetadata = context.createValidationPluginMetadataSnapshotLoader({
effectiveConfigRaw,
env: deps.env,
});
const validated = validateConfigObjectWithPlugins(validationConfigRaw, {
env: deps.env,
pluginValidation: context.options.pluginValidation,
loadPluginMetadataSnapshot: pluginMetadata.load,
sourceRaw: snapshotParsed,
preservedLegacyRootKeys: context.options.preservedLegacyRootKeys,
});
if (!validated.ok) {
context.observeLoadConfigSnapshot(
createConfigFileSnapshot({
path: configPath,
exists: true,
raw: snapshotRaw,
parsed: snapshotParsed,
sourceConfig: coerceConfig(effectiveConfigRaw),
valid: false,
runtimeConfig: coerceConfig(effectiveConfigRaw),
hash,
issues: validated.issues,
warnings: validated.warnings,
legacyIssues: [],
}),
);
throwInvalidConfig({
configPath,
issues: validated.issues,
logger: deps.logger,
loggedConfigPaths: loggedInvalidConfigs,
});
}
if (context.options.pluginValidation !== "skip") {
logConfigWarningsOnce({ configPath, warnings: validated.warnings, logger: deps.logger });
}
if (!deps.suppressFutureVersionWarning) {
warnIfConfigFromFuture(validated.config, deps.logger);
}
if (
deps.observe &&
!options.skipSuspiciousRecovery &&
!containsConfigIncludeDirective(parsed)
) {
const recovery = maybeRecoverSuspiciousConfigReadSync({
deps,
configPath,
raw,
parsed,
validateBackupSync: (backup) =>
context.resolveSuspiciousRecoveryBackupCandidate(backup.parsed) !== null,
});
if (recovery.raw !== raw) {
restoreEnvChangesIfUnchanged({
env: deps.env,
before: envBeforeRead,
after: snapshotEnv(deps.env),
});
return loadConfigFromContext(context, { skipSuspiciousRecovery: true });
}
}
const cfg = materializeConfigForLoad(
context,
validated.config,
effectiveConfigRaw,
pluginMetadata.getSnapshot(),
);
context.observeLoadConfigSnapshot(
createConfigFileSnapshot({
path: configPath,
exists: true,
raw: snapshotRaw,
parsed: snapshotParsed,
sourceConfig: coerceConfig(effectiveConfigRaw),
valid: true,
runtimeConfig: cfg,
hash,
issues: [],
warnings: validated.warnings,
legacyIssues: [],
}),
);
return context.finalizeLoadedRuntimeConfig(cfg);
} catch (error) {
if (error instanceof DuplicateAgentDirError) {
deps.logger.error(error.message);
throw error;
}
if ((error as { code?: string })?.code === "INVALID_CONFIG") {
throw error;
}
deps.logger.error(`Failed to read config at ${configPath}`, error);
throw error;
}
}