Skip to content

Commit df521a6

Browse files
authored
fix(gateway): guard fast-path startup migrations (#93118)
* fix(cron): run legacy cron store migration in gateway fast path * fix(cli): run gateway startup migrations * fix(gateway): guard startup migrations and config selection * fix(gateway): reconcile final startup environment * fix(gateway): preserve guarded startup env semantics * fix(gateway): guard service-mode recovery candidates * fix(config): reconcile normalized env precedence * fix(cli): clear replaced proxy signal handlers * fix(gateway): reject invalid final config * test(gateway): cover invalid future config reset guard * test(cli): remove unused recovery state
1 parent a0b16f3 commit df521a6

38 files changed

Lines changed: 4767 additions & 272 deletions

src/cli/command-bootstrap.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Shared command preflight: config readiness plus optional plugin registry activation.
2+
import type { ConfigFileSnapshot } from "../config/types.js";
23
import type { RuntimeEnv } from "../runtime.js";
34
import { createLazyImportLoader } from "../shared/lazy-promise.js";
45
import type { CliPluginRegistryPolicy } from "./command-catalog.js";
@@ -18,6 +19,7 @@ export async function ensureCliCommandBootstrap(params: {
1819
suppressDoctorStdout?: boolean;
1920
skipConfigGuard?: boolean;
2021
allowInvalid?: boolean;
22+
beforeStateMigrations?: (snapshot?: ConfigFileSnapshot) => Promise<boolean>;
2123
loadPlugins?: boolean;
2224
pluginRegistry?: CliPluginRegistryPolicy;
2325
}) {
@@ -27,6 +29,9 @@ export async function ensureCliCommandBootstrap(params: {
2729
runtime: params.runtime,
2830
commandPath: params.commandPath,
2931
...(params.allowInvalid ? { allowInvalid: true } : {}),
32+
...(params.beforeStateMigrations
33+
? { beforeStateMigrations: params.beforeStateMigrations }
34+
: {}),
3035
...(params.suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
3136
});
3237
}

src/cli/command-execution-startup.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// CLI startup context, banner/log presentation, and bootstrap orchestration.
2+
import type { ConfigFileSnapshot } from "../config/types.js";
23
import { routeLogsToStderr } from "../logging/console.js";
34
import type { RuntimeEnv } from "../runtime.js";
45
import { resolveCliArgvInvocation } from "./argv-invocation.js";
@@ -65,6 +66,7 @@ export async function ensureCliExecutionBootstrap(params: {
6566
commandPath: string[];
6667
startupPolicy: CliStartupPolicy;
6768
allowInvalid?: boolean;
69+
beforeStateMigrations?: (snapshot?: ConfigFileSnapshot) => Promise<boolean>;
6870
loadPlugins?: boolean;
6971
skipConfigGuard?: boolean;
7072
}) {
@@ -73,6 +75,9 @@ export async function ensureCliExecutionBootstrap(params: {
7375
commandPath: params.commandPath,
7476
suppressDoctorStdout: params.startupPolicy.suppressDoctorStdout,
7577
allowInvalid: params.allowInvalid,
78+
...(params.beforeStateMigrations
79+
? { beforeStateMigrations: params.beforeStateMigrations }
80+
: {}),
7681
loadPlugins: params.loadPlugins ?? params.startupPolicy.loadPlugins,
7782
pluginRegistry: params.startupPolicy.pluginRegistry,
7883
skipConfigGuard: params.skipConfigGuard ?? params.startupPolicy.skipConfigGuard,

src/cli/command-path-policy.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,13 @@ describe("command-path-policy", () => {
339339
expect(resolveCliNetworkProxyPolicy(argv)).toBe("default");
340340
});
341341

342+
it("resolves gateway runs after root options with values", () => {
343+
const argv = ["node", "openclaw", "--log-level", "debug", "gateway", "run"];
344+
345+
expect(resolveCliCatalogCommandPath(argv)).toEqual(["gateway"]);
346+
expect(resolveCliNetworkProxyPolicy(argv)).toBe("default");
347+
});
348+
342349
it("does not let gateway run option values spoof bypass subcommands", () => {
343350
for (const argv of [
344351
["node", "openclaw", "gateway", "--token", "status"],

src/cli/dotenv.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { resolveStateDir } from "../config/paths.js";
44
import { loadGlobalRuntimeDotEnvFiles, loadWorkspaceDotEnvFile } from "../infra/dotenv.js";
55

66
/** Load `.env` files for normal CLI commands without overriding existing process env. */
7-
export function loadCliDotEnv(opts?: { quiet?: boolean }) {
7+
export function loadCliDotEnv(opts?: { loadGlobalEnv?: boolean; quiet?: boolean }) {
88
const quiet = opts?.quiet ?? true;
99
const cwdEnvPath = path.join(process.cwd(), ".env");
1010
loadWorkspaceDotEnvFile(cwdEnvPath, { quiet });
1111

12+
if (opts?.loadGlobalEnv === false) {
13+
return;
14+
}
1215
// Then load the global fallback set without overriding any env vars that
1316
// were already set or loaded from CWD. This includes the Ubuntu fresh-install
1417
// gateway.env compatibility path.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
cloneEnvWithPlatformSemantics,
3+
createConfigRuntimeEnv,
4+
} from "../../config/config-env-vars.js";
5+
import {
6+
ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV,
7+
formatFutureConfigActionBlock,
8+
resolveFutureConfigActionBlock,
9+
} from "../../config/future-version-guard.js";
10+
// Gateway-specific future-config actions shared by pre-bootstrap and runtime startup.
11+
import type { ConfigFileSnapshot, OpenClawConfig } from "../../config/types.js";
12+
import type { RuntimeEnv } from "../../runtime.js";
13+
import type { GatewayRunOpts } from "./run-options.js";
14+
15+
export type GatewayRunPreBootstrapOptions = Pick<GatewayRunOpts, "force" | "reset">;
16+
17+
type GatewayRunFutureConfigGuardParams = {
18+
opts: GatewayRunPreBootstrapOptions;
19+
snapshot?: ConfigFileSnapshot | null;
20+
config?: Pick<OpenClawConfig, "env" | "meta"> | null;
21+
};
22+
23+
function resolveGatewayRunFutureConfigBlock(params: GatewayRunFutureConfigGuardParams) {
24+
const processServiceMode = Boolean(process.env.OPENCLAW_SERVICE_MARKER?.trim());
25+
const candidateConfig =
26+
params.config ??
27+
(params.snapshot?.valid ? (params.snapshot.sourceConfig ?? params.snapshot.config) : undefined);
28+
const candidateServiceMode =
29+
!params.opts.reset &&
30+
Boolean(
31+
candidateConfig
32+
? createConfigRuntimeEnv(candidateConfig, process.env).OPENCLAW_SERVICE_MARKER?.trim()
33+
: undefined,
34+
);
35+
const serviceMode = processServiceMode || candidateServiceMode;
36+
// Reset runs before service/force startup, while ordinary startup now runs state migrations.
37+
const futureAction = params.opts.reset
38+
? { action: "reset the dev gateway state", exitCode: 1 }
39+
: serviceMode
40+
? { action: "start the gateway service", exitCode: 78 }
41+
: params.opts.force
42+
? { action: "force-kill gateway port listeners", exitCode: 1 }
43+
: { action: "run automatic gateway startup migrations", exitCode: 1 };
44+
const guardEnv = serviceMode ? cloneEnvWithPlatformSemantics(process.env) : process.env;
45+
if (serviceMode) {
46+
delete guardEnv[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV];
47+
}
48+
const block = resolveFutureConfigActionBlock({
49+
action: futureAction.action,
50+
snapshot: params.snapshot,
51+
config: params.config,
52+
env: guardEnv,
53+
});
54+
return block ? { block, exitCode: futureAction.exitCode, serviceMode } : null;
55+
}
56+
57+
export function isGatewayRunFutureConfigAllowed(
58+
params: GatewayRunFutureConfigGuardParams,
59+
): boolean {
60+
return resolveGatewayRunFutureConfigBlock(params) === null;
61+
}
62+
63+
export function enforceGatewayRunFutureConfigGuard(
64+
params: GatewayRunFutureConfigGuardParams & { runtime: RuntimeEnv },
65+
): boolean {
66+
const resolved = resolveGatewayRunFutureConfigBlock(params);
67+
if (!resolved) {
68+
return true;
69+
}
70+
if (resolved.serviceMode) {
71+
delete process.env[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV];
72+
}
73+
params.runtime.error(formatFutureConfigActionBlock(resolved.block));
74+
params.runtime.exit(resolved.exitCode);
75+
return false;
76+
}

0 commit comments

Comments
 (0)