@@ -171,6 +171,136 @@ describe("Codex app-server dynamic tool build", () => {
171171 ] ) ;
172172 } ) ;
173173
174+ it ( "removes managed web_search when domain-restricted Codex hosted search is active" , async ( ) => {
175+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
176+ const params = createParams ( path . join ( tempDir , "session.jsonl" ) , workspaceDir ) ;
177+ params . disableTools = false ;
178+ params . config = {
179+ tools : {
180+ web : {
181+ search : { openaiCodex : { allowedDomains : [ "example.com" ] } } ,
182+ } ,
183+ } ,
184+ } as never ;
185+ setOpenClawCodingToolsFactoryForTests ( ( ) => [
186+ createRuntimeDynamicTool ( "web_search" ) ,
187+ createRuntimeDynamicTool ( "message" ) ,
188+ ] ) ;
189+ let webSearchAllowed = false ;
190+
191+ const tools = await buildDynamicToolsForTest ( params , workspaceDir , {
192+ onWebSearchPolicyResolved : ( allowed ) => {
193+ webSearchAllowed = allowed ;
194+ } ,
195+ } ) ;
196+
197+ expect ( tools . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
198+ expect ( webSearchAllowed ) . toBe ( true ) ;
199+ } ) ;
200+
201+ it ( "reports hosted search denied when effective tool policy removes web_search" , async ( ) => {
202+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
203+ const params = createParams ( path . join ( tempDir , "session.jsonl" ) , workspaceDir ) ;
204+ params . disableTools = false ;
205+ setOpenClawCodingToolsFactoryForTests ( ( ) => [ createRuntimeDynamicTool ( "message" ) ] ) ;
206+ let webSearchAllowed = true ;
207+
208+ const tools = await buildDynamicToolsForTest ( params , workspaceDir , {
209+ onWebSearchPolicyResolved : ( allowed ) => {
210+ webSearchAllowed = allowed ;
211+ } ,
212+ } ) ;
213+
214+ expect ( tools . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
215+ expect ( webSearchAllowed ) . toBe ( false ) ;
216+ } ) ;
217+
218+ it ( "separates persistent search policy from a runtime toolsAllow restriction" , async ( ) => {
219+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
220+ const params = createParams ( path . join ( tempDir , "session.jsonl" ) , workspaceDir ) ;
221+ params . disableTools = false ;
222+ params . toolsAllow = [ "message" ] ;
223+ setOpenClawCodingToolsFactoryForTests ( ( ) => [
224+ createRuntimeDynamicTool ( "web_search" ) ,
225+ createRuntimeDynamicTool ( "message" ) ,
226+ ] ) ;
227+ let persistentWebSearchAllowed = false ;
228+ let webSearchAllowed = true ;
229+
230+ const tools = await buildDynamicToolsForTest ( params , workspaceDir , {
231+ onPersistentWebSearchPolicyResolved : ( allowed ) => {
232+ persistentWebSearchAllowed = allowed ;
233+ } ,
234+ onWebSearchPolicyResolved : ( allowed ) => {
235+ webSearchAllowed = allowed ;
236+ } ,
237+ } ) ;
238+
239+ expect ( tools . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
240+ expect ( persistentWebSearchAllowed ) . toBe ( true ) ;
241+ expect ( webSearchAllowed ) . toBe ( false ) ;
242+ } ) ;
243+
244+ it ( "keeps persistent search denied when runtime toolsAllow also excludes it" , async ( ) => {
245+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
246+ const params = createParams ( path . join ( tempDir , "session.jsonl" ) , workspaceDir ) ;
247+ params . disableTools = false ;
248+ params . toolsAllow = [ "message" ] ;
249+ setOpenClawCodingToolsFactoryForTests ( ( ) => [ createRuntimeDynamicTool ( "message" ) ] ) ;
250+ let persistentWebSearchAllowed = true ;
251+ let webSearchAllowed = true ;
252+
253+ const tools = await buildDynamicToolsForTest ( params , workspaceDir , {
254+ onPersistentWebSearchPolicyResolved : ( allowed ) => {
255+ persistentWebSearchAllowed = allowed ;
256+ } ,
257+ onWebSearchPolicyResolved : ( allowed ) => {
258+ webSearchAllowed = allowed ;
259+ } ,
260+ } ) ;
261+
262+ expect ( tools . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
263+ expect ( persistentWebSearchAllowed ) . toBe ( false ) ;
264+ expect ( webSearchAllowed ) . toBe ( false ) ;
265+ } ) ;
266+
267+ it ( "keeps managed web_search when a managed provider is explicitly selected" , async ( ) => {
268+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
269+ const params = createParams ( path . join ( tempDir , "session.jsonl" ) , workspaceDir ) ;
270+ params . disableTools = false ;
271+ params . config = {
272+ tools : {
273+ web : {
274+ search : { provider : "brave" } ,
275+ } ,
276+ } ,
277+ } as never ;
278+ setOpenClawCodingToolsFactoryForTests ( ( ) => [
279+ createRuntimeDynamicTool ( "web_search" ) ,
280+ createRuntimeDynamicTool ( "message" ) ,
281+ ] ) ;
282+
283+ const tools = await buildDynamicToolsForTest ( params , workspaceDir ) ;
284+
285+ expect ( tools . map ( ( tool ) => tool . name ) ) . toEqual ( [ "web_search" , "message" ] ) ;
286+ } ) ;
287+
288+ it ( "keeps managed web_search when the active Codex provider lacks hosted search" , async ( ) => {
289+ const workspaceDir = path . join ( tempDir , "workspace" ) ;
290+ const params = createParams ( path . join ( tempDir , "session.jsonl" ) , workspaceDir ) ;
291+ params . disableTools = false ;
292+ setOpenClawCodingToolsFactoryForTests ( ( ) => [
293+ createRuntimeDynamicTool ( "web_search" ) ,
294+ createRuntimeDynamicTool ( "message" ) ,
295+ ] ) ;
296+
297+ const tools = await buildDynamicToolsForTest ( params , workspaceDir , {
298+ nativeProviderWebSearchSupported : false ,
299+ } ) ;
300+
301+ expect ( tools . map ( ( tool ) => tool . name ) ) . toEqual ( [ "web_search" , "message" ] ) ;
302+ } ) ;
303+
174304 it ( "applies additional Codex dynamic tool excludes without exposing Codex-native tools" , ( ) => {
175305 const tools = [ "read" , "exec" , "message" , "custom_tool" ] . map ( ( name ) => ( { name } ) ) ;
176306
0 commit comments