diff --git a/CHANGELOG.md b/CHANGELOG.md index 70bc9d1a943..cf5f51d2f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.test.ts index 8e94fd17bfe..5709c41ec80 100644 --- a/src/commands/onboard-non-interactive.gateway.test.ts +++ b/src/commands/onboard-non-interactive.gateway.test.ts @@ -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); diff --git a/src/commands/onboard-non-interactive/local/gateway-config.ts b/src/commands/onboard-non-interactive/local/gateway-config.ts index a786838cefb..0195fd620dc 100644 --- a/src/commands/onboard-non-interactive/local/gateway-config.ts +++ b/src/commands/onboard-non-interactive/local/gateway-config.ts @@ -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) { diff --git a/src/wizard/onboarding.gateway-config.test.ts b/src/wizard/onboarding.gateway-config.test.ts index 8163747289c..3d7963a9f25 100644 --- a/src/wizard/onboarding.gateway-config.test.ts +++ b/src/wizard/onboarding.gateway-config.test.ts @@ -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"); diff --git a/src/wizard/onboarding.gateway-config.ts b/src/wizard/onboarding.gateway-config.ts index 3eadaa5b5c8..a52c9452e56 100644 --- a/src/wizard/onboarding.gateway-config.ts +++ b/src/wizard/onboarding.gateway-config.ts @@ -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(); }