Skip to content

Commit 2d575bc

Browse files
committed
fix(onboarding): pin health auth during setup
1 parent 8b4a5d7 commit 2d575bc

7 files changed

Lines changed: 182 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Docs: https://docs.openclaw.ai
1515
- Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger `RangeError: Maximum call stack size exceeded`. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.
1616
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
1717
- Gateway/models: add `models.pricing.enabled` so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.
18-
- Onboarding: pin the final QuickStart health check to the just-configured setup token so stale `OPENCLAW_GATEWAY_TOKEN` values or older config tokens do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.
18+
- Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale `OPENCLAW_GATEWAY_TOKEN` or `OPENCLAW_GATEWAY_PASSWORD` values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.
1919
- Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.
2020
- Build/runtime: write the runtime-postbuild stamp after `pnpm build` writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.
2121
- Build/runtime: preserve staged bundled-plugin runtime dependency caches across source-checkout tsdown rebuilds, so local CLI and gateway-watch rebuilds no longer recreate large plugin dependency trees before starting. Refs #73205. Thanks @SymbolStar.

src/commands/health.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,21 @@ describe("healthCommand", () => {
106106
callGatewayMock.mockResolvedValueOnce(snapshot);
107107

108108
await healthCommand(
109-
{ json: true, timeoutMs: 5000, config: {}, token: "setup-token" },
109+
{
110+
json: true,
111+
timeoutMs: 5000,
112+
config: {},
113+
token: "setup-token",
114+
password: "setup-password",
115+
},
110116
runtime as never,
111117
);
112118

113119
expect(callGatewayMock).toHaveBeenCalledWith(
114120
expect.objectContaining({
115121
method: "health",
116122
token: "setup-token",
123+
password: "setup-password",
117124
}),
118125
);
119126
});

src/commands/onboard-non-interactive.gateway-health-auth.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ async function writeSecureFile(filePath: string, content: string): Promise<void>
2121

2222
describe("resolveGatewayHealthProbeToken", () => {
2323
const originalGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
24+
const originalGatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD;
2425

2526
afterEach(() => {
2627
if (originalGatewayToken === undefined) {
2728
delete process.env.OPENCLAW_GATEWAY_TOKEN;
2829
} else {
2930
process.env.OPENCLAW_GATEWAY_TOKEN = originalGatewayToken;
3031
}
32+
if (originalGatewayPassword === undefined) {
33+
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
34+
} else {
35+
process.env.OPENCLAW_GATEWAY_PASSWORD = originalGatewayPassword;
36+
}
3137
});
3238

3339
it("resolves file SecretRefs for the local onboarding health probe without persisting plaintext", async () => {
@@ -92,4 +98,24 @@ describe("resolveGatewayHealthProbeToken", () => {
9298
expect(resolved.unresolvedRefReason).toContain("gateway.auth.token SecretRef is unresolved");
9399
});
94100
});
101+
102+
it("resolves password auth for the local onboarding health probe", async () => {
103+
process.env.OPENCLAW_GATEWAY_TOKEN = "stale-env-token";
104+
process.env.OPENCLAW_GATEWAY_PASSWORD = "resolved-password"; // pragma: allowlist secret
105+
106+
const resolved = await resolveGatewayHealthProbeToken({
107+
gateway: {
108+
auth: {
109+
mode: "password",
110+
password: {
111+
source: "env",
112+
provider: "default",
113+
id: "OPENCLAW_GATEWAY_PASSWORD",
114+
},
115+
},
116+
},
117+
} as OpenClawConfig);
118+
119+
expect(resolved).toEqual({ password: "resolved-password" });
120+
});
95121
});

src/commands/onboard-non-interactive.gateway.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,44 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
456456
});
457457
}, 60_000);
458458

459+
it("passes pinned gateway auth through non-interactive health checks", async () => {
460+
await withStateDir("state-local-daemon-health-auth-", async (stateDir) => {
461+
const token = "tok_noninteractive_health";
462+
waitForGatewayReachableMock = vi.fn(async () => ({ ok: true }));
463+
464+
await runNonInteractiveSetup(
465+
{
466+
...createLocalDaemonSetupOptions(stateDir),
467+
gatewayAuth: "token",
468+
gatewayToken: token,
469+
},
470+
runtime,
471+
);
472+
473+
expect(waitForGatewayReachableMock).toHaveBeenCalledWith(
474+
expect.objectContaining({
475+
token,
476+
password: undefined,
477+
}),
478+
);
479+
expect(healthCommandMock).toHaveBeenCalledWith(
480+
expect.objectContaining({
481+
token,
482+
password: undefined,
483+
config: expect.objectContaining({
484+
gateway: expect.objectContaining({
485+
auth: expect.objectContaining({
486+
mode: "token",
487+
token,
488+
}),
489+
}),
490+
}),
491+
}),
492+
expect.any(Object),
493+
);
494+
});
495+
}, 60_000);
496+
459497
it("uses longer Windows health timings for daemon install probes", () => {
460498
expect(resolveInstallDaemonGatewayHealthTiming("win32")).toEqual({
461499
deadlineMs: 90_000,

src/commands/onboard-non-interactive/local.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { replaceConfigFile, resolveGatewayPort } from "../../config/config.js";
33
import { logConfigUpdated } from "../../config/logging.js";
44
import type { OpenClawConfig } from "../../config/types.openclaw.js";
55
import { resolveGatewayAuthToken } from "../../gateway/auth-token-resolution.js";
6+
import { resolveConfiguredSecretInputString } from "../../gateway/resolve-configured-secret-input-string.js";
67
import type { RuntimeEnv } from "../../runtime.js";
78
import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js";
89
import { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig } from "../onboard-config.js";
@@ -95,7 +96,21 @@ async function collectGatewayHealthFailureDiagnostics(): Promise<
9596

9697
export async function resolveGatewayHealthProbeToken(
9798
nextConfig: OpenClawConfig,
98-
): Promise<{ token?: string; unresolvedRefReason?: string }> {
99+
): Promise<{ token?: string; password?: string; unresolvedRefReason?: string }> {
100+
if (nextConfig.gateway?.auth?.mode === "password") {
101+
const resolved = await resolveConfiguredSecretInputString({
102+
config: nextConfig,
103+
env: process.env,
104+
value: nextConfig.gateway.auth.password,
105+
path: "gateway.auth.password",
106+
unresolvedReasonStyle: "detailed",
107+
});
108+
return {
109+
password: resolved.value,
110+
unresolvedRefReason: resolved.unresolvedRefReason,
111+
};
112+
}
113+
99114
const resolved = await resolveGatewayAuthToken({
100115
cfg: nextConfig,
101116
env: process.env,
@@ -269,6 +284,7 @@ export async function runNonInteractiveLocalSetup(params: {
269284
const probe = await waitForGatewayReachable({
270285
url: links.wsUrl,
271286
token: probeAuth.token,
287+
password: probeAuth.password,
272288
deadlineMs: opts.installDaemon
273289
? installDaemonGatewayHealthTiming.deadlineMs
274290
: ATTACH_EXISTING_GATEWAY_HEALTH_DEADLINE_MS,
@@ -318,6 +334,9 @@ export async function runNonInteractiveLocalSetup(params: {
318334
timeoutMs: opts.installDaemon
319335
? installDaemonGatewayHealthTiming.healthCommandTimeoutMs
320336
: 10_000,
337+
config: nextConfig,
338+
token: probeAuth.token,
339+
password: probeAuth.password,
321340
},
322341
runtime,
323342
);

src/wizard/setup.finalize.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,71 @@ describe("finalizeSetupWizard", () => {
614614
);
615615
});
616616

617+
it("uses the resolved setup password for health checks", async () => {
618+
vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", "env-password");
619+
resolveSetupSecretInputString.mockResolvedValueOnce("session-password");
620+
const prompter = createLaterPrompter();
621+
622+
await finalizeSetupWizard({
623+
flow: "quickstart",
624+
opts: {
625+
acceptRisk: true,
626+
authChoice: "skip",
627+
installDaemon: false,
628+
skipHealth: false,
629+
skipUi: true,
630+
},
631+
baseConfig: {},
632+
nextConfig: {
633+
gateway: {
634+
auth: {
635+
mode: "password",
636+
password: {
637+
source: "env",
638+
provider: "default",
639+
id: "OPENCLAW_GATEWAY_PASSWORD",
640+
},
641+
},
642+
},
643+
},
644+
workspaceDir: "/tmp",
645+
settings: {
646+
port: 18789,
647+
bind: "loopback",
648+
authMode: "password",
649+
gatewayToken: undefined,
650+
tailscaleMode: "off",
651+
tailscaleResetOnExit: false,
652+
},
653+
prompter,
654+
runtime: createRuntime(),
655+
});
656+
657+
expect(waitForGatewayReachable).toHaveBeenCalledWith(
658+
expect.objectContaining({
659+
url: "ws://127.0.0.1:18789",
660+
token: undefined,
661+
password: "session-password",
662+
}),
663+
);
664+
expect(healthCommand).toHaveBeenCalledWith(
665+
expect.objectContaining({
666+
json: false,
667+
timeoutMs: 10_000,
668+
token: undefined,
669+
password: "session-password",
670+
config: expect.objectContaining({
671+
gateway: expect.objectContaining({
672+
auth: expect.objectContaining({
673+
mode: "password",
674+
}),
675+
}),
676+
}),
677+
}),
678+
expect.any(Object),
679+
);
680+
});
681+
617682
it("shows actionable gateway guidance instead of a hard error in no-daemon onboarding", async () => {
618683
waitForGatewayReachable.mockResolvedValue({
619684
ok: false,

src/wizard/setup.finalize.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export async function finalizeSetupWizard(
6363
): Promise<{ launchedTui: boolean }> {
6464
const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options;
6565
let gatewayProbe: { ok: boolean; detail?: string } = { ok: true };
66+
let resolvedGatewayPassword = "";
6667

6768
const withWizardProgress = async <T>(
6869
label: string,
@@ -236,6 +237,26 @@ export async function finalizeSetupWizard(
236237
}
237238
}
238239

240+
if (settings.authMode === "password") {
241+
try {
242+
resolvedGatewayPassword =
243+
(await resolveSetupSecretInputString({
244+
config: nextConfig,
245+
value: nextConfig.gateway?.auth?.password,
246+
path: "gateway.auth.password",
247+
env: process.env,
248+
})) ?? "";
249+
} catch (error) {
250+
await prompter.note(
251+
[
252+
"Could not resolve gateway.auth.password SecretRef for setup auth.",
253+
formatErrorMessage(error),
254+
].join("\n"),
255+
"Gateway auth",
256+
);
257+
}
258+
}
259+
239260
if (!opts.skipHealth) {
240261
const probeLinks = resolveControlUiLinks({
241262
bind: nextConfig.gateway?.bind ?? "loopback",
@@ -247,7 +268,8 @@ export async function finalizeSetupWizard(
247268
// Daemon install/restart can briefly flap the WS; wait a bit so health check doesn't false-fail.
248269
gatewayProbe = await waitForGatewayReachable({
249270
url: probeLinks.wsUrl,
250-
token: settings.gatewayToken,
271+
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
272+
password: settings.authMode === "password" ? resolvedGatewayPassword : undefined,
251273
deadlineMs: 15_000,
252274
});
253275
if (gatewayProbe.ok) {
@@ -272,6 +294,7 @@ export async function finalizeSetupWizard(
272294
timeoutMs: 10_000,
273295
config: healthConfig,
274296
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
297+
password: settings.authMode === "password" ? resolvedGatewayPassword : undefined,
275298
},
276299
runtime,
277300
);
@@ -348,27 +371,6 @@ export async function finalizeSetupWizard(
348371
settings.authMode === "token" && settings.gatewayToken
349372
? `${links.httpUrl}#token=${encodeURIComponent(settings.gatewayToken)}`
350373
: links.httpUrl;
351-
let resolvedGatewayPassword = "";
352-
if (settings.authMode === "password") {
353-
try {
354-
resolvedGatewayPassword =
355-
(await resolveSetupSecretInputString({
356-
config: nextConfig,
357-
value: nextConfig.gateway?.auth?.password,
358-
path: "gateway.auth.password",
359-
env: process.env,
360-
})) ?? "";
361-
} catch (error) {
362-
await prompter.note(
363-
[
364-
"Could not resolve gateway.auth.password SecretRef for setup auth.",
365-
formatErrorMessage(error),
366-
].join("\n"),
367-
"Gateway auth",
368-
);
369-
}
370-
}
371-
372374
if (opts.skipHealth || !gatewayProbe.ok) {
373375
gatewayProbe = await probeGatewayReachable({
374376
url: links.wsUrl,

0 commit comments

Comments
 (0)