From ada4ee08d9659e26a8c3366a402007194dc3a98b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 23:41:35 +0000 Subject: [PATCH] fix(docker): land #33097 from @chengzhichao-xydt Landed from contributor PR #33097 by @chengzhichao-xydt. Co-authored-by: Zhichao Cheng --- CHANGELOG.md | 1 + docker-compose.yml | 4 +-- docker-setup.sh | 30 +++++++++++++++++-- src/docker-setup.e2e.test.ts | 56 ++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6eb2d81bf8..1c910f29abd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -306,6 +306,7 @@ Docs: https://docs.openclaw.ai - Nodes/system.run allow-always persistence: honor shell comment semantics during allowlist analysis so `#`-tailed payloads that never execute are not persisted as trusted follow-up commands. Thanks @tdjackey for reporting. - Signal/inbound attachment fan-in: forward all successfully fetched inbound attachments through `MediaPaths`/`MediaUrls`/`MediaTypes` (instead of only the first), and improve multi-attachment placeholder summaries in mention-gated pending history. (#39212) Thanks @joeykrug. - Nodes/system.run dispatch-wrapper boundary: keep shell-wrapper approval classification active at the depth boundary so `env` wrapper stacks cannot reach `/bin/sh -c` execution without the expected approval gate. Thanks @tdjackey for reporting. +- Docker/token persistence on reconfigure: reuse the existing `.env` gateway token during `docker-setup.sh` reruns and align compose token env defaults, so Docker installs stop silently rotating tokens and breaking existing dashboard sessions. Landed from contributor PR #33097 by @chengzhichao-xydt. Thanks @chengzhichao-xydt. ## 2026.3.2 diff --git a/docker-compose.yml b/docker-compose.yml index a17558157f7..cc7169d3a88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: environment: HOME: /home/node TERM: xterm-256color - OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} + OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-} OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: ${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-} CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-} CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-} @@ -59,7 +59,7 @@ services: environment: HOME: /home/node TERM: xterm-256color - OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} + OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-} OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: ${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-} BROWSER: echo CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-} diff --git a/docker-setup.sh b/docker-setup.sh index 205394ff36b..450c2025ffa 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -80,6 +80,24 @@ NODE fi } +read_env_gateway_token() { + local env_path="$1" + local line="" + local token="" + if [[ ! -f "$env_path" ]]; then + return 0 + fi + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then + token="${line#OPENCLAW_GATEWAY_TOKEN=}" + fi + done <"$env_path" + if [[ -n "$token" ]]; then + printf '%s' "$token" + fi +} + ensure_control_ui_allowed_origins() { if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then return 0 @@ -219,14 +237,20 @@ if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN" echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json" - elif command -v openssl >/dev/null 2>&1; then - OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" else - OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY' + DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)" + if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then + OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN" + echo "Reusing gateway token from $ROOT_DIR/.env" + elif command -v openssl >/dev/null 2>&1; then + OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" + else + OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY' import secrets print(secrets.token_hex(32)) PY )" + fi fi fi export OPENCLAW_GATEWAY_TOKEN diff --git a/src/docker-setup.e2e.test.ts b/src/docker-setup.e2e.test.ts index 813cc62edce..c6e60abe2c3 100644 --- a/src/docker-setup.e2e.test.ts +++ b/src/docker-setup.e2e.test.ts @@ -250,6 +250,55 @@ describe("docker-setup.sh", () => { expect(envFile).toContain("OPENCLAW_GATEWAY_TOKEN=config-token-123"); // pragma: allowlist secret }); + it("reuses existing .env token when OPENCLAW_GATEWAY_TOKEN and config token are unset", async () => { + const activeSandbox = requireSandbox(sandbox); + const configDir = join(activeSandbox.rootDir, "config-dotenv-token-reuse"); + const workspaceDir = join(activeSandbox.rootDir, "workspace-dotenv-token-reuse"); + await mkdir(configDir, { recursive: true }); + await writeFile( + join(activeSandbox.rootDir, ".env"), + "OPENCLAW_GATEWAY_TOKEN=dotenv-token-123\nOPENCLAW_GATEWAY_PORT=18789\n", + ); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_GATEWAY_TOKEN: undefined, + OPENCLAW_CONFIG_DIR: configDir, + OPENCLAW_WORKSPACE_DIR: workspaceDir, + }); + + expect(result.status).toBe(0); + const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + expect(envFile).toContain("OPENCLAW_GATEWAY_TOKEN=dotenv-token-123"); // pragma: allowlist secret + expect(result.stderr).toBe(""); + }); + + it("reuses the last non-empty .env token and strips CRLF without truncating '='", async () => { + const activeSandbox = requireSandbox(sandbox); + const configDir = join(activeSandbox.rootDir, "config-dotenv-last-wins"); + const workspaceDir = join(activeSandbox.rootDir, "workspace-dotenv-last-wins"); + await mkdir(configDir, { recursive: true }); + await writeFile( + join(activeSandbox.rootDir, ".env"), + [ + "OPENCLAW_GATEWAY_TOKEN=", + "OPENCLAW_GATEWAY_TOKEN=first-token", + "OPENCLAW_GATEWAY_TOKEN=last=token=value\r", + ].join("\n"), + ); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_GATEWAY_TOKEN: undefined, + OPENCLAW_CONFIG_DIR: configDir, + OPENCLAW_WORKSPACE_DIR: workspaceDir, + }); + + expect(result.status).toBe(0); + const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + expect(envFile).toContain("OPENCLAW_GATEWAY_TOKEN=last=token=value"); // pragma: allowlist secret + expect(envFile).not.toContain("OPENCLAW_GATEWAY_TOKEN=first-token"); + expect(envFile).not.toContain("\r"); + }); + it("treats OPENCLAW_SANDBOX=0 as disabled", async () => { const activeSandbox = requireSandbox(sandbox); await writeFile(activeSandbox.logPath, ""); @@ -399,4 +448,11 @@ describe("docker-setup.sh", () => { expect(compose).toContain('network_mode: "service:openclaw-gateway"'); expect(compose).toContain("depends_on:\n - openclaw-gateway"); }); + + it("keeps docker-compose gateway token env defaults aligned across services", async () => { + const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8"); + expect(compose.match(/OPENCLAW_GATEWAY_TOKEN: \$\{OPENCLAW_GATEWAY_TOKEN:-\}/g)).toHaveLength( + 2, + ); + }); });