@@ -37,6 +37,35 @@ func (provider *mockProvider) StreamCompletion(ctx context.Context, request zero
3737 return ch , nil
3838}
3939
40+ type recordingWebSearchTool struct {
41+ calls []map [string ]any
42+ }
43+
44+ func (tool * recordingWebSearchTool ) Name () string { return "web_search" }
45+ func (tool * recordingWebSearchTool ) Description () string { return "test web search tool" }
46+ func (tool * recordingWebSearchTool ) Parameters () tools.Schema {
47+ return tools.Schema {
48+ Type : "object" ,
49+ Properties : map [string ]tools.PropertySchema {
50+ "query" : {Type : "string" },
51+ },
52+ Required : []string {"query" },
53+ AdditionalProperties : false ,
54+ }
55+ }
56+ func (tool * recordingWebSearchTool ) Safety () tools.Safety {
57+ return tools.Safety {
58+ SideEffect : tools .SideEffectNetwork ,
59+ Permission : tools .PermissionPrompt ,
60+ Reason : "Sends model-provided search query text to the configured web search backend." ,
61+ AdvertiseInAuto : true ,
62+ }
63+ }
64+ func (tool * recordingWebSearchTool ) Run (_ context.Context , args map [string ]any ) tools.Result {
65+ tool .calls = append (tool .calls , cloneArgs (args ))
66+ return tools.Result {Status : tools .StatusOK , Output : "1. T — https://x.test" }
67+ }
68+
4069type sandboxDeniedRetryTool struct {
4170 calls []map [string ]any
4271}
@@ -865,7 +894,7 @@ func TestRunRejectsLocalWebFetchBeforePermissionRequest(t *testing.T) {
865894 }
866895}
867896
868- func TestRunAdvertisesAllowedWebSearchInAutoMode (t * testing.T ) {
897+ func TestRunAdvertisesPromptedWebSearchInAutoMode (t * testing.T ) {
869898 t .Setenv ("ZERO_WEBSEARCH_BASE_URL" , "https://search.example/api" )
870899 registry := tools .NewRegistry ()
871900 for _ , tool := range tools .CoreNetworkTools () {
@@ -896,6 +925,52 @@ func TestRunAdvertisesAllowedWebSearchInAutoMode(t *testing.T) {
896925 }
897926}
898927
928+ func TestRunRequestsPermissionBeforeWebSearchExecution (t * testing.T ) {
929+ search := & recordingWebSearchTool {}
930+ registry := tools .NewRegistry ()
931+ registry .Register (search )
932+ provider := & mockProvider {
933+ turns : [][]zeroruntime.StreamEvent {
934+ {
935+ {Type : zeroruntime .StreamEventToolCallStart , ToolCallID : "call-1" , ToolName : "web_search" },
936+ {Type : zeroruntime .StreamEventToolCallDelta , ToolCallID : "call-1" , ArgumentsFragment : `{"query":"private workspace detail"}` },
937+ {Type : zeroruntime .StreamEventToolCallEnd , ToolCallID : "call-1" },
938+ {Type : zeroruntime .StreamEventDone },
939+ },
940+ {
941+ {Type : zeroruntime .StreamEventText , Content : "done" },
942+ {Type : zeroruntime .StreamEventDone },
943+ },
944+ },
945+ }
946+ var requests []PermissionRequest
947+
948+ result , err := Run (context .Background (), "search" , provider , Options {
949+ Registry : registry ,
950+ PermissionMode : PermissionModeAsk ,
951+ OnPermissionRequest : func (_ context.Context , request PermissionRequest ) (PermissionDecision , error ) {
952+ requests = append (requests , request )
953+ return PermissionDecision {Action : PermissionDecisionDeny , Reason : "network not approved" }, nil
954+ },
955+ })
956+
957+ if err != nil {
958+ t .Fatal (err )
959+ }
960+ if result .FinalAnswer != "done" {
961+ t .Fatalf ("expected final answer after denied tool call, got %q" , result .FinalAnswer )
962+ }
963+ if len (requests ) != 1 {
964+ t .Fatalf ("expected one permission request, got %#v" , requests )
965+ }
966+ if requests [0 ].ToolName != "web_search" || requests [0 ].Permission != string (tools .PermissionPrompt ) || requests [0 ].SideEffect != string (tools .SideEffectNetwork ) {
967+ t .Fatalf ("unexpected permission request: %#v" , requests [0 ])
968+ }
969+ if len (search .calls ) != 0 {
970+ t .Fatalf ("web_search backend must not run when permission is denied, got calls %#v" , search .calls )
971+ }
972+ }
973+
899974func TestRunFiltersAdvertisedTools (t * testing.T ) {
900975 root := t .TempDir ()
901976 registry := tools .NewRegistry ()
0 commit comments