mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-24 14:51:14 +00:00
* 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
202 lines
6.7 KiB
TypeScript
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;
|
|
}
|
|
}
|