Files
openclaw/src/cli/gateway-cli/pre-bootstrap.ts
2026-07-10 00:53:10 +01:00

697 lines
25 KiB
TypeScript

// 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 type { ConfigFileSnapshot } from "../../config/types.js";
import { ExitError, type RuntimeEnv } from "../../runtime.js";
import type { GatewayRunPreBootstrapOptions } from "./future-config-guard.js";
import { enforceGatewayRunFutureConfigGuard } from "./future-config-guard.js";
import { getGatewayRunRuntimeHooks } from "./runtime-hooks.js";
type GatewayRunGuardParams = {
opts: GatewayRunPreBootstrapOptions;
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 preparedGatewayRunBootstrapSnapshot: ConfigFileSnapshot | undefined;
let preparedGatewayRunReset: PreparedGatewayRunReset | undefined;
let gatewayRunTargetSelectedByConfig = false;
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_CONFIG_SELECTION_ENV_KEYS = new Set([
"ANDROID_DATA",
"HOME",
"HOMEDRIVE",
"HOMEPATH",
"OPENCLAW_AGENT_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_HOME",
"OPENCLAW_INCLUDE_ROOTS",
"OPENCLAW_NIX_MODE",
"OPENCLAW_OAUTH_DIR",
"OPENCLAW_PACKAGE_DIR",
"OPENCLAW_PROFILE",
"OPENCLAW_STATE_DIR",
"OPENCLAW_TEST_FAST",
"OPENCLAW_WORKSPACE_DIR",
"PI_CODING_AGENT_DIR",
"PREFIX",
"USERPROFILE",
]);
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 snapshot = await readConfigFileSnapshot({ isolateEnv: true, observe: false });
return enforceGatewayRunFutureConfigGuard({
opts: params.opts,
runtime: params.runtime,
snapshot,
})
? snapshot
: null;
}
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 recoverGuardedGatewayRunConfig(
params: GatewayRunGuardParams & { restoreSuspicious: boolean },
): Promise<ConfigFileSnapshot | null> {
const { readConfigFileSnapshot } = await import("../../config/config.js");
let recoveryAllowed = true;
const recoveredSnapshot = await readConfigFileSnapshot({
isolateEnv: true,
recoverSuspicious: true,
allowSuspiciousRecovery: (config, current) => {
recoveryAllowed = enforceGatewayRunFutureConfigGuard({
opts: params.opts,
runtime: params.runtime,
config: current,
});
if (recoveryAllowed) {
recoveryAllowed = enforceGatewayRunFutureConfigGuard({
opts: params.opts,
runtime: params.runtime,
config,
});
}
return params.restoreSuspicious && recoveryAllowed;
},
});
if (!recoveryAllowed) {
return null;
}
// Recovery can select a different config, so enforce the same guard again before migrations.
return enforceGatewayRunFutureConfigGuard({
opts: params.opts,
runtime: params.runtime,
snapshot: recoveredSnapshot,
})
? recoveredSnapshot
: null;
}
async function guardGatewayRunSelectedConfig(
params: GatewayRunGuardParams & {
environmentSelection?: GatewayRunEnvironmentSelection;
recoverSuspicious: boolean;
restoreSuspicious: boolean;
},
): Promise<boolean> {
lastGuardedGatewayRunSnapshot = undefined;
const [
path,
{ applyConfigEnvVars, isConfigRuntimeEnvVarAllowed },
{ loadGlobalRuntimeDotEnvFiles },
{ normalizeEnv },
{ normalizeStateDirEnv, resolveStateDir },
{ resolveConfigDir },
] = 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"),
]);
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 } : {}),
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) {
// 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.
if (params.opts.reset) {
lastGuardedGatewayRunSnapshot = snapshot;
}
return params.opts.reset === true;
}
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
applySelectedConfigEnv(snapshot);
// 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;
}
if (!params.recoverSuspicious) {
lastGuardedGatewayRunSnapshot = snapshot;
return true;
}
// Recovery writes audit/config state, so run it only after config and state selection is stable.
const recoveredSnapshot = await recoverGuardedGatewayRunConfig(params);
if (!recoveredSnapshot) {
return false;
}
if (recoveredSnapshot.path !== snapshot.path || recoveredSnapshot.hash !== snapshot.hash) {
// Recovery replaced the selected config. Discard every env mutation from the old selection
// chain before converging again so rejected credentials cannot survive into the backup.
restoreSupersededGatewaySelectionEnv({
beforeCurrentPass: envBeforeTrustedApply,
environmentSelection: params.environmentSelection,
});
continue;
}
const envBeforeRecoveredApply = { ...process.env };
const recoveredSelectionSignature = resolveGatewayConfigSelectionSignature(process.env);
applySelectedConfigEnv(recoveredSnapshot);
if (resolveGatewayConfigSelectionSignature(process.env) === recoveredSelectionSignature) {
lastGuardedGatewayRunSnapshot = recoveredSnapshot;
return true;
}
restoreSupersededGatewaySelectionEnv({
beforeCurrentPass: envBeforeRecoveredApply,
environmentSelection: params.environmentSelection,
});
gatewayRunTargetSelectedByConfig = true;
}
}
async function guardGatewayRunReset(params: GatewayRunGuardParams): Promise<boolean> {
gatewayRunTargetSelectedByConfig = false;
const envBeforeGuard = { ...process.env };
try {
return await guardGatewayRunSelectedConfig({
...params,
recoverSuspicious: true,
restoreSuspicious: false,
});
} 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 = preparedGatewayRunBootstrapSnapshot;
preparedGatewayRunBootstrapSnapshot = 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, collectConfigRuntimeEnvVars },
{ normalizeEnv },
{ normalizeStateDirEnv },
{ clearShellEnvAppliedKeys },
] = await Promise.all([
import("../../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();
applyConfigEnvVars(params.snapshot.sourceConfig, process.env, {
lowerPrecedenceEnv: params.lowerPrecedenceEnv,
onLowerPrecedenceKeysReplaced: clearShellEnvAppliedKeys,
});
normalizeStateDirEnv(process.env);
normalizeEnv();
applyInvocationDestructiveOverride(invocationDestructiveOverride);
appliedGatewayRunConfigEnvironment = {
before: envBeforeApply,
after: { ...process.env },
};
if (resolveGatewayConfigSelectionSignature(process.env) === selectionSignature) {
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();
}
export async function reloadTrustedGatewayRunEnvironment(params: {
runtime: RuntimeEnv;
}): Promise<boolean> {
const [
path,
{ isConfigRuntimeEnvVarAllowed },
{ loadGlobalRuntimeDotEnvFiles },
{ normalizeEnv },
{ normalizeStateDirEnv, resolveStateDir },
{ resolveConfigDir },
] = 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"),
]);
const envBeforeReload = { ...process.env };
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const invocationDestructiveOverride = resolveInvocationDestructiveOverride();
normalizeStateDirEnv(process.env);
loadGlobalRuntimeDotEnvFiles({
...(gatewayRunTargetSelectedByConfig ? { entryFilter: isConfigRuntimeEnvVarAllowed } : {}),
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;
preparedGatewayRunBootstrapSnapshot = undefined;
preparedGatewayRunReset = undefined;
restoreAppliedGatewayRunConfigEnvironment(params.opts.reset !== true);
const envBeforeGuard = { ...process.env };
selectedGatewayRunEnvironment = undefined;
let guarded: boolean;
try {
guarded = await guardGatewayRunSelectedConfig({
...params,
recoverSuspicious: false,
restoreSuspicious: false,
});
} 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;
// Stop the early proxy before recovery can select 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,
recoverSuspicious: true,
restoreSuspicious: true,
});
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;
preparedGatewayRunBootstrapSnapshot = shouldBootstrap ? lastGuardedGatewayRunSnapshot : undefined;
if (guarded && params.opts.reset && lastGuardedGatewayRunSnapshot) {
preparedGatewayRunReset = {
selectionEnvironment: snapshotGatewayConfigSelectionEnvironment(process.env),
selectionSignature: resolveGatewayConfigSelectionSignature(process.env),
snapshot: lastGuardedGatewayRunSnapshot,
};
}
return shouldBootstrap;
}
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 = preparedGatewayRunBootstrapSnapshot;
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;
}
if (
await isSameGatewayRunConfigSnapshot(expected, current, {
allowPathChange: params.snapshot !== undefined,
})
) {
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);
}