diff --git a/internal/clierr/clierr.go b/internal/clierr/clierr.go index c5f3052..e8f7fa7 100644 --- a/internal/clierr/clierr.go +++ b/internal/clierr/clierr.go @@ -21,10 +21,13 @@ import ( "errors" "fmt" "io" + "io/fs" "net" + "net/http" "syscall" "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard" ) // Code is a symbolic error identifier. @@ -111,11 +114,26 @@ func CardResolution(err error) *Error { Code: CodeCardInvalid, Message: msg, Exit: 4, - Hint: "check the --agent-card reference (host, full card URL, or local file path)", + Hint: cardInvalidHint(err), err: err, } } +// cardInvalidHint picks remediation for a card that was reached but not usable, +// based on the HTTP status of the card request when there is one. +func cardInvalidHint(err error) string { + var status *agentcard.ErrStatusNotOK + if errors.As(err, &status) { + switch { + case status.StatusCode == http.StatusUnauthorized || status.StatusCode == http.StatusForbidden: + return "pass credentials with --auth, --svc-param or --credential-helper" + case status.StatusCode >= http.StatusInternalServerError: + return "the agent failed to serve its card, check its logs or retry" + } + } + return "check the --agent-card reference (host, full card URL, or local file path)" +} + // MarshalJSON implements [json.Marshaler]. func (e *Error) MarshalJSON() ([]byte, error) { type body struct { @@ -186,10 +204,12 @@ func Classify(err error) *Error { // isIOError reports whether err is a transport-level failure — a network error, // a broken stream, or a reset/closed connection — identified by type and -// sentinel rather than by matching message text. +// sentinel rather than by matching message text. A local file error is not one, +// even though the syscall.Errno it wraps satisfies net.Error. func isIOError(err error) bool { var netErr net.Error - if errors.As(err, &netErr) { + var pathErr *fs.PathError + if errors.As(err, &netErr) && !errors.As(err, &pathErr) { return true } return errors.Is(err, io.ErrUnexpectedEOF) || diff --git a/internal/clierr/clierr_test.go b/internal/clierr/clierr_test.go index aee4cac..eb248ed 100644 --- a/internal/clierr/clierr_test.go +++ b/internal/clierr/clierr_test.go @@ -18,11 +18,13 @@ import ( "context" "errors" "fmt" + "io/fs" "net" "syscall" "testing" "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/a2aproject/a2a-go/v2/a2aclient/agentcard" ) func TestClassify(t *testing.T) { @@ -85,6 +87,18 @@ func TestClassify(t *testing.T) { wantCode: CodeIO, wantExit: 3, }, + { + name: "local file error is not io, exit 1", + err: fmt.Errorf("failed to save file parts: %w", &fs.PathError{Op: "open", Path: "out/x.txt", Err: syscall.EACCES}), + wantCode: CodeInternal, + wantExit: 1, + }, + { + name: "broken pipe on a file is still io", + err: fmt.Errorf("writing to plugin: %w", &fs.PathError{Op: "write", Path: "|1", Err: syscall.EPIPE}), + wantCode: CodeIO, + wantExit: 3, + }, { name: "unclassified error is internal, exit 1", err: errors.New("something odd happened"), @@ -158,6 +172,27 @@ func TestCardResolution(t *testing.T) { wantExit: 4, wantHint: "check the --agent-card reference (host, full card URL, or local file path)", }, + { + name: "unauthorized card request asks for credentials", + err: &agentcard.ErrStatusNotOK{StatusCode: 401, Status: "401 Unauthorized"}, + wantCode: CodeCardInvalid, + wantExit: 4, + wantHint: "pass credentials with --auth, --svc-param or --credential-helper", + }, + { + name: "forbidden card request asks for credentials", + err: &agentcard.ErrStatusNotOK{StatusCode: 403, Status: "403 Forbidden"}, + wantCode: CodeCardInvalid, + wantExit: 4, + wantHint: "pass credentials with --auth, --svc-param or --credential-helper", + }, + { + name: "server error on card request points at the agent", + err: &agentcard.ErrStatusNotOK{StatusCode: 500, Status: "500 Internal Server Error"}, + wantCode: CodeCardInvalid, + wantExit: 4, + wantHint: "the agent failed to serve its card, check its logs or retry", + }, { name: "deadline exceeded is a timeout", err: context.DeadlineExceeded,