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: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0
# Unique per build to ensure each image gets a fresh auth token.
# Pass --build-arg NEMOCLAW_BUILD_ID=$(date +%s) to bust the cache.
ARG NEMOCLAW_BUILD_ID=default
# Sandbox egress proxy host/port. Defaults match the OpenShell-injected
# gateway (10.200.0.1:3128). Operators on non-default networks can override
# at sandbox creation time by exporting NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT
# before running `nemoclaw onboard`. See #1409.
ARG NEMOCLAW_PROXY_HOST=10.200.0.1
ARG NEMOCLAW_PROXY_PORT=3128

# SECURITY: Promote build-args to env vars so the Python script reads them
# via os.environ, never via string interpolation into Python source code.
Expand All @@ -87,7 +93,9 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_WEB_CONFIG_B64=${NEMOCLAW_WEB_CONFIG_B64} \
NEMOCLAW_MESSAGING_CHANNELS_B64=${NEMOCLAW_MESSAGING_CHANNELS_B64} \
NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=${NEMOCLAW_MESSAGING_ALLOWED_IDS_B64} \
NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH}
NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} \
NEMOCLAW_PROXY_HOST=${NEMOCLAW_PROXY_HOST} \
NEMOCLAW_PROXY_PORT=${NEMOCLAW_PROXY_PORT}

WORKDIR /sandbox
USER sandbox
Expand Down
20 changes: 20 additions & 0 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,26 @@ function patchStagedDockerfile(
/^ARG NEMOCLAW_BUILD_ID=.*$/m,
`ARG NEMOCLAW_BUILD_ID=${buildId}`,
);
// Honor NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT exported in the host
// shell so the sandbox-side nemoclaw-start.sh sees them via $ENV at runtime.
// Without this, the host export is silently dropped at image build time and
// the sandbox falls back to the default 10.200.0.1:3128 proxy. See #1409.
const PROXY_HOST_RE = /^[A-Za-z0-9._:-]+$/;
const PROXY_PORT_RE = /^[0-9]{1,5}$/;
const proxyHostEnv = process.env.NEMOCLAW_PROXY_HOST;
if (proxyHostEnv && PROXY_HOST_RE.test(proxyHostEnv)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_PROXY_HOST=.*$/m,
`ARG NEMOCLAW_PROXY_HOST=${proxyHostEnv}`,
);
}
const proxyPortEnv = process.env.NEMOCLAW_PROXY_PORT;
if (proxyPortEnv && PROXY_PORT_RE.test(proxyPortEnv)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_PROXY_PORT=.*$/m,
`ARG NEMOCLAW_PROXY_PORT=${proxyPortEnv}`,
);
Comment on lines +943 to +956
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate proxy port range, not just format.

Line 943 allows values like 00000 and 99999, which are not valid TCP ports, yet they are baked into the Dockerfile at Line 955 and can silently break proxy egress.

Suggested fix
-  const PROXY_PORT_RE = /^[0-9]{1,5}$/;
+  const PROXY_PORT_RE = /^[0-9]{1,5}$/;

   const proxyPortEnv = process.env.NEMOCLAW_PROXY_PORT;
-  if (proxyPortEnv && PROXY_PORT_RE.test(proxyPortEnv)) {
+  if (proxyPortEnv !== undefined && proxyPortEnv !== "") {
+    if (!PROXY_PORT_RE.test(proxyPortEnv)) {
+      throw new Error("Invalid NEMOCLAW_PROXY_PORT: expected numeric TCP port (1-65535)");
+    }
+    const proxyPort = Number(proxyPortEnv);
+    if (!Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
+      throw new Error("Invalid NEMOCLAW_PROXY_PORT: expected numeric TCP port (1-65535)");
+    }
     dockerfile = dockerfile.replace(
       /^ARG NEMOCLAW_PROXY_PORT=.*$/m,
-      `ARG NEMOCLAW_PROXY_PORT=${proxyPortEnv}`,
+      `ARG NEMOCLAW_PROXY_PORT=${proxyPort}`,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard.js` around lines 943 - 956, The proxy port check only
enforces numeric format via PROXY_PORT_RE and can accept invalid ports like
00000 or >65535; update the validation around proxyPortEnv used in the ARG
replacement to (1) parse the value with parseInt(proxyPortEnv, 10), (2) ensure
the parsed number is between 1 and 65535 inclusive, and (optionally) reject
values with leading zeros by verifying String(parsed) === proxyPortEnv to avoid
things like "00000"; only perform the dockerfile.replace when the numeric range
check passes. Reference the symbols PROXY_PORT_RE, proxyPortEnv, and the
dockerfile.replace block for where to change the logic.

}
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_WEB_CONFIG_B64=.*$/m,
`ARG NEMOCLAW_WEB_CONFIG_B64=${webSearch.buildWebSearchDockerConfig(
Expand Down
145 changes: 145 additions & 0 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,151 @@ describe("onboard helpers", () => {
}
});

it("regression #1409: bakes NEMOCLAW_PROXY_HOST/PORT env into the staged Dockerfile", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-proxy-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfilePath,
[
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_CONFIG_B64=e30=",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_PROXY_HOST=10.200.0.1",
"ARG NEMOCLAW_PROXY_PORT=3128",
].join("\n"),
);

const priorHost = process.env.NEMOCLAW_PROXY_HOST;
const priorPort = process.env.NEMOCLAW_PROXY_PORT;
process.env.NEMOCLAW_PROXY_HOST = "1.2.3.4";
process.env.NEMOCLAW_PROXY_PORT = "9999";
try {
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
"build-proxy",
"openai-api",
);
const patched = fs.readFileSync(dockerfilePath, "utf8");
assert.match(patched, /^ARG NEMOCLAW_PROXY_HOST=1\.2\.3\.4$/m);
assert.match(patched, /^ARG NEMOCLAW_PROXY_PORT=9999$/m);
} finally {
if (priorHost === undefined) {
delete process.env.NEMOCLAW_PROXY_HOST;
} else {
process.env.NEMOCLAW_PROXY_HOST = priorHost;
}
if (priorPort === undefined) {
delete process.env.NEMOCLAW_PROXY_PORT;
} else {
process.env.NEMOCLAW_PROXY_PORT = priorPort;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("regression #1409: leaves Dockerfile defaults when proxy env is unset", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-proxy-default-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfilePath,
[
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_CONFIG_B64=e30=",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_PROXY_HOST=10.200.0.1",
"ARG NEMOCLAW_PROXY_PORT=3128",
].join("\n"),
);

const priorHost = process.env.NEMOCLAW_PROXY_HOST;
const priorPort = process.env.NEMOCLAW_PROXY_PORT;
delete process.env.NEMOCLAW_PROXY_HOST;
delete process.env.NEMOCLAW_PROXY_PORT;
try {
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
"build-proxy-default",
"openai-api",
);
const patched = fs.readFileSync(dockerfilePath, "utf8");
// Defaults must be preserved when no env override is in effect.
assert.match(patched, /^ARG NEMOCLAW_PROXY_HOST=10\.200\.0\.1$/m);
assert.match(patched, /^ARG NEMOCLAW_PROXY_PORT=3128$/m);
} finally {
if (priorHost !== undefined) process.env.NEMOCLAW_PROXY_HOST = priorHost;
if (priorPort !== undefined) process.env.NEMOCLAW_PROXY_PORT = priorPort;
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("regression #1409: rejects malformed NEMOCLAW_PROXY_HOST/PORT and keeps defaults", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-proxy-bad-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfilePath,
[
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_CONFIG_B64=e30=",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_PROXY_HOST=10.200.0.1",
"ARG NEMOCLAW_PROXY_PORT=3128",
].join("\n"),
);

const priorHost = process.env.NEMOCLAW_PROXY_HOST;
const priorPort = process.env.NEMOCLAW_PROXY_PORT;
// Inject malicious values that could break out of the ARG line if not validated.
process.env.NEMOCLAW_PROXY_HOST = "1.2.3.4\nRUN rm -rf /";
process.env.NEMOCLAW_PROXY_PORT = "abcd";
try {
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
"build-proxy-bad",
"openai-api",
);
const patched = fs.readFileSync(dockerfilePath, "utf8");
assert.match(patched, /^ARG NEMOCLAW_PROXY_HOST=10\.200\.0\.1$/m);
assert.match(patched, /^ARG NEMOCLAW_PROXY_PORT=3128$/m);
assert.doesNotMatch(patched, /RUN rm -rf/);
} finally {
if (priorHost === undefined) {
delete process.env.NEMOCLAW_PROXY_HOST;
} else {
process.env.NEMOCLAW_PROXY_HOST = priorHost;
}
if (priorPort === undefined) {
delete process.env.NEMOCLAW_PROXY_PORT;
} else {
process.env.NEMOCLAW_PROXY_PORT = priorPort;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("patches the staged Dockerfile with Brave Search config when enabled", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-web-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
Expand Down