mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(onboarding): pin health auth during setup
This commit is contained in:
@@ -15,7 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -106,7 +106,13 @@ describe("healthCommand", () => {
|
||||
callGatewayMock.mockResolvedValueOnce(snapshot);
|
||||
|
||||
await healthCommand(
|
||||
{ json: true, timeoutMs: 5000, config: {}, token: "setup-token" },
|
||||
{
|
||||
json: true,
|
||||
timeoutMs: 5000,
|
||||
config: {},
|
||||
token: "setup-token",
|
||||
password: "setup-password",
|
||||
},
|
||||
runtime as never,
|
||||
);
|
||||
|
||||
@@ -114,6 +120,7 @@ describe("healthCommand", () => {
|
||||
expect.objectContaining({
|
||||
method: "health",
|
||||
token: "setup-token",
|
||||
password: "setup-password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ async function writeSecureFile(filePath: string, content: string): Promise<void>
|
||||
|
||||
describe("resolveGatewayHealthProbeToken", () => {
|
||||
const originalGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
const originalGatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalGatewayToken === undefined) {
|
||||
@@ -28,6 +29,11 @@ describe("resolveGatewayHealthProbeToken", () => {
|
||||
} else {
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = originalGatewayToken;
|
||||
}
|
||||
if (originalGatewayPassword === undefined) {
|
||||
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
} else {
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD = originalGatewayPassword;
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves file SecretRefs for the local onboarding health probe without persisting plaintext", async () => {
|
||||
@@ -92,4 +98,24 @@ describe("resolveGatewayHealthProbeToken", () => {
|
||||
expect(resolved.unresolvedRefReason).toContain("gateway.auth.token SecretRef is unresolved");
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves password auth for the local onboarding health probe", async () => {
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = "stale-env-token";
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD = "resolved-password"; // pragma: allowlist secret
|
||||
|
||||
const resolved = await resolveGatewayHealthProbeToken({
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENCLAW_GATEWAY_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect(resolved).toEqual({ password: "resolved-password" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -456,6 +456,44 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("passes pinned gateway auth through non-interactive health checks", async () => {
|
||||
await withStateDir("state-local-daemon-health-auth-", async (stateDir) => {
|
||||
const token = "tok_noninteractive_health";
|
||||
waitForGatewayReachableMock = vi.fn(async () => ({ ok: true }));
|
||||
|
||||
await runNonInteractiveSetup(
|
||||
{
|
||||
...createLocalDaemonSetupOptions(stateDir),
|
||||
gatewayAuth: "token",
|
||||
gatewayToken: token,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(waitForGatewayReachableMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token,
|
||||
password: undefined,
|
||||
}),
|
||||
);
|
||||
expect(healthCommandMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token,
|
||||
password: undefined,
|
||||
config: expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
mode: "token",
|
||||
token,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("uses longer Windows health timings for daemon install probes", () => {
|
||||
expect(resolveInstallDaemonGatewayHealthTiming("win32")).toEqual({
|
||||
deadlineMs: 90_000,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { replaceConfigFile, resolveGatewayPort } from "../../config/config.js";
|
||||
import { logConfigUpdated } from "../../config/logging.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveGatewayAuthToken } from "../../gateway/auth-token-resolution.js";
|
||||
import { resolveConfiguredSecretInputString } from "../../gateway/resolve-configured-secret-input-string.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js";
|
||||
import { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig } from "../onboard-config.js";
|
||||
@@ -95,7 +96,21 @@ async function collectGatewayHealthFailureDiagnostics(): Promise<
|
||||
|
||||
export async function resolveGatewayHealthProbeToken(
|
||||
nextConfig: OpenClawConfig,
|
||||
): Promise<{ token?: string; unresolvedRefReason?: string }> {
|
||||
): Promise<{ token?: string; password?: string; unresolvedRefReason?: string }> {
|
||||
if (nextConfig.gateway?.auth?.mode === "password") {
|
||||
const resolved = await resolveConfiguredSecretInputString({
|
||||
config: nextConfig,
|
||||
env: process.env,
|
||||
value: nextConfig.gateway.auth.password,
|
||||
path: "gateway.auth.password",
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
return {
|
||||
password: resolved.value,
|
||||
unresolvedRefReason: resolved.unresolvedRefReason,
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = await resolveGatewayAuthToken({
|
||||
cfg: nextConfig,
|
||||
env: process.env,
|
||||
@@ -269,6 +284,7 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
const probe = await waitForGatewayReachable({
|
||||
url: links.wsUrl,
|
||||
token: probeAuth.token,
|
||||
password: probeAuth.password,
|
||||
deadlineMs: opts.installDaemon
|
||||
? installDaemonGatewayHealthTiming.deadlineMs
|
||||
: ATTACH_EXISTING_GATEWAY_HEALTH_DEADLINE_MS,
|
||||
@@ -318,6 +334,9 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
timeoutMs: opts.installDaemon
|
||||
? installDaemonGatewayHealthTiming.healthCommandTimeoutMs
|
||||
: 10_000,
|
||||
config: nextConfig,
|
||||
token: probeAuth.token,
|
||||
password: probeAuth.password,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
@@ -614,6 +614,71 @@ describe("finalizeSetupWizard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the resolved setup password for health checks", async () => {
|
||||
vi.stubEnv("OPENCLAW_GATEWAY_PASSWORD", "env-password");
|
||||
resolveSetupSecretInputString.mockResolvedValueOnce("session-password");
|
||||
const prompter = createLaterPrompter();
|
||||
|
||||
await finalizeSetupWizard({
|
||||
flow: "quickstart",
|
||||
opts: {
|
||||
acceptRisk: true,
|
||||
authChoice: "skip",
|
||||
installDaemon: false,
|
||||
skipHealth: false,
|
||||
skipUi: true,
|
||||
},
|
||||
baseConfig: {},
|
||||
nextConfig: {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENCLAW_GATEWAY_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp",
|
||||
settings: {
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
authMode: "password",
|
||||
gatewayToken: undefined,
|
||||
tailscaleMode: "off",
|
||||
tailscaleResetOnExit: false,
|
||||
},
|
||||
prompter,
|
||||
runtime: createRuntime(),
|
||||
});
|
||||
|
||||
expect(waitForGatewayReachable).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: undefined,
|
||||
password: "session-password",
|
||||
}),
|
||||
);
|
||||
expect(healthCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
json: false,
|
||||
timeoutMs: 10_000,
|
||||
token: undefined,
|
||||
password: "session-password",
|
||||
config: expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
mode: "password",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows actionable gateway guidance instead of a hard error in no-daemon onboarding", async () => {
|
||||
waitForGatewayReachable.mockResolvedValue({
|
||||
ok: false,
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function finalizeSetupWizard(
|
||||
): Promise<{ launchedTui: boolean }> {
|
||||
const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options;
|
||||
let gatewayProbe: { ok: boolean; detail?: string } = { ok: true };
|
||||
let resolvedGatewayPassword = "";
|
||||
|
||||
const withWizardProgress = async <T>(
|
||||
label: string,
|
||||
@@ -236,6 +237,26 @@ export async function finalizeSetupWizard(
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.authMode === "password") {
|
||||
try {
|
||||
resolvedGatewayPassword =
|
||||
(await resolveSetupSecretInputString({
|
||||
config: nextConfig,
|
||||
value: nextConfig.gateway?.auth?.password,
|
||||
path: "gateway.auth.password",
|
||||
env: process.env,
|
||||
})) ?? "";
|
||||
} catch (error) {
|
||||
await prompter.note(
|
||||
[
|
||||
"Could not resolve gateway.auth.password SecretRef for setup auth.",
|
||||
formatErrorMessage(error),
|
||||
].join("\n"),
|
||||
"Gateway auth",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.skipHealth) {
|
||||
const probeLinks = resolveControlUiLinks({
|
||||
bind: nextConfig.gateway?.bind ?? "loopback",
|
||||
@@ -247,7 +268,8 @@ export async function finalizeSetupWizard(
|
||||
// Daemon install/restart can briefly flap the WS; wait a bit so health check doesn't false-fail.
|
||||
gatewayProbe = await waitForGatewayReachable({
|
||||
url: probeLinks.wsUrl,
|
||||
token: settings.gatewayToken,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
password: settings.authMode === "password" ? resolvedGatewayPassword : undefined,
|
||||
deadlineMs: 15_000,
|
||||
});
|
||||
if (gatewayProbe.ok) {
|
||||
@@ -272,6 +294,7 @@ export async function finalizeSetupWizard(
|
||||
timeoutMs: 10_000,
|
||||
config: healthConfig,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
password: settings.authMode === "password" ? resolvedGatewayPassword : undefined,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
@@ -348,27 +371,6 @@ export async function finalizeSetupWizard(
|
||||
settings.authMode === "token" && settings.gatewayToken
|
||||
? `${links.httpUrl}#token=${encodeURIComponent(settings.gatewayToken)}`
|
||||
: links.httpUrl;
|
||||
let resolvedGatewayPassword = "";
|
||||
if (settings.authMode === "password") {
|
||||
try {
|
||||
resolvedGatewayPassword =
|
||||
(await resolveSetupSecretInputString({
|
||||
config: nextConfig,
|
||||
value: nextConfig.gateway?.auth?.password,
|
||||
path: "gateway.auth.password",
|
||||
env: process.env,
|
||||
})) ?? "";
|
||||
} catch (error) {
|
||||
await prompter.note(
|
||||
[
|
||||
"Could not resolve gateway.auth.password SecretRef for setup auth.",
|
||||
formatErrorMessage(error),
|
||||
].join("\n"),
|
||||
"Gateway auth",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.skipHealth || !gatewayProbe.ok) {
|
||||
gatewayProbe = await probeGatewayReachable({
|
||||
url: links.wsUrl,
|
||||
|
||||
Reference in New Issue
Block a user