diff --git a/.github/workflows/openclaw-release-telegram-qa.yml b/.github/workflows/openclaw-release-telegram-qa.yml index 25fd8f32c5d6..750b7e22b3d2 100644 --- a/.github/workflows/openclaw-release-telegram-qa.yml +++ b/.github/workflows/openclaw-release-telegram-qa.yml @@ -1520,10 +1520,14 @@ jobs: export XDG_CONFIG_HOME="${temp_root}/xdg-config" export XDG_DATA_HOME="${temp_root}/xdg-data" - chown "$RUNNER_UID:$RUNNER_GID" "$temp_root" - chmod 0711 "$temp_root" + # The SUT must create config lock/temp entries beside the runner-owned + # config. Sticky group-write prevents it from replacing that config. + chown "$RUNNER_UID:$SUT_GID" "$temp_root" + chmod 1770 "$temp_root" chown "$RUNNER_UID:$SUT_GID" "$config_path" chmod 0640 "$config_path" + [[ "$(stat -c '%F:%a:%u:%g' "$temp_root")" == "directory:1770:${RUNNER_UID}:${SUT_GID}" ]] + [[ "$(stat -c '%F:%a:%u:%g' "$config_path")" == "regular file:640:${RUNNER_UID}:${SUT_GID}" ]] sut_tmp="${temp_root}/sut-tmp" install -d -o "$SUT_UID" -g "$SUT_GID" -m 0700 "$sut_tmp" export TMPDIR="$sut_tmp" @@ -1834,14 +1838,18 @@ jobs: runtime_stage=write-sandbox-proof printf "%s" "$runtime_sandbox_payload_b64" | base64 -d >&3 exec 3>&- - runtime_stage=exec-runtime - exec "$runtime_node_bin" \ - --import "$runtime_preload_path" \ - "$runtime_candidate_root/dist/index.js" \ - "$@" + runtime_node_args=( + --import "$runtime_preload_path" + "$runtime_candidate_root/dist/index.js" "$@" + ) + else + runtime_node_args=("$runtime_candidate_root/dist/index.js" "$@") fi + # Login Bash reads /etc/bash.bashrc with inherited nounset. + # Add PS1 only after the attested inbound env-key comparison. + export PS1= runtime_stage=exec-runtime - exec "$runtime_node_bin" "$runtime_candidate_root/dist/index.js" "$@" + exec "$runtime_node_bin" "${runtime_node_args[@]}" '\'' openclaw-sut "$@" ' openclaw-sut "$@" LAUNCHER diff --git a/src/gateway/desktop/node-observe.integration.test.ts b/src/gateway/desktop/node-observe.integration.test.ts index 8f6093368989..312363e01cd7 100644 --- a/src/gateway/desktop/node-observe.integration.test.ts +++ b/src/gateway/desktop/node-observe.integration.test.ts @@ -13,6 +13,12 @@ import { createDesktopSessionRegistry } from "./session-registry.js"; const VERSION = Buffer.from("RFB 003.008\n", "ascii"); const cleanups: Array<() => Promise> = []; +function handleExpectedPeerTeardownError(error: NodeJS.ErrnoException): void { + if (error.code !== "ECONNRESET" && error.code !== "EPIPE") { + throw error; + } +} + afterEach(async () => { await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())); }); @@ -86,6 +92,8 @@ describe("paired node desktop observe integration", () => { const rfbServer = net.createServer((socket) => { rfbPeers.add(socket); socket.once("close", () => rfbPeers.delete(socket)); + // Session teardown destroys client sockets; the synthetic server owns the matching resets. + socket.on("error", handleExpectedPeerTeardownError); connectionCount += 1; const connectionIndex = connectionCount; const reader = new SocketReader(socket); diff --git a/src/node-host/desktop-stream-command.test.ts b/src/node-host/desktop-stream-command.test.ts index 916a346ab643..9b33d2b25ede 100644 --- a/src/node-host/desktop-stream-command.test.ts +++ b/src/node-host/desktop-stream-command.test.ts @@ -7,6 +7,12 @@ import { invokeNodeDesktopStream } from "./desktop-stream-command.js"; const TICKET = "a".repeat(48); const cleanups: Array<() => Promise> = []; +function handleExpectedPeerTeardownError(error: NodeJS.ErrnoException): void { + if (error.code !== "ECONNRESET" && error.code !== "EPIPE") { + throw error; + } +} + afterEach(async () => { await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())); }); @@ -46,6 +52,8 @@ describe("node desktop stream command", () => { const rfbServer = net.createServer((socket) => { rfbPeers.add(socket); socket.once("close", () => rfbPeers.delete(socket)); + // Cancellation destroys the client socket; the synthetic server owns the matching reset. + socket.on("error", handleExpectedPeerTeardownError); socket.write(Buffer.from("RFB 003.008\n", "ascii")); socket.once("data", () => socket.write(Buffer.from([1, 2]))); }); diff --git a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts index 044d217a5879..37dd25b20289 100644 --- a/test/scripts/openclaw-release-telegram-qa-workflow.test.ts +++ b/test/scripts/openclaw-release-telegram-qa-workflow.test.ts @@ -853,4 +853,42 @@ describe("release Telegram QA workflow", () => { 'for path in \\\n "$temp_root/workspace" \\\n "${OPENCLAW_HOME:?}"', ); }); + + it("lets the SUT create suite locks without exposing the runner-owned config", () => { + const createSut = requireRun( + "run_telegram", + "Create isolated Telegram SUT identity and launcher", + ); + + expect(createSut).toContain('chown "$RUNNER_UID:$SUT_GID" "$temp_root"'); + expect(createSut).toContain('chmod 1770 "$temp_root"'); + expect(createSut).toContain( + '"$(stat -c \'%F:%a:%u:%g\' "$temp_root")" == "directory:1770:${RUNNER_UID}:${SUT_GID}"', + ); + expect(createSut).toContain('chown "$RUNNER_UID:$SUT_GID" "$config_path"'); + expect(createSut).toContain('chmod 0640 "$config_path"'); + expect(createSut).toContain( + '"$(stat -c \'%F:%a:%u:%g\' "$config_path")" == "regular file:640:${RUNNER_UID}:${SUT_GID}"', + ); + expect(createSut).not.toContain('chmod 0711 "$temp_root"'); + expect(createSut).not.toContain('chmod 1777 "$temp_root"'); + }); + + it("adds an empty PS1 only after attested runtime environment verification", () => { + const createSut = requireRun( + "run_telegram", + "Create isolated Telegram SUT identity and launcher", + ); + const launcher = extractHereDocument(createSut, "LAUNCHER"); + const verification = '[[ "$actual_env_keys_b64" == "$runtime_expected_env_keys_b64" ]]'; + const ps1Export = "export PS1="; + const candidateExec = 'exec "$runtime_node_bin" "${runtime_node_args[@]}"'; + + expect(launcher.match(/export PS1=/gu)).toHaveLength(1); + expect(launcher.indexOf(verification)).toBeGreaterThan(-1); + expect(launcher.indexOf(ps1Export)).toBeGreaterThan(launcher.indexOf(verification)); + expect(launcher.indexOf(candidateExec)).toBeGreaterThan(launcher.indexOf(ps1Export)); + expect(launcher.match(/exec "\$runtime_node_bin"/gu)).toHaveLength(1); + expect(launcher).toContain('grep -Ev "^(PWD|SHLVL|_)$"'); + }); });