From a40499b21a2b3e52e4a58db2589471df45e0c076 Mon Sep 17 00:00:00 2001 From: Altay Date: Wed, 13 May 2026 17:34:45 +0300 Subject: [PATCH] fix(test): isolate auth profile secrets in test state (#81393) Merged via squash. Prepared head SHA: fde8787cb7174777c4ddb4c10e8e53da15ed2d61 Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + scripts/lib/openclaw-test-state.mjs | 25 ++++++++++ test/scripts/openclaw-test-state.test.ts | 63 ++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984d38f38e3..a9b12050bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ Docs: https://docs.openclaw.ai - WhatsApp: finish handling pending debounced inbound messages before closing the socket. (#81246) Thanks @mcaxtr. - CLI/commitments: write `--json` output to stdout instead of diagnostic logs so automation can parse commitment list and dismiss results. (#81215) Thanks @giodl73-repo. - Update: allow pnpm GitHub-source OpenClaw updates to approve the OpenClaw package build, so source installs complete their prepare/prepack lifecycle. (#81294) Thanks @fuller-stack-dev. +- Test state: seed isolated auth-profile secret keys for generated homes, preventing helper-backed proof runs from falling back to host Keychain secrets. (#81393) Thanks @altaywtf. ### Changes diff --git a/scripts/lib/openclaw-test-state.mjs b/scripts/lib/openclaw-test-state.mjs index d4cd1f2284c..a8986b7bda1 100644 --- a/scripts/lib/openclaw-test-state.mjs +++ b/scripts/lib/openclaw-test-state.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { randomBytes } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -252,6 +253,27 @@ function renderExports(env) { .join("\n"); } +function generateAuthProfileSecretKey() { + return randomBytes(32).toString("hex"); +} + +function renderAuthProfileSecretKeyExport() { + return [ + 'OPENCLAW_AUTH_PROFILE_SECRET_KEY_FILE="$OPENCLAW_TEST_STATE_HOME/.openclaw-test-auth-profile-secret-key"', + 'if [ -s "$OPENCLAW_AUTH_PROFILE_SECRET_KEY_FILE" ]; then', + ' OPENCLAW_AUTH_PROFILE_SECRET_KEY="$(cat "$OPENCLAW_AUTH_PROFILE_SECRET_KEY_FILE")"', + "else", + ' OPENCLAW_AUTH_PROFILE_SECRET_KEY="$(od -An -N 32 -tx1 /dev/urandom | tr -d " \\n")"', + ' ( umask 077; printf "%s\\n" "$OPENCLAW_AUTH_PROFILE_SECRET_KEY" > "$OPENCLAW_AUTH_PROFILE_SECRET_KEY_FILE" )', + "fi", + 'if [ -z "$OPENCLAW_AUTH_PROFILE_SECRET_KEY" ]; then', + ' echo "failed to generate OPENCLAW_AUTH_PROFILE_SECRET_KEY" >&2', + " return 1 2>/dev/null || exit 1", + "fi", + "export OPENCLAW_AUTH_PROFILE_SECRET_KEY", + ]; +} + function renderConfigWrite(configPathExpression, config) { if (config === undefined) { return ""; @@ -282,6 +304,7 @@ function buildCreatePlan(options = {}) { OPENCLAW_HOME: home, OPENCLAW_STATE_DIR: stateDir, OPENCLAW_CONFIG_PATH: configPath, + OPENCLAW_AUTH_PROFILE_SECRET_KEY: generateAuthProfileSecretKey(), ...scenarioEnv(scenario), }; return { @@ -330,6 +353,7 @@ export function renderShellSnippet(options = {}) { 'export OPENCLAW_HOME="$OPENCLAW_TEST_STATE_HOME"', 'export OPENCLAW_STATE_DIR="$OPENCLAW_TEST_STATE_HOME/.openclaw"', 'export OPENCLAW_CONFIG_PATH="$OPENCLAW_STATE_DIR/openclaw.json"', + ...renderAuthProfileSecretKeyExport(), 'export OPENCLAW_TEST_WORKSPACE_DIR="$OPENCLAW_TEST_STATE_HOME/workspace"', 'mkdir -p "$OPENCLAW_STATE_DIR" "$OPENCLAW_TEST_WORKSPACE_DIR"', ]; @@ -373,6 +397,7 @@ export function renderShellFunction() { export OPENCLAW_HOME="$OPENCLAW_TEST_STATE_HOME" export OPENCLAW_STATE_DIR="$OPENCLAW_TEST_STATE_HOME/.openclaw" export OPENCLAW_CONFIG_PATH="$OPENCLAW_STATE_DIR/openclaw.json" + ${renderAuthProfileSecretKeyExport().join("\n ")} export OPENCLAW_TEST_WORKSPACE_DIR="$OPENCLAW_TEST_STATE_HOME/workspace" unset OPENCLAW_AGENT_DIR unset PI_CODING_AGENT_DIR diff --git a/test/scripts/openclaw-test-state.test.ts b/test/scripts/openclaw-test-state.test.ts index a5665c2960f..3c3fc9285f1 100644 --- a/test/scripts/openclaw-test-state.test.ts +++ b/test/scripts/openclaw-test-state.test.ts @@ -19,6 +19,8 @@ function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } +const secretKeyPattern = /^[a-f0-9]{64}$/u; + describe("scripts/lib/openclaw-test-state", () => { it("creates a sourceable env file and JSON description", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-state-script-")); @@ -47,12 +49,14 @@ describe("scripts/lib/openclaw-test-state", () => { expect(payload.stateDir).toBe(path.join(payload.home, ".openclaw")); expect(payload.configPath).toBe(path.join(payload.stateDir, "openclaw.json")); expect(payload.workspaceDir).toBe(path.join(payload.home, "workspace")); + expect(payload.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY).toMatch(secretKeyPattern); expect(payload.env).toEqual({ HOME: payload.home, USERPROFILE: payload.home, OPENCLAW_HOME: payload.home, OPENCLAW_STATE_DIR: payload.stateDir, OPENCLAW_CONFIG_PATH: payload.configPath, + OPENCLAW_AUTH_PROFILE_SECRET_KEY: payload.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY, }); expect(payload.config).toEqual({ update: { @@ -66,14 +70,16 @@ describe("scripts/lib/openclaw-test-state", () => { expect(envFileText).toContain("export OPENCLAW_HOME="); expect(envFileText).toContain("export OPENCLAW_STATE_DIR="); expect(envFileText).toContain("export OPENCLAW_CONFIG_PATH="); + expect(envFileText).toContain("export OPENCLAW_AUTH_PROFILE_SECRET_KEY="); const probe = await execFileAsync("bash", [ "-lc", - `source ${shellQuote(envFile)}; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,stateDir:process.env.OPENCLAW_STATE_DIR,channel:config.update.channel}));'`, + `source ${shellQuote(envFile)}; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,stateDir:process.env.OPENCLAW_STATE_DIR,secretKey:process.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY,channel:config.update.channel}));'`, ]); expect(JSON.parse(probe.stdout)).toEqual({ home: payload.home, stateDir: payload.stateDir, + secretKey: payload.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY, channel: "stable", }); await fs.rm(payload.root, { recursive: true, force: true }); @@ -106,7 +112,7 @@ describe("scripts/lib/openclaw-test-state", () => { const probe = await execFileAsync("bash", [ "-lc", - `source ${shellQuote(snippetFile)}; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,openclawHome:process.env.OPENCLAW_HOME,workspace:process.env.OPENCLAW_TEST_WORKSPACE_DIR,channel:config.update.channel}));'; rm -rf "$HOME"`, + `source ${shellQuote(snippetFile)}; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,openclawHome:process.env.OPENCLAW_HOME,workspace:process.env.OPENCLAW_TEST_WORKSPACE_DIR,secretKey:process.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY,channel:config.update.channel}));'; rm -rf "$HOME"`, ]); const payload = JSON.parse(probe.stdout); @@ -116,6 +122,7 @@ describe("scripts/lib/openclaw-test-state", () => { ); expect(payload.openclawHome).toBe(payload.home); expect(payload.workspace).toBe(`${payload.home}/workspace`); + expect(payload.secretKey).toMatch(secretKeyPattern); expect(payload.channel).toBe("stable"); const customTemp = path.join(tempRoot, "state-tmp"); @@ -135,6 +142,52 @@ describe("scripts/lib/openclaw-test-state", () => { } }); + it("keeps shell key generation independent of node", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-state-path-node-")); + const fakeBin = path.join(tempRoot, "bin"); + const snippetFile = path.join(tempRoot, "state.sh"); + const functionFile = path.join(tempRoot, "state-function.sh"); + try { + await fs.mkdir(fakeBin, { recursive: true }); + await fs.writeFile( + path.join(fakeBin, "node"), + "#!/bin/sh\necho 'fake node should not be used for key generation' >&2\nexit 42\n", + "utf8", + ); + await fs.chmod(path.join(fakeBin, "node"), 0o755); + + const shell = await execFileAsync(process.execPath, [ + scriptPath, + "shell", + "--label", + "path-node", + "--scenario", + "empty", + ]); + await fs.writeFile(snippetFile, shell.stdout, "utf8"); + + const shellProbe = await execFileAsync("bash", [ + "-lc", + `export PATH=${shellQuote(fakeBin)}:$PATH; source ${shellQuote(snippetFile)}; printf '%s' "$OPENCLAW_AUTH_PROFILE_SECRET_KEY"; rm -rf "$HOME"`, + ]); + expect(shellProbe.stdout).toMatch(secretKeyPattern); + + const renderedFunction = await execFileAsync(process.execPath, [ + scriptPath, + "shell-function", + ]); + await fs.writeFile(functionFile, renderedFunction.stdout, "utf8"); + + const functionProbe = await execFileAsync("bash", [ + "-lc", + `export PATH=${shellQuote(fakeBin)}:$PATH; export OPENCLAW_TEST_STATE_TMPDIR=${shellQuote(path.join(tempRoot, "function-tmp"))}; source ${shellQuote(functionFile)}; openclaw_test_state_create "path node" minimal; printf '%s' "$OPENCLAW_AUTH_PROFILE_SECRET_KEY"; rm -rf "$HOME"`, + ]); + expect(functionProbe.stdout).toMatch(secretKeyPattern); + } finally { + await fs.rm(tempRoot, { recursive: true, force: true }); + } + }); + it("creates the upgrade survivor scenario", async () => { const { stdout } = await execFileAsync(process.execPath, [ scriptPath, @@ -182,7 +235,7 @@ describe("scripts/lib/openclaw-test-state", () => { const probe = await execFileAsync("bash", [ "-lc", - `export OPENCLAW_TEST_STATE_TMPDIR=${shellQuote(path.join(tempRoot, "function-tmp"))}; source ${shellQuote(snippetFile)}; export OPENCLAW_AGENT_DIR=/tmp/outside-agent; openclaw_test_state_create "onboard case" minimal; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,tmpDir:process.env.OPENCLAW_TEST_STATE_TMPDIR,agentDir:process.env.OPENCLAW_AGENT_DIR || null,workspace:process.env.OPENCLAW_TEST_WORKSPACE_DIR,config}));'; rm -rf "$HOME"`, + `export OPENCLAW_TEST_STATE_TMPDIR=${shellQuote(path.join(tempRoot, "function-tmp"))}; source ${shellQuote(snippetFile)}; export OPENCLAW_AGENT_DIR=/tmp/outside-agent; openclaw_test_state_create "onboard case" minimal; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,tmpDir:process.env.OPENCLAW_TEST_STATE_TMPDIR,agentDir:process.env.OPENCLAW_AGENT_DIR || null,workspace:process.env.OPENCLAW_TEST_WORKSPACE_DIR,secretKey:process.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY,config}));'; rm -rf "$HOME"`, ]); const payload = JSON.parse(probe.stdout); @@ -190,16 +243,18 @@ describe("scripts/lib/openclaw-test-state", () => { expect(payload.home).toContain("/openclaw-onboard-case-minimal-home."); expect(payload.agentDir).toBeNull(); expect(payload.workspace).toBe(`${payload.home}/workspace`); + expect(payload.secretKey).toMatch(secretKeyPattern); expect(payload.config).toStrictEqual({}); const existingHome = path.join(tempRoot, "existing-home"); const existingProbe = await execFileAsync("bash", [ "-lc", - `source ${shellQuote(snippetFile)}; openclaw_test_state_create ${shellQuote(existingHome)} minimal; printf '{"kept":true}\\n' > "$OPENCLAW_CONFIG_PATH"; openclaw_test_state_create ${shellQuote(existingHome)} empty; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,config}));'`, + `source ${shellQuote(snippetFile)}; openclaw_test_state_create ${shellQuote(existingHome)} minimal; firstKey="$OPENCLAW_AUTH_PROFILE_SECRET_KEY"; export firstKey; printf '{"kept":true}\\n' > "$OPENCLAW_CONFIG_PATH"; openclaw_test_state_create ${shellQuote(existingHome)} empty; node -e 'const fs=require("node:fs"); const config=JSON.parse(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8")); process.stdout.write(JSON.stringify({home:process.env.HOME,secretKey:process.env.OPENCLAW_AUTH_PROFILE_SECRET_KEY,firstKey:process.env.firstKey,config}));'`, ]); const existingPayload = JSON.parse(existingProbe.stdout); expect(existingPayload.home).toBe(existingHome); + expect(existingPayload.secretKey).toBe(existingPayload.firstKey); expect(existingPayload.config).toEqual({ kept: true }); } finally { await fs.rm(tempRoot, { recursive: true, force: true });