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
30 changes: 30 additions & 0 deletions cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,28 @@ func (i *initAction) Run(ctx context.Context) (*actions.ActionResult, error) {
}

func (i *initAction) initAppWithAgent(ctx context.Context, azdCtx *azdcontext.AzdContext) error {
// Warn the user if the working directory has uncommitted git changes
dirty, err := i.gitCli.IsDirty(ctx, azdCtx.ProjectDirectory())
if err != nil && !errors.Is(err, git.ErrNotRepository) {
return fmt.Errorf("checking git status: %w", err)
}

if dirty {
defaultNo := false
confirm := uxlib.NewConfirm(&uxlib.ConfirmOptions{
Message: "Your working directory has uncommitted changes. Continue initializing?",
DefaultValue: &defaultNo,
})
result, promptErr := confirm.Ask(ctx)
i.console.Message(ctx, "")
if promptErr != nil {
return promptErr
}
if result == nil || !*result {
return errors.New("user declined to continue with uncommitted changes")
}
}

// Show alpha warning
i.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: fmt.Sprintf("Agentic mode init is in preview. The agent will scan your repository and "+
Expand All @@ -411,6 +433,13 @@ func (i *initAction) initAppWithAgent(ctx context.Context, azdCtx *azdcontext.Az
output.WithLinkFormat("https://aka.ms/azd-feature-stages")),
})

// Prompt for upfront tool access before starting the agent
if err := i.consentManager.PromptWorkflowConsent(ctx,
[]string{"copilot", "azure", "azd"},
); err != nil {
return err
}

// Create agent
copilotAgent, err := i.agentFactory.Create(ctx,
agent.WithMode(agent.AgentModeInteractive),
Expand Down Expand Up @@ -466,6 +495,7 @@ func (i *initAction) initAppWithAgent(ctx context.Context, azdCtx *azdcontext.Az
i.console.Message(ctx, output.WithSuccessFormat("Session resumed"))
i.console.Message(ctx, "")
}
i.console.Message(ctx, "")
}
}

Expand Down
12 changes: 6 additions & 6 deletions cli/azd/internal/agent/consent/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,17 @@ func (cc *ConsentChecker) promptForToolConsent(

choices := []*ux.SelectChoice{
{
Value: "always",
Label: "Yes, always allow this tool",
Value: "once",
Label: "Yes, just this once",
},
{
Value: "session",
Label: "Yes, until I restart azd",
},
{
Value: "always",
Label: "Yes, always allow this tool",
},
}

// Add trust-all option for this provider if not already trusted
Expand All @@ -290,10 +294,6 @@ func (cc *ConsentChecker) promptForToolConsent(
}

choices = append(choices,
&ux.SelectChoice{
Value: "once",
Label: "Yes, just this once",
},
&ux.SelectChoice{
Value: "skip",
Label: "No, skip this tool",
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/internal/agent/consent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ type ConsentManager interface {
ListConsentRules(ctx context.Context, options ...FilterOption) ([]ConsentRule, error)
ClearConsentRules(ctx context.Context, options ...FilterOption) error

// PromptWorkflowConsent shows an upfront consent prompt asking the user whether to grant
// blanket access to the given MCP tool servers. If all servers are already trusted, the
// prompt is skipped.
PromptWorkflowConsent(ctx context.Context, servers []string) error

// Environment context methods
IsProjectScopeAvailable(ctx context.Context) bool
}
139 changes: 139 additions & 0 deletions cli/azd/internal/agent/consent/workflow_consent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package consent

import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/ux"
"github.com/mark3labs/mcp-go/mcp"
)

// PromptWorkflowConsent on consentManager implements the ConsentManager interface method.
// It shows an upfront consent prompt asking the user whether to grant blanket access to the
// given MCP tool servers. If all servers are already trusted, the prompt is skipped.
func (cm *consentManager) PromptWorkflowConsent(ctx context.Context, servers []string) error {
// Skip if every server is already trusted for tool execution
if allServersTrusted(ctx, cm, servers) {
return nil
}

scope, err := promptForWorkflowConsent(ctx, servers)
if err != nil {
return err
}

// Empty scope means the user chose "No, prompt me for each operation" — no rules to add
if scope == "" {
return nil
}

return grantWorkflowRules(ctx, cm, servers, scope)
}

// allServersTrusted returns true when every server already has a consent rule that
// would allow tool execution (any action, not just read-only).
func allServersTrusted(ctx context.Context, mgr ConsentManager, servers []string) bool {
for _, server := range servers {
request := ConsentRequest{
ToolID: fmt.Sprintf("%s/test-tool", server),
ServerName: server,
Operation: OperationTypeTool,
Annotations: mcp.ToolAnnotation{}, // no read-only hint — checks for full access
}

decision, err := mgr.CheckConsent(ctx, request)
if err != nil || !decision.Allowed {
return false
}
}

return true
}

// promptForWorkflowConsent displays the consent choices and returns the selected scope
// (ScopeSession or ScopeGlobal), or empty string for "no, prompt me".
func promptForWorkflowConsent(ctx context.Context, servers []string) (Scope, error) {
serverList := strings.Join(servers, ", ")

message := fmt.Sprintf(
"Grant access to tools for %s to read and write files in your current workspace?",
output.WithHighLightFormat(serverList),
)

helpMessage := "This allows the agent workflow to use built-in tools (file read/write, " +
"Azure MCP, azd CLI) without prompting for each tool individually.\n\n" +
"This command does not create or provision any resources in Azure. " +
"It only generates configuration files locally in your workspace.\n\n" +
"You can review or revoke permissions at any time with:\n" +
" azd copilot consent list\n" +
" azd copilot consent revoke"

choices := []*ux.SelectChoice{
{
Value: string(ScopeSession),
Label: "Yes, approve for this session",
},
{
Value: string(ScopeGlobal),
Label: "Yes, always approve",
},
{
Value: "prompt",
Label: "No, prompt me for each operation",
},
}

selector := ux.NewSelect(&ux.SelectOptions{
Message: message,
HelpMessage: helpMessage,
Choices: choices,
EnableFiltering: new(false),
DisplayCount: len(choices),
})

choiceIndex, err := selector.Ask(ctx)
if err != nil {
return "", err
}

if choiceIndex == nil || *choiceIndex < 0 || *choiceIndex >= len(choices) {
return "", fmt.Errorf("invalid choice selected")
}

selected := choices[*choiceIndex].Value
if selected == "prompt" {
return "", nil
}

return Scope(selected), nil
}

// grantWorkflowRules creates server-level allow rules for every server for tool operations.
func grantWorkflowRules(ctx context.Context, mgr ConsentManager, servers []string, scope Scope) error {
now := time.Now()

for _, server := range servers {
rule := ConsentRule{
Scope: scope,
Target: NewServerTarget(server),
Action: ActionAny,
Operation: OperationTypeTool,
Permission: PermissionAllow,
GrantedAt: now,
}

if err := mgr.GrantConsent(ctx, rule); err != nil {
log.Printf("[consent] failed to persist workflow consent for %s (scope=%s): %v",
server, scope, err)
}
}
Comment thread
wbreza marked this conversation as resolved.

return nil
}
Loading
Loading