Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/go-vgo/robotgo v1.1.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/inference-gateway/adk v0.33.0
github.com/inference-gateway/adk v0.34.0
github.com/inference-gateway/sdk v1.41.0
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728
github.com/lib/pq v1.12.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inference-gateway/adk v0.33.0 h1:uRJArPCI2jYEVsrOwx/y1ohZwpIAtCRHfLNpoRmQw9s=
github.com/inference-gateway/adk v0.33.0/go.mod h1:XhX6mteE8YTMwsNIF78BMIYwRZSv6AjFCTDVPbcWzGI=
github.com/inference-gateway/adk v0.34.0 h1:me6aGRtkQ8m45K+ayzV1OyL94SkYhOrIQ8SS28+FaUI=
github.com/inference-gateway/adk v0.34.0/go.mod h1:XhX6mteE8YTMwsNIF78BMIYwRZSv6AjFCTDVPbcWzGI=
github.com/inference-gateway/sdk v1.41.0 h1:8ZYQti0fFIN2MXspPNiMbrP8n2QBeFJPc/56rTtGiJk=
github.com/inference-gateway/sdk v1.41.0/go.mod h1:niH2yIl/reMfdMwH/jtE4P2G3FJh63nC4Ji/jLZ0qaA=
github.com/inference-gateway/tokenless v0.9.0 h1:10KQ12GGtaBOCFjiYgGeCdl892vxP+d+PRD5F+YFOhM=
Expand Down
36 changes: 10 additions & 26 deletions internal/protocols/a2a/infrastructure/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/http"
"net/url"
"os"
"regexp"
"slices"
"strings"
"sync"
Expand All @@ -35,11 +34,6 @@ func (e *AuthError) Error() string {
return fmt.Sprintf("Authentication failed for A2A agent %q: %s", e.Agent, e.Reason)
}

// rejectedStatus matches the ADK client's error for a 401 or 403 response.
// ponytail: ADK has no typed HTTP error, so the status is read from the
// message. Switch to errors.As once ADK exposes the status code.
var rejectedStatus = regexp.MustCompile(`unexpected status code[^:]*: (401|403)\b`)

// AuthFailure describes err for the model when it is an authentication
// failure: credentials that could not be obtained, or a 401 or 403 from the
// agent. The message names the agent and leaves the response body out.
Expand All @@ -51,13 +45,13 @@ func AuthFailure(agentURL string, err error) (string, bool) {
if errors.As(err, &authErr) {
return authErr.Error(), true
}
match := rejectedStatus.FindStringSubmatch(err.Error())
if match == nil {
var status *adk.HTTPStatusError
if !errors.As(err, &status) || (status.StatusCode != http.StatusUnauthorized && status.StatusCode != http.StatusForbidden) {
return "", false
}
rejected := &AuthError{
Agent: agentDisplayName(agentURL),
Reason: fmt.Sprintf("the agent rejected the request with status %s, check %s", match[1], credentialSetting(agentURL)),
Reason: fmt.Sprintf("the agent rejected the request with status %d, check %s", status.StatusCode, credentialSetting(agentURL)),
}
return rejected.Error(), true
}
Expand All @@ -70,39 +64,29 @@ func credentialSetting(agentURL string) string {
return "its auth settings in agents.yaml"
}

// refusedByPolicy matches the ADK client's error for a 403 response and captures its body.
var refusedByPolicy = regexp.MustCompile(`unexpected status code[^:]*: 403, body: (\{.*\})`)

// PolicyRefusal describes err for the model when the gateway's guardrails
// refused the request: a 403 whose body is a JSON-RPC error. The message
// carries the policy's own words.
func PolicyRefusal(err error) (string, bool) {
if err == nil {
return "", false
}
match := refusedByPolicy.FindStringSubmatch(err.Error())
if match == nil {
var status *adk.HTTPStatusError
if !errors.As(err, &status) || status.StatusCode != http.StatusForbidden {
return "", false
}
var envelope struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
if json.Unmarshal([]byte(match[1]), &envelope) != nil || envelope.Error.Message == "" {
if json.Unmarshal([]byte(status.Body), &envelope) != nil || envelope.Error.Message == "" {
return "", false
}
return "The request was refused by a guardrail policy: " + envelope.Error.Message, true
}

// methodNotFound is how the ADK client ends the error for a JSON-RPC
// "method not found" answer.
const methodNotFound = "(code: -32601)"

// MethodUnsupported describes err for the model when the agent does not
// methodUnsupported describes err for the model when the agent does not
// implement the A2A method that was called, which no retry can change.
func MethodUnsupported(agentURL string, err error) (string, bool) {
if err == nil || !strings.Contains(err.Error(), methodNotFound) {
func methodUnsupported(agentURL string, err error) (string, bool) {
if !errors.Is(err, adk.ErrMethodNotFound) {
return "", false
}
return fmt.Sprintf("A2A agent %q does not implement the A2A method that was called, so it likely speaks another A2A protocol version than this CLI (v1.0). Retrying will not help.", agentDisplayName(agentURL)), true
Expand All @@ -115,7 +99,7 @@ func Rejection(agentURL string, err error) (string, bool) {
if message, ok := PolicyRefusal(err); ok {
return message, true
}
if message, ok := MethodUnsupported(agentURL, err); ok {
if message, ok := methodUnsupported(agentURL, err); ok {
return message, true
}
return AuthFailure(agentURL, err)
Expand Down
5 changes: 3 additions & 2 deletions internal/protocols/a2a/submit_task_error_handling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package a2a
import (
"context"
"errors"
"net/http"
"testing"

assert "github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -421,7 +422,7 @@ func TestSubmitTaskTool_RejectedSubmissionIsAnAuthFailure(t *testing.T) {
t.Chdir(t.TempDir())
t.Setenv("HOME", t.TempDir())
client := &adkmocks.FakeA2AClient{}
client.SendTaskReturns(nil, errors.New(`unexpected status code: 401, body: {"error":"invalid token"}`))
client.SendTaskReturns(nil, &adk.HTTPStatusError{StatusCode: http.StatusUnauthorized, Body: `{"error":"invalid token"}`})
tool := NewSubmitTaskToolWithClient(authTestConfig(), nil, nil, nil, client)

result, err := tool.Execute(t.Context(), map[string]any{
Expand All @@ -441,7 +442,7 @@ func TestSubmitTaskTool_PollingStopsOnAuthFailure(t *testing.T) {
t.Chdir(t.TempDir())
t.Setenv("HOME", t.TempDir())
client := &adkmocks.FakeA2AClient{}
client.GetTaskReturns(nil, errors.New("unexpected status code: 403, body: forbidden"))
client.GetTaskReturns(nil, &adk.HTTPStatusError{StatusCode: http.StatusForbidden, Body: "forbidden"})
tool := NewSubmitTaskToolWithClient(authTestConfig(), nil, nil, nil, client)
state := &a2adomain.TaskPollingState{TaskID: "t1", ContextID: "ctx1", AgentURL: "https://research.example.com"}

Expand Down
Loading