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
19 changes: 10 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,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.
Expand All @@ -86,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.
Expand All @@ -97,13 +101,13 @@ 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} \
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
Expand All @@ -123,7 +127,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')); \
Expand Down Expand Up @@ -166,14 +169,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 {}); \
}) 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)"
Expand Down
10 changes: 8 additions & 2 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,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).
Expand Down Expand Up @@ -2705,6 +2705,12 @@ async function createSandbox(
// end-to-end with the stricter filtering. The allowlist rejects unknown
// env vars by default, which is safer but needs careful rollout.
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)
Expand Down
30 changes: 4 additions & 26 deletions src/lib/web-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
19 changes: 0 additions & 19 deletions src/lib/web-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
27 changes: 11 additions & 16 deletions test/onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
);
Expand Down Expand Up @@ -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=",
Expand Down Expand Up @@ -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=",
Expand Down Expand Up @@ -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"),
);
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"),
);
Expand All @@ -639,14 +639,9 @@ 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);
// 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;
Expand Down