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
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,8 @@
FROM scratch AS openclaw-patch-payload

COPY scripts/patch-openclaw-tool-catalog.mts /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.mts
COPY scripts/lib/patch-openclaw-container-restart.mts scripts/lib/patch-openclaw-agent-message-input.mts /usr/local/lib/nemoclaw/
COPY scripts/lib/patch-openclaw-npm12-pack-json.mts /usr/local/lib/nemoclaw/npm12.mts
COPY scripts/lib/patch-openclaw-container-restart.mts /usr/local/lib/nemoclaw/patch-openclaw-container-restart.mts
COPY scripts/patch-openclaw-mcp-npx.mts /usr/local/lib/nemoclaw/patch-openclaw-mcp-npx.mts
COPY scripts/patch-openclaw-mcp-reliability.mts /usr/local/lib/nemoclaw/patch-openclaw-mcp-reliability.mts
COPY scripts/patch-openclaw-mcp-tools-list-timeout.mts /usr/local/lib/nemoclaw/patch-openclaw-mcp-tools-list-timeout.mts
Expand Down Expand Up @@ -1424,11 +1424,11 @@
if grep -REq --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = (1e4|15e3)' "$OC_DIST"; then echo "ERROR: Patch 5 left a short handshake-timeout constant" >&2; exit 1; fi; \
if ! grep -REq --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 6e4' "$OC_DIST"; then echo "ERROR: Patch 5 did not find patched 6e4 constant" >&2; exit 1; fi

# Native OpenClaw restart must reload updated ESM plugins in OpenShell sandboxes.
# Remove this bridge when upstream container restart refreshes the module graph.
# Native OpenClaw restart must reload; repair stdin input too.
# hadolint ignore=DL3059
RUN node /usr/local/lib/nemoclaw/patch-openclaw-container-restart.mts \
/usr/local/lib/node_modules/openclaw/dist
RUN set -e; for patch in container-restart agent-message-input; do \
node "/usr/local/lib/nemoclaw/patch-openclaw-${patch}.mts" /usr/local/lib/node_modules/openclaw/dist; \
done

# Keep OpenClaw 2026.9.1 scope-upgrade approvals inside the gateway's
# canonical locked pairing writer (#4462). The upstream devices CLI otherwise
Expand Down Expand Up @@ -1615,8 +1615,8 @@
# OpenClaw 2026.9.1 retired the device-auth bypass, so NemoClaw validates the
# provenance value but does not emit an upstream configuration key for either
# input. Remove these arguments after all external callers have transitioned.
ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0

Check warning on line 1618 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1618 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1618 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1618 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE=operator

Check warning on line 1619 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1619 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1619 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1619 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Compatibility build arg for older custom Dockerfiles and rebuild tooling.
# NemoClaw-managed images intentionally do not consume it; gateway auth tokens
# are generated at container startup and are never baked into image layers.
Expand Down Expand Up @@ -1650,7 +1650,7 @@
# NEMOCLAW_MESSAGING_PLAN_B64 intentionally remains ARG-only: Docker exposes it
# to build RUN processes without retaining the full plan in the final image env.
# Direct ARG interpolation into inline source is a code injection vector (C-2).
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1653 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw, arm64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
NEMOCLAW_INFERENCE_PROVIDER_ID=${NEMOCLAW_INFERENCE_PROVIDER_ID} \
NEMOCLAW_UPSTREAM_PROVIDER=${NEMOCLAW_UPSTREAM_PROVIDER} \
NEMOCLAW_SERVING_PRESET=${NEMOCLAW_SERVING_PRESET} \
Expand Down
24 changes: 21 additions & 3 deletions agents/langchain-deepagents-code/dependency-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,8 @@ aliases. They are not a new provider profile and do not modify the reviewed
canonical NVIDIA profile.

The two managed model IDs remain language-local constants in the TypeScript
config generator, the managed package patch, and the isolated Python
image/plugin validators. NemoClaw
identity module (`src/lib/inference/managed-dcode/identity.ts`), the managed
package patch, and the isolated Python image/plugin validators. NemoClaw
registers both IDs under the managed OpenAI adapter and the managed OpenRouter
adapter because Deep Agents Code applies provider-native request shaping before
it reaches the shared `inference.local` route. Those components run on opposite
Expand Down Expand Up @@ -287,7 +287,10 @@ and tool calls can otherwise carry empty assistant content. That response shape
is outside NemoClaw; this repository owns the generated DCode provider
configuration and the managed package patch, so each supplies the model-specific
template argument at its own boundary. `generate-config.ts` writes the per-model
`config.toml` entry. The patched `_get_provider_kwargs` resolver derives the
`config.toml` entry. Its TypeScript model-and-endpoint predicate lives in
`src/lib/inference/managed-dcode/identity.ts`; the sandbox invocation probe uses
that same predicate so readiness and runtime requests agree.
The patched `_get_provider_kwargs` resolver derives the
same argument from its language-local ID set because it never consumes the
mutable `config.toml` params table (#7441). Fixing the serving template, model,
or third-party client in this repository would require vendoring an upstream
Expand Down Expand Up @@ -424,3 +427,18 @@ restore behavior. The E2E EXIT trap is still required for early probe failures
so the ordered suite cannot leave the current sandbox broader than the registry.
Remove that trap only when each check receives an isolated sandbox or no longer
mutates policy.

### Auto classifier route

Deep Agents Code 0.1.71 resolves `[models].auto_classifier` before constructing
its separate authorization classifier with `create_model`. Its default can
select an OpenAI model that the managed sandbox's selected endpoint does not
serve. The generated configuration sets this option to the selected managed
provider and model identity. The classifier uses the existing managed model
constructor and OpenShell credential placeholder. It retains the upstream
structured-decision policy, deadlines, denials and fallback to human approval.
The capability remains disabled until explicitly selected; this configuration
does not activate Auto or select unrestricted approval.
The config-generator regression verifies both managed provider identities.
The existing thread-auto-approval live check owns real classifier execution,
sequential tool completion, network/credential boundaries and baseline recovery.
10 changes: 5 additions & 5 deletions agents/langchain-deepagents-code/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { join } from "node:path";

import {
type ManagedDcodeProvider,
managedDcodeRequiresNonemptyContent,
normalizeManagedDcodeEndpointUrl,
resolveManagedDcodeIdentity,
} from "../../src/lib/inference/managed-dcode/identity.ts";
Expand All @@ -42,10 +43,6 @@ type ManagedDeepAgentsConfig = {
defaultModel: string;
};

const NEMOTRON_ULTRA_MODEL_IDS = new Set([
"nvidia/nemotron-3-ultra-550b-a55b",
"nvidia/nvidia/nemotron-3-ultra",
]);
const MANAGED_INFERENCE_API_KEY_ENV = "DEEPAGENTS_CODE_OPENAI_API_KEY";
const ATTACHED_PROVIDER_API_KEY_ENV = "NVIDIA_INFERENCE_API_KEY";

Expand Down Expand Up @@ -144,7 +141,7 @@ function openAiModelRequestParamLines(
// parameters. Keep the exact invalid state, regression proof, and separate
// removal conditions for this option and the dispatch guard in
// dependency-review.md under "Managed Ultra compatibility workarounds."
const isUltra = NEMOTRON_ULTRA_MODEL_IDS.has(model) && baseUrl !== NVIDIA_HOSTED_NATIVE_ENDPOINT;
const isUltra = managedDcodeRequiresNonemptyContent(model, baseUrl);
const extraBodyEntries = [
...(isUltra ? ["chat_template_kwargs = { force_nonempty_content = true }"] : []),
...(reasoningEffort ? [`reasoning_effort = ${tomlString(reasoningEffort)}`] : []),
Expand Down Expand Up @@ -214,6 +211,9 @@ function buildConfig(settings: Settings): ManagedDeepAgentsConfig {
"",
"[models]",
`default = ${tomlString(defaultModel)}`,
// A managed sandbox has credentials only for its selected inference route.
// Use a separate classifier request on that route, preserving Auto's policy.
`auto_classifier = ${tomlString(defaultModel)}`,
"",
...providerConfigLines(
provider,
Expand Down
3 changes: 3 additions & 0 deletions docs/manage-sandboxes/run-deep-agents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ nemo-deepagents <sandbox-name> rebuild --dcode-auto-approval thread-opt-in --yes

The `thread-opt-in` setting grants permission to use Deep Agents Code's native approval modes, but it does not activate one by itself.
Use `dcode -y` or **Enable Auto for this thread** for classifier-backed Auto mode.
The generated configuration selects the managed provider and model for the separate classifier request.
It uses the same OpenShell credential placeholder and endpoint as the agent.
If classification fails, DCode retains its native denial and fallback to human approval.
Use the native `--yolo` or `/yolo` path for unrestricted approval after its warning and acknowledgement.
The TUI shows the upstream active-state indicator for the selected mode.

Expand Down
4 changes: 4 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,10 @@ This prevents an unused pipe from delaying execution while OpenShell waits for E
Nonempty `--message-file` arguments and calls without a recognized inline message preserve a pipe or redirect.
The sandbox resolves message-file paths, including symlinks to stdin.
For example, the wrapper preserves redirected input for `printf 'ping' | $$nemoclaw my-assistant agent --agent main`.
The producer must close standard input after writing the message.
The managed OpenClaw parser waits up to 30 seconds for piped input, including `--message-file /dev/stdin` and symlinks to stdin.
A positive `--timeout <seconds>` sets this input wait as well as the agent turn timeout.
If the input deadline expires, the command exits nonzero with guidance to close the producer or provide an inline message.

When the top-level OpenClaw `--json` output flag is present, the wrapper uses a captured no-TTY path with a `64 MiB` buffer so `stdout` stays parseable JSON. Raw `stderr`, including structured JSON diagnostics, is forwarded unchanged. NemoClaw appends failed-tool or untrusted-child provenance only from the `stdout` JSON. The wrapper reads completion markers only from the final matching OpenClaw response envelope: a local `{ payloads, meta }` response or a gateway `{ status, result: { payloads, meta } }` response. It ignores earlier JSON progress or log records. It exits with status `1` when that metadata contains `error.kind: "incomplete_turn"`, `livenessState: "abandoned"`, or a `timeoutPhase` value, even when the envelope reports success. `replayInvalid: true` means replaying the turn may repeat side effects. The wrapper accepts it only with evidence of a completed run, a successful tool summary, and final assistant text matching the reply payload. Otherwise it exits with status `1` and the existing inspect-before-retry guidance. Marker-shaped values inside tool results, tool-call arguments, or other descendants do not change the exit status. A turn can run every tool successfully and still become abandoned before it produces a reply. The wrapper writes the unchanged JSON trace to `stdout` before it reports the incomplete turn, so the partial tool trace remains available. The wrapper writes the verdict, the detected markers, and verify-before-retry guidance to `stderr`. A `timeoutPhase` value names the phase the deadline fired in, so the wrapper writes deadline guidance in place of the generic incomplete-turn text. Tool calls in a partial trace may have already applied side effects, so verify what the turn changed before you retry it. The wrapper passes through an upstream non-zero exit status unchanged. Literal `--json` values consumed by flags such as `-m` or `--reply-channel`, or arguments after `--`, stay on the normal passthrough path. Documented value flags written as `--flag=value`, such as `--session-id=s1`, are recognized the same way as separated value flags. If an unrecognized OpenClaw option appears before `--json`, NemoClaw also keeps the command on the normal passthrough path so OpenClaw remains the argv source of truth.

Expand Down
141 changes: 141 additions & 0 deletions scripts/lib/patch-openclaw-agent-message-input.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env node
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Managed-runtime bridge for #13130. Remove when the pinned native parser
// accepts implicit stdin and reads inherited descriptors without reopening them.
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const MARKER = "// nemoclaw: preserve inherited agent message input";
const IMPORT = 'import { fstatSync as nemoclawStdinStat } from "node:fs";';
const ORIGINAL_OPEN = `async function readAgentMessageFile(messageFile) {
\tlet handle;
\ttry {
\t\thandle = await fs.open(messageFile, "r");
\t} catch (err) {
\t\tthrow new Error(formatMessageFileReadFailure(messageFile, err), { cause: err });
\t}`;
const PATCHED_OPEN = `${MARKER}
async function readInheritedAgentMessage(timeout) {
\tconst chunks = [];
\tlet size = 0;
\tconst seconds = Number(timeout);
\tconst budget = Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds * 1000, 2147483647) : 30000;
\tconst timer = setTimeout(() => process.stdin.destroy(new Error("Timed out waiting for stdin; close the producer, provide --message, or set --timeout <seconds>.")), budget);
\ttry {
\t\tfor await (const chunk of process.stdin) {
\t\t\tsize += chunk.length;
\t\t\tif (size > AGENT_MESSAGE_FILE_MAX_BYTES) throw new Error("File exceeds " + AGENT_MESSAGE_FILE_MAX_BYTES + " bytes");
\t\t\tchunks.push(chunk);
\t\t}
\t\treturn Buffer.concat(chunks, size);
\t} finally {
\t\tclearTimeout(timer);
\t}
}
function borrowAgentMessageStdin() {
\treturn { fd: 0, inheritedStdin: true, stat: async () => nemoclawStdinStat(0), close: async () => {} };
}
async function readAgentMessageFile(messageFile, inheritedStdin = false, timeout) {
\tlet handle;
\ttry {
\t\thandle = inheritedStdin ? borrowAgentMessageStdin() : await fs.open(messageFile, "r");
\t} catch (err) {
\t\t// A sandbox may deny reopening a pipe that the process can already read.
\t\t// Retain the native denial unless the requested path identifies fd 0.
\t\tif (err?.code === "EACCES") {
\t\t\ttry {
\t\t\t\tconst input = nemoclawStdinStat(0);
\t\t\t\tconst file = await fs.stat(messageFile);
\t\t\t\tif (input.dev === file.dev && input.ino === file.ino) handle = borrowAgentMessageStdin();
\t\t\t} catch {}
\t\t}
\t\tif (!handle) throw new Error(formatMessageFileReadFailure(messageFile, err), { cause: err });
\t}`;
const ORIGINAL_MESSAGE = '\tconst message = opts.message ?? "";';
const PATCHED_MESSAGE = `\tconst message = opts.message ?? (process.stdin.isTTY === true ? "" : await readAgentMessageFile("stdin", true, opts.timeout));`;

const ORIGINAL_READ =
"buffer = await readFileDescriptorBounded(handle.fd, AGENT_MESSAGE_FILE_MAX_BYTES);";
const PATCHED_READ = `let inherited = handle.inheritedStdin;
\t\tif (!inherited && !stat.isFile()) {
\t\t\ttry {
\t\t\t\tconst input = nemoclawStdinStat(0);
\t\t\t\tinherited = input.dev === stat.dev && input.ino === stat.ino;
\t\t\t} catch {}
\t\t}
\t\tbuffer = inherited ? await readInheritedAgentMessage(timeout) : await readFileDescriptorBounded(handle.fd, AGENT_MESSAGE_FILE_MAX_BYTES);`;

const ORIGINAL_FILE = "await readAgentMessageFile(messageFile);";
const PATCHED_FILE = "await readAgentMessageFile(messageFile, false, opts.timeout);";

function digest(value: string): string {
return createHash("sha256").update(value).digest("hex");
}

export function patchAgentMessageInput(source: string): string {
const region = source.match(
/function missingAgentMessageError\(\) \{[\s\S]*?async function resolveAgentMessageOpts\(opts\) \{[\s\S]*?\n\}/u,
)?.[0];
if (!region) throw new Error("Missing native OpenClaw agent message parser");
const restored = region
.replace(PATCHED_OPEN, ORIGINAL_OPEN)
.replace(PATCHED_MESSAGE, ORIGINAL_MESSAGE)
.replace(PATCHED_READ, ORIGINAL_READ)
.replace(PATCHED_FILE, ORIGINAL_FILE);
if (digest(restored) !== "6ac2ff76ee85a5a9fd957a920a812325d77979a6ee02255c148a9dc96ca92e32")
throw new Error("Unrecognized native OpenClaw agent message parser");
const patched = restored
.replace(ORIGINAL_OPEN, PATCHED_OPEN)
.replace(ORIGINAL_MESSAGE, PATCHED_MESSAGE)
.replace(ORIGINAL_READ, PATCHED_READ)
.replace(ORIGINAL_FILE, PATCHED_FILE);
if (source.includes(MARKER)) {
if (
region !== patched ||
source.split(IMPORT).length !== 2 ||
source.split(MARKER).length !== 2
)
throw new Error("Incomplete OpenClaw agent message input patch");
return source;
}
if (source.includes(IMPORT) || region !== restored)
throw new Error("Incomplete OpenClaw agent message input patch");
return `${IMPORT}\n${source.replace(region, () => patched)}`;
}

export function patchOpenClawAgentMessageInput(distDir: string, audit = false): void {
const { version } = JSON.parse(fs.readFileSync(path.join(distDir, "..", "package.json"), "utf8"));
if (["2026.3.11", "2026.4.24"].includes(version)) return;
if (version !== "2026.9.2") throw new Error(`Unsupported OpenClaw version: ${version}`);
const targets = fs
.readdirSync(distDir)
.filter((name) => /^agent-via-gateway-[\w-]+\.js$/u.test(name));
if (targets.length !== 1) throw new Error("Expected one native OpenClaw agent message module");
const target = path.join(distDir, targets[0]!);
const source = fs.readFileSync(target, "utf8");
const patched = patchAgentMessageInput(source);
if (audit) {
if (patched !== source) throw new Error("OpenClaw agent message input patch is missing");
} else if (patched !== source) fs.writeFileSync(target, patched);
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const args = process.argv.slice(2);
const audit = args[0] === "--audit";
const directory = args[audit ? 1 : 0];
if (!directory || args.length !== (audit ? 2 : 1)) {
console.error("Usage: patch-openclaw-agent-message-input.mts [--audit] <openclaw-dist-dir>");
process.exitCode = 2;
} else {
try {
patchOpenClawAgentMessageInput(directory, audit);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
}
69 changes: 69 additions & 0 deletions src/lib/actions/sandbox/inference-invocation-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1087,3 +1087,72 @@ describe("OpenClaw native local probe routing (#13338)", () => {
},
);
});

it.each(["nvidia/nemotron-3-ultra-550b-a55b", "nvidia/nvidia/nemotron-3-ultra"])(
"keeps DCode Ultra readiness on its managed model profile (%s)",
(model) => {
const selected = {
...input,
agentName: "langchain-deepagents-code",
model,
nativeProvider: true,
nativeEndpointUrl: "https://inference-api.nvidia.com/v1",
};
const result = runProbeCommandWithBody(
"200",
'{"choices":[{"message":{"content":"OK"}}]}',
tmpdir(),
selected,
{ COMPATIBLE_API_KEY: "openshell:resolve:env:COMPATIBLE_API_KEY" },
);
const payload = JSON.parse(result.argv[result.argv.indexOf("--data-binary") + 1]!);
expect(payload.chat_template_kwargs).toEqual({ force_nonempty_content: true });
},
);

it("rejects the native E2E preamble without copying its response into diagnostics", async () => {
const result = await probeSandboxInferenceInvocation(
{
...input,
agentName: "langchain-deepagents-code",
nativeProvider: true,
nativeEndpointUrl: "https://inference-api.nvidia.com/v1",
},
{
commandExecutor: {
runBuffered: async () => bufferedResult(0, "UNTRUSTED PREAMBLE\nBROKEN 000\n", ""),
},
},
);
expect(result).toEqual({
ok: false,
httpStatus: null,
detail: "sandbox inference invocation probe returned an invalid response body",
endpoint: "https://inference-api.nvidia.com/v1/chat/completions",
});
});

it.each([
{
agentName: "langchain-deepagents-code",
model: "nvidia/nvidia/nemotron-3-ultra",
nativeEndpointUrl: "https://integrate.api.nvidia.com/v1",
},
{
agentName: "openclaw",
model: "nvidia/nvidia/nemotron-3-ultra",
nativeEndpointUrl: "https://inference-api.nvidia.com/v1",
},
{
agentName: "langchain-deepagents-code",
model: "another-model",
nativeEndpointUrl: "https://inference-api.nvidia.com/v1",
},
])(
"preserves unrelated request profiles for $agentName $model at $nativeEndpointUrl",
(variant) => {
expect(
buildSandboxInferenceInvocationCommand({ ...input, nativeProvider: true, ...variant }),
).not.toContain("force_nonempty_content");
},
);
7 changes: 7 additions & 0 deletions src/lib/actions/sandbox/inference-invocation-probe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { managedDcodeRequiresNonemptyContent } from "../../inference/managed-dcode/identity";
import { nativeHostedAuthHeaderScript } from "../../inference/native-provider/contract";
import { NATIVE_LOCAL_AUTH_HEADER_SCRIPT } from "../../inference/native-local/agent-config";
import { NATIVE_NVIDIA_AUTH_HEADER_SCRIPT } from "../../inference/native-nvidia/contract";
Expand Down Expand Up @@ -189,6 +190,12 @@ function buildProbeRequest(input: SandboxInferenceInvocationInput): {
model: input.model,
[resolveMaxTokensField(input.model)]: resolveProbeReplyTokens(input.provider, input.model),
messages: [{ role: "user", content: "Reply with OK" }],
// Match DCode's pinned Ultra profile: the hosted alias can otherwise
// exhaust a short probe on reasoning and return no visible content.
...(input.agentName === DCODE_AGENT_NAME &&
managedDcodeRequiresNonemptyContent(input.model, baseUrl)
? { chat_template_kwargs: { force_nonempty_content: true } }
: {}),
stream: false,
},
};
Expand Down
Loading
Loading