Skip to content
Open
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
25 changes: 25 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ func checkAiModelServiceAvailable(ctx context.Context, azdClient *azdext.AzdClie
return nil
}

// ensureLoggedIn verifies that the user is authenticated before any file-modifying
// operations take place. It calls ListSubscriptions as a lightweight auth probe;
// only gRPC Unauthenticated errors are treated as failures. Other errors (e.g.
// network issues) are ignored so they don't block init for unrelated reasons.
func ensureLoggedIn(ctx context.Context, azdClient *azdext.AzdClient) error {
_, err := azdClient.Account().ListSubscriptions(ctx, &azdext.ListSubscriptionsRequest{})
Copy link
Copy Markdown
Member

@vhvb1989 vhvb1989 Apr 9, 2026

Choose a reason for hiding this comment

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

this function ensureLogged in should not depend on trying to run one API that requires logged in.

If there is not a unique API to check for auth, consider using the workflow api like in

You you can call azd auth status here

if err == nil {
return nil
}

if st, ok := status.FromError(err); ok && st.Code() == codes.Unauthenticated {
return exterrors.Auth(
exterrors.CodeNotLoggedIn,
"not logged in",
"run `azd auth login` to authenticate before running init",
)
}
Comment on lines +140 to +146
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The hardcoded "not logged in" message discards whatever the gRPC server actually reported. The exterrors package already has authFromGrpcMessage (unexported) that classifies "not logged in" vs "token expired" vs generic auth failure based on the server's message.

At minimum, forwarding st.Message() preserves useful diagnostic info:

Suggested change
if st, ok := status.FromError(err); ok && st.Code() == codes.Unauthenticated {
return exterrors.Auth(
exterrors.CodeNotLoggedIn,
"not logged in",
"run `azd auth login` to authenticate before running init",
)
}
if st, ok := status.FromError(err); ok && st.Code() == codes.Unauthenticated {
msg := st.Message()
if msg == "" {
msg = "not logged in"
}
return exterrors.Auth(
exterrors.CodeNotLoggedIn,
msg,
"run `azd auth login` to authenticate before running init",
)
}

Longer term, exporting authFromGrpcMessage (or wrapping it) would let callers here get proper code classification too.


Comment on lines +132 to +147
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

ensureLoggedIn currently returns nil for all non-Unauthenticated errors, which also swallows cancellation/deadline errors (e.g., Ctrl+C or context timeout). That can cause init to continue after the user has cancelled. Consider explicitly propagating context cancellations (errors.Is(err, context.Canceled) or gRPC codes.Canceled/DeadlineExceeded) instead of returning nil.

Suggested change
// only gRPC Unauthenticated errors are treated as failures. Other errors (e.g.
// network issues) are ignored so they don't block init for unrelated reasons.
func ensureLoggedIn(ctx context.Context, azdClient *azdext.AzdClient) error {
_, err := azdClient.Account().ListSubscriptions(ctx, &azdext.ListSubscriptionsRequest{})
if err == nil {
return nil
}
if st, ok := status.FromError(err); ok && st.Code() == codes.Unauthenticated {
return exterrors.Auth(
exterrors.CodeNotLoggedIn,
"not logged in",
"run `azd auth login` to authenticate before running init",
)
}
// gRPC Unauthenticated errors are treated as login failures, cancellation and
// deadline errors are propagated, and other errors (e.g. network issues) are
// ignored so they don't block init for unrelated reasons.
func ensureLoggedIn(ctx context.Context, azdClient *azdext.AzdClient) error {
_, err := azdClient.Account().ListSubscriptions(ctx, &azdext.ListSubscriptionsRequest{})
if err == nil {
return nil
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if st, ok := status.FromError(err); ok {
if st.Code() == codes.Canceled || st.Code() == codes.DeadlineExceeded {
return err
}
if st.Code() == codes.Unauthenticated {
return exterrors.Auth(
exterrors.CodeNotLoggedIn,
"not logged in",
"run `azd auth login` to authenticate before running init",
)
}
}

Copilot uses AI. Check for mistakes.
return nil
}

// runInitFromManifest sets up Azure context, credentials, console, and runs the
// InitAction for a given manifest pointer. This is the shared code path used when
// initializing from a manifest URL/path (the -m flag, agent template, or azd template
Expand Down Expand Up @@ -228,6 +249,10 @@ func newInitCommand(rootFlags *rootFlagsDefinition) *cobra.Command {
return err
}

if err := ensureLoggedIn(ctx, azdClient); err != nil {
return err
}
Comment on lines +252 to +254
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The new fail-fast auth behavior via ensureLoggedIn is important user-facing flow control, but there’s no unit test asserting it. Consider adding a test that starts a minimal in-process gRPC server with AccountService.ListSubscriptions returning codes.Unauthenticated and verifies init errors out before doing any work; also cover the non-Unauthenticated path (should not block).

Copilot uses AI. Check for mistakes.

Comment on lines +252 to +255
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.

It might be better to arrange this after WaitForDebugger so debugging still works when logged out

// Wait for debugger if AZD_EXT_DEBUG is set
if err := azdext.WaitForDebugger(ctx, azdClient); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, azdext.ErrDebuggerAborted) {
Expand Down
Loading