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
33 changes: 25 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +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==

# 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 @@ -88,24 +90,39 @@ RUN chmod 755 /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.js \
# silently skipping patches (leaving the sandbox unpatched), upgrade OpenClaw
# in-place so every build gets the version the patches expect.
#
# The minimum required version comes from nemoclaw-blueprint/blueprint.yaml
# (already COPYed to /opt/nemoclaw-blueprint/ above).
# OPENCLAW_VERSION is the NemoClaw runtime build target. It must be at least the
# blueprint minimum, which also supports the legacy direct-blueprint image path.
# hadolint ignore=DL3059,DL4006
RUN set -eu; \
echo "$OPENCLAW_VERSION" | grep -qxE '[0-9]+(\.[0-9]+)*' \
|| { echo "ERROR: OPENCLAW_VERSION='$OPENCLAW_VERSION' is invalid (expected e.g. 2026.3.11)" >&2; exit 1; }; \
MIN_VER=$(grep -m 1 'min_openclaw_version' /opt/nemoclaw-blueprint/blueprint.yaml | awk '{print $2}' | tr -d '"'); \
[ -n "$MIN_VER" ] || { echo "ERROR: Could not parse min_openclaw_version from blueprint.yaml" >&2; exit 1; }; \
if [ "$(printf '%s\n%s' "$MIN_VER" "$OPENCLAW_VERSION" | sort -V | head -n1)" != "$MIN_VER" ]; then \
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 [ -n "$EXPECTED_INTEGRITY" ]; then \
REGISTRY_INTEGRITY=$(npm view "openclaw@${OPENCLAW_VERSION}" dist.integrity); \
if [ "$REGISTRY_INTEGRITY" != "$EXPECTED_INTEGRITY" ]; then \
echo "ERROR: OpenClaw ${OPENCLAW_VERSION} npm integrity mismatch" >&2; \
echo "Expected: ${EXPECTED_INTEGRITY}" >&2; \
echo "Actual: ${REGISTRY_INTEGRITY}" >&2; exit 1; \
fi; \
fi; \
CUR_VER=$(openclaw --version 2>/dev/null | awk '{print $2}' || echo "0.0.0"); \
if [ "$(printf '%s\n%s' "$MIN_VER" "$CUR_VER" | sort -V | head -n1)" = "$MIN_VER" ]; then \
echo "INFO: OpenClaw $CUR_VER is current (>= $MIN_VER), no upgrade needed"; \
if [ "$(printf '%s\n%s' "$OPENCLAW_VERSION" "$CUR_VER" | sort -V | head -n1)" = "$OPENCLAW_VERSION" ]; then \
echo "INFO: OpenClaw $CUR_VER is current (>= $OPENCLAW_VERSION), no upgrade needed"; \
else \
echo "INFO: Base image has OpenClaw $CUR_VER, upgrading to $MIN_VER (minimum required)"; \
echo "INFO: Base image has OpenClaw $CUR_VER, upgrading to $OPENCLAW_VERSION"; \
# npm 10's atomic-move install can hit EROFS on overlayfs when the
# prior install spans multiple image layers (e.g. openclaw was
# baked into sandbox-base, then we upgrade on top here). Clearing
# at the shell level first gives npm a clean slate and avoids the
# rmdir failure inside npm's own install path.
rm -rf /usr/local/lib/node_modules/openclaw /usr/local/bin/openclaw; \
npm install -g --no-audit --no-fund --no-progress "openclaw@${MIN_VER}"; \
npm install -g --no-audit --no-fund --no-progress "openclaw@${OPENCLAW_VERSION}"; \
fi; \
# Pre-install the codex-acp package so the embedded ACPx runtime can
# call the local binary instead of `npx @zed-industries/codex-acp`.
Expand Down Expand Up @@ -170,7 +187,7 @@ RUN set -eu; \
# build time. They apply the legacy patch when the old target exists, skip
# only when the dist shape proves OpenClaw no longer needs that patch, and
# fail with the OpenClaw version plus dist path for mixed or unknown shapes.
# When bumping OPENCLAW_VERSION or min_openclaw_version, verify the new dist
# When bumping OPENCLAW_VERSION, verify the new dist
# takes the expected branch and update the regex / sed replacement if needed.
# hadolint ignore=SC2016,DL3059,DL4006
RUN set -eu; \
Expand Down Expand Up @@ -308,7 +325,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.18 compiled selection runtime to expose a
# Patch OpenClaw's pinned 2026.5.22 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
13 changes: 12 additions & 1 deletion Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,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.18
ARG OPENCLAW_VERSION=2026.5.22
ARG OPENCLAW_2026_5_22_INTEGRITY=sha512-m+zgBELGbCHjWB1IWF5WSWNPr480cMKOMff2OF72c8A0AMD4hC/9+qwYtzjYmGkETcffnB711JymlVsQnh2Tow==

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

Expand All @@ -199,6 +200,16 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep
echo "Error: OpenClaw version ${OPENCLAW_VERSION} not found on npm registry"; \
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 [ -n "$EXPECTED_INTEGRITY" ]; then \
REGISTRY_INTEGRITY=$(npm view "openclaw@${OPENCLAW_VERSION}" dist.integrity); \
if [ "$REGISTRY_INTEGRITY" != "$EXPECTED_INTEGRITY" ]; then \
echo "Error: OpenClaw ${OPENCLAW_VERSION} npm integrity mismatch"; \
echo "Expected: ${EXPECTED_INTEGRITY}"; \
echo "Actual: ${REGISTRY_INTEGRITY}"; exit 1; \
fi; \
fi; \
npm install -g "openclaw@${OPENCLAW_VERSION}" \
&& pip3 install --no-cache-dir --break-system-packages "pyyaml==6.0.3"

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.18"
expected_version: "2026.5.22"
gateway_command: "openclaw gateway run"

# ── Health probe ────────────────────────────────────────────────
Expand Down
4 changes: 3 additions & 1 deletion docs/about/release-notes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ The installer error message in v0.0.35+ surfaces all three invocations directly

## Component Version Policy

NemoClaw pins the OpenClaw version inside the sandbox at build time via `min_openclaw_version` in `nemoclaw-blueprint/blueprint.yaml`; existing sandboxes do not auto-upgrade.
NemoClaw pins the OpenClaw version inside the sandbox at build time via `OPENCLAW_VERSION` in the NemoClaw Dockerfiles.
The `min_openclaw_version` field in `nemoclaw-blueprint/blueprint.yaml` is the compatibility floor for direct blueprint consumers and may be lower than the NemoClaw runtime target.
Existing sandboxes do not auto-upgrade.
Run `nemoclaw <name> status` to see the OpenClaw version currently running in a sandbox, and `nemoclaw <name> rebuild` to pick up a newer pin from a NemoClaw upgrade.
See [Checking the OpenClaw version](/reference/commands#checking-the-openclaw-version) for the full policy.
8 changes: 5 additions & 3 deletions docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ flowchart LR

## Sandbox Environment

The sandbox runs the
[`ghcr.io/nvidia/openshell-community/sandboxes/openclaw`](https://github.com/NVIDIA/OpenShell-Community)
container image. Inside the sandbox:
Normal NemoClaw onboarding builds from the
[`ghcr.io/nvidia/nemoclaw/sandbox-base`](https://github.com/NVIDIA/NemoClaw/pkgs/container/nemoclaw%2Fsandbox-base)
base image and layers the NemoClaw runtime Dockerfile on top. The direct blueprint
runner still carries a pinned OpenShell Community OpenClaw image for legacy
`openshell sandbox create --from` compatibility. Inside the sandbox:

- OpenClaw runs with the NemoClaw plugin pre-installed.
- Inference calls are routed through OpenShell to the configured provider.
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -407,15 +407,16 @@ $ nemoclaw my-assistant status
#### Checking the OpenClaw version

NemoClaw pins the OpenClaw version inside the sandbox at build time, not at runtime.
The minimum version comes from `min_openclaw_version` in `nemoclaw-blueprint/blueprint.yaml`, and the sandbox image upgrades OpenClaw to that version during `docker build` if the cached base image is older.
The NemoClaw runtime build target is declared by `OPENCLAW_VERSION` in the NemoClaw Dockerfiles.
The `min_openclaw_version` field in `nemoclaw-blueprint/blueprint.yaml` remains the compatibility floor for direct blueprint consumers, so it can be lower than the Dockerfile target.
Existing sandboxes do not auto-upgrade when a newer NemoClaw release ships a newer pin — you upgrade by rebuilding the sandbox.

`nemoclaw <name> status` prints the running OpenClaw version on the `Agent` line:

```console
$ nemoclaw my-assistant status
...
Agent: OpenClaw v2026.5.18
Agent: OpenClaw v2026.5.22
...
```

Expand Down
2 changes: 1 addition & 1 deletion nemoclaw-blueprint/blueprint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
version: "0.1.0"
min_openshell_version: "0.0.44"
max_openshell_version: "0.0.44"
min_openclaw_version: "2026.5.18"
min_openclaw_version: "2026.3.11"
# Mirrors the components.sandbox.image manifest digest below. Lets a
# downstream consumer (or release tooling) verify the blueprint declares
# a specific sandbox image without parsing the components tree, and
Expand Down
6 changes: 3 additions & 3 deletions nemoclaw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"./dist/index.js"
],
"compat": {
"pluginApi": ">=2026.5.18",
"minGatewayVersion": "2026.5.18"
"pluginApi": ">=2026.5.22",
"minGatewayVersion": "2026.5.22"
},
"build": {
"openclawVersion": "2026.5.18"
"openclawVersion": "2026.5.22"
}
},
"scripts": {
Expand Down
6 changes: 3 additions & 3 deletions nemoclaw/src/package-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const packageJson = JSON.parse(

describe("OpenClaw package metadata", () => {
it("declares the required external plugin compatibility fields", () => {
expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.5.18");
expect(packageJson.openclaw?.compat?.minGatewayVersion).toBe("2026.5.18");
expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.5.18");
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");
});
});
2 changes: 1 addition & 1 deletion schemas/blueprint.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"min_openclaw_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
"description": "Minimum compatible OpenClaw version."
"description": "Minimum compatible OpenClaw version for direct blueprint consumers. NemoClaw's built runtime target is declared by Dockerfile OPENCLAW_VERSION."
},
"digest": {
"type": "string",
Expand Down
12 changes: 11 additions & 1 deletion scripts/patch-openclaw-chat-send.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,18 @@ function patchFollowupRunnerFile(file) {
}

if (!source.includes("preserve chat.send run ids in followup queue")) {
const hasOptsBinding =
/\bfunction\s+runQueuedFollowup\(\s*queued,\s*opts\b/.test(source) ||
/\bconst\s+\{[^}]*\bopts\b[^}]*\}\s*=\s*params;/.test(source);
if (!hasOptsBinding) {
fail(`OpenClaw followup runner opts binding not recognized in ${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
// scope before this NemoClaw run-id preservation shim is inserted.
const 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\(\);/,
/(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)`,
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.18" : "2026.5.16",
expectedVersion: name === "openclaw" ? "2026.5.22" : "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.18",
agentVersion: "2026.5.22",
});

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

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

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

vi.mocked(spawnSync).mockReturnValue({
status: 0,
stdout: "OpenClaw 2026.5.18 (abc123)\n",
stdout: "OpenClaw 2026.5.22 (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.18");
expect(result.sandboxVersion).toBe("2026.5.22");
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.18");
expect(updated?.agentVersion).toBe("2026.5.22");
});

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.18 (abc123)\n",
stdout: "OpenClaw 2026.5.22 (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.18");
expect(result.sandboxVersion).toBe("2026.5.22");
});
});

Expand All @@ -219,14 +219,14 @@ describe("formatStalenessWarning", () => {
it("includes sandbox name, versions, and rebuild hint", () => {
const lines = formatStalenessWarning("my-sb", {
sandboxVersion: "2026.3.11",
expectedVersion: "2026.5.18",
expectedVersion: "2026.5.22",
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.18");
expect(joined).toContain("2026.5.22");
expect(joined).toContain("rebuild");
});
});
10 changes: 5 additions & 5 deletions src/lib/verify-deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,13 @@ describe("verifyDeployment", () => {
const deps = makeDeps({
executeSandboxCommand: (_name: string, script: string) => {
if (script.includes("openclaw --version")) {
return { status: 0, stdout: "2026.5.18", stderr: "" };
return { status: 0, stdout: "2026.5.22", stderr: "" };
}
return { status: 0, stdout: "200", stderr: "" };
},
});
const result = await verifyDeployment("my-sandbox", chain, deps, NO_RETRY);
expect(result.verification.gatewayVersion).toBe("2026.5.18");
expect(result.verification.gatewayVersion).toBe("2026.5.22");
});

it("reports null version when gateway is down (skips version probe)", async () => {
Expand Down Expand Up @@ -179,7 +179,7 @@ describe("verifyDeployment", () => {
const deps = makeDeps({
executeSandboxCommand: (_name: string, script: string) => {
if (script.includes("openclaw --version")) {
return { status: 0, stdout: "2026.5.18", stderr: "" };
return { status: 0, stdout: "2026.5.22", stderr: "" };
}
if (script.includes("inference.local")) {
return { status: 0, stdout: "200", stderr: "" };
Expand Down Expand Up @@ -240,14 +240,14 @@ describe("formatVerificationDiagnostics", () => {
const result = await verifyDeployment("my-sandbox", chain, makeDeps({
executeSandboxCommand: (_name: string, script: string) => {
if (script.includes("openclaw --version")) {
return { status: 0, stdout: "2026.5.18", stderr: "" };
return { status: 0, stdout: "2026.5.22", stderr: "" };
}
return { status: 0, stdout: "200", stderr: "" };
},
}), NO_RETRY);
const lines = formatVerificationDiagnostics(result);
expect(lines.some((l) => l.includes("verified"))).toBe(true);
expect(lines.some((l) => l.includes("2026.5.18"))).toBe(true);
expect(lines.some((l) => l.includes("2026.5.22"))).toBe(true);
});

it("prints failure diagnostics with hints when unhealthy", async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/test-openclaw-tui-chat-correlation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ openclaw_version="$(
openshell sandbox exec --name "$SANDBOX_NAME" -- openclaw --version 2>&1 || true
)"
echo "Sandbox OpenClaw version: ${openclaw_version}"
if ! grep -q "2026.5.18" <<<"$openclaw_version"; then
echo "Expected fresh sandbox to run OpenClaw 2026.5.18" >&2
if ! grep -q "2026.5.22" <<<"$openclaw_version"; then
echo "Expected fresh sandbox to run OpenClaw 2026.5.22" >&2
exit 1
fi

Expand Down
Loading
Loading