Files
openclaw/src/config/io.context.ts
T

257 lines
10 KiB
TypeScript

import crypto from "node:crypto";
import { collectManifestModelIdNormalizationPolicies } from "@openclaw/model-catalog-core/provider-model-id-normalization";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope-config.js";
import { ensureOwnerDisplaySecret } from "../agents/owner-display.js";
import { classifyOtelGrpcMigrationOwnership } from "../commands/doctor/shared/include-migration-ownership.js";
import { applyLegacyDoctorMigrations } from "../commands/doctor/shared/legacy-config-compat.js";
import {
loadShellEnvFallback,
resolveShellEnvFallbackTimeoutMs,
shouldDeferShellEnvFallback,
shouldEnableShellEnvFallback,
} from "../infra/shell-env.js";
import { createConfigValidationMetadataPluginIdScope } from "../plugins/gateway-startup-plugin-ids.js";
import {
resolvePluginMetadataSnapshot,
type PluginMetadataSnapshot,
} from "../plugins/plugin-metadata-snapshot.js";
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
import { applyConfigEnvVars, cloneEnvWithPlatformSemantics } from "./config-env-vars.js";
import { observeConfigSnapshotSync } from "./io.observe.js";
import { retainGeneratedOwnerDisplaySecret } from "./io.owner-display-secret.js";
import {
coerceConfig,
normalizeConfigIoDeps,
resolveConfigForRead,
resolveConfigIncludesForRead,
resolveConfigPathForDeps,
} from "./io.read-helpers.js";
import { autoOwnerDisplaySecretByPath } from "./io.state.js";
import type {
ConfigIoFactoryOptions,
ConfigRecoveryCandidate,
ConfigRecoveryCandidatePreparation,
NormalizedConfigIoDeps,
} from "./io.types.js";
import { formatConfigIssueSummary } from "./issue-format.js";
import { migratePersistedImplicitMainRoster } from "./legacy.roster.js";
import { materializeRuntimeConfig } from "./materialize.js";
import { applyConfigOverrides } from "./runtime-overrides.js";
import { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js";
import { validateConfigObjectWithPlugins } from "./validation.js";
type ValidationPluginMetadataSnapshotLoader = {
load: (config: OpenClawConfig) => PluginMetadataSnapshot;
getSnapshot: () => PluginMetadataSnapshot | undefined;
};
export type ConfigIoContext = {
deps: NormalizedConfigIoDeps;
configPath: string;
options: ConfigIoFactoryOptions;
observeLoadConfigSnapshot: (snapshot: ConfigFileSnapshot) => ConfigFileSnapshot;
finalizeLoadedRuntimeConfig: (config: OpenClawConfig) => OpenClawConfig;
createValidationPluginMetadataSnapshotLoader: (params: {
effectiveConfigRaw: unknown;
env: NodeJS.ProcessEnv;
allowCurrentPluginMetadata?: boolean;
}) => ValidationPluginMetadataSnapshotLoader;
resolveRuntimePreflightSourceConfig: (candidate: OpenClawConfig) => OpenClawConfig;
prepareRecoveryBackupCandidate: (
candidate: ConfigRecoveryCandidate,
) => ConfigRecoveryCandidatePreparation;
};
export function createConfigIoContext(options: ConfigIoFactoryOptions = {}): ConfigIoContext {
const deps = normalizeConfigIoDeps(options);
const configPath = resolveConfigPathForDeps(deps);
function observeLoadConfigSnapshot(snapshot: ConfigFileSnapshot): ConfigFileSnapshot {
if (deps.observe) {
observeConfigSnapshotSync(deps, snapshot);
}
return snapshot;
}
function finalizeLoadedRuntimeConfig(cfg: OpenClawConfig): OpenClawConfig {
const duplicates = findDuplicateAgentDirs(cfg, { env: deps.env, homedir: deps.homedir });
if (duplicates.length > 0) {
throw new DuplicateAgentDirError(duplicates);
}
applyConfigEnvVars(cfg, deps.env);
const enabled = shouldEnableShellEnvFallback(deps.env) || cfg.env?.shellEnv?.enabled === true;
if (enabled && options.shellEnvFallback !== "defer" && !shouldDeferShellEnvFallback(deps.env)) {
loadShellEnvFallback({
enabled: true,
env: deps.env,
expectedKeys: resolveShellEnvExpectedKeys(deps.env),
logger: deps.logger,
timeoutMs: cfg.env?.shellEnv?.timeoutMs ?? resolveShellEnvFallbackTimeoutMs(deps.env),
});
}
const pendingValue = autoOwnerDisplaySecretByPath.get(configPath);
const { config: resolvedConfig, generatedSecret } = ensureOwnerDisplaySecret(
cfg,
() => pendingValue ?? crypto.randomBytes(32).toString("hex"),
);
return applyConfigOverrides(
retainGeneratedOwnerDisplaySecret({
config: resolvedConfig,
configPath,
generatedSecret,
state: { pendingByPath: autoOwnerDisplaySecretByPath },
}),
);
}
function createValidationPluginMetadataSnapshotLoader(params: {
effectiveConfigRaw: unknown;
env: NodeJS.ProcessEnv;
allowCurrentPluginMetadata?: boolean;
}): ValidationPluginMetadataSnapshotLoader {
let snapshot: PluginMetadataSnapshot | undefined;
return {
load: (config) => {
if (snapshot) {
return snapshot;
}
const metadataConfig = config;
const defaultAgentId = resolveDefaultAgentId(metadataConfig);
snapshot = resolvePluginMetadataSnapshot({
config: metadataConfig,
workspaceDir: resolveAgentWorkspaceDir(metadataConfig, defaultAgentId, params.env),
env: params.env,
allowCurrent: params.allowCurrentPluginMetadata,
allowWorkspaceScopedCurrent: true,
pluginIdScope: createConfigValidationMetadataPluginIdScope({
config: metadataConfig,
env: params.env,
}),
});
return snapshot;
},
getSnapshot: () => snapshot,
};
}
function resolveRuntimePreflightSourceConfig(candidate: OpenClawConfig): OpenClawConfig {
const env = { ...deps.env } as NodeJS.ProcessEnv;
const resolvedIncludes = resolveConfigIncludesForRead(candidate, configPath, { ...deps, env });
const resolution = resolveConfigForRead(resolvedIncludes, env, deps.lowerPrecedenceEnv);
return coerceConfig(migratePersistedImplicitMainRoster(resolution.resolvedConfigRaw).config);
}
function prepareRecoveryBackupCandidate(
candidate: ConfigRecoveryCandidate,
): ConfigRecoveryCandidatePreparation {
try {
const originalEnv = cloneEnvWithPlatformSemantics(deps.env);
const includeProvenance: NonNullable<ConfigFileSnapshot["includeProvenance"]>[number][] = [];
const originalResolvedIncludes = resolveConfigIncludesForRead(
candidate.parsed,
configPath,
{ ...deps, env: originalEnv },
undefined,
undefined,
undefined,
(event) => {
const { value: _value, ...ownership } = event;
includeProvenance.push(ownership);
},
);
const originalResolution = resolveConfigForRead(
originalResolvedIncludes,
originalEnv,
deps.lowerPrecedenceEnv,
);
const otelOwnership = classifyOtelGrpcMigrationOwnership({
snapshot: { path: configPath, includeProvenance },
authoredConfig: candidate.parsed,
resolvedConfig: originalResolution.resolvedConfigRaw,
});
if (otelOwnership && otelOwnership.kind !== "direct") {
return {
ok: false,
reason:
otelOwnership.kind === "resolved-only"
? "candidate migration cannot persist an env-resolved diagnostics.otel.protocol repair"
: "candidate migration requires an include-owned diagnostics.otel.protocol repair",
};
}
// Recovery is a migration boundary, not runtime compatibility: the canonical Doctor
// registry owns historical shapes before current-schema validation and any disk write.
const migrated = applyLegacyDoctorMigrations(candidate.parsed, {
authoredRaw: candidate.parsed,
resolvedRaw: originalResolution.resolvedConfigRaw,
});
const authoredCandidate = migrated.next ?? candidate.parsed;
const candidateEnv = cloneEnvWithPlatformSemantics(deps.env);
const resolved = resolveConfigIncludesForRead(authoredCandidate, configPath, {
...deps,
env: candidateEnv,
});
const resolution = resolveConfigForRead(resolved, candidateEnv, deps.lowerPrecedenceEnv);
const effectiveConfigRaw = resolution.resolvedConfigRaw;
const pluginMetadata = createValidationPluginMetadataSnapshotLoader({
effectiveConfigRaw,
env: candidateEnv,
});
const validated = validateConfigObjectWithPlugins(effectiveConfigRaw, {
env: candidateEnv,
pluginValidation: options.pluginValidation,
loadPluginMetadataSnapshot: pluginMetadata.load,
sourceRaw: authoredCandidate,
preservedLegacyRootKeys: options.preservedLegacyRootKeys,
});
if (!validated.ok) {
const issueSummary = formatConfigIssueSummary(validated.issues.slice(0, 3)) ?? "";
const detail = issueSummary.length > 800 ? `${issueSummary.slice(0, 799)}…` : issueSummary;
return {
ok: false,
reason: `candidate remains invalid after legacy migration${detail ? `: ${detail}` : ""}`,
};
}
return {
ok: true,
candidate: {
config: validated.config,
parsed: authoredCandidate,
raw: migrated.next
? JSON.stringify(authoredCandidate, null, 2).trimEnd().concat("\n")
: candidate.raw,
},
};
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
return { ok: false, reason: `candidate preparation failed: ${detail}` };
}
}
return {
deps,
configPath,
options,
observeLoadConfigSnapshot,
finalizeLoadedRuntimeConfig,
createValidationPluginMetadataSnapshotLoader,
resolveRuntimePreflightSourceConfig,
prepareRecoveryBackupCandidate,
};
}
export function resolveModelIdNormalizationPolicies(snapshot: PluginMetadataSnapshot | undefined) {
return snapshot ? collectManifestModelIdNormalizationPolicies(snapshot.plugins) : undefined;
}
export function materializeConfigForLoad(
_context: ConfigIoContext,
config: OpenClawConfig,
_effectiveConfigRaw: unknown,
pluginMetadata: PluginMetadataSnapshot | undefined,
): OpenClawConfig {
return materializeRuntimeConfig(config, "load", {
manifestRegistry: pluginMetadata?.manifestRegistry,
});
}