From 56bd23b1ec80bcd72829ab1246a85f1c14a53e4d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2026 09:47:33 +0000 Subject: [PATCH 1/5] feat(toolapproval): add auto-approval funcs (heuristics) to Config Port microsoft/agent-framework#6335: add heuristic auto-approval rules to ToolApprovalAgent. AutoApprovalFuncs in Config lets callers provide predicate functions that are evaluated after standing rules but before surfacing an approval request to the user. The first function returning true auto-approves the call without interrupting the caller. - Add AutoApprovalFuncs []func(*message.FunctionCallContent) bool to Config - Change New to capture cfg via closure instead of ignoring it - Change run to accept Config and thread it through to drainAutoApprovable - Add matchesAutoApprovalFuncs helper (first-match-wins semantics) - Apply auto-approval funcs in drainAutoApprovable (queued requests) and in the outbound classification loop (new requests from inner agent) - Add 5 tests covering: matching, non-matching, first-match-wins, queued request drain, and standing-rule precedence Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/harness/toolapproval/toolapproval.go | 53 +++- .../harness/toolapproval/toolapproval_test.go | 237 ++++++++++++++++++ docs/dotnet-go-sdk-feature-comparison.md | 2 +- 3 files changed, 280 insertions(+), 12 deletions(-) diff --git a/agent/harness/toolapproval/toolapproval.go b/agent/harness/toolapproval/toolapproval.go index 3683b7f8..f4e46098 100644 --- a/agent/harness/toolapproval/toolapproval.go +++ b/agent/harness/toolapproval/toolapproval.go @@ -77,15 +77,22 @@ func saveState(opts []agent.Option, s state) { // New creates a tool-approval middleware that wraps agent runs with // human-in-the-loop approval management. func New(cfg Config) agent.Middleware { - // Config is currently empty and reserved for future extensibility. - _ = cfg - return agent.MiddlewareFunc(run) + return agent.MiddlewareFunc(func(next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { + return run(cfg, next, ctx, messages, opts...) + }) } // Config configures tool-approval middleware behavior. -type Config struct{} +type Config struct { + // AutoApprovalFuncs is an optional list of heuristic functions evaluated after + // standing rules (derived from prior user approvals) but before surfacing the + // approval request to the caller. Each function receives the tool call and returns + // true to auto-approve it. Functions are evaluated in order; the first returning + // true causes the request to be auto-approved without prompting the caller. + AutoApprovalFuncs []func(*message.FunctionCallContent) bool +} -func run(next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { +func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { return func(yield func(*agent.ResponseUpdate, error) bool) { st := loadState(opts) @@ -94,7 +101,7 @@ func run(next agent.RunFunc, ctx context.Context, messages []*message.Message, o // Step 2: If we have queued requests from a previous turn, drain any // that are now auto-approvable and surface the next one. - drainAutoApprovable(&st) + drainAutoApprovable(cfg, &st) if len(st.QueuedRequests) > 0 { next := st.QueuedRequests[0] st.QueuedRequests = st.QueuedRequests[1:] @@ -144,6 +151,8 @@ func run(next agent.RunFunc, ctx context.Context, messages []*message.Message, o for _, req := range approvalRequests { if matchesRule(st.Rules, req) { autoApproved = append(autoApproved, req.CreateResponse(true, "")) + } else if matchesAutoApprovalFuncs(cfg.AutoApprovalFuncs, req) { + autoApproved = append(autoApproved, req.CreateResponse(true, "")) } else { needsApproval = append(needsApproval, req) } @@ -243,15 +252,18 @@ func prepareInbound(messages []*message.Message, st state) ([]*message.Message, return messages, st } -// drainAutoApprovable removes queued requests that now match a standing rule, -// adding auto-approve responses to collected. -func drainAutoApprovable(st *state) { - if len(st.QueuedRequests) == 0 || len(st.Rules) == 0 { +// drainAutoApprovable removes queued requests that now match a standing rule +// or an auto-approval func, adding auto-approve responses to collected. +func drainAutoApprovable(cfg Config, st *state) { + if len(st.QueuedRequests) == 0 { + return + } + if len(st.Rules) == 0 && len(cfg.AutoApprovalFuncs) == 0 { return } var remaining []*message.ToolApprovalRequestContent for _, req := range st.QueuedRequests { - if matchesRule(st.Rules, req) { + if matchesRule(st.Rules, req) || matchesAutoApprovalFuncs(cfg.AutoApprovalFuncs, req) { st.CollectedResponses = append(st.CollectedResponses, req.CreateResponse(true, "")) } else { remaining = append(remaining, req) @@ -277,6 +289,25 @@ func matchesRule(rules []Rule, req *message.ToolApprovalRequestContent) bool { return false } +// matchesAutoApprovalFuncs returns true if any configured auto-approval func +// approves the request. Funcs are evaluated in order; the first returning true +// wins. Returns false when funcs is empty or the request is not a function call. +func matchesAutoApprovalFuncs(funcs []func(*message.FunctionCallContent) bool, req *message.ToolApprovalRequestContent) bool { + if len(funcs) == 0 { + return false + } + fc, ok := req.ToolCall.(*message.FunctionCallContent) + if !ok || fc == nil { + return false + } + for _, fn := range funcs { + if fn(fc) { + return true + } + } + return false +} + func serializeArguments(arguments string) (map[string]string, error) { if strings.TrimSpace(arguments) == "" { return nil, nil diff --git a/agent/harness/toolapproval/toolapproval_test.go b/agent/harness/toolapproval/toolapproval_test.go index ccbfa145..31d895b2 100644 --- a/agent/harness/toolapproval/toolapproval_test.go +++ b/agent/harness/toolapproval/toolapproval_test.go @@ -367,3 +367,240 @@ func TestToolApproval_AlwaysApproveToolWithArgumentsMatchesByValue(t *testing.T) t.Fatal("expected done after auto-approval with argument-value match") } } + +func TestToolApproval_AutoApprovalFunc_ApprovesMatchingTool(t *testing.T) { + fcc := &message.FunctionCallContent{CallID: "c1", Name: "ReadTool", Arguments: `{}`} + + // Turn 1: inner returns an approval request. + // Turn 2 (triggered automatically after auto-approval): inner returns done. + runner := &agenttest.Runner{ + Responses: agenttest.NewResponseBuilder(). + Add(&agent.ResponseUpdate{ + Role: message.RoleAssistant, + Contents: []message.Content{ + &message.ToolApprovalRequestContent{RequestID: "r1", ToolCall: fcc}, + }, + }). + NewTurn(). + AddText("done"). + Build(), + } + + cfg := toolapproval.Config{ + AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ + func(fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, + }, + } + mw := toolapproval.New(cfg) + session := agenttest.CreateSession() + + updates := collectUpdates(t, mw, runner.Run, + []*message.Message{{Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}}, + agent.WithSession(session), + ) + + // Should receive "done" without any approval request surfaced to the caller. + var gotDone bool + for _, u := range updates { + for _, c := range u.Contents { + if tc, ok := c.(*message.TextContent); ok && tc.Text == "done" { + gotDone = true + } + if _, ok := c.(*message.ToolApprovalRequestContent); ok { + t.Fatal("expected no approval request to be surfaced when auto-approval func matches") + } + } + } + if !gotDone { + t.Error("expected 'done' text after auto-approval func approved the tool") + } +} + +func TestToolApproval_AutoApprovalFunc_DoesNotMatchSurfacesToCaller(t *testing.T) { + fcc := &message.FunctionCallContent{CallID: "c1", Name: "DangerousTool", Arguments: `{}`} + + runner := &agenttest.Runner{ + Responses: agenttest.NewResponseBuilder(). + Add(&agent.ResponseUpdate{ + Role: message.RoleAssistant, + Contents: []message.Content{ + &message.ToolApprovalRequestContent{RequestID: "r1", ToolCall: fcc}, + }, + }). + Build(), + } + + cfg := toolapproval.Config{ + AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ + func(fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, // only approves ReadTool + }, + } + mw := toolapproval.New(cfg) + + updates := collectUpdates(t, mw, runner.Run, + []*message.Message{{Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}}, + ) + + var approvalReqs []*message.ToolApprovalRequestContent + for _, u := range updates { + for _, c := range u.Contents { + if req, ok := c.(*message.ToolApprovalRequestContent); ok { + approvalReqs = append(approvalReqs, req) + } + } + } + if len(approvalReqs) != 1 { + t.Fatalf("expected 1 approval request surfaced, got %d", len(approvalReqs)) + } + fc, ok := approvalReqs[0].ToolCall.(*message.FunctionCallContent) + if !ok || fc.Name != "DangerousTool" { + t.Errorf("expected DangerousTool to be surfaced, got %v", approvalReqs[0].ToolCall) + } +} + +func TestToolApproval_MultipleAutoApprovalFuncs_FirstMatchWins(t *testing.T) { + fcc := &message.FunctionCallContent{CallID: "c1", Name: "SpecialTool", Arguments: `{}`} + + runner := &agenttest.Runner{ + Responses: agenttest.NewResponseBuilder(). + Add(&agent.ResponseUpdate{ + Role: message.RoleAssistant, + Contents: []message.Content{ + &message.ToolApprovalRequestContent{RequestID: "r1", ToolCall: fcc}, + }, + }). + NewTurn(). + AddText("done"). + Build(), + } + + rule1Called := false + rule2Called := false + cfg := toolapproval.Config{ + AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ + func(fc *message.FunctionCallContent) bool { + rule1Called = true + return fc.Name == "SpecialTool" + }, + func(_ *message.FunctionCallContent) bool { + rule2Called = true + return true // should not be reached + }, + }, + } + mw := toolapproval.New(cfg) + session := agenttest.CreateSession() + + collectUpdates(t, mw, runner.Run, + []*message.Message{{Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}}, + agent.WithSession(session), + ) + + if !rule1Called { + t.Error("expected first auto-approval func to be called") + } + if rule2Called { + t.Error("expected second auto-approval func to NOT be called when first already matched") + } +} + +func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalFunc(t *testing.T) { + fcc := &message.FunctionCallContent{CallID: "c1", Name: "MyTool", Arguments: `{}`} + + runner := &agenttest.Runner{ + Responses: agenttest.NewResponseBuilder(). + Add(&agent.ResponseUpdate{ + Role: message.RoleAssistant, + Contents: []message.Content{ + &message.ToolApprovalRequestContent{RequestID: "r1", ToolCall: fcc}, + }, + }). + NewTurn(). + AddText("done"). + Build(), + } + + heuristicCalled := false + cfg := toolapproval.Config{ + AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ + func(_ *message.FunctionCallContent) bool { + heuristicCalled = true + return true + }, + }, + } + mw := toolapproval.New(cfg) + session := agenttest.CreateSession() + opts := []agent.Option{agent.WithSession(session)} + + // Turn 1: auto-approval func is called (no standing rule yet). + updates := collectUpdates(t, mw, runner.Run, + []*message.Message{{Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}}, + opts..., + ) + + if !heuristicCalled { + t.Error("expected auto-approval func to be called on first turn") + } + + var gotDone bool + for _, u := range updates { + for _, c := range u.Contents { + if tc, ok := c.(*message.TextContent); ok && tc.Text == "done" { + gotDone = true + } + } + } + if !gotDone { + t.Error("expected 'done' after auto-approval func approved on first turn") + } +} + +func TestToolApproval_AutoApprovalFunc_ApprovesQueuedRequests(t *testing.T) { + fcc1 := &message.FunctionCallContent{CallID: "c1", Name: "SafeTool", Arguments: `{}`} + fcc2 := &message.FunctionCallContent{CallID: "c2", Name: "DangerousTool", Arguments: `{}`} + + runner := &agenttest.Runner{ + Responses: agenttest.NewResponseBuilder(). + Add(&agent.ResponseUpdate{ + Role: message.RoleAssistant, + Contents: []message.Content{ + &message.ToolApprovalRequestContent{RequestID: "r1", ToolCall: fcc1}, + &message.ToolApprovalRequestContent{RequestID: "r2", ToolCall: fcc2}, + }, + }). + Build(), + } + + // AutoApprovalFunc approves SafeTool but not DangerousTool. + cfg := toolapproval.Config{ + AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ + func(fc *message.FunctionCallContent) bool { return fc.Name == "SafeTool" }, + }, + } + mw := toolapproval.New(cfg) + session := agenttest.CreateSession() + opts := []agent.Option{agent.WithSession(session)} + + // Turn 1: r1 (SafeTool) is auto-approved; r2 (DangerousTool) is surfaced. + updates := collectUpdates(t, mw, runner.Run, + []*message.Message{{Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}}, + opts..., + ) + + var surfacedReqs []*message.ToolApprovalRequestContent + for _, u := range updates { + for _, c := range u.Contents { + if req, ok := c.(*message.ToolApprovalRequestContent); ok { + surfacedReqs = append(surfacedReqs, req) + } + } + } + if len(surfacedReqs) != 1 { + t.Fatalf("expected 1 surfaced request (DangerousTool), got %d", len(surfacedReqs)) + } + fc, ok := surfacedReqs[0].ToolCall.(*message.FunctionCallContent) + if !ok || fc.Name != "DangerousTool" { + t.Errorf("expected DangerousTool to be surfaced, got %v", surfacedReqs[0].ToolCall) + } +} diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 5d224030..02980925 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -39,7 +39,7 @@ Within overlapping features, the main misalignments are API shape and ecosystem | Function tools | `AIFunction`, `AITool`, function tools, plugins, dynamic function tools, tool argument matching in evals. | `tool.Tool`, `tool.FuncTool`, `functool.New`, typed input/output schemas. | Partial | Go has typed function tools but no first-class plugin or dynamic tool sample equivalent to .NET steps 12 and 20. | | Shell tool and environment context | `Microsoft.Agents.AI.Tools.Shell`: `LocalShellExecutor`, `ShellPolicy` (allow/deny-list), `ShellResult`, stateless and persistent shell execution modes, approval-in-the-loop gate, head-tail output truncation, `ShellEnvironmentProvider`, `ShellEnvironmentSnapshot`, shell-family instructions, common CLI probing. | `tool/shelltool.NewLocal`, `shelltool.LocalConfig` (mode, timeout, max output, policy, acknowledge unsafe), `shelltool.Policy`, `shelltool.Result.FormatForModel`, `shelltool.Executor`, `shelltool.NewEnvironmentProvider`, `EnvironmentProviderConfig`, `ShellEnvironmentSnapshot`, `DefaultShellEnvironmentInstructions`. | Aligned | Go mirrors the .NET design for local execution, policy allow/deny-list, approval-required by default, stateless/persistent modes, output truncation, environment snapshot probing, cached first-probe behavior, refresh, current snapshot access, shell-family prompt instructions, invalid/duplicate probe handling, stderr version fallback, caller cancellation, and probe timeout handling. Docker shell executor not ported (Go has no equivalent `DockerShellExecutor`). Go represents tool-version nullability with `ToolVersion{Found bool}` rather than nullable strings. | | Tool auto-calling | Provider/tool-call loop, tool approval agent, and message injection during the function loop (`EnableMessageInjection` / `MessageInjectingChatClient`). | `agent/harness/toolautocall`, default provider middleware unless disabled. Message injection supported via `Config.EnableMessageInjection` and `toolautocall.MessageInjectorFromContext(ctx)`. | Aligned | Go implements auto-call as explicit middleware; .NET uses agent/tool abstractions and provider adapters. | -| Tool approval | Tool approval request/response content, tool approval agent and builder extensions. | `message.ToolApprovalRequestContent`, `message.ToolApprovalResponseContent`, `tool.ApprovalRequiredFunc`, `agent/harness/toolautocall` approval flow, `agent/harness/toolapproval` middleware for standing-rule approval management, AGUI HITL sample. | Aligned | API shape differs: .NET uses a `ToolApprovalAgent` delegating-agent wrapper with a `.UseToolApproval()` builder extension; Go uses idiomatic middleware (`toolapproval.New`). Standing approval rules, queued-request batching, and `AlwaysApprove*` response content are now present in both SDKs. | +| Tool approval | Tool approval request/response content, tool approval agent and builder extensions, auto-approval rules (heuristics). | `message.ToolApprovalRequestContent`, `message.ToolApprovalResponseContent`, `tool.ApprovalRequiredFunc`, `agent/harness/toolautocall` approval flow, `agent/harness/toolapproval` middleware for standing-rule and auto-approval-func approval management, AGUI HITL sample. | Aligned | API shape differs: .NET uses a `ToolApprovalAgent` delegating-agent wrapper with `ToolApprovalAgentOptions`; Go uses idiomatic middleware (`toolapproval.New(Config{AutoApprovalFuncs: ...})`). Standing approval rules, queued-request batching, `AlwaysApprove*` response content, and auto-approval funcs (heuristics) are now present in both SDKs. | | Hosted/server-side tools | Foundry/OpenAI samples for code interpreter, file search, web search, OpenAPI, Bing custom search, SharePoint, Microsoft Fabric, memory search, Toolbox, hosted MCP. | `tool/hostedtool` declarations for web search, file search, code interpreter, MCP server. | Partial | Go has declaration types but less provider/sample coverage and fewer service-specific hosted tool integrations. | | Agent as function tool | Agents can be converted/bound as tools in samples and workflow builders. | `tool/agenttool.New` wraps an agent as a `FuncTool`. | Aligned | API shape differs; Go exposes a direct package. | | Agent as MCP tool/server | .NET sample `Agent_Step07_AsMcpTool` and durable sample for agent as MCP tool. | `tool/mcptool.AddTool`, `examples/02-agents/mcp/agent_mcp_server`, `step10_as_mcp_tool`. | Aligned | Durable MCP hosting is .NET only. | From 0817fc99f78abb1a385c6fdc1d7267a07c4a173e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 07:51:37 +0000 Subject: [PATCH 2/5] docs(toolapproval): clarify auto-approval callback expectations and naming parity note --- agent/harness/toolapproval/toolapproval.go | 2 ++ docs/dotnet-go-sdk-feature-comparison.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/agent/harness/toolapproval/toolapproval.go b/agent/harness/toolapproval/toolapproval.go index f4e46098..f7b00286 100644 --- a/agent/harness/toolapproval/toolapproval.go +++ b/agent/harness/toolapproval/toolapproval.go @@ -89,6 +89,8 @@ type Config struct { // approval request to the caller. Each function receives the tool call and returns // true to auto-approve it. Functions are evaluated in order; the first returning // true causes the request to be auto-approved without prompting the caller. + // Functions are invoked synchronously and should be fast, deterministic, and + // non-blocking (no network or disk I/O). AutoApprovalFuncs []func(*message.FunctionCallContent) bool } diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 02980925..63fea096 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -39,7 +39,7 @@ Within overlapping features, the main misalignments are API shape and ecosystem | Function tools | `AIFunction`, `AITool`, function tools, plugins, dynamic function tools, tool argument matching in evals. | `tool.Tool`, `tool.FuncTool`, `functool.New`, typed input/output schemas. | Partial | Go has typed function tools but no first-class plugin or dynamic tool sample equivalent to .NET steps 12 and 20. | | Shell tool and environment context | `Microsoft.Agents.AI.Tools.Shell`: `LocalShellExecutor`, `ShellPolicy` (allow/deny-list), `ShellResult`, stateless and persistent shell execution modes, approval-in-the-loop gate, head-tail output truncation, `ShellEnvironmentProvider`, `ShellEnvironmentSnapshot`, shell-family instructions, common CLI probing. | `tool/shelltool.NewLocal`, `shelltool.LocalConfig` (mode, timeout, max output, policy, acknowledge unsafe), `shelltool.Policy`, `shelltool.Result.FormatForModel`, `shelltool.Executor`, `shelltool.NewEnvironmentProvider`, `EnvironmentProviderConfig`, `ShellEnvironmentSnapshot`, `DefaultShellEnvironmentInstructions`. | Aligned | Go mirrors the .NET design for local execution, policy allow/deny-list, approval-required by default, stateless/persistent modes, output truncation, environment snapshot probing, cached first-probe behavior, refresh, current snapshot access, shell-family prompt instructions, invalid/duplicate probe handling, stderr version fallback, caller cancellation, and probe timeout handling. Docker shell executor not ported (Go has no equivalent `DockerShellExecutor`). Go represents tool-version nullability with `ToolVersion{Found bool}` rather than nullable strings. | | Tool auto-calling | Provider/tool-call loop, tool approval agent, and message injection during the function loop (`EnableMessageInjection` / `MessageInjectingChatClient`). | `agent/harness/toolautocall`, default provider middleware unless disabled. Message injection supported via `Config.EnableMessageInjection` and `toolautocall.MessageInjectorFromContext(ctx)`. | Aligned | Go implements auto-call as explicit middleware; .NET uses agent/tool abstractions and provider adapters. | -| Tool approval | Tool approval request/response content, tool approval agent and builder extensions, auto-approval rules (heuristics). | `message.ToolApprovalRequestContent`, `message.ToolApprovalResponseContent`, `tool.ApprovalRequiredFunc`, `agent/harness/toolautocall` approval flow, `agent/harness/toolapproval` middleware for standing-rule and auto-approval-func approval management, AGUI HITL sample. | Aligned | API shape differs: .NET uses a `ToolApprovalAgent` delegating-agent wrapper with `ToolApprovalAgentOptions`; Go uses idiomatic middleware (`toolapproval.New(Config{AutoApprovalFuncs: ...})`). Standing approval rules, queued-request batching, `AlwaysApprove*` response content, and auto-approval funcs (heuristics) are now present in both SDKs. | +| Tool approval | Tool approval request/response content, tool approval agent and builder extensions, auto-approval rules (heuristics). | `message.ToolApprovalRequestContent`, `message.ToolApprovalResponseContent`, `tool.ApprovalRequiredFunc`, `agent/harness/toolautocall` approval flow, `agent/harness/toolapproval` middleware for standing-rule and auto-approval-func approval management, AGUI HITL sample. | Aligned | API shape differs: .NET uses a `ToolApprovalAgent` delegating-agent wrapper with `ToolApprovalAgentOptions`; Go uses idiomatic middleware (`toolapproval.New(toolapproval.Config{AutoApprovalFuncs: ...})`). Go also names heuristic callbacks `AutoApprovalFuncs` (vs .NET `AutoApprovalRules`). Standing approval rules, queued-request batching, `AlwaysApprove*` response content, and auto-approval funcs (heuristics) are now present in both SDKs. | | Hosted/server-side tools | Foundry/OpenAI samples for code interpreter, file search, web search, OpenAPI, Bing custom search, SharePoint, Microsoft Fabric, memory search, Toolbox, hosted MCP. | `tool/hostedtool` declarations for web search, file search, code interpreter, MCP server. | Partial | Go has declaration types but less provider/sample coverage and fewer service-specific hosted tool integrations. | | Agent as function tool | Agents can be converted/bound as tools in samples and workflow builders. | `tool/agenttool.New` wraps an agent as a `FuncTool`. | Aligned | API shape differs; Go exposes a direct package. | | Agent as MCP tool/server | .NET sample `Agent_Step07_AsMcpTool` and durable sample for agent as MCP tool. | `tool/mcptool.AddTool`, `examples/02-agents/mcp/agent_mcp_server`, `step10_as_mcp_tool`. | Aligned | Durable MCP hosting is .NET only. | From fd446e7f6486a7ff1b55f0570b5b2c00fcd42ab3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:14:18 +0000 Subject: [PATCH 3/5] refactor(toolapproval): rename auto approval rules and pass context --- agent/harness/toolapproval/toolapproval.go | 32 ++++++------- .../harness/toolapproval/toolapproval_test.go | 48 +++++++++---------- docs/dotnet-go-sdk-feature-comparison.md | 2 +- 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/agent/harness/toolapproval/toolapproval.go b/agent/harness/toolapproval/toolapproval.go index f7b00286..8058fdc9 100644 --- a/agent/harness/toolapproval/toolapproval.go +++ b/agent/harness/toolapproval/toolapproval.go @@ -84,14 +84,12 @@ func New(cfg Config) agent.Middleware { // Config configures tool-approval middleware behavior. type Config struct { - // AutoApprovalFuncs is an optional list of heuristic functions evaluated after + // AutoApprovalRules is an optional list of heuristic functions evaluated after // standing rules (derived from prior user approvals) but before surfacing the // approval request to the caller. Each function receives the tool call and returns // true to auto-approve it. Functions are evaluated in order; the first returning // true causes the request to be auto-approved without prompting the caller. - // Functions are invoked synchronously and should be fast, deterministic, and - // non-blocking (no network or disk I/O). - AutoApprovalFuncs []func(*message.FunctionCallContent) bool + AutoApprovalRules []func(context.Context, *message.FunctionCallContent) bool } func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { @@ -103,7 +101,7 @@ func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*messag // Step 2: If we have queued requests from a previous turn, drain any // that are now auto-approvable and surface the next one. - drainAutoApprovable(cfg, &st) + drainAutoApprovable(ctx, cfg, &st) if len(st.QueuedRequests) > 0 { next := st.QueuedRequests[0] st.QueuedRequests = st.QueuedRequests[1:] @@ -153,7 +151,7 @@ func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*messag for _, req := range approvalRequests { if matchesRule(st.Rules, req) { autoApproved = append(autoApproved, req.CreateResponse(true, "")) - } else if matchesAutoApprovalFuncs(cfg.AutoApprovalFuncs, req) { + } else if matchesAutoApprovalRules(ctx, cfg.AutoApprovalRules, req) { autoApproved = append(autoApproved, req.CreateResponse(true, "")) } else { needsApproval = append(needsApproval, req) @@ -255,17 +253,17 @@ func prepareInbound(messages []*message.Message, st state) ([]*message.Message, } // drainAutoApprovable removes queued requests that now match a standing rule -// or an auto-approval func, adding auto-approve responses to collected. -func drainAutoApprovable(cfg Config, st *state) { +// or an auto-approval rule, adding auto-approve responses to collected. +func drainAutoApprovable(ctx context.Context, cfg Config, st *state) { if len(st.QueuedRequests) == 0 { return } - if len(st.Rules) == 0 && len(cfg.AutoApprovalFuncs) == 0 { + if len(st.Rules) == 0 && len(cfg.AutoApprovalRules) == 0 { return } var remaining []*message.ToolApprovalRequestContent for _, req := range st.QueuedRequests { - if matchesRule(st.Rules, req) || matchesAutoApprovalFuncs(cfg.AutoApprovalFuncs, req) { + if matchesRule(st.Rules, req) || matchesAutoApprovalRules(ctx, cfg.AutoApprovalRules, req) { st.CollectedResponses = append(st.CollectedResponses, req.CreateResponse(true, "")) } else { remaining = append(remaining, req) @@ -291,19 +289,19 @@ func matchesRule(rules []Rule, req *message.ToolApprovalRequestContent) bool { return false } -// matchesAutoApprovalFuncs returns true if any configured auto-approval func -// approves the request. Funcs are evaluated in order; the first returning true -// wins. Returns false when funcs is empty or the request is not a function call. -func matchesAutoApprovalFuncs(funcs []func(*message.FunctionCallContent) bool, req *message.ToolApprovalRequestContent) bool { - if len(funcs) == 0 { +// matchesAutoApprovalRules returns true if any configured auto-approval rule +// approves the request. Rules are evaluated in order; the first returning true +// wins. Returns false when rules is empty or the request is not a function call. +func matchesAutoApprovalRules(ctx context.Context, rules []func(context.Context, *message.FunctionCallContent) bool, req *message.ToolApprovalRequestContent) bool { + if len(rules) == 0 { return false } fc, ok := req.ToolCall.(*message.FunctionCallContent) if !ok || fc == nil { return false } - for _, fn := range funcs { - if fn(fc) { + for _, rule := range rules { + if rule != nil && rule(ctx, fc) { return true } } diff --git a/agent/harness/toolapproval/toolapproval_test.go b/agent/harness/toolapproval/toolapproval_test.go index 31d895b2..68f5743a 100644 --- a/agent/harness/toolapproval/toolapproval_test.go +++ b/agent/harness/toolapproval/toolapproval_test.go @@ -368,7 +368,7 @@ func TestToolApproval_AlwaysApproveToolWithArgumentsMatchesByValue(t *testing.T) } } -func TestToolApproval_AutoApprovalFunc_ApprovesMatchingTool(t *testing.T) { +func TestToolApproval_AutoApprovalRule_ApprovesMatchingTool(t *testing.T) { fcc := &message.FunctionCallContent{CallID: "c1", Name: "ReadTool", Arguments: `{}`} // Turn 1: inner returns an approval request. @@ -387,8 +387,8 @@ func TestToolApproval_AutoApprovalFunc_ApprovesMatchingTool(t *testing.T) { } cfg := toolapproval.Config{ - AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ - func(fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ + func(_ context.Context, fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, }, } mw := toolapproval.New(cfg) @@ -407,16 +407,16 @@ func TestToolApproval_AutoApprovalFunc_ApprovesMatchingTool(t *testing.T) { gotDone = true } if _, ok := c.(*message.ToolApprovalRequestContent); ok { - t.Fatal("expected no approval request to be surfaced when auto-approval func matches") + t.Fatal("expected no approval request to be surfaced when auto-approval rule matches") } } } if !gotDone { - t.Error("expected 'done' text after auto-approval func approved the tool") + t.Error("expected 'done' text after auto-approval rule approved the tool") } } -func TestToolApproval_AutoApprovalFunc_DoesNotMatchSurfacesToCaller(t *testing.T) { +func TestToolApproval_AutoApprovalRule_DoesNotMatchSurfacesToCaller(t *testing.T) { fcc := &message.FunctionCallContent{CallID: "c1", Name: "DangerousTool", Arguments: `{}`} runner := &agenttest.Runner{ @@ -431,8 +431,8 @@ func TestToolApproval_AutoApprovalFunc_DoesNotMatchSurfacesToCaller(t *testing.T } cfg := toolapproval.Config{ - AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ - func(fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, // only approves ReadTool + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ + func(_ context.Context, fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, // only approves ReadTool }, } mw := toolapproval.New(cfg) @@ -458,7 +458,7 @@ func TestToolApproval_AutoApprovalFunc_DoesNotMatchSurfacesToCaller(t *testing.T } } -func TestToolApproval_MultipleAutoApprovalFuncs_FirstMatchWins(t *testing.T) { +func TestToolApproval_MultipleAutoApprovalRules_FirstMatchWins(t *testing.T) { fcc := &message.FunctionCallContent{CallID: "c1", Name: "SpecialTool", Arguments: `{}`} runner := &agenttest.Runner{ @@ -477,12 +477,12 @@ func TestToolApproval_MultipleAutoApprovalFuncs_FirstMatchWins(t *testing.T) { rule1Called := false rule2Called := false cfg := toolapproval.Config{ - AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ - func(fc *message.FunctionCallContent) bool { + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ + func(_ context.Context, fc *message.FunctionCallContent) bool { rule1Called = true return fc.Name == "SpecialTool" }, - func(_ *message.FunctionCallContent) bool { + func(_ context.Context, _ *message.FunctionCallContent) bool { rule2Called = true return true // should not be reached }, @@ -497,14 +497,14 @@ func TestToolApproval_MultipleAutoApprovalFuncs_FirstMatchWins(t *testing.T) { ) if !rule1Called { - t.Error("expected first auto-approval func to be called") + t.Error("expected first auto-approval rule to be called") } if rule2Called { - t.Error("expected second auto-approval func to NOT be called when first already matched") + t.Error("expected second auto-approval rule to NOT be called when first already matched") } } -func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalFunc(t *testing.T) { +func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalRule(t *testing.T) { fcc := &message.FunctionCallContent{CallID: "c1", Name: "MyTool", Arguments: `{}`} runner := &agenttest.Runner{ @@ -522,8 +522,8 @@ func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalFunc(t *testing heuristicCalled := false cfg := toolapproval.Config{ - AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ - func(_ *message.FunctionCallContent) bool { + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ + func(_ context.Context, _ *message.FunctionCallContent) bool { heuristicCalled = true return true }, @@ -533,14 +533,14 @@ func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalFunc(t *testing session := agenttest.CreateSession() opts := []agent.Option{agent.WithSession(session)} - // Turn 1: auto-approval func is called (no standing rule yet). + // Turn 1: auto-approval rule is called (no standing rule yet). updates := collectUpdates(t, mw, runner.Run, []*message.Message{{Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}}, opts..., ) if !heuristicCalled { - t.Error("expected auto-approval func to be called on first turn") + t.Error("expected auto-approval rule to be called on first turn") } var gotDone bool @@ -552,11 +552,11 @@ func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalFunc(t *testing } } if !gotDone { - t.Error("expected 'done' after auto-approval func approved on first turn") + t.Error("expected 'done' after auto-approval rule approved on first turn") } } -func TestToolApproval_AutoApprovalFunc_ApprovesQueuedRequests(t *testing.T) { +func TestToolApproval_AutoApprovalRule_ApprovesQueuedRequests(t *testing.T) { fcc1 := &message.FunctionCallContent{CallID: "c1", Name: "SafeTool", Arguments: `{}`} fcc2 := &message.FunctionCallContent{CallID: "c2", Name: "DangerousTool", Arguments: `{}`} @@ -572,10 +572,10 @@ func TestToolApproval_AutoApprovalFunc_ApprovesQueuedRequests(t *testing.T) { Build(), } - // AutoApprovalFunc approves SafeTool but not DangerousTool. + // AutoApprovalRule approves SafeTool but not DangerousTool. cfg := toolapproval.Config{ - AutoApprovalFuncs: []func(*message.FunctionCallContent) bool{ - func(fc *message.FunctionCallContent) bool { return fc.Name == "SafeTool" }, + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ + func(_ context.Context, fc *message.FunctionCallContent) bool { return fc.Name == "SafeTool" }, }, } mw := toolapproval.New(cfg) diff --git a/docs/dotnet-go-sdk-feature-comparison.md b/docs/dotnet-go-sdk-feature-comparison.md index 63fea096..a8e75a62 100644 --- a/docs/dotnet-go-sdk-feature-comparison.md +++ b/docs/dotnet-go-sdk-feature-comparison.md @@ -39,7 +39,7 @@ Within overlapping features, the main misalignments are API shape and ecosystem | Function tools | `AIFunction`, `AITool`, function tools, plugins, dynamic function tools, tool argument matching in evals. | `tool.Tool`, `tool.FuncTool`, `functool.New`, typed input/output schemas. | Partial | Go has typed function tools but no first-class plugin or dynamic tool sample equivalent to .NET steps 12 and 20. | | Shell tool and environment context | `Microsoft.Agents.AI.Tools.Shell`: `LocalShellExecutor`, `ShellPolicy` (allow/deny-list), `ShellResult`, stateless and persistent shell execution modes, approval-in-the-loop gate, head-tail output truncation, `ShellEnvironmentProvider`, `ShellEnvironmentSnapshot`, shell-family instructions, common CLI probing. | `tool/shelltool.NewLocal`, `shelltool.LocalConfig` (mode, timeout, max output, policy, acknowledge unsafe), `shelltool.Policy`, `shelltool.Result.FormatForModel`, `shelltool.Executor`, `shelltool.NewEnvironmentProvider`, `EnvironmentProviderConfig`, `ShellEnvironmentSnapshot`, `DefaultShellEnvironmentInstructions`. | Aligned | Go mirrors the .NET design for local execution, policy allow/deny-list, approval-required by default, stateless/persistent modes, output truncation, environment snapshot probing, cached first-probe behavior, refresh, current snapshot access, shell-family prompt instructions, invalid/duplicate probe handling, stderr version fallback, caller cancellation, and probe timeout handling. Docker shell executor not ported (Go has no equivalent `DockerShellExecutor`). Go represents tool-version nullability with `ToolVersion{Found bool}` rather than nullable strings. | | Tool auto-calling | Provider/tool-call loop, tool approval agent, and message injection during the function loop (`EnableMessageInjection` / `MessageInjectingChatClient`). | `agent/harness/toolautocall`, default provider middleware unless disabled. Message injection supported via `Config.EnableMessageInjection` and `toolautocall.MessageInjectorFromContext(ctx)`. | Aligned | Go implements auto-call as explicit middleware; .NET uses agent/tool abstractions and provider adapters. | -| Tool approval | Tool approval request/response content, tool approval agent and builder extensions, auto-approval rules (heuristics). | `message.ToolApprovalRequestContent`, `message.ToolApprovalResponseContent`, `tool.ApprovalRequiredFunc`, `agent/harness/toolautocall` approval flow, `agent/harness/toolapproval` middleware for standing-rule and auto-approval-func approval management, AGUI HITL sample. | Aligned | API shape differs: .NET uses a `ToolApprovalAgent` delegating-agent wrapper with `ToolApprovalAgentOptions`; Go uses idiomatic middleware (`toolapproval.New(toolapproval.Config{AutoApprovalFuncs: ...})`). Go also names heuristic callbacks `AutoApprovalFuncs` (vs .NET `AutoApprovalRules`). Standing approval rules, queued-request batching, `AlwaysApprove*` response content, and auto-approval funcs (heuristics) are now present in both SDKs. | +| Tool approval | Tool approval request/response content, tool approval agent and builder extensions, auto-approval rules (heuristics). | `message.ToolApprovalRequestContent`, `message.ToolApprovalResponseContent`, `tool.ApprovalRequiredFunc`, `agent/harness/toolautocall` approval flow, `agent/harness/toolapproval` middleware for standing-rule and auto-approval-rule approval management, AGUI HITL sample. | Aligned | API shape differs: .NET uses a `ToolApprovalAgent` delegating-agent wrapper with `ToolApprovalAgentOptions`; Go uses idiomatic middleware (`toolapproval.New(toolapproval.Config{AutoApprovalRules: ...})`). Standing approval rules, queued-request batching, `AlwaysApprove*` response content, and auto-approval rules (heuristics) are now present in both SDKs. | | Hosted/server-side tools | Foundry/OpenAI samples for code interpreter, file search, web search, OpenAPI, Bing custom search, SharePoint, Microsoft Fabric, memory search, Toolbox, hosted MCP. | `tool/hostedtool` declarations for web search, file search, code interpreter, MCP server. | Partial | Go has declaration types but less provider/sample coverage and fewer service-specific hosted tool integrations. | | Agent as function tool | Agents can be converted/bound as tools in samples and workflow builders. | `tool/agenttool.New` wraps an agent as a `FuncTool`. | Aligned | API shape differs; Go exposes a direct package. | | Agent as MCP tool/server | .NET sample `Agent_Step07_AsMcpTool` and durable sample for agent as MCP tool. | `tool/mcptool.AddTool`, `examples/02-agents/mcp/agent_mcp_server`, `step10_as_mcp_tool`. | Aligned | Durable MCP hosting is .NET only. | From 7a7efa2ec53037b83e319d74a9f5997192ef900e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:16:34 +0000 Subject: [PATCH 4/5] docs(toolapproval): align config comment terminology with AutoApprovalRules --- agent/harness/toolapproval/toolapproval.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent/harness/toolapproval/toolapproval.go b/agent/harness/toolapproval/toolapproval.go index 8058fdc9..ba55372e 100644 --- a/agent/harness/toolapproval/toolapproval.go +++ b/agent/harness/toolapproval/toolapproval.go @@ -86,8 +86,8 @@ func New(cfg Config) agent.Middleware { type Config struct { // AutoApprovalRules is an optional list of heuristic functions evaluated after // standing rules (derived from prior user approvals) but before surfacing the - // approval request to the caller. Each function receives the tool call and returns - // true to auto-approve it. Functions are evaluated in order; the first returning + // approval request to the caller. Each rule receives the tool call and returns + // true to auto-approve it. Rules are evaluated in order; the first returning // true causes the request to be auto-approved without prompting the caller. AutoApprovalRules []func(context.Context, *message.FunctionCallContent) bool } From 79eb87b313c11e505f063d728e43b7a4ac8e9100 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:36:15 +0000 Subject: [PATCH 5/5] toolapproval: support auto-approval rule errors --- agent/harness/toolapproval/toolapproval.go | 58 ++++++++++----- .../harness/toolapproval/toolapproval_test.go | 70 +++++++++++++++---- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/agent/harness/toolapproval/toolapproval.go b/agent/harness/toolapproval/toolapproval.go index ba55372e..2c458cbc 100644 --- a/agent/harness/toolapproval/toolapproval.go +++ b/agent/harness/toolapproval/toolapproval.go @@ -87,9 +87,11 @@ type Config struct { // AutoApprovalRules is an optional list of heuristic functions evaluated after // standing rules (derived from prior user approvals) but before surfacing the // approval request to the caller. Each rule receives the tool call and returns - // true to auto-approve it. Rules are evaluated in order; the first returning - // true causes the request to be auto-approved without prompting the caller. - AutoApprovalRules []func(context.Context, *message.FunctionCallContent) bool + // (approved, error). Returning approved=true auto-approves the request. Rules + // are evaluated in order; the first returning approved=true causes the request + // to be auto-approved without prompting the caller. Returning an error fails + // the current run. + AutoApprovalRules []func(context.Context, *message.FunctionCallContent) (bool, error) } func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] { @@ -101,7 +103,10 @@ func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*messag // Step 2: If we have queued requests from a previous turn, drain any // that are now auto-approvable and surface the next one. - drainAutoApprovable(ctx, cfg, &st) + if err := drainAutoApprovable(ctx, cfg, &st); err != nil { + yield(nil, err) + return + } if len(st.QueuedRequests) > 0 { next := st.QueuedRequests[0] st.QueuedRequests = st.QueuedRequests[1:] @@ -151,10 +156,17 @@ func run(cfg Config, next agent.RunFunc, ctx context.Context, messages []*messag for _, req := range approvalRequests { if matchesRule(st.Rules, req) { autoApproved = append(autoApproved, req.CreateResponse(true, "")) - } else if matchesAutoApprovalRules(ctx, cfg.AutoApprovalRules, req) { - autoApproved = append(autoApproved, req.CreateResponse(true, "")) } else { - needsApproval = append(needsApproval, req) + matches, err := matchesAutoApprovalRules(ctx, cfg.AutoApprovalRules, req) + if err != nil { + yield(nil, err) + return + } + if matches { + autoApproved = append(autoApproved, req.CreateResponse(true, "")) + } else { + needsApproval = append(needsApproval, req) + } } } @@ -254,22 +266,27 @@ func prepareInbound(messages []*message.Message, st state) ([]*message.Message, // drainAutoApprovable removes queued requests that now match a standing rule // or an auto-approval rule, adding auto-approve responses to collected. -func drainAutoApprovable(ctx context.Context, cfg Config, st *state) { +func drainAutoApprovable(ctx context.Context, cfg Config, st *state) error { if len(st.QueuedRequests) == 0 { - return + return nil } if len(st.Rules) == 0 && len(cfg.AutoApprovalRules) == 0 { - return + return nil } var remaining []*message.ToolApprovalRequestContent for _, req := range st.QueuedRequests { - if matchesRule(st.Rules, req) || matchesAutoApprovalRules(ctx, cfg.AutoApprovalRules, req) { + matches, err := matchesAutoApprovalRules(ctx, cfg.AutoApprovalRules, req) + if err != nil { + return err + } + if matchesRule(st.Rules, req) || matches { st.CollectedResponses = append(st.CollectedResponses, req.CreateResponse(true, "")) } else { remaining = append(remaining, req) } } st.QueuedRequests = remaining + return nil } func matchesRule(rules []Rule, req *message.ToolApprovalRequestContent) bool { @@ -292,20 +309,27 @@ func matchesRule(rules []Rule, req *message.ToolApprovalRequestContent) bool { // matchesAutoApprovalRules returns true if any configured auto-approval rule // approves the request. Rules are evaluated in order; the first returning true // wins. Returns false when rules is empty or the request is not a function call. -func matchesAutoApprovalRules(ctx context.Context, rules []func(context.Context, *message.FunctionCallContent) bool, req *message.ToolApprovalRequestContent) bool { +func matchesAutoApprovalRules(ctx context.Context, rules []func(context.Context, *message.FunctionCallContent) (bool, error), req *message.ToolApprovalRequestContent) (bool, error) { if len(rules) == 0 { - return false + return false, nil } fc, ok := req.ToolCall.(*message.FunctionCallContent) if !ok || fc == nil { - return false + return false, nil } for _, rule := range rules { - if rule != nil && rule(ctx, fc) { - return true + if rule == nil { + continue + } + matches, err := rule(ctx, fc) + if err != nil { + return false, err + } + if matches { + return true, nil } } - return false + return false, nil } func serializeArguments(arguments string) (map[string]string, error) { diff --git a/agent/harness/toolapproval/toolapproval_test.go b/agent/harness/toolapproval/toolapproval_test.go index 68f5743a..20140886 100644 --- a/agent/harness/toolapproval/toolapproval_test.go +++ b/agent/harness/toolapproval/toolapproval_test.go @@ -4,6 +4,7 @@ package toolapproval_test import ( "context" + "errors" "iter" "testing" @@ -387,8 +388,10 @@ func TestToolApproval_AutoApprovalRule_ApprovesMatchingTool(t *testing.T) { } cfg := toolapproval.Config{ - AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ - func(_ context.Context, fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) (bool, error){ + func(_ context.Context, fc *message.FunctionCallContent) (bool, error) { + return fc.Name == "ReadTool", nil + }, }, } mw := toolapproval.New(cfg) @@ -431,8 +434,10 @@ func TestToolApproval_AutoApprovalRule_DoesNotMatchSurfacesToCaller(t *testing.T } cfg := toolapproval.Config{ - AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ - func(_ context.Context, fc *message.FunctionCallContent) bool { return fc.Name == "ReadTool" }, // only approves ReadTool + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) (bool, error){ + func(_ context.Context, fc *message.FunctionCallContent) (bool, error) { + return fc.Name == "ReadTool", nil + }, // only approves ReadTool }, } mw := toolapproval.New(cfg) @@ -477,14 +482,14 @@ func TestToolApproval_MultipleAutoApprovalRules_FirstMatchWins(t *testing.T) { rule1Called := false rule2Called := false cfg := toolapproval.Config{ - AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ - func(_ context.Context, fc *message.FunctionCallContent) bool { + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) (bool, error){ + func(_ context.Context, fc *message.FunctionCallContent) (bool, error) { rule1Called = true - return fc.Name == "SpecialTool" + return fc.Name == "SpecialTool", nil }, - func(_ context.Context, _ *message.FunctionCallContent) bool { + func(_ context.Context, _ *message.FunctionCallContent) (bool, error) { rule2Called = true - return true // should not be reached + return true, nil // should not be reached }, }, } @@ -522,10 +527,10 @@ func TestToolApproval_StandingRuleTakesPrecedenceOverAutoApprovalRule(t *testing heuristicCalled := false cfg := toolapproval.Config{ - AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ - func(_ context.Context, _ *message.FunctionCallContent) bool { + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) (bool, error){ + func(_ context.Context, _ *message.FunctionCallContent) (bool, error) { heuristicCalled = true - return true + return true, nil }, }, } @@ -574,8 +579,10 @@ func TestToolApproval_AutoApprovalRule_ApprovesQueuedRequests(t *testing.T) { // AutoApprovalRule approves SafeTool but not DangerousTool. cfg := toolapproval.Config{ - AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) bool{ - func(_ context.Context, fc *message.FunctionCallContent) bool { return fc.Name == "SafeTool" }, + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) (bool, error){ + func(_ context.Context, fc *message.FunctionCallContent) (bool, error) { + return fc.Name == "SafeTool", nil + }, }, } mw := toolapproval.New(cfg) @@ -604,3 +611,38 @@ func TestToolApproval_AutoApprovalRule_ApprovesQueuedRequests(t *testing.T) { t.Errorf("expected DangerousTool to be surfaced, got %v", surfacedReqs[0].ToolCall) } } + +func TestToolApproval_AutoApprovalRule_ErrorFailsRun(t *testing.T) { + ruleErr := errors.New("auto-approval rule failed") + fcc := &message.FunctionCallContent{CallID: "c1", Name: "ReadTool", Arguments: `{}`} + + runner := &agenttest.Runner{ + Responses: agenttest.NewResponseBuilder(). + Add(&agent.ResponseUpdate{ + Role: message.RoleAssistant, + Contents: []message.Content{ + &message.ToolApprovalRequestContent{RequestID: "r1", ToolCall: fcc}, + }, + }). + Build(), + } + + mw := toolapproval.New(toolapproval.Config{ + AutoApprovalRules: []func(context.Context, *message.FunctionCallContent) (bool, error){ + func(_ context.Context, _ *message.FunctionCallContent) (bool, error) { return false, ruleErr }, + }, + }) + + var gotErr error + for _, err := range mw.Run(runner.Run, context.Background(), []*message.Message{ + {Role: message.RoleUser, Contents: []message.Content{&message.TextContent{Text: "go"}}}, + }) { + if err != nil { + gotErr = err + break + } + } + if !errors.Is(gotErr, ruleErr) { + t.Fatalf("expected rule error %v, got %v", ruleErr, gotErr) + } +}