import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import JSON5 from "json5"; import { ensureOwnerDisplaySecret } from "../agents/owner-display.js"; import { applyRuntimeLegacyConfigMigrations } from "../commands/doctor/shared/runtime-compat-api.js"; import { loadDotEnv } from "../infra/dotenv.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; import {
loadShellEnvFallback,
resolveShellEnvFallbackTimeoutMs,
shouldDeferShellEnvFallback,
shouldEnableShellEnvFallback,
} from "../infra/shell-env.js"; import {
collectRelevantDoctorPluginIds,
listPluginDoctorLegacyConfigRules,
} from "../plugins/doctor-contract-registry.js"; import { sanitizeTerminalText } from "../terminal/safe-text.js"; import { isRecord } from "../utils.js"; import { VERSION } from "../version.js"; import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js"; import { maintainConfigBackups } from "./backup-rotation.js"; import { restoreEnvVarRefs } from "./env-preserve.js"; import {
type EnvSubstitutionWarning,
MissingEnvVarError,
containsEnvVarReference,
resolveConfigEnvVars,
} from "./env-substitution.js"; import { applyConfigEnvVars } from "./env-vars.js"; import {
ConfigIncludeError,
readConfigIncludeFileWithGuards,
resolveConfigIncludes,
} from "./includes.js"; import {
appendConfigAuditRecord,
appendConfigAuditRecordSync,
createConfigWriteAuditRecordBase,
finalizeConfigWriteAuditRecord,
formatConfigOverwriteLogMessage,
type ConfigWriteAuditResult,
} from "./io.audit.js"; import { throwInvalidConfig } from "./io.invalid-config.js"; import {
maybeRecoverSuspiciousConfigRead,
maybeRecoverSuspiciousConfigReadSync,
promoteConfigSnapshotToLastKnownGood as promoteConfigSnapshotToLastKnownGoodWithDeps,
recoverConfigFromLastKnownGood as recoverConfigFromLastKnownGoodWithDeps,
} from "./io.observe-recovery.js"; import { persistGeneratedOwnerDisplaySecret } from "./io.owner-display-secret.js"; import {
collectChangedPaths,
createMergePatch,
formatConfigValidationFailure,
projectSourceOntoRuntimeShape,
restoreEnvRefsFromMap,
resolvePersistCandidateForWrite,
resolveWriteEnvSnapshotForPath,
unsetPathForWrite,
} from "./io.write-prepare.js"; import { findLegacyConfigIssues } from "./legacy.js"; import {
asResolvedSourceConfig,
asRuntimeConfig,
materializeRuntimeConfig,
} from "./materialize.js"; import { applyMergePatch } from "./merge-patch.js"; import { resolveConfigPath, resolveStateDir } from "./paths.js"; import { applyConfigOverrides } from "./runtime-overrides.js"; import {
clearRuntimeConfigSnapshot as clearRuntimeConfigSnapshotState,
finalizeRuntimeSnapshotWrite,
getRuntimeConfigSnapshot as getRuntimeConfigSnapshotState,
getRuntimeConfigSourceSnapshot as getRuntimeConfigSourceSnapshotState,
loadPinnedRuntimeConfig,
notifyRuntimeConfigWriteListeners,
registerRuntimeConfigWriteListener,
resetConfigRuntimeState as resetConfigRuntimeStateState,
selectApplicableRuntimeConfig,
setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState,
getRuntimeConfigSnapshotRefreshHandler as getRuntimeConfigSnapshotRefreshHandlerState,
setRuntimeConfigSnapshotRefreshHandler as setRuntimeConfigSnapshotRefreshHandlerState,
type RuntimeConfigWriteNotification,
} from "./runtime-snapshot.js"; import { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js"; import type { OpenClawConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js"; import {
validateConfigObjectRawWithPlugins,
validateConfigObjectWithPlugins,
} from "./validation.js"; import { shouldWarnOnTouchedVersion } from "./version.js";
export {
clearRuntimeConfigSnapshotState as clearRuntimeConfigSnapshot,
getRuntimeConfigSnapshotState as getRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshotState as getRuntimeConfigSourceSnapshot,
resetConfigRuntimeStateState as resetConfigRuntimeState,
selectApplicableRuntimeConfig,
setRuntimeConfigSnapshotState as setRuntimeConfigSnapshot,
setRuntimeConfigSnapshotRefreshHandlerState as setRuntimeConfigSnapshotRefreshHandler,
};
// Re-export for backwards compatibility
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
export { MissingEnvVarError } from "./env-substitution.js";
export { resolveShellEnvExpectedKeys } from "./shell-env-expected-keys.js";
const CONFIG_HEALTH_STATE_FILENAME = "config-health.json"; const loggedInvalidConfigs = new Set<string>();
type ConfigHealthFingerprint = {
hash: string;
bytes: number;
mtimeMs: number | null;
ctimeMs: number | null;
dev: string | null;
ino: string | null;
mode: number | null;
nlink: number | null;
uid: number | null;
gid: number | null;
hasMeta: boolean;
gatewayMode: string | null;
observedAt: string;
};
type ConfigHealthState = {
entries?: Record<string, ConfigHealthEntry>;
};
export type ParseConfigJson5Result = { ok: true; parsed: unknown } | { ok: false; error: string };
export type ConfigWriteOptions = { /** * Read-time env snapshot used to validate `${VAR}` restoration decisions. * If omitted, write falls back to current process env.
*/
envSnapshotForRestore?: Record<string, string | undefined>; /** * Optional safety check: only use envSnapshotForRestore when writing the * same config file path that produced the snapshot.
*/
expectedConfigPath?: string; /** * Paths that must be explicitly removed from the persisted file payload, * even if schema/default normalization reintroduces them.
*/
unsetPaths?: string[][]; /** * Internal fast path for callers that already hold a fresh config snapshot. * Avoids rereading the full config just to prepare an immediate write.
*/
baseSnapshot?: ConfigFileSnapshot; /** * Internal one-shot CLI fast path. When no runtime snapshot is active, skip * the post-write runtime snapshot refresh/reload tail entirely.
*/
skipRuntimeSnapshotRefresh?: boolean; /** * Allow intentionally destructive config writes, such as explicit reset flows. * Normal writers must keep this false so clobbers are rejected before disk commit.
*/
allowDestructiveWrite?: boolean; /** * Suppress human-readable output logs (overwrite/anomaly messages). * Useful when the caller wants machine-readable output only (--json mode).
*/
skipOutputLogs?: boolean;
};
export type ReadConfigFileSnapshotForWriteResult = {
snapshot: ConfigFileSnapshot;
writeOptions: ConfigWriteOptions;
};
export type ConfigWriteNotification = RuntimeConfigWriteNotification;
function coerceConfig(value: unknown): OpenClawConfig { if (!value || typeof value !== "object" || Array.isArray(value)) { return {};
} return value as OpenClawConfig;
}
function hasConfigMeta(value: unknown): boolean { if (!isRecord(value)) { returnfalse;
} const meta = value.meta; return isRecord(meta);
}
function normalizeStatNumber(value: number | null | undefined): number | null { returntypeof value === "number" && Number.isFinite(value) ? value : null;
}
function normalizeStatId(value: number | bigint | null | undefined): string | null { if (typeof value === "bigint") { return value.toString();
} if (typeof value === "number" && Number.isFinite(value)) { return String(value);
} returnnull;
}
function warnOnConfigMiskeys(raw: unknown, logger: Pick<typeof console, "warn">): void { if (!raw || typeof raw !== "object") { return;
} const gateway = (raw as Record<string, unknown>).gateway; if (!gateway || typeof gateway !== "object") { return;
} if ("token" in (gateway as Record<string, unknown>)) {
logger.warn( 'Config uses "gateway.token". This key is ignored; use "gateway.auth.token" instead.',
);
}
}
function stampConfigVersion(cfg: OpenClawConfig): OpenClawConfig { const now = new Date().toISOString(); return {
...cfg,
meta: {
...cfg.meta,
lastTouchedVersion: VERSION,
lastTouchedAt: now,
},
};
}
function warnIfConfigFromFuture(cfg: OpenClawConfig, logger: Pick<typeof console, "warn">): void { const touched = cfg.meta?.lastTouchedVersion; if (!touched) { return;
} if (shouldWarnOnTouchedVersion(VERSION, touched)) {
logger.warn(
`Config was last written by a newer OpenClaw (${touched}); current version is ${VERSION}.`,
);
}
}
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string { if (deps.configPath) { return deps.configPath;
} return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
}
function maybeLoadDotEnvForConfig(env: NodeJS.ProcessEnv): void { // Only hydrate dotenv for the real process env. Callers using injected env // objects (tests/diagnostics) should stay isolated. if (env !== process.env) { return;
}
loadDotEnv({ quiet: true });
}
function resolveConfigForRead(
resolvedIncludes: unknown,
env: NodeJS.ProcessEnv,
): ConfigReadResolution { // Apply config.env to process.env BEFORE substitution so ${VAR} can reference config-defined vars. if (resolvedIncludes && typeof resolvedIncludes === "object" && "env" in resolvedIncludes) {
applyConfigEnvVars(resolvedIncludes as OpenClawConfig, env);
}
// Collect missing env var references as warnings instead of throwing, // so non-critical config sections with unset vars don't crash the gateway. const envWarnings: EnvSubstitutionWarning[] = []; return {
resolvedConfigRaw: resolveConfigEnvVars(resolvedIncludes, env, {
onMissing: (w) => envWarnings.push(w),
}), // Capture env snapshot after substitution for write-time ${VAR} restoration.
envSnapshotForRestore: { ...env } as Record<string, string | undefined>,
envWarnings,
};
}
// Convert missing env var references to config warnings instead of fatal errors. // This allows the gateway to start in degraded mode when non-critical config // sections reference unset env vars (e.g. optional provider API keys). const envVarWarnings = readResolution.envWarnings.map((w) => ({
path: w.configPath,
message: `Missing env var"${w.varName}" - feature using this value will be unavailable`,
}));
// Restore ${VAR} env var references that were resolved during config loading. // Read the current file (pre-substitution) and restore any references whose // resolved values match the incoming config - so we don't overwrite // "${ANTHROPIC_API_KEY}" with "sk-ant-..." when the caller didn't change it. // // We use only the root file's parsed content (no $include resolution) to avoid // pulling values from included files into the root config on write-back. // Use persistCandidate (the merge-patched value before validation) rather than // validated.config, because plugin/channel AJV validation may inject schema // defaults (e.g., enrichGroupParticipantsFromContacts) that should not be // persisted to disk (issue #56772). // Apply legacy web-search normalization so that migration results are still // persisted even though we bypass validated.config.
let cfgToWrite = persistCandidate as OpenClawConfig; try { if (deps.fs.existsSync(configPath)) { const currentRaw = await deps.fs.promises.readFile(configPath, "utf-8"); const parsedRes = parseConfigJson5(currentRaw, deps.json5); if (parsedRes.ok) { // Use env snapshot from when config was loaded (if available) to avoid // TOCTOU issues where env changes between load and write. Falls back to // live env if no snapshot exists (e.g., first write before any load). const envForRestore = options.envSnapshotForRestore ?? deps.env;
cfgToWrite = restoreEnvVarRefs(
cfgToWrite,
parsedRes.parsed,
envForRestore,
) as OpenClawConfig;
}
}
} catch { // If reading the current file fails, write cfg as-is (no env restoration)
}
// NOTE: These wrappers intentionally do *not* cache the resolved config path at // module scope. `OPENCLAW_CONFIG_PATH` (and friends) are expected to work even // when set after the module has been imported (tests, one-off scripts, etc.). const AUTO_OWNER_DISPLAY_SECRET_BY_PATH = new Map<string, string>(); const AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT = new Set<string>(); const AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED = new Set<string>();
export function clearConfigCache(): void { // Compat shim: runtime snapshot is the only in-process cache now.
}
export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig): OpenClawConfig { const runtimeConfigSnapshot = getRuntimeConfigSnapshotState(); const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState(); if (!runtimeConfigSnapshot || !runtimeConfigSourceSnapshot) { return config;
} if (config === runtimeConfigSnapshot) { return runtimeConfigSourceSnapshot;
} // This projection expects callers to pass config objects derived from the // active runtime snapshot (for example shallow/deep clones with targeted edits). // For structurally unrelated configs, skip projection to avoid accidental // merge-patch deletions or reintroducing resolved values into source refs. if (
!isCompatibleTopLevelRuntimeProjectionShape({
runtimeSnapshot: runtimeConfigSnapshot,
candidate: config,
})
) { return config;
} const projectedSource = coerceConfig(
projectSourceOntoRuntimeShape(runtimeConfigSourceSnapshot, runtimeConfigSnapshot),
); const runtimePatch = createMergePatch(runtimeConfigSnapshot, config); return coerceConfig(applyMergePatch(projectedSource, runtimePatch));
}
export function loadConfig(): OpenClawConfig { // First successful load becomes the process snapshot. Long-lived runtimes // should swap this snapshot via explicit reload/watcher paths instead of // reparsing openclaw.json on hot code paths. return loadPinnedRuntimeConfig(() => createConfigIO().loadConfig());
}
export function getRuntimeConfig(): OpenClawConfig { return loadConfig();
}
export async function readBestEffortConfig(): Promise<OpenClawConfig> { return await createConfigIO().readBestEffortConfig();
}
export async function readSourceConfigBestEffort(): Promise<OpenClawConfig> { return await createConfigIO().readSourceConfigBestEffort();
}
export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> { return await createConfigIO().readConfigFileSnapshot();
}
export async function readSourceConfigSnapshot(): Promise<ConfigFileSnapshot> { return await readConfigFileSnapshot();
}
export async function readConfigFileSnapshotForWrite(): Promise<ReadConfigFileSnapshotForWriteResult> { return await createConfigIO().readConfigFileSnapshotForWrite();
}
export async function readSourceConfigSnapshotForWrite(): Promise<ReadConfigFileSnapshotForWriteResult> { return await readConfigFileSnapshotForWrite();
}
export async function writeConfigFile(
cfg: OpenClawConfig,
options: ConfigWriteOptions = {},
): Promise<void> { const io = createConfigIO();
let nextCfg = cfg; const runtimeConfigSnapshot = getRuntimeConfigSnapshotState(); const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState(); const hadRuntimeSnapshot = Boolean(runtimeConfigSnapshot); const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot); if (hadBothSnapshots) { const runtimePatch = createMergePatch(runtimeConfigSnapshot!, cfg);
nextCfg = coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot!, runtimePatch));
} const writeResult = await io.writeConfigFile(nextCfg, {
envSnapshotForRestore: resolveWriteEnvSnapshotForPath({
actualConfigPath: io.configPath,
expectedConfigPath: options.expectedConfigPath,
envSnapshotForRestore: options.envSnapshotForRestore,
}),
unsetPaths: options.unsetPaths,
allowDestructiveWrite: options.allowDestructiveWrite,
skipRuntimeSnapshotRefresh: options.skipRuntimeSnapshotRefresh,
skipOutputLogs: options.skipOutputLogs,
}); if (
options.skipRuntimeSnapshotRefresh &&
!hadRuntimeSnapshot &&
!getRuntimeConfigSnapshotRefreshHandlerState()
) { return;
} const notifyCommittedWrite = () => { const currentRuntimeConfig = getRuntimeConfigSnapshotState(); if (!currentRuntimeConfig) { return;
}
notifyRuntimeConfigWriteListeners({
configPath: io.configPath,
sourceConfig: nextCfg,
runtimeConfig: currentRuntimeConfig,
persistedHash: writeResult.persistedHash,
writtenAtMs: Date.now(),
});
}; // Keep the last-known-good runtime snapshot active until the specialized refresh path // succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh.
await finalizeRuntimeSnapshotWrite({
nextSourceConfig: nextCfg,
hadRuntimeSnapshot,
hadBothSnapshots,
loadFreshConfig: () => io.loadConfig(),
notifyCommittedWrite,
formatRefreshError: (error) => formatErrorMessage(error),
createRefreshError: (detail, cause) => new ConfigRuntimeRefreshError(
`Config was written to ${io.configPath}, but runtime snapshot refresh failed: ${detail}`,
{ cause },
),
});
}
Messung V0.5 in Prozent
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.33Angebot
(Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-06-06)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.