diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 9c348d06862..6a9f7517500 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2749,7 +2749,7 @@ async function createSandboxWithBaseImageResolution( messagingTokenDefs, reusableMessagingChannels, reusableMessagingProviders, - extraProviders: registry.listExtraProviders(), + ...sandboxCreatePlan.extraProviderSelection(registry.listExtraProviders(), webSearchConfig), hermesToolGateways, sandboxGpuConfig: effectiveSandboxGpuConfig, dockerDriverGateway, diff --git a/src/lib/onboard/sandbox-create-plan.test.ts b/src/lib/onboard/sandbox-create-plan.test.ts index 3a135c25e64..0bd8553edea 100644 --- a/src/lib/onboard/sandbox-create-plan.test.ts +++ b/src/lib/onboard/sandbox-create-plan.test.ts @@ -589,6 +589,46 @@ describe("prepareSandboxCreatePlan", () => { expect(providerArgs).toEqual(["tavily-search", "custom-provider"]); }); + it("keeps only the selected managed web-search extra provider", () => { + const result = prepareSandboxCreatePlan({ + basePolicyPath: "/repo/policy.yaml", + buildCtx: "/tmp/nemoclaw-build-1", + sandboxName: "sandbox", + channels, + enabledChannels: [], + disabledChannelNames: new Set(), + messagingTokenDefs: [], + reusableMessagingChannels: [], + reusableMessagingProviders: [], + extraProviders: ["brave-search", "tavily-search", "custom-provider"], + webSearchConfig: { fetchEnabled: true, provider: "brave" }, + hermesToolGateways: [], + sandboxGpuConfig, + dockerDriverGateway: true, + appendResourceFlags: vi.fn(), + runProviderPreDeleteCleanup: vi.fn(), + upsertMessagingProviders: vi.fn(() => []), + getMessagingChannelForEnvKey: () => null, + getHermesToolGatewayProviderName: vi.fn(), + deps: { + resolveDockerGpuSandboxCreatePlan: vi.fn(() => ({ + useDockerGpuPatch: false, + logMessage: null, + })), + prepareInitialSandboxCreatePolicy: vi.fn(() => ({ + policyPath: "/tmp/policy.yaml", + appliedPresets: [], + })), + buildSandboxGpuCreateArgs: vi.fn(() => []), + }, + }); + + const providerArgs = result.createArgs + .map((arg, index) => (arg === "--provider" ? result.createArgs[index + 1] : null)) + .filter((value): value is string => value !== null); + expect(providerArgs).toEqual(["brave-search", "custom-provider"]); + }); + it("does not duplicate an extra provider that is already a messaging provider", () => { const result = prepareSandboxCreatePlan({ basePolicyPath: "/repo/policy.yaml", diff --git a/src/lib/onboard/sandbox-create-plan.ts b/src/lib/onboard/sandbox-create-plan.ts index 84ec37405fc..8d7963a374b 100644 --- a/src/lib/onboard/sandbox-create-plan.ts +++ b/src/lib/onboard/sandbox-create-plan.ts @@ -1,6 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { + WEB_SEARCH_PROVIDERS, + type WebSearchConfig, + webSearchProviderForConfig, +} from "../inference/web-search"; import { listMessagingCredentialMetadata, type MessagingCredentialMetadata, @@ -32,6 +37,9 @@ export type { // tier there requires updating this set so an explicit tier env value reaches // the create-time policy decision. const KNOWN_POLICY_TIER_NAMES = new Set(["restricted", "balanced", "open"]); +const MANAGED_WEB_SEARCH_EXTRA_PROVIDERS = new Set( + WEB_SEARCH_PROVIDERS.map((provider) => `${provider}-search`), +); function readPolicyTierEnv(): string | null { // Only trust the env value in non-interactive mode. Interactive flows let the @@ -70,6 +78,11 @@ export type PrepareSandboxCreatePlanInput = { reusableMessagingChannels: string[]; reusableMessagingProviders: string[]; extraProviders?: readonly string[]; + /** + * Undefined keeps generic planner callers unchanged. Null is an + * authoritative onboard/rebuild decision to disable managed web search. + */ + webSearchConfig?: WebSearchConfig | null; hermesToolGateways: string[]; sandboxGpuConfig: SandboxGpuCreateConfig; dockerDriverGateway: boolean; @@ -97,6 +110,13 @@ export type SandboxCreatePlan = { sandboxGpuLogMessage: string | null; }; +export function extraProviderSelection( + extraProviders: readonly string[], + webSearchConfig: WebSearchConfig | null, +): Pick { + return { extraProviders, webSearchConfig }; +} + function getDockerGpuSandboxCreatePlan( ...args: Parameters ): ReturnType { @@ -148,6 +168,21 @@ function filterMessagingProvidersByEnabledChannel( }); } +function normalizeExtraProvidersForWebSearch( + extraProviders: readonly string[] | undefined, + webSearchConfig: WebSearchConfig | null | undefined, +): string[] { + const normalized = [...new Set(extraProviders ?? [])].filter(Boolean); + if (webSearchConfig === undefined) return normalized; + const selectedWebSearchProvider = webSearchConfig + ? `${webSearchProviderForConfig(webSearchConfig)}-search` + : null; + return normalized.filter( + (provider) => + !MANAGED_WEB_SEARCH_EXTRA_PROVIDERS.has(provider) || provider === selectedWebSearchProvider, + ); +} + function resolveActiveMessagingChannels({ channels, disabledChannelNames, @@ -411,6 +446,7 @@ export function prepareSandboxCreatePlan({ reusableMessagingChannels, reusableMessagingProviders, extraProviders, + webSearchConfig, hermesToolGateways, sandboxGpuConfig, dockerDriverGateway, @@ -446,7 +482,7 @@ export function prepareSandboxCreatePlan({ primaryMessagingCredentialEnvKeys: [...getPrimaryCredentialEnvKeys()], reusableMessagingChannels, reusableMessagingProviders, - extraProviders, + extraProviders: normalizeExtraProvidersForWebSearch(extraProviders, webSearchConfig), hermesToolGateways, sandboxGpuConfig, gpuCreateArgs, diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 4b8534bc8e4..5c641c5403c 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -1877,7 +1877,7 @@ const { createSandbox } = require(${onboardPath}); entry.command.includes("sandbox create"), ); assert.ok(createCommand, "expected sandbox create command"); - assert.match(createCommand.command, /(?=.*nemoclaw-start)(?=.*--provider tavily-search)/); + assert.match(createCommand.command, /^(?=.*nemoclaw-start)(?!.*--provider tavily-search)/s); assert.doesNotMatch(createCommand.command, /--upload/); assert.doesNotMatch(createCommand.command, /OPENCLAW_CONFIG_PATH/); assert.doesNotMatch(createCommand.command, /NVIDIA_INFERENCE_API_KEY=/);