From 4533ef68de96211618674c1480796cb0ca9a848e Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 10 Apr 2026 13:09:01 -0400 Subject: [PATCH 1/8] fix(security): mitigate Brave API key exposure in Docker build args Migrate Brave API key to OpenShell generic provider system to inject tokens at proxy egress, eliminating plaintext credential leaks in build logs and 'openclaw.json' baked configurations. --- src/lib/onboard.ts | 24 ++++++++++-------------- src/lib/web-search.test.ts | 17 +++++------------ src/lib/web-search.ts | 12 +++--------- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index b28bf162f19..d8dba035f95 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -771,13 +771,7 @@ function isAffirmativeAnswer(value) { ); } -function printBraveExposureWarning() { - console.log(""); - for (const line of webSearch.getBraveExposureWarningLines()) { - console.log(` ${line}`); - } - console.log(""); -} + function validateBraveSearchApiKey(apiKey) { return runCurlProbe([ @@ -882,7 +876,6 @@ async function configureWebSearch(existingConfig = null) { return null; } note(" [non-interactive] Brave Web Search requested."); - printBraveExposureWarning(); const validation = validateBraveSearchApiKey(braveApiKey); if (!validation.ok) { console.error(" Brave Search API key validation failed."); @@ -895,8 +888,6 @@ async function configureWebSearch(existingConfig = null) { process.env[webSearch.BRAVE_API_KEY_ENV] = braveApiKey; return { fetchEnabled: true }; } - - printBraveExposureWarning(); const enableAnswer = await prompt(" Enable Brave Web Search? [y/N]: "); if (!isAffirmativeAnswer(enableAnswer)) { return null; @@ -1019,10 +1010,7 @@ function patchStagedDockerfile( } dockerfile = dockerfile.replace( /^ARG NEMOCLAW_WEB_CONFIG_B64=.*$/m, - `ARG NEMOCLAW_WEB_CONFIG_B64=${webSearch.buildWebSearchDockerConfig( - webSearchConfig, - webSearchConfig ? getCredential(webSearch.BRAVE_API_KEY_ENV) : null, - )}`, + `ARG NEMOCLAW_WEB_CONFIG_B64=${webSearch.buildWebSearchDockerConfig(webSearchConfig)}`, ); // Onboard flow expects immediate dashboard access without device pairing, // so disable device auth for images built during onboard (see #1217). @@ -2324,6 +2312,14 @@ async function createSandbox( token: getMessagingToken("TELEGRAM_BOT_TOKEN"), }, ].filter(({ envKey }) => !enabledEnvKeys || enabledEnvKeys.has(envKey)); + + if (webSearchConfig) { + messagingTokenDefs.push({ + name: `${sandboxName}-brave-search`, + envKey: webSearch.BRAVE_API_KEY_ENV, + token: getCredential(webSearch.BRAVE_API_KEY_ENV), + }); + } const hasMessagingTokens = messagingTokenDefs.some(({ token }) => !!token); // Reconcile local registry state with the live OpenShell gateway state. diff --git a/src/lib/web-search.test.ts b/src/lib/web-search.test.ts index ffa87ac664c..8ac611c23b3 100644 --- a/src/lib/web-search.test.ts +++ b/src/lib/web-search.test.ts @@ -5,12 +5,11 @@ import { describe, expect, it } from "vitest"; import { buildWebSearchDockerConfig, - getBraveExposureWarningLines, } from "./web-search"; describe("web-search helpers", () => { it("emits empty docker config when web search is disabled", () => { - expect(Buffer.from(buildWebSearchDockerConfig(null, null), "base64").toString("utf8")).toBe( + expect(Buffer.from(buildWebSearchDockerConfig(null), "base64").toString("utf8")).toBe( "{}", ); }); @@ -18,24 +17,18 @@ describe("web-search helpers", () => { it("emits empty docker config when fetchEnabled is false", () => { expect( Buffer.from( - buildWebSearchDockerConfig({ fetchEnabled: false }, null), + buildWebSearchDockerConfig({ fetchEnabled: false }), "base64", ).toString("utf8"), ).toBe("{}"); }); - it("encodes Brave Search docker config including the api key", () => { - const encoded = buildWebSearchDockerConfig({ fetchEnabled: true }, "brv-x"); + it("encodes Brave Search docker config using proxy placeholder for api key", () => { + const encoded = buildWebSearchDockerConfig({ fetchEnabled: true }); expect(JSON.parse(Buffer.from(encoded, "base64").toString("utf8"))).toEqual({ provider: "brave", fetchEnabled: true, - apiKey: "brv-x", + apiKey: "openshell:resolve:env:BRAVE_API_KEY", }); }); - - it("includes the explicit exposure caveat in the warning text", () => { - const warning = getBraveExposureWarningLines().join(" "); - expect(warning).toContain("sandbox agent config"); - expect(warning).toContain("sandboxed agent will be able to read"); - }); }); diff --git a/src/lib/web-search.ts b/src/lib/web-search.ts index a545d28f90b..fc31f390090 100644 --- a/src/lib/web-search.ts +++ b/src/lib/web-search.ts @@ -11,23 +11,17 @@ export function encodeDockerJsonArg(value: unknown): string { return Buffer.from(JSON.stringify(value ?? {}), "utf8").toString("base64"); } -export function getBraveExposureWarningLines(): string[] { - return [ - "NemoClaw will store the Brave API key in the sandbox agent config.", - "The sandboxed agent will be able to read that key.", - ]; -} - export function buildWebSearchDockerConfig( config: WebSearchConfig | null, - braveApiKey: string | null, ): string { if (!config || config.fetchEnabled !== true) return encodeDockerJsonArg({}); const payload = { provider: "brave", fetchEnabled: Boolean(config.fetchEnabled), - apiKey: braveApiKey || "", + // Use the OpenShell proxy placeholder instead of the raw API key to ensure + // credentials are never baked into Docker images or raw sandbox configuration. + apiKey: `openshell:resolve:env:${BRAVE_API_KEY_ENV}`, }; return encodeDockerJsonArg(payload); } From f5b778b9a3b1bc162527aa269371c64f5284a518 Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 10 Apr 2026 13:35:45 -0400 Subject: [PATCH 2/8] fix(security): block BRAVE_API_KEY from sandbox env passthrough Also fix onboard.test.ts signature mismatch. --- src/lib/onboard.ts | 1 + test/onboard.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index b618b93f4f4..af9df5854d3 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2550,6 +2550,7 @@ async function createSandbox( "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "TELEGRAM_BOT_TOKEN", + webSearch.BRAVE_API_KEY_ENV, ]); const sandboxEnv = Object.fromEntries( Object.entries(process.env).filter(([name]) => !blockedSandboxEnvNames.has(name)), diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 147fda80966..0398170797f 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -604,7 +604,7 @@ describe("onboard helpers", () => { { fetchEnabled: true }, ); const patched = fs.readFileSync(dockerfilePath, "utf8"); - const expected = buildWebSearchDockerConfig({ fetchEnabled: true }, "brv-test-key"); + const expected = buildWebSearchDockerConfig({ fetchEnabled: true }); assert.match( patched, new RegExp( From d117dc7269e38903aaa5b9a897fa87d6a52314e2 Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 10 Apr 2026 13:49:48 -0400 Subject: [PATCH 3/8] fix(security): recreate sandbox on resume if brave search config changes --- src/lib/onboard.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index af9df5854d3..ef52f6ca265 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4573,13 +4573,22 @@ async function onboard(opts = {}) { } const sandboxReuseState = getSandboxReuseState(sandboxName); + const webSearchConfigChanged = Boolean(session?.webSearchConfig) !== Boolean(webSearchConfig); const resumeSandbox = - resume && session?.steps?.sandbox?.status === "complete" && sandboxReuseState === "ready"; + resume && + !webSearchConfigChanged && + session?.steps?.sandbox?.status === "complete" && + sandboxReuseState === "ready"; if (resumeSandbox) { skippedStepMessage("sandbox", sandboxName); } else { if (resume && session?.steps?.sandbox?.status === "complete") { - if (sandboxReuseState === "not_ready") { + if (webSearchConfigChanged) { + note(" [resume] Web Search configuration changed; recreating sandbox."); + if (sandboxName) { + registry.removeSandbox(sandboxName); + } + } else if (sandboxReuseState === "not_ready") { note( ` [resume] Recorded sandbox '${sandboxName}' exists but is not ready; recreating it.`, ); From 43fc36a0d75835c373ed6f35f62c33fa8620765c Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 10 Apr 2026 15:05:23 -0400 Subject: [PATCH 4/8] style: fix trailing whitespaces across codebase --- src/lib/onboard.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index ef52f6ca265..95116506bb4 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4575,9 +4575,9 @@ async function onboard(opts = {}) { const sandboxReuseState = getSandboxReuseState(sandboxName); const webSearchConfigChanged = Boolean(session?.webSearchConfig) !== Boolean(webSearchConfig); const resumeSandbox = - resume && - !webSearchConfigChanged && - session?.steps?.sandbox?.status === "complete" && + resume && + !webSearchConfigChanged && + session?.steps?.sandbox?.status === "complete" && sandboxReuseState === "ready"; if (resumeSandbox) { skippedStepMessage("sandbox", sandboxName); From 077305888c2c5acdf0b2462fbc7f505093eba665 Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Mon, 13 Apr 2026 09:29:56 -0400 Subject: [PATCH 5/8] feat(security): remove web search config from docker build --- Dockerfile | 11 +++-------- src/lib/onboard.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 165e17af2c6..fffd6a4ca5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,7 +60,6 @@ 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= # Base64-encoded JSON list of messaging channel names to pre-configure # (e.g. ["discord","telegram"]). Channels are added with placeholder tokens # so the L7 proxy can rewrite them at egress. Default: empty list. @@ -96,7 +95,6 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \ NEMOCLAW_INFERENCE_BASE_URL=${NEMOCLAW_INFERENCE_BASE_URL} \ NEMOCLAW_INFERENCE_API=${NEMOCLAW_INFERENCE_API} \ NEMOCLAW_INFERENCE_COMPAT_B64=${NEMOCLAW_INFERENCE_COMPAT_B64} \ - 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_DISCORD_GUILDS_B64=${NEMOCLAW_DISCORD_GUILDS_B64} \ @@ -122,7 +120,6 @@ primary_model_ref = os.environ['NEMOCLAW_PRIMARY_MODEL_REF']; \ inference_base_url = os.environ['NEMOCLAW_INFERENCE_BASE_URL']; \ inference_api = os.environ['NEMOCLAW_INFERENCE_API']; \ inference_compat = json.loads(base64.b64decode(os.environ['NEMOCLAW_INFERENCE_COMPAT_B64']).decode('utf-8')); \ -web_config = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_WEB_CONFIG_B64', 'e30=') or 'e30=').decode('utf-8')); \ msg_channels = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_MESSAGING_CHANNELS_B64', 'W10=') or 'W10=').decode('utf-8')); \ _allowed_ids = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_MESSAGING_ALLOWED_IDS_B64', 'e30=') or 'e30=').decode('utf-8')); \ _discord_guilds = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_DISCORD_GUILDS_B64', 'e30=') or 'e30=').decode('utf-8')); \ @@ -165,14 +162,12 @@ config.update({ \ 'search': { \ 'enabled': True, \ 'provider': 'brave', \ - **({'apiKey': web_config.get('apiKey', '')} if web_config.get('apiKey', '') else {}) \ + 'apiKey': 'openshell:resolve:env:BRAVE_API_KEY' \ }, \ - 'fetch': { \ - 'enabled': bool(web_config.get('fetchEnabled', True)) \ - } \ + 'fetch': {'enabled': True} \ } \ } \ -} if web_config.get('provider') == 'brave' else {}); \ +}); \ path = os.path.expanduser('~/.openclaw/openclaw.json'); \ json.dump(config, open(path, 'w'), indent=2); \ os.chmod(path, 0o600)" diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 95116506bb4..c1b1f280433 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -1008,10 +1008,6 @@ function patchStagedDockerfile( `ARG NEMOCLAW_PROXY_PORT=${proxyPortEnv}`, ); } - dockerfile = dockerfile.replace( - /^ARG NEMOCLAW_WEB_CONFIG_B64=.*$/m, - `ARG NEMOCLAW_WEB_CONFIG_B64=${webSearch.buildWebSearchDockerConfig(webSearchConfig)}`, - ); // Onboard flow expects immediate dashboard access without device pairing, // so disable device auth for images built during onboard (see #1217). dockerfile = dockerfile.replace( @@ -2540,6 +2536,12 @@ async function createSandbox( // See: crates/openshell-sandbox/src/secrets.rs (placeholder rewriting), // crates/openshell-router/src/backend.rs (inference auth injection). const envArgs = [formatEnvAssignment("CHAT_UI_URL", chatUiUrl)]; + if (webSearchConfig?.fetchEnabled) { + const braveKey = getCredential(webSearch.BRAVE_API_KEY_ENV) || process.env[webSearch.BRAVE_API_KEY_ENV]; + if (braveKey) { + envArgs.push(formatEnvAssignment(webSearch.BRAVE_API_KEY_ENV, braveKey)); + } + } const blockedSandboxEnvNames = new Set([ // Derived from REMOTE_PROVIDER_CONFIG to prevent drift ...Object.values(REMOTE_PROVIDER_CONFIG) From 745655d2b9c09dd7aab153a18c5d109754292bde Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 14 Apr 2026 13:17:32 -0700 Subject: [PATCH 6/8] fix(security): gate web search on non-secret flag, not always-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review: the Dockerfile always enabled web search with a placeholder API key, even when the user didn't configure Brave. This caused config/runtime drift — openclaw.json had search.enabled but no actual key at runtime. - Add NEMOCLAW_WEB_SEARCH_ENABLED build arg (non-secret boolean) - Gate the web search config block on this flag - Replace NEMOCLAW_WEB_CONFIG_B64 patching with NEMOCLAW_WEB_SEARCH_ENABLED - Update tests to match new arg name Signed-off-by: Prekshi Vyas Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 32 ++++++++++++++++++++------------ src/lib/onboard.ts | 4 ++-- test/onboard.test.ts | 25 +++++++++---------------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Dockerfile b/Dockerfile index 48c8b979397..889980cf3fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,6 +85,11 @@ ARG NEMOCLAW_BUILD_ID=default # before running `nemoclaw onboard`. See #1409. ARG NEMOCLAW_PROXY_HOST=10.200.0.1 ARG NEMOCLAW_PROXY_PORT=3128 +# Non-secret flag: set to "1" when the user configured Brave Search during +# onboard. Controls whether the web search block is written to openclaw.json. +# The actual API key is injected at runtime via openshell:resolve:env, never +# baked into the image. +ARG NEMOCLAW_WEB_SEARCH_ENABLED=0 # SECURITY: Promote build-args to env vars so the Python script reads them # via os.environ, never via string interpolation into Python source code. @@ -101,7 +106,8 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \ NEMOCLAW_DISCORD_GUILDS_B64=${NEMOCLAW_DISCORD_GUILDS_B64} \ NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} \ NEMOCLAW_PROXY_HOST=${NEMOCLAW_PROXY_HOST} \ - NEMOCLAW_PROXY_PORT=${NEMOCLAW_PROXY_PORT} + NEMOCLAW_PROXY_PORT=${NEMOCLAW_PROXY_PORT} \ + NEMOCLAW_WEB_SEARCH_ENABLED=${NEMOCLAW_WEB_SEARCH_ENABLED} WORKDIR /sandbox USER sandbox @@ -157,18 +163,20 @@ config = { \ 'auth': {'token': secrets.token_hex(32)} \ } \ }; \ -config.update({ \ - 'tools': { \ - 'web': { \ - 'search': { \ - 'enabled': True, \ - 'provider': 'brave', \ - 'apiKey': 'openshell:resolve:env:BRAVE_API_KEY' \ - }, \ - 'fetch': {'enabled': True} \ +web_search_enabled = os.environ.get('NEMOCLAW_WEB_SEARCH_ENABLED', '') == '1'; \ +if web_search_enabled: \ + config.update({ \ + 'tools': { \ + 'web': { \ + 'search': { \ + 'enabled': True, \ + 'provider': 'brave', \ + 'apiKey': 'openshell:resolve:env:BRAVE_API_KEY' \ + }, \ + 'fetch': {'enabled': True} \ + } \ } \ - } \ -}); \ + }); \ path = os.path.expanduser('~/.openclaw/openclaw.json'); \ json.dump(config, open(path, 'w'), indent=2); \ os.chmod(path, 0o600)" diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 6039574362f..8ef54d5b70c 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -1037,8 +1037,8 @@ function patchStagedDockerfile( ); } dockerfile = dockerfile.replace( - /^ARG NEMOCLAW_WEB_CONFIG_B64=.*$/m, - `ARG NEMOCLAW_WEB_CONFIG_B64=${webSearch.buildWebSearchDockerConfig(webSearchConfig)}`, + /^ARG NEMOCLAW_WEB_SEARCH_ENABLED=.*$/m, + `ARG NEMOCLAW_WEB_SEARCH_ENABLED=${webSearchConfig ? "1" : "0"}`, ); // Onboard flow expects immediate dashboard access without device pairing, // so disable device auth for images built during onboard (see #1217). diff --git a/test/onboard.test.ts b/test/onboard.test.ts index de6782f51b0..3542ab0f355 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -151,7 +151,7 @@ describe("onboard helpers", () => { "ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b", "ARG CHAT_UI_URL=http://127.0.0.1:18789", "ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=", - "ARG NEMOCLAW_WEB_CONFIG_B64=e30=", + "ARG NEMOCLAW_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_BUILD_ID=default", ].join("\n"), ); @@ -186,7 +186,7 @@ describe("onboard helpers", () => { "ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b", "ARG CHAT_UI_URL=http://127.0.0.1:18789", "ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=", - "ARG NEMOCLAW_WEB_CONFIG_B64=e30=", + "ARG NEMOCLAW_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_MESSAGING_CHANNELS_B64=W10=", "ARG NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=e30=", "ARG NEMOCLAW_DISCORD_GUILDS_B64=e30=", @@ -244,7 +244,7 @@ describe("onboard helpers", () => { "ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b", "ARG CHAT_UI_URL=http://127.0.0.1:18789", "ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=", - "ARG NEMOCLAW_WEB_CONFIG_B64=e30=", + "ARG NEMOCLAW_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_MESSAGING_CHANNELS_B64=W10=", "ARG NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=e30=", "ARG NEMOCLAW_DISCORD_GUILDS_B64=e30=", @@ -437,7 +437,7 @@ describe("onboard helpers", () => { "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_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_BUILD_ID=default", ].join("\n"), ); @@ -474,7 +474,7 @@ describe("onboard helpers", () => { "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_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_BUILD_ID=default", "ARG NEMOCLAW_PROXY_HOST=10.200.0.1", "ARG NEMOCLAW_PROXY_PORT=3128", @@ -526,7 +526,7 @@ describe("onboard helpers", () => { "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_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_BUILD_ID=default", "ARG NEMOCLAW_PROXY_HOST=10.200.0.1", "ARG NEMOCLAW_PROXY_PORT=3128", @@ -569,7 +569,7 @@ describe("onboard helpers", () => { "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_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_BUILD_ID=default", "ARG NEMOCLAW_PROXY_HOST=10.200.0.1", "ARG NEMOCLAW_PROXY_PORT=3128", @@ -621,7 +621,7 @@ describe("onboard helpers", () => { "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_WEB_SEARCH_ENABLED=0", "ARG NEMOCLAW_BUILD_ID=default", ].join("\n"), ); @@ -639,14 +639,7 @@ describe("onboard helpers", () => { { fetchEnabled: true }, ); const patched = fs.readFileSync(dockerfilePath, "utf8"); - const expected = buildWebSearchDockerConfig({ fetchEnabled: true }); - assert.match( - patched, - new RegExp( - `^ARG NEMOCLAW_WEB_CONFIG_B64=${expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, - "m", - ), - ); + assert.match(patched, /^ARG NEMOCLAW_WEB_SEARCH_ENABLED=1$/m); } finally { if (priorBraveKey === undefined) { delete process.env.BRAVE_API_KEY; From ea02938a440e7c2a968ff1683a5d12f03d77437f Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 14 Apr 2026 13:28:12 -0700 Subject: [PATCH 7/8] fix(dockerfile): use inline conditional for web search config The Dockerfile's Python script is a single-line python3 -c command. An if-block with indented body is invalid in this context. Use the inline ternary form: config.update({...}) if condition else None. Signed-off-by: Prekshi Vyas Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 889980cf3fb..5e0b8e25b84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -163,20 +163,18 @@ config = { \ 'auth': {'token': secrets.token_hex(32)} \ } \ }; \ -web_search_enabled = os.environ.get('NEMOCLAW_WEB_SEARCH_ENABLED', '') == '1'; \ -if web_search_enabled: \ - config.update({ \ - 'tools': { \ - 'web': { \ - 'search': { \ - 'enabled': True, \ - 'provider': 'brave', \ - 'apiKey': 'openshell:resolve:env:BRAVE_API_KEY' \ - }, \ - 'fetch': {'enabled': True} \ - } \ +config.update({ \ + 'tools': { \ + 'web': { \ + 'search': { \ + 'enabled': True, \ + 'provider': 'brave', \ + 'apiKey': 'openshell:resolve:env:BRAVE_API_KEY' \ + }, \ + 'fetch': {'enabled': True} \ } \ - }); \ + } \ +}) if os.environ.get('NEMOCLAW_WEB_SEARCH_ENABLED', '') == '1' else None; \ path = os.path.expanduser('~/.openclaw/openclaw.json'); \ json.dump(config, open(path, 'w'), indent=2); \ os.chmod(path, 0o600)" From 73773f1ee435ca3a8a9325f962d7d62be9bf6117 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 14 Apr 2026 13:34:53 -0700 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20address=20nitpicks=20=E2=80=94=20r?= =?UTF-8?q?egression=20guard=20and=20dead=20code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add negative assertion: patched Dockerfile must NOT contain NEMOCLAW_WEB_CONFIG_B64 (prevents secret-bearing arg reintroduction) - Remove dead buildWebSearchDockerConfig() and encodeDockerJsonArg() from web-search.ts — no longer called after build arg removal - Update web-search tests to match simplified module Signed-off-by: Prekshi Vyas Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/web-search.test.ts | 30 ++++-------------------------- src/lib/web-search.ts | 19 ------------------- test/onboard.test.ts | 2 ++ 3 files changed, 6 insertions(+), 45 deletions(-) diff --git a/src/lib/web-search.test.ts b/src/lib/web-search.test.ts index 8ac611c23b3..cf98d55d781 100644 --- a/src/lib/web-search.test.ts +++ b/src/lib/web-search.test.ts @@ -3,32 +3,10 @@ import { describe, expect, it } from "vitest"; -import { - buildWebSearchDockerConfig, -} from "./web-search"; +import { BRAVE_API_KEY_ENV } from "./web-search"; -describe("web-search helpers", () => { - it("emits empty docker config when web search is disabled", () => { - expect(Buffer.from(buildWebSearchDockerConfig(null), "base64").toString("utf8")).toBe( - "{}", - ); - }); - - it("emits empty docker config when fetchEnabled is false", () => { - expect( - Buffer.from( - buildWebSearchDockerConfig({ fetchEnabled: false }), - "base64", - ).toString("utf8"), - ).toBe("{}"); - }); - - it("encodes Brave Search docker config using proxy placeholder for api key", () => { - const encoded = buildWebSearchDockerConfig({ fetchEnabled: true }); - expect(JSON.parse(Buffer.from(encoded, "base64").toString("utf8"))).toEqual({ - provider: "brave", - fetchEnabled: true, - apiKey: "openshell:resolve:env:BRAVE_API_KEY", - }); +describe("web-search module", () => { + it("exports BRAVE_API_KEY_ENV constant", () => { + expect(BRAVE_API_KEY_ENV).toBe("BRAVE_API_KEY"); }); }); diff --git a/src/lib/web-search.ts b/src/lib/web-search.ts index fc31f390090..dd6d7682ac9 100644 --- a/src/lib/web-search.ts +++ b/src/lib/web-search.ts @@ -6,22 +6,3 @@ export interface WebSearchConfig { } export const BRAVE_API_KEY_ENV = "BRAVE_API_KEY"; - -export function encodeDockerJsonArg(value: unknown): string { - return Buffer.from(JSON.stringify(value ?? {}), "utf8").toString("base64"); -} - -export function buildWebSearchDockerConfig( - config: WebSearchConfig | null, -): string { - if (!config || config.fetchEnabled !== true) return encodeDockerJsonArg({}); - - const payload = { - provider: "brave", - fetchEnabled: Boolean(config.fetchEnabled), - // Use the OpenShell proxy placeholder instead of the raw API key to ensure - // credentials are never baked into Docker images or raw sandbox configuration. - apiKey: `openshell:resolve:env:${BRAVE_API_KEY_ENV}`, - }; - return encodeDockerJsonArg(payload); -} diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 3542ab0f355..0f18b63703c 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -640,6 +640,8 @@ describe("onboard helpers", () => { ); const patched = fs.readFileSync(dockerfilePath, "utf8"); assert.match(patched, /^ARG NEMOCLAW_WEB_SEARCH_ENABLED=1$/m); + // Regression guard: the old secret-bearing build arg must not reappear. + assert.doesNotMatch(patched, /NEMOCLAW_WEB_CONFIG_B64/); } finally { if (priorBraveKey === undefined) { delete process.env.BRAVE_API_KEY;