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
2 changes: 1 addition & 1 deletion ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"src/lib/inference/nim.test.ts": 2068,
"src/lib/onboard/preflight.test.ts": 1905,
"test/channels-add-preset.test.ts": 1871,
"test/generate-openclaw-config.test.ts": 1990,
"test/generate-openclaw-config.test.ts": 1989,
"test/install-preflight.test.ts": 4207,
"test/nemoclaw-start.test.ts": 5231,
"test/onboard-messaging.test.ts": 2063,
Expand Down
14 changes: 11 additions & 3 deletions scripts/generate-openclaw-config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -950,10 +950,18 @@ export function buildConfig(env: Env = process.env): JsonObject {
tools.web.fetch = { enabled: true, useTrustedEnvProxy: true };

if (env.NEMOCLAW_WEB_SEARCH_ENABLED === "1") {
tools.web.search = {
// OpenClaw 2026.5.x: web-search providers are external plugins. The
// provider-owned apiKey lives under plugins.entries.<plugin>.config,
// not inline in tools.web.search. Writing the legacy inline shape makes
// the build-time `openclaw plugins install` exit non-zero during its
// pre-install config validation (the brave plugin is not installed yet),
// aborting the image build under `set -eu` before `doctor --fix` can
// migrate it. Emit the current schema directly so install validates
// cleanly. See NemoClaw #5266 (follow-up to #4955 / #3948).
tools.web.search = { enabled: true, provider: "brave" };
config.plugins.entries.brave = {
enabled: true,
provider: "brave",
apiKey: "openshell:resolve:env:BRAVE_API_KEY",
config: { webSearch: { apiKey: "openshell:resolve:env:BRAVE_API_KEY" } },
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
34 changes: 29 additions & 5 deletions src/lib/onboard/web-search-verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ describe("verifyWebSearchInsideSandbox", () => {
});

it("verifies OpenClaw Brave Search egress through the subscription-token header", () => {
// Current schema: the provider-owned apiKey lives under
// plugins.entries.brave.config.webSearch, not inline on tools.web.search.
const d = deps([
JSON.stringify({
tools: {
web: {
search: {
tools: { web: { search: { enabled: true, provider: "brave" } } },
plugins: {
entries: {
brave: {
enabled: true,
provider: "brave",
apiKey: "openshell:resolve:env:BRAVE_API_KEY",
config: { webSearch: { apiKey: "openshell:resolve:env:BRAVE_API_KEY" } },
},
},
},
Expand All @@ -70,6 +72,28 @@ describe("verifyWebSearchInsideSandbox", () => {
expect(d.log).toHaveBeenCalledWith(" ✓ Brave Search egress verified inside sandbox");
});

it("still probes legacy configs that carry the apiKey inline on tools.web.search", () => {
const d = deps([
JSON.stringify({
tools: {
web: {
search: {
enabled: true,
provider: "brave",
apiKey: "openshell:resolve:env:BRAVE_API_KEY",
},
},
},
}),
JSON.stringify({ web: { results: [{ title: "NVIDIA" }] } }) + "\nHTTP_STATUS:200\n",
]);

verifyWebSearchInsideSandbox("alpha", { name: "openclaw" }, d);

expect(d.runCaptureOpenshell).toHaveBeenCalledTimes(2);
expect(d.log).toHaveBeenCalledWith(" ✓ Brave Search egress verified inside sandbox");
});

it("warns when OpenClaw Brave Search egress rejects the placeholder", () => {
const d = deps([
JSON.stringify({
Expand Down
12 changes: 9 additions & 3 deletions src/lib/onboard/web-search-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,21 @@ export function verifyWebSearchInsideSandbox(
log(" ✓ Web search is active inside sandbox");
return;
}
if (typeof search.apiKey !== "string" || search.apiKey.trim() === "") {
// Current OpenClaw schema keeps the provider-owned apiKey under
// plugins.entries.<provider>.config.webSearch; older configs carried
// it inline on tools.web.search. Accept both so the probe keeps
// working across schema generations.
const pluginApiKey = parsed?.plugins?.entries?.[search.provider]?.config?.webSearch?.apiKey;
const apiKey = typeof pluginApiKey === "string" ? pluginApiKey : search.apiKey;
if (typeof apiKey !== "string" || apiKey.trim() === "") {
warn(" ⚠ Brave Search is enabled but openclaw.json has no API key placeholder.");
return;
}
// Refuse to interpolate raw secrets into the curl argv. The probe
// only proves the L7 proxy rewrites a placeholder, so a literal key
// would expose itself in host/sandbox process listings without
// testing the thing we care about.
if (!/^openshell:resolve:env:[A-Za-z0-9_]+$/.test(search.apiKey.trim())) {
if (!/^openshell:resolve:env:[A-Za-z0-9_]+$/.test(apiKey.trim())) {
warn(
" ⚠ Brave Search apiKey in openclaw.json is not an OpenShell placeholder; skipping egress probe.",
);
Expand All @@ -144,7 +150,7 @@ export function verifyWebSearchInsideSandbox(
"--",
"sh",
"-lc",
buildBraveEgressProbeCommand(search.apiKey),
buildBraveEgressProbeCommand(apiKey),
],
{ ignoreError: true, timeout: 30_000 },
);
Expand Down
15 changes: 7 additions & 8 deletions test/generate-openclaw-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,18 +796,17 @@ describe("generate-openclaw-config.mts: config generation", () => {
expect(config.tools?.web?.search).toBeUndefined();
});

it("enables web search when env is '1'", () => {
it("enables web search when env is '1' using the current plugin schema", () => {
const config = runConfigScript({ NEMOCLAW_WEB_SEARCH_ENABLED: "1" });
expect(config.tools?.toolSearch).toBe(true);
expect(config.tools?.web?.search).toEqual({
// #5266: apiKey lives under plugins.entries.brave.config (not inline on
// tools.web.search) so build-time `openclaw plugins install` validates.
expect(config.tools?.web?.search).toEqual({ enabled: true, provider: "brave" });
expect(config.plugins?.entries?.brave).toEqual({
enabled: true,
provider: "brave",
apiKey: "openshell:resolve:env:BRAVE_API_KEY",
});
expect(config.tools?.web?.fetch).toEqual({
enabled: true,
useTrustedEnvProxy: true,
config: { webSearch: { apiKey: "openshell:resolve:env:BRAVE_API_KEY" } },
});
expect(config.tools?.web?.fetch).toEqual({ enabled: true, useTrustedEnvProxy: true });
});

it("omits web search when env is not set", () => {
Expand Down
Loading