openclaw / src /cli /gateway-cli /pre-bootstrap.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw
History Blame Contribute Delete
29.6 kB
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import * as startupRepair from "../../commands/doctor/shared/automatic-startup-config-repair.js";
import {
cloneEnvWithPlatformSemantics,
resetPublishedConfigRuntimeEnv,
} from "../../config/config-env-vars.js";
// Gateway startup checks that must run before shared CLI bootstrap can migrate state.
import { ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV } from "../../config/future-version-guard.js";
import { GATEWAY_CONFIG_SELECTION_ENV_KEYS } from "../../config/gateway-env-selection.js";
import { CONFIG_AUDIT_STORE_LABEL } from "../../config/io.audit.js";
import type { ConfigFileSnapshot } from "../../config/types.js";
import { ExitError, type RuntimeEnv } from "../../runtime.js";
import { withArtifactPreservingStateReads } from "../../state/openclaw-state-db-readonly.js";
import { formatCliCommand } from "../command-format.js";
import type { GatewayRunPreBootstrapOptions } from "./future-config-guard.js";
import { enforceGatewayRunFutureConfigGuard } from "./future-config-guard.js";
import type { GatewayRunOpts } from "./run-options.js";
import { getGatewayRunRuntimeHooks } from "./runtime-hooks.js";
type GatewayRunGuardParams = {
opts: GatewayRunPreBootstrapOptions & Pick<GatewayRunOpts, "allowUnconfigured" | "dev">;
runtime: RuntimeEnv;
};
type GatewayRunEnvironmentSelection = {
after: Record<string, string | undefined>;
before: Record<string, string | undefined>;
};
type PreparedGatewayRunReset = {
selectionEnvironment: Record<string, string | undefined>;
selectionSignature: string;
snapshot: ConfigFileSnapshot;
};
let selectedGatewayRunEnvironment: GatewayRunEnvironmentSelection | undefined;
let appliedGatewayRunConfigEnvironment: GatewayRunEnvironmentSelection | undefined;
let lastGuardedGatewayRunSnapshot: ConfigFileSnapshot | undefined;
let preparedGatewayRunBootstrap:
| (Pick<GatewayRunOpts, "allowUnconfigured" | "dev"> & { snapshot: ConfigFileSnapshot })
| undefined;
let preparedGatewayRunStateWasPristine = false;
let preparedGatewayRunCoreStateWasPristine = false;
let preparedGatewayRunReset: PreparedGatewayRunReset | undefined;
let gatewayRunTargetSelectedByConfig = false;
export function getGatewayStartGuardErrors(params: {
allowUnconfigured?: boolean;
configExists: boolean;
mode: string | undefined;
}): string[] {
if (
(params.allowUnconfigured ?? preparedGatewayRunBootstrap?.allowUnconfigured) ||
params.mode === "local" ||
(!params.configExists && preparedGatewayRunBootstrap?.dev)
) {
return [];
}
if (!params.configExists) {
return [
`Missing config. Run \`${formatCliCommand("openclaw setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
];
}
return [
params.mode === undefined
? [
"Gateway start blocked: existing config is missing gateway.mode.",
"Treat this as suspicious or clobbered config.",
`Re-run \`${formatCliCommand("openclaw onboard --mode local")}\` or \`${formatCliCommand("openclaw setup")}\`, set gateway.mode=local manually, or pass --allow-unconfigured.`,
].join(" ")
: `Gateway start blocked: set gateway.mode=local (current: ${params.mode}) or pass --allow-unconfigured.`,
`Config write audit: ${CONFIG_AUDIT_STORE_LABEL}`,
];
}
async function pinGatewayRunRuntimePaths(): Promise<void> {
const [{ pinRuntimePaths }, { pinConfigDir }] = await Promise.all([
import("../../config/paths.js"),
import("../../utils.js"),
]);
pinRuntimePaths(process.env);
pinConfigDir(process.env);
}
const GATEWAY_RESET_SELECTION_ENV_KEYS = new Set([
...GATEWAY_CONFIG_SELECTION_ENV_KEYS,
"OPENCLAW_PROFILE",
"OPENCLAW_WORKSPACE_DIR",
]);
function resolveGatewayConfigSelectionSignature(env: NodeJS.ProcessEnv): string {
return JSON.stringify([...GATEWAY_CONFIG_SELECTION_ENV_KEYS].map((key) => [key, env[key]]));
}
function snapshotGatewayConfigSelectionEnvironment(
env: NodeJS.ProcessEnv,
): Record<string, string | undefined> {
return Object.fromEntries([...GATEWAY_CONFIG_SELECTION_ENV_KEYS].map((key) => [key, env[key]]));
}
function restoreGatewayConfigSelectionEnvironment(
snapshot: Record<string, string | undefined>,
): void {
for (const key of GATEWAY_CONFIG_SELECTION_ENV_KEYS) {
const value = snapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
function resolveGatewayRunDotEnvPaths(params: {
env: NodeJS.ProcessEnv;
join: (...paths: string[]) => string;
resolve: (path: string) => string;
resolveConfigDir: (env: NodeJS.ProcessEnv) => string;
resolveStateDir: (env: NodeJS.ProcessEnv) => string;
}): { additionalEnvPaths?: string[]; stateEnvPath: string } {
const stateEnvPath = params.join(params.resolveStateDir(params.env), ".env");
const configEnvPath = params.join(params.resolveConfigDir(params.env), ".env");
return params.resolve(stateEnvPath) === params.resolve(configEnvPath)
? { stateEnvPath }
: { additionalEnvPaths: [configEnvPath], stateEnvPath };
}
function resolveInvocationDestructiveOverride(): string | undefined {
if (process.env.OPENCLAW_SERVICE_MARKER?.trim()) {
delete process.env[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV];
return undefined;
}
return process.env[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV];
}
function applyInvocationDestructiveOverride(value: string | undefined): void {
if (process.env.OPENCLAW_SERVICE_MARKER?.trim() || value === undefined) {
delete process.env[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV];
} else {
process.env[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV] = value;
}
}
function restoreGatewayEnvChanges(params: {
before: Record<string, string | undefined>;
after: Record<string, string | undefined>;
preservedKeys?: ReadonlySet<string>;
}): void {
const keys = new Set([...Object.keys(params.before), ...Object.keys(params.after)]);
for (const key of keys) {
const preservedKey = process.platform === "win32" ? key.toUpperCase() : key;
if (params.preservedKeys?.has(preservedKey)) {
continue;
}
if (params.before[key] === params.after[key] || process.env[key] !== params.after[key]) {
continue;
}
const previous = params.before[key];
if (previous === undefined) {
delete process.env[key];
} else {
process.env[key] = previous;
}
}
}
function restoreSupersededGatewaySelectionEnv(params: {
beforeCurrentPass: Record<string, string | undefined>;
environmentSelection?: GatewayRunEnvironmentSelection;
}): void {
restoreGatewayEnvChanges({
before: params.beforeCurrentPass,
after: { ...process.env },
preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS,
});
if (params.environmentSelection) {
// Remove only values introduced by the early selection phase. Later mutations such as
// managed-proxy env differ from the recorded after-snapshot and remain intact.
restoreGatewayEnvChanges({
before: params.environmentSelection.before,
after: params.environmentSelection.after,
preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS,
});
}
}
function restoreAppliedGatewayRunConfigEnvironment(preserveSelection = true): void {
const applied = appliedGatewayRunConfigEnvironment;
appliedGatewayRunConfigEnvironment = undefined;
if (!applied) {
return;
}
restoreGatewayEnvChanges({
before: applied.before,
after: applied.after,
...(preserveSelection ? { preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS } : {}),
});
}
async function readGuardedGatewayRunConfig(
params: GatewayRunGuardParams,
): Promise<ConfigFileSnapshot | null> {
const { readConfigFileSnapshot } = await import("../../config/config.js");
const { createConfigIO } = await import("../../config/io.factory.js");
return await withArtifactPreservingStateReads(async () => {
const current = await readConfigFileSnapshot({
isolateEnv: true,
observe: false,
pluginValidation: "core-only",
});
const guard = (snapshot: ConfigFileSnapshot) =>
enforceGatewayRunFutureConfigGuard({ ...params, snapshot });
if (!guard(current)) {
return null;
}
const recovery = await createConfigIO({
configPath: current.path,
env: cloneEnvWithPlatformSemantics(process.env),
observe: false,
pluginValidation: "core-only",
}).prepareConfigRecovery(current);
return recovery ? (guard(recovery.snapshot) ? recovery.snapshot : null) : current;
});
}
async function isSameGatewayRunConfigSnapshot(
expected: ConfigFileSnapshot,
current: ConfigFileSnapshot,
options: { allowPathChange?: boolean } = {},
): Promise<boolean> {
const { hashRuntimeConfigValue } = await import("../../config/runtime-snapshot.js");
return (
(options.allowPathChange || current.path === expected.path) &&
current.exists === expected.exists &&
(current.hash ?? current.raw) === (expected.hash ?? expected.raw) &&
hashRuntimeConfigValue(current.sourceConfig) === hashRuntimeConfigValue(expected.sourceConfig)
);
}
function resolveGatewayConfigSelectionDeclarationSignature(
entries: Record<string, string>,
): string {
const normalized = new Map(
Object.entries(entries).map(([key, value]) => [key.toUpperCase(), value]),
);
return JSON.stringify(
[...GATEWAY_CONFIG_SELECTION_ENV_KEYS].map((key) => [key, normalized.get(key)]),
);
}
async function guardGatewayRunSelectedConfig(
params: GatewayRunGuardParams & {
environmentSelection?: GatewayRunEnvironmentSelection;
},
): Promise<boolean> {
lastGuardedGatewayRunSnapshot = undefined;
const [
path,
{ applyConfigEnvVars, isConfigRuntimeEnvVarAllowed },
{ loadGlobalRuntimeDotEnvFiles },
{ normalizeEnv },
{ normalizeStateDirEnv, resolveStateDir },
{ resolveConfigDir },
{ collectEnvSecretRefIds },
{ clearMissingManagedServiceEnvKeys, readManagedSystemdServiceEnvKeysFromEnvironment },
] = await Promise.all([
import("node:path"),
import("../../config/config-env-vars.js"),
import("../../infra/dotenv-global.js"),
import("../../infra/env.js"),
import("../../config/paths.js"),
import("../../utils.js"),
import("../../config/resolution-facts.js"),
import("../../daemon/service-managed-env.js"),
]);
const invocationDestructiveOverride = resolveInvocationDestructiveOverride();
if (params.environmentSelection) {
restoreAppliedGatewayRunConfigEnvironment();
restoreGatewayEnvChanges({
before: params.environmentSelection.before,
after: params.environmentSelection.after,
preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS,
});
}
const applyTrustedGatewayEnv = () => {
normalizeStateDirEnv(process.env);
const loaded = loadGlobalRuntimeDotEnvFiles({
...(gatewayRunTargetSelectedByConfig ? { entryFilter: isConfigRuntimeEnvVarAllowed } : {}),
overrideKeys: readManagedSystemdServiceEnvKeysFromEnvironment(process.env),
quiet: true,
...resolveGatewayRunDotEnvPaths({
env: process.env,
join: path.join,
resolve: path.resolve,
resolveConfigDir,
resolveStateDir,
}),
});
normalizeStateDirEnv(process.env);
normalizeEnv();
applyInvocationDestructiveOverride(invocationDestructiveOverride);
return loaded;
};
const applySelectedConfigEnv = (snapshot: ConfigFileSnapshot) => {
restoreAppliedGatewayRunConfigEnvironment(params.opts.reset !== true);
if (snapshot.valid && params.opts.reset !== true) {
const envBeforeApply = { ...process.env };
applyConfigEnvVars(snapshot.sourceConfig, process.env);
normalizeStateDirEnv(process.env);
normalizeEnv();
appliedGatewayRunConfigEnvironment = {
before: envBeforeApply,
after: { ...process.env },
};
}
applyInvocationDestructiveOverride(invocationDestructiveOverride);
};
for (;;) {
const envBeforeTrustedApply = { ...process.env };
const trustedSelectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const trustedEnvLoad = applyTrustedGatewayEnv();
if (resolveGatewayConfigSelectionSignature(process.env) !== trustedSelectionSignature) {
const stateEnvSelectedTarget = trustedEnvLoad.stateEnvAppliedKeys.some((key) =>
GATEWAY_CONFIG_SELECTION_ENV_KEYS.has(key.toUpperCase()),
);
if (stateEnvSelectedTarget) {
const fallbackSelectorKeys = new Set(
trustedEnvLoad.gatewayEnvAppliedKeys
.map((key) => key.toUpperCase())
.filter((key) => GATEWAY_CONFIG_SELECTION_ENV_KEYS.has(key)),
);
restoreGatewayEnvChanges({
before: envBeforeTrustedApply,
after: { ...process.env },
preservedKeys: new Set(
[...GATEWAY_CONFIG_SELECTION_ENV_KEYS].filter((key) => !fallbackSelectorKeys.has(key)),
),
});
}
// A trusted dotenv selected another state/config target. Keep only its selectors so
// credentials from the superseded dotenv cannot win over the selected target's dotenv.
restoreSupersededGatewaySelectionEnv({
beforeCurrentPass: envBeforeTrustedApply,
environmentSelection: params.environmentSelection,
});
continue;
}
const snapshot = await readGuardedGatewayRunConfig(params);
if (!snapshot) {
return false;
}
if (!snapshot.valid && params.opts.reset) {
// Invalid config source is untrusted. In particular, applying its env block could let an
// off-root $include self-authorize OPENCLAW_INCLUDE_ROOTS on the next read. Only explicit dev
// reset may proceed as the recovery path; ordinary startup skips mutation-capable bootstrap.
lastGuardedGatewayRunSnapshot = snapshot;
return true;
}
const trustedSnapshot = startupRepair.resolveStartupConfigSnapshot(snapshot);
if (!trustedSnapshot) {
return false;
}
// The service marker also owns config SecretRefs. Only dotenv-absent keys with no current
// config reference are stale; clearing the broad marker blindly would drop file-backed refs.
clearMissingManagedServiceEnvKeys({
environment: process.env,
managedKeys: readManagedSystemdServiceEnvKeysFromEnvironment(process.env),
presentKeys: trustedEnvLoad.dotenvPresentKeys,
// Startup repair may relocate a referenced setting, which retires the recorded path along
// with it. The read that produced this snapshot still names every variable the config
// depends on, and keeping a key one boot too long only defers cleanup, while dropping a
// live one refuses startup outright.
preserveKeys: new Set([
...collectEnvSecretRefIds(trustedSnapshot.sourceConfig),
...collectEnvSecretRefIds(snapshot.sourceConfig),
]),
});
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
applySelectedConfigEnv(trustedSnapshot);
// Only selection inputs survive a selection hop. Reload credentials once the final config and
// state dotenv are stable so a superseded profile cannot contaminate the selected gateway.
if (resolveGatewayConfigSelectionSignature(process.env) !== selectionSignature) {
// Config-selected roots have only config-level trust. Their dotenv files must keep the same
// blocked-key boundary instead of becoming operator-trusted sources on the next pass.
gatewayRunTargetSelectedByConfig = true;
restoreSupersededGatewaySelectionEnv({
beforeCurrentPass: envBeforeTrustedApply,
environmentSelection: params.environmentSelection,
});
continue;
}
// Migration admission owns repairs; selection cannot write config health or restore backups.
lastGuardedGatewayRunSnapshot = snapshot;
return true;
}
}
async function guardGatewayRunReset(params: GatewayRunGuardParams): Promise<boolean> {
gatewayRunTargetSelectedByConfig = false;
const envBeforeGuard = { ...process.env };
try {
return await guardGatewayRunSelectedConfig(params);
} finally {
// Config being deleted cannot authorize or retarget its own reset. Restore its env layer first,
// then retain only invocation/trusted selectors through deletion and recreation.
restoreAppliedGatewayRunConfigEnvironment(false);
// Reset keeps only the selected config/state target. Credentials and other env from the
// config being deleted must not survive into the replacement config or gateway runtime.
restoreGatewayEnvChanges({
before: envBeforeGuard,
after: { ...process.env },
preservedKeys: GATEWAY_RESET_SELECTION_ENV_KEYS,
});
}
}
export async function recheckGatewayRunReset(params: GatewayRunGuardParams): Promise<boolean> {
const expected = preparedGatewayRunReset;
preparedGatewayRunReset = undefined;
const rejectDrift = async () => {
if (expected) {
restoreGatewayConfigSelectionEnvironment(expected.selectionEnvironment);
await pinGatewayRunRuntimePaths();
}
params.runtime.error(
"Refusing to reset the dev gateway state because the selected config or state target changed during startup. Retry the reset so the new target can be validated.",
);
params.runtime.exit(1);
return false;
};
if (
!expected ||
resolveGatewayConfigSelectionSignature(process.env) !== expected.selectionSignature
) {
return await rejectDrift();
}
if (!(await guardGatewayRunReset(params))) {
return false;
}
const current = lastGuardedGatewayRunSnapshot;
if (
resolveGatewayConfigSelectionSignature(process.env) !== expected.selectionSignature ||
!current ||
!(await isSameGatewayRunConfigSnapshot(expected.snapshot, current))
) {
return await rejectDrift();
}
return true;
}
export async function applyFinalGatewayRunConfigEnv(params: {
lowerPrecedenceEnv?: Readonly<Record<string, string>>;
runtime: RuntimeEnv;
snapshot: ConfigFileSnapshot;
}): Promise<boolean> {
const preparedSnapshot = preparedGatewayRunBootstrap?.snapshot;
preparedGatewayRunBootstrap = undefined;
if (!params.snapshot.valid) {
restoreAppliedGatewayRunConfigEnvironment(false);
if (preparedSnapshot) {
params.runtime.error(
"Refusing to start the gateway because the final config read became invalid. Retry startup after fixing the config.",
);
params.runtime.exit(1);
return false;
}
await pinGatewayRunRuntimePaths();
return true;
}
const invocationDestructiveOverride = resolveInvocationDestructiveOverride();
const envBeforeApply = { ...process.env };
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const [
{
applyConfigEnvVars,
collectConfigRuntimeEnvOwnership,
collectConfigRuntimeEnvVars,
initializePublishedConfigRuntimeEnv,
},
{ normalizeEnv },
{ normalizeStateDirEnv },
{ clearShellEnvAppliedKeys },
] = await Promise.all([
import("../../config/config-env-vars.js"),
import("../../infra/env.js"),
import("../../config/paths.js"),
import("../../infra/shell-env.js"),
]);
const finalConfigEnv = collectConfigRuntimeEnvVars(params.snapshot.sourceConfig);
if (
preparedSnapshot &&
resolveGatewayConfigSelectionDeclarationSignature(
collectConfigRuntimeEnvVars(preparedSnapshot.sourceConfig),
) !== resolveGatewayConfigSelectionDeclarationSignature(finalConfigEnv)
) {
params.runtime.error(
"Refusing to start the gateway because the final config read changed config or state selection. Retry startup so the selected target can be validated.",
);
params.runtime.exit(1);
return false;
}
restoreAppliedGatewayRunConfigEnvironment();
const envBeforeConfigApply = { ...process.env };
const replacedLowerPrecedenceKeys: string[] = [];
applyConfigEnvVars(params.snapshot.sourceConfig, process.env, {
lowerPrecedenceEnv: params.lowerPrecedenceEnv,
onLowerPrecedenceKeysReplaced: (keys) => {
replacedLowerPrecedenceKeys.push(...keys);
clearShellEnvAppliedKeys(keys);
},
});
normalizeStateDirEnv(process.env);
normalizeEnv();
applyInvocationDestructiveOverride(invocationDestructiveOverride);
appliedGatewayRunConfigEnvironment = {
before: envBeforeApply,
after: { ...process.env },
};
if (resolveGatewayConfigSelectionSignature(process.env) === selectionSignature) {
initializePublishedConfigRuntimeEnv(params.snapshot.sourceConfig, {
ownedEnv: collectConfigRuntimeEnvOwnership(
params.snapshot.sourceConfig,
envBeforeConfigApply,
process.env,
{ replacedLowerPrecedenceKeys },
),
});
return true;
}
appliedGatewayRunConfigEnvironment = undefined;
restoreGatewayEnvChanges({ before: envBeforeApply, after: { ...process.env } });
params.runtime.error(
"Refusing to start the gateway because the final config read changed config or state selection. Retry startup so the selected target can be validated.",
);
params.runtime.exit(1);
return false;
}
export function clearGatewayRunConfigEnvironment(): void {
restoreAppliedGatewayRunConfigEnvironment();
resetPublishedConfigRuntimeEnv();
}
export async function reloadTrustedGatewayRunEnvironment(params: {
runtime: RuntimeEnv;
}): Promise<boolean> {
const [
path,
{ isConfigRuntimeEnvVarAllowed },
{ loadGlobalRuntimeDotEnvFiles },
{ normalizeEnv },
{ normalizeStateDirEnv, resolveStateDir },
{ resolveConfigDir },
{ readManagedSystemdServiceEnvKeysFromEnvironment },
] = await Promise.all([
import("node:path"),
import("../../config/env-vars.js"),
import("../../infra/dotenv-global.js"),
import("../../infra/env.js"),
import("../../config/paths.js"),
import("../../utils.js"),
import("../../daemon/service-managed-env.js"),
]);
const envBeforeReload = { ...process.env };
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const invocationDestructiveOverride = resolveInvocationDestructiveOverride();
normalizeStateDirEnv(process.env);
loadGlobalRuntimeDotEnvFiles({
...(gatewayRunTargetSelectedByConfig ? { entryFilter: isConfigRuntimeEnvVarAllowed } : {}),
overrideKeys: readManagedSystemdServiceEnvKeysFromEnvironment(process.env),
quiet: true,
...resolveGatewayRunDotEnvPaths({
env: process.env,
join: path.join,
resolve: path.resolve,
resolveConfigDir,
resolveStateDir,
}),
});
normalizeStateDirEnv(process.env);
normalizeEnv();
applyInvocationDestructiveOverride(invocationDestructiveOverride);
if (resolveGatewayConfigSelectionSignature(process.env) !== selectionSignature) {
// Runtime modules already derived process-stable paths before startup mutations. A replacement
// dotenv cannot select another target without splitting the running gateway across state dirs.
restoreGatewayEnvChanges({ before: envBeforeReload, after: { ...process.env } });
applyInvocationDestructiveOverride(invocationDestructiveOverride);
await pinGatewayRunRuntimePaths();
params.runtime.error(
"Refusing to start the gateway because trusted dotenv reload after startup mutations changed config or state selection. Retry startup so the selected target can be validated.",
);
params.runtime.exit(1);
return false;
}
await pinGatewayRunRuntimePaths();
return true;
}
export async function selectGatewayRunEnvironment(params: GatewayRunGuardParams): Promise<boolean> {
gatewayRunTargetSelectedByConfig = false;
preparedGatewayRunBootstrap = undefined;
preparedGatewayRunReset = undefined;
restoreAppliedGatewayRunConfigEnvironment(params.opts.reset !== true);
const envBeforeGuard = { ...process.env };
selectedGatewayRunEnvironment = undefined;
let guarded: boolean;
try {
guarded = await guardGatewayRunSelectedConfig(params);
} finally {
if (params.opts.reset) {
restoreAppliedGatewayRunConfigEnvironment(false);
restoreGatewayEnvChanges({
before: envBeforeGuard,
after: { ...process.env },
preservedKeys: GATEWAY_RESET_SELECTION_ENV_KEYS,
});
}
}
selectedGatewayRunEnvironment = {
before: envBeforeGuard,
after: { ...process.env },
};
await pinGatewayRunRuntimePaths();
return guarded;
}
export async function prepareGatewayRunBootstrap(params: GatewayRunGuardParams): Promise<boolean> {
preparedGatewayRunReset = undefined;
preparedGatewayRunStateWasPristine = false;
preparedGatewayRunCoreStateWasPristine = false;
const pristineSelectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const { planPristineStartupConfigMigrations, planPristineStartupStateMigrations } =
await import("../../commands/doctor/shared/pristine-startup-state.js");
const pristineStatePlan = planPristineStartupStateMigrations(process.env);
// Stop the early proxy before selection can choose another config/state target. Its lifecycle
// restores the underlying env snapshot so the selected target's trusted dotenv can replace it.
await getGatewayRunRuntimeHooks().releaseManagedProxy?.();
const environmentSelection = selectedGatewayRunEnvironment;
selectedGatewayRunEnvironment = undefined;
if (!environmentSelection) {
gatewayRunTargetSelectedByConfig = false;
}
const guarded = params.opts.reset
? await guardGatewayRunReset(params)
: await guardGatewayRunSelectedConfig({
...params,
environmentSelection,
});
// Config can change without changing its selected path. Revalidate the final authored
// file while retaining the pre-guard physical-state fact, or stateful config could skip.
const guardedConfigPlan = planPristineStartupConfigMigrations(
guarded ? lastGuardedGatewayRunSnapshot?.parsed : undefined,
process.env,
);
preparedGatewayRunStateWasPristine =
guarded &&
!params.opts.reset &&
pristineStatePlan.skipAllStateMigrations &&
guardedConfigPlan.skipAllStateMigrations &&
resolveGatewayConfigSelectionSignature(process.env) === pristineSelectionSignature;
preparedGatewayRunCoreStateWasPristine =
guarded &&
!params.opts.reset &&
pristineStatePlan.skipCoreStateMigrations &&
guardedConfigPlan.skipCoreStateMigrations &&
resolveGatewayConfigSelectionSignature(process.env) === pristineSelectionSignature;
await pinGatewayRunRuntimePaths();
// Dev reset deletes the state directory before recreating config. Migrating first would
// archive legacy state and then delete its imported SQLite rows.
const shouldBootstrap = guarded && !params.opts.reset;
preparedGatewayRunBootstrap =
shouldBootstrap && lastGuardedGatewayRunSnapshot
? {
snapshot: lastGuardedGatewayRunSnapshot,
allowUnconfigured: params.opts.allowUnconfigured === true,
dev:
Boolean(params.opts.dev) ||
normalizeOptionalLowercaseString(process.env.OPENCLAW_PROFILE) === "dev",
}
: undefined;
if (guarded && params.opts.reset && lastGuardedGatewayRunSnapshot) {
preparedGatewayRunReset = {
selectionEnvironment: snapshotGatewayConfigSelectionEnvironment(process.env),
selectionSignature: resolveGatewayConfigSelectionSignature(process.env),
snapshot: lastGuardedGatewayRunSnapshot,
};
}
return shouldBootstrap;
}
/** Prepared fact captured before Gateway bootstrap can create runtime state. */
export function wasPreparedGatewayRunStatePristine(): boolean {
return preparedGatewayRunStateWasPristine;
}
/** Prepared fact keeps plugin-only configs out of unrelated core migration discovery. */
export function wasPreparedGatewayRunCoreStatePristine(): boolean {
return preparedGatewayRunCoreStateWasPristine;
}
export async function recheckGatewayRunBootstrap(
params: GatewayRunGuardParams & { snapshot?: ConfigFileSnapshot },
): Promise<boolean> {
// This callback can run while startup preflight owns the shared migration lease.
// Throw a typed exit so its finally releases the lease before the CLI exits.
const deferredExitRuntime: RuntimeEnv = {
...params.runtime,
exit: (code) => {
throw new ExitError(code);
},
};
const expected = preparedGatewayRunBootstrap?.snapshot;
if (!expected) {
params.runtime.error(
"Refusing to run automatic gateway startup migrations without a prepared config snapshot. Retry startup.",
);
throw new ExitError(1);
}
const current = params.snapshot
? enforceGatewayRunFutureConfigGuard({
opts: params.opts,
runtime: deferredExitRuntime,
snapshot: params.snapshot,
})
? params.snapshot
: null
: await readGuardedGatewayRunConfig({ ...params, runtime: deferredExitRuntime });
if (!current) {
return false;
}
// The writer-stamped repair is the only config mutation allowed between selection and launch;
// accepting a broader difference here would turn the drift guard into an invalid-config bypass.
if (
(await isSameGatewayRunConfigSnapshot(expected, current, {
allowPathChange: params.snapshot !== undefined,
})) ||
startupRepair.isStartupConfigRepairResult(expected, current)
) {
return true;
}
params.runtime.error(
"Refusing to run automatic gateway startup migrations because the selected config changed during startup. Retry startup so the new config can be validated.",
);
throw new ExitError(1);
}