Fix onboard ignoring OPENCLAW_GATEWAY_TOKEN env var (#22658)

* Fix onboard ignoring OPENCLAW_GATEWAY_TOKEN env var

When running onboard via docker-setup.sh, the QuickStart wizard
generates its own 48-char token instead of using the 64-char token
already set in OPENCLAW_GATEWAY_TOKEN. This causes a token mismatch
that breaks all CLI commands after setup.

Check process.env.OPENCLAW_GATEWAY_TOKEN before falling back to
randomToken() in both the interactive QuickStart path and the
non-interactive path.

Closes #22638

Co-authored-by: Clawborn <tianrun.yang103@gmail.com>

* Tests: cover quickstart env token fallback

* Changelog: note docker onboarding token parity fix

* Tests: restore env var after non-interactive token fallback test

* Update CHANGELOG.md

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Clawborn
2026-03-02 11:40:40 +08:00
committed by GitHub
parent 8e69fd80e0
commit 77ccd35e5e
5 changed files with 88 additions and 4 deletions

View File

@@ -151,6 +151,7 @@ Docs: https://docs.openclaw.ai
- Agents/FS workspace default: honor documented host file-tool default `tools.fs.workspaceOnly=false` when unset so host `write`/`edit` calls are not incorrectly workspace-restricted unless explicitly enabled. Landed from contributor PR #31128 by @SaucePackets. Thanks @SaucePackets.
- Cron/Timer hot-loop guard: enforce a minimum timer re-arm delay when stale past-due jobs would otherwise trigger repeated `setTimeout(0)` loops, preventing event-loop saturation and log-flood behavior. (#29853) Thanks @FlamesCN.
- Gateway/CLI session recovery: handle expired CLI session IDs gracefully by clearing stale session state and retrying without crashing gateway runs. Landed from contributor PR #31090 by @frankekn. Thanks @frankekn.
- Onboarding/Docker token parity: use `OPENCLAW_GATEWAY_TOKEN` as the default gateway token in interactive and non-interactive onboarding when `--gateway-token` is not provided, so `docker-setup.sh` token env/config values stay aligned. (#22658) Fixes #22638. Thanks @Clawborn and @vincentkoc.
- Slack/Subagent completion delivery: stop forcing bound conversation IDs into `threadId` so Slack completion announces do not send invalid `thread_ts` for DMs/top-level channels. Landed from contributor PR #31105 by @stakeswky. Thanks @stakeswky.
- Signal/Loop protection: evaluate own-account detection before sync-message filtering (including UUID-only `accountUuid` configs) so `sentTranscript` sync events cannot bypass loop protection and self-reply loops. Landed from contributor PR #31093 by @kevinWangSheng. Thanks @kevinWangSheng.
- Gateway/Control UI origins: support wildcard `"*"` in `gateway.controlUi.allowedOrigins` for trusted remote access setups. Landed from contributor PR #31088 by @frankekn. Thanks @frankekn.

View File

@@ -149,6 +149,46 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
});
}, 60_000);
it("uses OPENCLAW_GATEWAY_TOKEN when --gateway-token is omitted", async () => {
await withStateDir("state-env-token-", async (stateDir) => {
const envToken = "tok_env_fallback_123";
const workspace = path.join(stateDir, "openclaw");
const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
process.env.OPENCLAW_GATEWAY_TOKEN = envToken;
try {
await runNonInteractiveOnboarding(
{
nonInteractive: true,
mode: "local",
workspace,
authChoice: "skip",
skipSkills: true,
skipHealth: true,
installDaemon: false,
gatewayBind: "loopback",
gatewayAuth: "token",
},
runtime,
);
const configPath = resolveStateConfigPath(process.env, stateDir);
const cfg = await readJsonFile<{
gateway?: { auth?: { mode?: string; token?: string } };
}>(configPath);
expect(cfg?.gateway?.auth?.mode).toBe("token");
expect(cfg?.gateway?.auth?.token).toBe(envToken);
} finally {
if (prevToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
}
}
});
}, 60_000);
it("writes gateway.remote url/token and callGateway uses them", async () => {
await withStateDir("state-remote-", async () => {
const port = getPseudoPort(30_000);

View File

@@ -1,6 +1,6 @@
import type { OpenClawConfig } from "../../../config/config.js";
import type { RuntimeEnv } from "../../../runtime.js";
import { randomToken } from "../../onboard-helpers.js";
import { normalizeGatewayTokenInput, randomToken } from "../../onboard-helpers.js";
import type { OnboardOptions } from "../../onboard-types.js";
export function applyNonInteractiveGatewayConfig(params: {
@@ -49,7 +49,10 @@ export function applyNonInteractiveGatewayConfig(params: {
}
let nextConfig = params.nextConfig;
let gatewayToken = opts.gatewayToken?.trim() || undefined;
let gatewayToken =
normalizeGatewayTokenInput(opts.gatewayToken) ||
normalizeGatewayTokenInput(process.env.OPENCLAW_GATEWAY_TOKEN) ||
undefined;
if (authMode === "token") {
if (!gatewayToken) {

View File

@@ -88,6 +88,40 @@ describe("configureGatewayForOnboarding", () => {
"reminders.add",
]);
});
it("prefers OPENCLAW_GATEWAY_TOKEN during quickstart token setup", async () => {
const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
process.env.OPENCLAW_GATEWAY_TOKEN = "token-from-env";
mocks.randomToken.mockReturnValue("generated-token");
mocks.randomToken.mockClear();
const prompter = createPrompter({
selectQueue: ["loopback", "token", "off"],
textQueue: [],
});
const runtime = createRuntime();
try {
const result = await configureGatewayForOnboarding({
flow: "quickstart",
baseConfig: {},
nextConfig: {},
localPort: 18789,
quickstartGateway: createQuickstartGateway("token"),
prompter,
runtime,
});
expect(result.settings.gatewayToken).toBe("token-from-env");
} finally {
if (prevToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
}
}
});
it("does not set password to literal 'undefined' when prompt returns undefined", async () => {
mocks.randomToken.mockReturnValue("unused");

View File

@@ -161,12 +161,18 @@ export async function configureGatewayForOnboarding(
let gatewayToken: string | undefined;
if (authMode === "token") {
if (flow === "quickstart") {
gatewayToken = quickstartGateway.token ?? randomToken();
gatewayToken =
(quickstartGateway.token ??
normalizeGatewayTokenInput(process.env.OPENCLAW_GATEWAY_TOKEN)) ||
randomToken();
} else {
const tokenInput = await prompter.text({
message: "Gateway token (blank to generate)",
placeholder: "Needed for multi-machine or non-loopback access",
initialValue: quickstartGateway.token ?? "",
initialValue:
quickstartGateway.token ??
normalizeGatewayTokenInput(process.env.OPENCLAW_GATEWAY_TOKEN) ??
"",
});
gatewayToken = normalizeGatewayTokenInput(tokenInput) || randomToken();
}