Skip to content
Open
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
1 change: 1 addition & 0 deletions cli/azd/extensions/azure.ai.agents/cspell.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
words:
- aadsts
- aiservices
- agentserver
- anonymousconnection
Expand Down
11 changes: 11 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -112,6 +113,16 @@ func postdeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *a
// Ensure agent identity RBAC is configured when vnext is enabled.
// Runs post-deploy because the platform provisions the identity during agent deployment.
if err := project.EnsureAgentIdentityRBAC(ctx, azdClient); err != nil {
// If the error is already structured, return it unchanged so gRPC
// serialization preserves the message, code, and suggestion.
var localErr *azdext.LocalError
if errors.As(err, &localErr) {
return err
}
var svcErr *azdext.ServiceError
if errors.As(err, &svcErr) {
Comment on lines +118 to +123
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this handler, repo guidance prefers errors.AsType[T](err) over errors.As(...) for type checks (see cli/azd/AGENTS.md). Switching to errors.AsType[*azdext.LocalError] / errors.AsType[*azdext.ServiceError] will keep style consistent and avoid errorlint complaints.

Suggested change
var localErr *azdext.LocalError
if errors.As(err, &localErr) {
return err
}
var svcErr *azdext.ServiceError
if errors.As(err, &svcErr) {
if _, ok := errors.AsType[*azdext.LocalError](err); ok {
return err
}
if _, ok := errors.AsType[*azdext.ServiceError](err); ok {

Copilot uses AI. Check for mistakes.
return err
}
return fmt.Errorf("agent identity RBAC setup failed: %w", err)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
CodeNotLoggedIn = "not_logged_in"
CodeLoginExpired = "login_expired"
CodeAuthFailed = "auth_failed"
CodeRbacAuthFailed = "rbac_auth_failed"
)

// Error codes for compatibility errors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ package project

import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"

"azureaiagent/internal/exterrors"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
Expand Down Expand Up @@ -183,6 +186,15 @@ func ensureAgentIdentityRBACWithCred(
for attempt := range identityLookupMaxAttempts {
agentIdentities, err = discoverAgentIdentity(ctx, graphClient, displayName)
if err != nil {
if isCredentialAuthError(err) {
return exterrors.Auth(
exterrors.CodeRbacAuthFailed,
"agent identity RBAC setup failed: "+
"could not acquire a Microsoft Graph token",
"run 'azd auth login --scope https://graph.microsoft.com/.default' "+
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Original error discarded without debug logging

The structured error provides a clean user message (correctly per @jongio's resolved feedback), but the original error is completely discarded — not wrapped, not logged. The correlation IDs and specific failure reason from the Azure SDK are lost for post-incident debugging. Consider logging the original error at debug level before returning.

"and re-run 'azd deploy'",
Comment on lines +194 to +195
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion string uses single quotes around commands. Elsewhere in this extension structured auth suggestions use backticks (e.g., exterrors/authFromGrpcMessage) which renders better in terminals and is more consistent. Consider switching these command quotes to backticks for consistent UX/copy-paste.

Suggested change
"run 'azd auth login --scope https://graph.microsoft.com/.default' "+
"and re-run 'azd deploy'",
"run `azd auth login --scope https://graph.microsoft.com/.default` "+
"and re-run `azd deploy`",

Copilot uses AI. Check for mistakes.
)
}
return fmt.Errorf("failed to discover agent identity: %w", err)
}
if len(agentIdentities) > 0 {
Expand Down Expand Up @@ -453,3 +465,16 @@ func extractSubscriptionID(resourceID string) string {
}
return ""
}

// isCredentialAuthError returns true when an error originates from the Azure Identity
// library failing to acquire a token (e.g. expired login, missing scope consent).
func isCredentialAuthError(err error) bool {
if err == nil {
return false
}
if _, ok := errors.AsType[*azidentity.AuthenticationFailedError](err); ok {
return true
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] AADSTS string fallback breadth

strings.Contains(err.Error(), "AADSTS") matches any error containing "AADSTS", not just token acquisition failures. While Azure consistently returns uppercase "AADSTS", add a test case for lowercase aadsts (expected false) to document the case-sensitivity assumption and lock in the behavior contract.

// Fallback: AADSTS codes may appear in errors not wrapped as AuthenticationFailedError.
return strings.Contains(err.Error(), "AADSTS")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
package project

import (
"errors"
"fmt"
"net/http"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -198,3 +202,54 @@ func TestConstants(t *testing.T) {
assert.Equal(t, "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd", roleCognitiveServicesOpenAIUser)
assert.Equal(t, "3913510d-42f4-4e42-8a64-420c390055eb", roleMonitoringMetricsPublisher)
}

func TestIsCredentialAuthError(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "nil error",
err: nil,
want: false,
},
{
name: "unrelated error",
err: fmt.Errorf("network timeout"),
want: false,
},
{
name: "typed AuthenticationFailedError",
err: &azidentity.AuthenticationFailedError{
RawResponse: &http.Response{StatusCode: 401},
},
want: true,
},
{
name: "wrapped AuthenticationFailedError",
err: fmt.Errorf("graph query failed: %w",
&azidentity.AuthenticationFailedError{
RawResponse: &http.Response{StatusCode: 401},
}),
want: true,
},
{
name: "AADSTS error code fallback",
err: fmt.Errorf("AADSTS70043: The refresh token has expired"),
want: true,
},
{
name: "string-only error without AADSTS not matched",
err: errors.New("AzureDeveloperCLICredential: please run azd auth login"),
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isCredentialAuthError(tt.err)
assert.Equal(t, tt.want, got)
})
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Missing test for structured error return path

isCredentialAuthError is well-tested here, but no test verifies that ensureAgentIdentityRBACWithCred returns exterrors.Auth with code CodeRbacAuthFailed and the Graph scope suggestion when an auth error occurs. A mock of discoverAgentIdentity returning AuthenticationFailedError would close this gap.

Loading