Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,25 @@ reviews:
- path: "test/openclaw-tui-chat-correlation.test.ts"
instructions: *e2e-openclaw-tui-chat-correlation

- path: "test/e2e/test-issue-4434-tui-unreachable-inference.sh"
instructions: &e2e-issue-4434-tui-unreachable-inference |
This script is the privileged live repro for #4434. Changes affect
the Linux-only NVIDIA endpoint firewall block, OpenClaw TUI error
visibility, and active-spinner shutdown proof.

**E2E test recommendation:**
- `issue-4434-tui-unreachable-inference-e2e` - opt-in live proof
that unreachable NVIDIA inference surfaces a visible TUI error and
stops the active spinner

To run selectively:
```
gh workflow run nightly-e2e.yaml --ref <branch> -f jobs=issue-4434-tui-unreachable-inference-e2e
```

- path: "test/issue-4434-tui-unreachable-inference.test.ts"
instructions: *e2e-issue-4434-tui-unreachable-inference

- path: "test/e2e/test-openclaw-slack-pairing.sh"
instructions: |
This script validates OpenClaw Slack DM pairing across the gateway
Expand Down
98 changes: 98 additions & 0 deletions .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
# openclaw-inference-switch-e2e
# Switches a running OpenClaw sandbox with `nemoclaw inference set`
# and verifies route, openclaw.json, hashes, and live requests.
# issue-4434-tui-unreachable-inference-e2e
# Recreates #4434's NVIDIA endpoint firewall block and verifies
# OpenClaw TUI shows a visible error and stops the active spinner.
# credential-migration-e2e Validates legacy ~/.nemoclaw/credentials.json migration to the
# OpenShell gateway, secure zero-fill on unlink, allowlist filter
# on non-credential env keys, and symlink-safe deletion.
Expand Down Expand Up @@ -94,6 +97,7 @@ on:
skill-agent-e2e, openclaw-skill-cli-e2e, docs-validation-e2e, messaging-providers-e2e,
openclaw-slack-pairing-e2e,
openclaw-tui-chat-correlation-e2e,
issue-4434-tui-unreachable-inference-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
Expand Down Expand Up @@ -395,6 +399,97 @@ jobs:
path: /tmp/nemoclaw-e2e-openclaw-tui-correlation-install.log
if-no-files-found: ignore

issue-4434-tui-unreachable-inference-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' &&
(github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',issue-4434-tui-unreachable-inference-e2e,'))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# This privileged proof mutates host firewall state and receives
# NVIDIA_API_KEY. Keep the runner script from the trusted workflow ref;
# the product under test is selected separately via
# NEMOCLAW_PUBLIC_INSTALL_REF.
ref: ${{ github.ref }}
fetch-depth: 0
persist-credentials: false

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Resolve trusted public install ref
id: public_install_ref
shell: bash
env:
TARGET_REF: ${{ inputs.target_ref }}
run: |
set -euo pipefail
trusted_head="$(git rev-parse HEAD)"
ref="${TARGET_REF:-$trusted_head}"
if [[ ! "$ref" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::issue #4434 privileged E2E requires target_ref to be a full commit SHA"
exit 1
fi
if ! git cat-file -e "${ref}^{commit}" 2>/dev/null; then
echo "::error::target_ref ${ref} is not present in the trusted workflow checkout"
exit 1
fi
if ! git merge-base --is-ancestor "$ref" "$trusted_head"; then
echo "::error::target_ref ${ref} is not reachable from trusted workflow ref ${trusted_head}"
exit 1
fi
printf 'ref=%s\n' "$ref" >> "$GITHUB_OUTPUT"

- name: "Install issue #4434 test dependencies"
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y expect iptables

- name: "Run issue #4434 TUI unreachable inference E2E test"
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
NEMOCLAW_ISSUE_4434_LIVE: "1"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-issue-4434-tui-unreachable"
NEMOCLAW_PUBLIC_INSTALL_REF: ${{ steps.public_install_ref.outputs.ref }}
GITHUB_TOKEN: ${{ github.token }}
run: bash test/e2e/test-issue-4434-tui-unreachable-inference.sh

- name: "Sanitize issue #4434 logs on failure"
if: failure()
shell: bash
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
for file in /tmp/nemoclaw-e2e-issue-4434-install.log /tmp/nemoclaw-issue-4434.*; do
[ -f "$file" ] || continue
if [ -n "${NVIDIA_API_KEY:-}" ]; then
perl -0pi -e 's/\Q$ENV{NVIDIA_API_KEY}\E/[REDACTED_NVIDIA_API_KEY]/g' "$file"
fi
if [ -n "${GITHUB_TOKEN:-}" ]; then
perl -0pi -e 's/\Q$ENV{GITHUB_TOKEN}\E/[REDACTED_GITHUB_TOKEN]/g' "$file"
fi
perl -0pi -e 's/nvapi-[A-Za-z0-9._-]+/[REDACTED_NVIDIA_API_KEY]/g; s/gh[pousr]_[A-Za-z0-9_]+/[REDACTED_GITHUB_TOKEN]/g' "$file"
done

- name: "Upload issue #4434 logs on failure"
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: issue-4434-tui-unreachable-inference-logs
path: |
/tmp/nemoclaw-e2e-issue-4434-install.log
/tmp/nemoclaw-issue-4434.*
if-no-files-found: ignore

# ── DGX Station GPU optional proof validation (#3600) ──────────
# CI cannot emulate GB300, but this guards the release-blocker mitigation:
# optional direct GPU proofs must not abort onboard before the fatal throw.
Expand Down Expand Up @@ -1919,6 +2014,7 @@ jobs:
messaging-providers-e2e,
openclaw-slack-pairing-e2e,
openclaw-tui-chat-correlation-e2e,
issue-4434-tui-unreachable-inference-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
Expand Down Expand Up @@ -2028,6 +2124,7 @@ jobs:
messaging-providers-e2e,
openclaw-slack-pairing-e2e,
openclaw-tui-chat-correlation-e2e,
issue-4434-tui-unreachable-inference-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
Expand Down Expand Up @@ -2194,6 +2291,7 @@ jobs:
messaging-providers-e2e,
openclaw-slack-pairing-e2e,
openclaw-tui-chat-correlation-e2e,
issue-4434-tui-unreachable-inference-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ RUN npm ci && npm run build
# Stage 2: Runtime image — pull cached base from GHCR
# hadolint ignore=DL3006
FROM ${BASE_IMAGE}
ARG OPENCLAW_VERSION=2026.5.22
ARG OPENCLAW_2026_5_22_INTEGRITY=sha512-m+zgBELGbCHjWB1IWF5WSWNPr480cMKOMff2OF72c8A0AMD4hC/9+qwYtzjYmGkETcffnB711JymlVsQnh2Tow==
ARG OPENCLAW_VERSION=2026.5.27
ARG OPENCLAW_2026_5_27_INTEGRITY=sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==

# Harden: remove unnecessary build tools and network probes from base image (#830)
# Protect runtime tools before autoremove — the GHCR base may predate the
Expand Down Expand Up @@ -110,7 +110,7 @@ RUN set -eu; \
echo "ERROR: OpenClaw build target ${OPENCLAW_VERSION} is below blueprint minimum ${MIN_VER}" >&2; exit 1; \
fi; \
EXPECTED_INTEGRITY=""; \
if [ "$OPENCLAW_VERSION" = "2026.5.22" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_5_22_INTEGRITY"; fi; \
if [ "$OPENCLAW_VERSION" = "2026.5.27" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_5_27_INTEGRITY"; fi; \
if [ -n "$EXPECTED_INTEGRITY" ]; then \
REGISTRY_INTEGRITY=$(npm view "openclaw@${OPENCLAW_VERSION}" dist.integrity); \
if [ "$REGISTRY_INTEGRITY" != "$EXPECTED_INTEGRITY" ]; then \
Expand Down Expand Up @@ -287,7 +287,7 @@ RUN set -eu; \
fi; \
fi; \
# --- Patch 2b: allow OpenShell host gateway only through web_fetch trusted env proxy --- \
# Reviewed against openclaw@2026.5.22 dist: fetchWithWebToolsNetworkGuard \
# Reviewed against openclaw@2026.5.27 dist: fetchWithWebToolsNetworkGuard \
# passes useEnvProxy into withTrustedEnvProxyGuardedFetchMode(resolved), and \
# the SSRF guard consumes policy.allowedHostnames to skip private-network \
# checks for an exact normalized hostname. hostnameAllowlist only gates \
Expand Down Expand Up @@ -379,7 +379,7 @@ RUN set -eu; \
RUN node /usr/local/lib/nemoclaw/patch-openclaw-chat-send.js \
/usr/local/lib/node_modules/openclaw/dist

# Patch OpenClaw's pinned 2026.5.22 compiled selection runtime to expose a
# Patch OpenClaw's pinned 2026.5.27 compiled selection runtime to expose a
# compact searchable tool catalog to the model while preserving the full
# effective tool set behind tool_call. NEMOCLAW_TOOL_CATALOG=0 disables this
# wrapper if an emergency rollback is needed. The script fails closed if the
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ RUN printf '%s\n' \
# OpenClaw version: change the OPENCLAW_VERSION ARG default so CI rebuilds
# the base image on push to main, or use workflow_dispatch on base-image.yaml
# with the openclaw_version input for a one-off build without editing this file.
ARG OPENCLAW_VERSION=2026.5.22
ARG OPENCLAW_2026_5_22_INTEGRITY=sha512-m+zgBELGbCHjWB1IWF5WSWNPr480cMKOMff2OF72c8A0AMD4hC/9+qwYtzjYmGkETcffnB711JymlVsQnh2Tow==
ARG OPENCLAW_VERSION=2026.5.27
ARG OPENCLAW_2026_5_27_INTEGRITY=sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

Expand All @@ -202,7 +202,7 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep
echo "Hint: Check available versions with: npm view openclaw versions"; exit 1; \
fi; \
EXPECTED_INTEGRITY=""; \
if [ "$OPENCLAW_VERSION" = "2026.5.22" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_5_22_INTEGRITY"; fi; \
if [ "$OPENCLAW_VERSION" = "2026.5.27" ]; then EXPECTED_INTEGRITY="$OPENCLAW_2026_5_27_INTEGRITY"; fi; \
if [ -n "$EXPECTED_INTEGRITY" ]; then \
REGISTRY_INTEGRITY=$(npm view "openclaw@${OPENCLAW_VERSION}" dist.integrity); \
if [ "$REGISTRY_INTEGRITY" != "$EXPECTED_INTEGRITY" ]; then \
Expand Down
2 changes: 1 addition & 1 deletion agents/openclaw/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ homepage: "https://openclaw.ai"
install_method: npm # npm install -g openclaw@<version>
binary_path: /usr/local/bin/openclaw
version_command: "openclaw --version"
expected_version: "2026.5.22"
expected_version: "2026.5.27"
gateway_command: "openclaw gateway run"

# ── Health probe ────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ Existing sandboxes do not auto-upgrade when a newer NemoClaw release ships a new
```console
$ $$nemoclaw my-assistant status
...
Agent: OpenClaw v2026.5.22
Agent: OpenClaw v2026.5.27
...
```

Expand Down
2 changes: 1 addition & 1 deletion nemoclaw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"minGatewayVersion": "2026.5.22"
},
"build": {
"openclawVersion": "2026.5.22"
"openclawVersion": "2026.5.27"
}
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion nemoclaw/src/package-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ describe("OpenClaw package metadata", () => {
it("declares the required external plugin compatibility fields", () => {
expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.5.22");
expect(packageJson.openclaw?.compat?.minGatewayVersion).toBe("2026.5.22");
expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.5.22");
expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.5.27");
});
});
19 changes: 16 additions & 3 deletions scripts/patch-openclaw-chat-send.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,24 @@ function patchFollowupRunnerFile(file) {
}

// Source boundary: OpenClaw 2026.5.18 passed opts into runQueuedFollowup,
// while 2026.5.22 closes over params.opts. Both shapes must have opts in
// 2026.5.22 closes over params.opts and uses createReplyOperation, and
// 2026.5.27 closes over params.opts and admits a queued reply turn before
// creating the run id. All reviewed shapes must have opts and queued in
// scope before this NemoClaw run-id preservation shim is inserted.
const next = source.replace(
let next = source.replace(
/(replyOperation = createReplyOperation\(\{\n\s*sessionId: run\.sessionId,\n\s*sessionKey: replySessionKey \?\? "",\n\s*resetTriggered: false,\n\s*upstreamAbortSignal: queued\.abortSignal(?: \?\? opts\?\.abortSignal)?\n\s*\}\);\n\s*)const runId = crypto\.randomUUID\(\);/,
(_match, prefix) =>
`${prefix}const runId = queued.runId ?? opts?.runId ?? crypto.randomUUID(); ` +
`// nemoclaw: preserve chat.send run ids in followup queue (#2603, #3145)`,
);
if (next === source) {
next = source.replace(
/(const admission = await admitReplyTurn\(\{\n\s*sessionId: run\.sessionId,\n\s*sessionKey: replySessionKey \?\? "",\n\s*kind: "queued_followup",\n\s*resetTriggered: false,\n\s*upstreamAbortSignal: queued\.abortSignal\n\s*\}\);[\s\S]*?replyOperation = admission\.operation;[\s\S]*?\n\s*)const runId = crypto\.randomUUID\(\);/,
(_match, prefix) =>
`${prefix}const runId = queued.runId ?? opts?.runId ?? crypto.randomUUID(); ` +
`// nemoclaw: preserve chat.send run ids in followup queue (#2603, #3145)`,
);
}
if (next === source) {
fail(`OpenClaw followup runner run-id shape not recognized in ${file}`);
}
Expand Down Expand Up @@ -215,7 +225,10 @@ const followupCandidates = listJsFiles(distDir).filter((file) => {
const source = fs.readFileSync(file, "utf8");
return (
source.includes("function createFollowupRunner") &&
source.includes("replyOperation = createReplyOperation") &&
(source.includes("replyOperation = createReplyOperation") ||
(source.includes("admitReplyTurn") &&
source.includes("replyOperation = admission.operation")) ||
source.includes("preserve chat.send run ids in followup queue")) &&
(source.includes("const runId = crypto.randomUUID();") ||
source.includes("preserve chat.send run ids in followup queue"))
);
Expand Down
22 changes: 11 additions & 11 deletions src/lib/sandbox/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ vi.mock("../agent/defs.js", () => ({
name,
displayName: name === "openclaw" ? "OpenClaw" : "Hermes Agent",
versionCommand: name === "openclaw" ? "openclaw --version" : "hermes --version",
expectedVersion: name === "openclaw" ? "2026.5.22" : "2026.5.16",
expectedVersion: name === "openclaw" ? "2026.5.27" : "2026.5.16",
stateDirs: [],
configPaths: { dir: "/sandbox/.openclaw" },
})),
Expand Down Expand Up @@ -77,12 +77,12 @@ describe("checkAgentVersion", () => {
registry.registerSandbox({
name: "test-sb",
agent: null,
agentVersion: "2026.5.22",
agentVersion: "2026.5.27",
});

const result = checkAgentVersion("test-sb");
expect(result.detectionMethod).toBe("registry");
expect(result.sandboxVersion).toBe("2026.5.22");
expect(result.sandboxVersion).toBe("2026.5.27");
expect(result.isStale).toBe(false);
});

Expand All @@ -103,7 +103,7 @@ describe("checkAgentVersion", () => {
registry.registerSandbox({
name: "test-sb",
agent: null,
agentVersion: "2026.5.22",
agentVersion: "2026.5.27",
});

const result = checkAgentVersion("test-sb");
Expand All @@ -120,7 +120,7 @@ describe("checkAgentVersion", () => {

vi.mocked(spawnSync).mockReturnValue({
status: 0,
stdout: "OpenClaw 2026.5.22 (abc123)\n",
stdout: "OpenClaw 2026.5.27 (abc123)\n",
stderr: "",
pid: 1234,
output: [],
Expand All @@ -129,7 +129,7 @@ describe("checkAgentVersion", () => {

const result = checkAgentVersion("test-sb");
expect(result.detectionMethod).toBe("ssh-exec");
expect(result.sandboxVersion).toBe("2026.5.22");
expect(result.sandboxVersion).toBe("2026.5.27");
expect(result.isStale).toBe(false);
expect(captureSandboxSshConfigCommand).toHaveBeenCalledWith(
"/usr/local/bin/openshell",
Expand All @@ -139,7 +139,7 @@ describe("checkAgentVersion", () => {

// Should have cached the version in registry
const updated = registry.getSandbox("test-sb");
expect(updated?.agentVersion).toBe("2026.5.22");
expect(updated?.agentVersion).toBe("2026.5.27");
});

it("returns unavailable when SSH config fails", () => {
Expand Down Expand Up @@ -183,7 +183,7 @@ describe("checkAgentVersion", () => {

vi.mocked(spawnSync).mockReturnValue({
status: 0,
stdout: "OpenClaw 2026.5.22 (abc123)\n",
stdout: "OpenClaw 2026.5.27 (abc123)\n",
stderr: "",
pid: 1234,
output: [],
Expand All @@ -192,7 +192,7 @@ describe("checkAgentVersion", () => {

const result = checkAgentVersion("test-sb", { forceProbe: true });
expect(result.detectionMethod).toBe("ssh-exec");
expect(result.sandboxVersion).toBe("2026.5.22");
expect(result.sandboxVersion).toBe("2026.5.27");
});

it("force probe does not trust cached metadata when live version probing is unavailable", () => {
Expand Down Expand Up @@ -240,14 +240,14 @@ describe("formatStalenessWarning", () => {
it("includes sandbox name, versions, and rebuild hint", () => {
const lines = formatStalenessWarning("my-sb", {
sandboxVersion: "2026.3.11",
expectedVersion: "2026.5.22",
expectedVersion: "2026.5.27",
isStale: true,
detectionMethod: "registry",
});
const joined = lines.join("\n");
expect(joined).toContain("my-sb");
expect(joined).toContain("2026.3.11");
expect(joined).toContain("2026.5.22");
expect(joined).toContain("2026.5.27");
expect(joined).toContain("rebuild");
});
});
Loading
Loading