diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index ad724b274d8..fe4511fb65d 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -1270,6 +1270,26 @@ async function configureWebSearch( // getSandboxInferenceConfig — moved to onboard-providers.ts +// Shared validators for NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT. +// Both `patchStagedDockerfile()` (build-time Dockerfile ARG override) and +// `createSandbox()` (runtime sandbox env whitelist) must reject the same +// inputs, otherwise the build and runtime paths can diverge — e.g. a +// build-time-accepted value silently no-ops at runtime, leaving the +// container running with the default proxy. Hostname regex deliberately +// excludes `:` so raw IPv6 literals are rejected: the runtime +// `http://${HOST}:${PORT}` template does not bracket them and would +// produce a malformed URL. Port is range-checked because a 5-digit +// length filter alone would accept out-of-range values like 70000. +const PROXY_HOST_RE = /^[A-Za-z0-9._-]+$/; +function isValidProxyHost(value: string): boolean { + return PROXY_HOST_RE.test(value); +} +function isValidProxyPort(value: string): boolean { + if (!/^[0-9]{1,5}$/.test(value)) return false; + const port = Number(value); + return port >= 1 && port <= 65535; +} + function patchStagedDockerfile( dockerfilePath: string, model: string, @@ -1381,17 +1401,15 @@ function patchStagedDockerfile( // 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)) { + if (proxyHostEnv && isValidProxyHost(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)) { + if (proxyPortEnv && isValidProxyPort(proxyPortEnv)) { dockerfile = dockerfile.replace( /^ARG NEMOCLAW_PROXY_PORT=.*$/m, `ARG NEMOCLAW_PROXY_PORT=${proxyPortEnv}`, @@ -3588,6 +3606,24 @@ async function createSandbox( // 18789 and the gateway listens on the wrong port. (#2267, #1925) const effectiveDashboardPort = getDashboardForwardPort(chatUiUrl); envArgs.push(formatEnvAssignment("NEMOCLAW_DASHBOARD_PORT", effectiveDashboardPort)); + // Propagate NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT to the runtime + // sandbox container. patchStagedDockerfile() already substitutes them + // into the build-time Dockerfile ARG/ENV, but `openshell sandbox create + // -- env … nemoclaw-start` only forwards the explicitly listed env vars + // — image-baked ENV does not propagate into the running pod. Without + // this, nemoclaw-start.sh:898 falls back to the default 10.200.0.1:3128 + // and `HTTPS_PROXY` inside the sandbox ignores the host override. The + // build-time substitution and runtime env stay in sync as a result. + // Fixes #2424. Uses the shared isValidProxyHost / isValidProxyPort + // helpers so build-time and runtime validation stay aligned. + const sandboxProxyHost = process.env.NEMOCLAW_PROXY_HOST; + if (sandboxProxyHost && isValidProxyHost(sandboxProxyHost)) { + envArgs.push(formatEnvAssignment("NEMOCLAW_PROXY_HOST", sandboxProxyHost)); + } + const sandboxProxyPort = process.env.NEMOCLAW_PROXY_PORT; + if (sandboxProxyPort && isValidProxyPort(sandboxProxyPort)) { + envArgs.push(formatEnvAssignment("NEMOCLAW_PROXY_PORT", sandboxProxyPort)); + } if (webSearchConfig?.fetchEnabled) { const braveKey = getCredential(webSearch.BRAVE_API_KEY_ENV) || process.env[webSearch.BRAVE_API_KEY_ENV];