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
47 changes: 40 additions & 7 deletions cli/azd/cmd/auto_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log"
"os"
"slices"
"strconv"
"strings"

"github.com/azure/azure-dev/cli/azd/internal"
Expand Down Expand Up @@ -617,6 +618,11 @@ func CreateGlobalFlagSet() *pflag.FlagSet {
"no-prompt",
false,
"Accepts the default value instead of prompting, or it fails if there is no default.")
globalFlags.Bool(
"non-interactive",
false,
"Alias for --no-prompt.")
_ = globalFlags.MarkHidden("non-interactive")

// The telemetry system is responsible for reading these flags value and using it to configure the telemetry
// system, but we still need to add it to our flag set so that when we parse the command line with Cobra we
Expand Down Expand Up @@ -666,15 +672,42 @@ func ParseGlobalFlags(args []string, opts *internal.GlobalCommandOptions) error
opts.EnableDebugLogging = boolVal
}

if boolVal, err := globalFlagSet.GetBool("no-prompt"); err == nil {
opts.NoPrompt = boolVal
}
// --non-interactive is an alias for --no-prompt; either flag sets NoPrompt.
// When both are present, true wins (either flag opting in is sufficient).
noPromptVal, _ := globalFlagSet.GetBool("no-prompt")
nonInteractiveVal, _ := globalFlagSet.GetBool("non-interactive")
opts.NoPrompt = noPromptVal || nonInteractiveVal

// Agent Detection: If --no-prompt was not explicitly set and we detect an AI coding agent
// as the caller, automatically enable no-prompt mode for non-interactive execution.
// Check if either flag was explicitly provided on the command line
noPromptFlag := globalFlagSet.Lookup("no-prompt")
noPromptExplicitlySet := noPromptFlag != nil && noPromptFlag.Changed
if !noPromptExplicitlySet && agentdetect.IsRunningInAgent() {
nonInteractiveFlag := globalFlagSet.Lookup("non-interactive")
flagExplicitlySet := (noPromptFlag != nil && noPromptFlag.Changed) ||
(nonInteractiveFlag != nil && nonInteractiveFlag.Changed)

// Environment variable: AZD_NON_INTERACTIVE enables no-prompt mode when set to a
// truthy value (parsed via strconv.ParseBool: "true", "1", "TRUE", etc.).
// Explicit flags take precedence over this env var.
// When this env var is present (regardless of value), it also suppresses
// agent auto-detection since the user has made an explicit choice.
envVarPresent := false
if !flagExplicitlySet {
if envVal, ok := os.LookupEnv("AZD_NON_INTERACTIVE"); ok {
envVarPresent = true
if parsed, err := strconv.ParseBool(envVal); err == nil && parsed {
opts.NoPrompt = true
} else if err != nil {
log.Printf(
"warning: AZD_NON_INTERACTIVE=%q is not a valid boolean"+
" (expected true/false/1/0), ignoring",
envVal,
)
}
}
}

// Agent Detection: If no explicit flag or env var was set and we detect an AI coding
// agent as the caller, automatically enable no-prompt mode for non-interactive execution.
if !flagExplicitlySet && !envVarPresent && agentdetect.IsRunningInAgent() {
opts.NoPrompt = true
}

Expand Down
2 changes: 2 additions & 0 deletions cli/azd/cmd/auto_install_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ func clearAgentEnvVarsForTest(t *testing.T) {
"GEMINI_CLI", "GEMINI_CLI_NO_RELAUNCH",
// OpenCode
"OPENCODE",
// Non-interactive env var
"AZD_NON_INTERACTIVE",
// User agent
internal.AzdUserAgentEnvVar,
}
Expand Down
140 changes: 140 additions & 0 deletions cli/azd/cmd/auto_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,143 @@ func TestParseGlobalFlags_AgentDetection(t *testing.T) {
})
}
}

func TestParseGlobalFlags_NonInteractiveAliasAndEnvVar(t *testing.T) {
tests := []struct {
name string
args []string
envKey string
envVal string
wantNoPrompt bool
}{
{
name: "no flags or env",
args: []string{},
wantNoPrompt: false,
},
{
name: "--no-prompt sets NoPrompt",
args: []string{"--no-prompt"},
wantNoPrompt: true,
},
{
name: "--non-interactive sets NoPrompt",
args: []string{"--non-interactive"},
wantNoPrompt: true,
},
{
name: "--no-prompt=false keeps NoPrompt false",
args: []string{"--no-prompt=false"},
wantNoPrompt: false,
},
{
name: "AZD_NON_INTERACTIVE=true sets NoPrompt",
args: []string{},
envKey: "AZD_NON_INTERACTIVE",
envVal: "true",
wantNoPrompt: true,
},
{
name: "AZD_NON_INTERACTIVE=1 sets NoPrompt",
args: []string{},
envKey: "AZD_NON_INTERACTIVE",
envVal: "1",
wantNoPrompt: true,
},
{
name: "AZD_NON_INTERACTIVE=false does not set NoPrompt",
args: []string{},
envKey: "AZD_NON_INTERACTIVE",
envVal: "false",
wantNoPrompt: false,
},
{
name: "AZD_NON_INTERACTIVE=0 does not set NoPrompt",
args: []string{},
envKey: "AZD_NON_INTERACTIVE",
envVal: "0",
wantNoPrompt: false,
},
{
name: "explicit --no-prompt=false overrides env true",
args: []string{"--no-prompt=false"},
envKey: "AZD_NON_INTERACTIVE",
envVal: "true",
wantNoPrompt: false,
},
{
name: "explicit --no-prompt overrides env false",
args: []string{"--no-prompt"},
envKey: "AZD_NON_INTERACTIVE",
envVal: "false",
wantNoPrompt: true,
},
{
name: "--non-interactive overrides env false",
args: []string{"--non-interactive"},
envKey: "AZD_NON_INTERACTIVE",
envVal: "false",
wantNoPrompt: true,
},
{
name: "AZD_NON_INTERACTIVE=TRUE (uppercase)",
args: []string{},
envKey: "AZD_NON_INTERACTIVE",
envVal: "TRUE",
wantNoPrompt: true,
},
{
name: "both flags coexist",
args: []string{"--no-prompt", "--non-interactive"},
wantNoPrompt: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear agent detection and AZD_NON_INTERACTIVE to isolate
// from the ambient environment.
clearAgentEnvVarsForTest(t)
agentdetect.ResetDetection()

// Skip if we're inside an agent and expect false
if !tt.wantNoPrompt && tt.envKey == "" && len(tt.args) == 0 {
if agentdetect.GetCallingAgent().Detected {
t.Skip("skipping: agent process detected")
}
agentdetect.ResetDetection()
}

if tt.envKey != "" {
t.Setenv(tt.envKey, tt.envVal)
}

opts := &internal.GlobalCommandOptions{}
err := ParseGlobalFlags(tt.args, opts)
require.NoError(t, err)
assert.Equal(t, tt.wantNoPrompt, opts.NoPrompt)

agentdetect.ResetDetection()
})
}

// Standalone test: prove that AZD_NON_INTERACTIVE presence suppresses agent detection.
// CLAUDE_CODE=1 would normally trigger NoPrompt via agent detection, but
// AZD_NON_INTERACTIVE=false should suppress agent detection entirely.
t.Run("AZD_NON_INTERACTIVE=false suppresses agent detection with CLAUDE_CODE set", func(t *testing.T) {
clearAgentEnvVarsForTest(t)
agentdetect.ResetDetection()

t.Setenv("CLAUDE_CODE", "1")
t.Setenv("AZD_NON_INTERACTIVE", "false")
agentdetect.ResetDetection()

opts := &internal.GlobalCommandOptions{}
err := ParseGlobalFlags([]string{}, opts)
require.NoError(t, err)
assert.False(t, opts.NoPrompt,
"AZD_NON_INTERACTIVE=false should suppress agent detection from setting NoPrompt")

agentdetect.ResetDetection()
})
}
11 changes: 9 additions & 2 deletions cli/azd/internal/global_command_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ type GlobalCommandOptions struct {
// launched tools. It's enabled with `--debug`, for any command.
EnableDebugLogging bool

// when true, interactive prompts should behave as if the user selected the default value.
// if there is no default value the prompt returns an error.
// NoPrompt controls non-interactive mode. When true, interactive prompts should behave as
// if the user selected the default value. If there is no default value the prompt returns
// an error.
//
// Can be enabled via:
// - --no-prompt flag
// - --non-interactive flag (alias for --no-prompt)
// - AZD_NON_INTERACTIVE=true environment variable
// - Automatic agent detection (lowest priority)
NoPrompt bool

// EnableTelemetry indicates if telemetry should be sent.
Expand Down
27 changes: 8 additions & 19 deletions cli/azd/pkg/azdext/mcp_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestMCPSecurityCheckURL_BlocksMetadataEndpoints(t *testing.T) {
Expand Down Expand Up @@ -318,25 +320,12 @@ func TestMCPSecurityFluentBuilder(t *testing.T) {
RedactHeaders("Authorization").
ValidatePathsWithinBase("/tmp")

if policy == nil {
t.Fatal("fluent builder should return non-nil policy")
}

if !policy.blockMetadata {
t.Error("blockMetadata should be true")
}
if !policy.blockPrivate {
t.Error("blockPrivate should be true")
}
if !policy.requireHTTPS {
t.Error("requireHTTPS should be true")
}
if !policy.IsHeaderBlocked("Authorization") {
t.Error("Authorization should be blocked")
}
if len(policy.allowedBasePaths) != 1 {
t.Errorf("expected 1 base path, got %d", len(policy.allowedBasePaths))
}
require.NotNil(t, policy, "fluent builder should return non-nil policy")
require.True(t, policy.blockMetadata, "blockMetadata should be true")
require.True(t, policy.blockPrivate, "blockPrivate should be true")
require.True(t, policy.requireHTTPS, "requireHTTPS should be true")
require.True(t, policy.IsHeaderBlocked("Authorization"), "Authorization should be blocked")
require.Len(t, policy.allowedBasePaths, 1, "expected 1 base path")
}

func TestSSRFSafeRedirect_SchemeDowngrade(t *testing.T) {
Expand Down
11 changes: 7 additions & 4 deletions cli/azd/pkg/ux/ux_additional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ func TestConsoleWidth_empty_COLUMNS_uses_default(t *testing.T) {
func TestPtr(t *testing.T) {
intVal := 42
p := Ptr(intVal)
if p == nil {
switch {
case p == nil:
t.Fatal("Ptr should return non-nil pointer")
}
if *p != 42 {
case *p != 42:
t.Fatalf("*Ptr(42) = %d, want 42", *p)
}

strVal := "hello"
sp := Ptr(strVal)
if *sp != "hello" {
switch {
case sp == nil:
t.Fatal("Ptr should return non-nil pointer for string")
case *sp != "hello":
t.Fatalf("*Ptr(hello) = %q, want hello", *sp)
}
}
Expand Down
Loading