-
Notifications
You must be signed in to change notification settings - Fork 365
a2a: honour timeout and allow_private_ips config (with SSRF protection)
#2882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package a2a | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| goa2a "github.com/a2aproject/a2a-go/a2a" | ||
| "github.com/a2aproject/a2a-go/a2asrv" | ||
| "github.com/a2aproject/a2a-go/a2asrv/eventqueue" | ||
|
|
||
| "github.com/docker/docker-agent/pkg/tools" | ||
| ) | ||
|
|
||
| func TestToolSetRejectsPrivateIPForAgentCard(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| toolSet := NewToolset("test", "http://127.0.0.1/.well-known/agent-card.json", nil) | ||
|
|
||
| if err := toolSet.Start(t.Context()); err == nil { | ||
| t.Fatal("Start() expected error") | ||
| } | ||
| } | ||
|
|
||
| func TestToolSetStreamingWithAllowPrivateIPs(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| server := httptest.NewServer(a2asrv.NewJSONRPCHandler(a2asrv.NewHandler(testA2AHandler{}))) | ||
| t.Cleanup(server.Close) | ||
|
|
||
| cardServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| _ = json.NewEncoder(w).Encode(goa2a.AgentCard{ | ||
| Name: "test", | ||
| Description: "test", | ||
| URL: server.URL, | ||
| Version: "1.0.0", | ||
| ProtocolVersion: string(goa2a.Version), | ||
| PreferredTransport: goa2a.TransportProtocolJSONRPC, | ||
| Capabilities: goa2a.AgentCapabilities{Streaming: true}, | ||
| DefaultInputModes: []string{"text/plain"}, | ||
| DefaultOutputModes: []string{"text/plain"}, | ||
| Skills: []goa2a.AgentSkill{{ | ||
| ID: "test", | ||
| Name: "test", | ||
| Description: "test", | ||
| Tags: []string{"test"}, | ||
| }}, | ||
| }) | ||
| })) | ||
| t.Cleanup(cardServer.Close) | ||
|
|
||
| toolSet := NewToolset("test", cardServer.URL, nil, WithAllowPrivateIPs(true)) | ||
|
|
||
| if err := toolSet.Start(t.Context()); err != nil { | ||
| t.Fatalf("Start() error = %v", err) | ||
| } | ||
|
|
||
| toolList, err := toolSet.Tools(t.Context()) | ||
| if err != nil { | ||
| t.Fatalf("Tools() error = %v", err) | ||
| } | ||
| if len(toolList) != 1 { | ||
| t.Fatalf("Tools() returned %d tools, want 1", len(toolList)) | ||
| } | ||
|
|
||
| result, err := toolList[0].Handler(t.Context(), tools.ToolCall{Function: tools.FunctionCall{Arguments: `{"message":"hello"}`}}) | ||
| if err != nil { | ||
| t.Fatalf("Handler() error = %v", err) | ||
| } | ||
| if result == nil || result.Output != "ok" { | ||
| t.Fatalf("Handler() result = %+v, want output %q", result, "ok") | ||
| } | ||
| } | ||
|
|
||
| type testA2AHandler struct{} | ||
|
|
||
| func (testA2AHandler) Execute(ctx context.Context, reqCtx *a2asrv.RequestContext, queue eventqueue.Queue) error { | ||
| return queue.Write(ctx, goa2a.NewMessageForTask(goa2a.MessageRoleAgent, reqCtx, goa2a.TextPart{Text: "ok"})) | ||
| } | ||
|
|
||
| func (testA2AHandler) Cancel(context.Context, *a2asrv.RequestContext, eventqueue.Queue) error { | ||
| return nil | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No test for |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error is only reachable for
type: mcpwith a local command (thecase "mcp"block, insidet.AllowPrivateIPsEnabled() && t.Remote.URL == "" && t.Ref == ""). Listing'a2a'in the message can mislead an operator into thinking they should switch toa2a, rather than understanding the real issue: their MCP toolset must use a remote URL or ref.A more precise message, e.g.:
would make the corrective action obvious. Nit — not a blocker.