From 73a456b4871999daaf15853f1b297a82df5767dd Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 4 Aug 2026 15:19:30 +0800 Subject: [PATCH 1/5] fix: narrow Agent manifest adoption detection --- .../azure.ai.agents/internal/cmd/init.go | 8 +++--- .../internal/cmd/init_adopt.go | 26 +++++-------------- .../internal/cmd/init_adopt_test.go | 21 +++++++++++++-- .../azure.ai.agents/internal/cmd/init_env.go | 8 +++--- .../internal/cmd/init_env_test.go | 12 +++++++++ 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index eebd8d81776..c3adfccb590 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -1056,10 +1056,10 @@ func newInitCommand(extCtx *azdext.ExtensionContext) *cobra.Command { Long: `Initialize a new AI agent project. When -m points at a sample's unified azure.yaml (a project manifest that -declares services with host: azure.ai.project / azure.ai.agent / ...), that -azure.yaml is adopted as the project manifest and its referenced files are -placed at the project root. When -m points at an agent manifest instead, the -project's azure.yaml is generated from it. +declares a service with host: azure.ai.agent), that azure.yaml is adopted as +the project manifest and its referenced files are placed at the project root. +When -m points at an agent manifest instead, the project's azure.yaml is +generated from it. The agent name written to agent.yaml is the Foundry agent identity. Foundry agents are unique by name within a project, so deploying with an existing name diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index 858ba630703..c4bf04d04be 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -34,25 +34,13 @@ import ( "gopkg.in/yaml.v3" ) -// foundryServiceHosts are the azure.yaml service `host` values that identify a -// unified Microsoft Foundry project manifest. The legacy `microsoft.foundry` -// host is included for backward compatibility with older non-split files. -var foundryServiceHosts = map[string]struct{}{ - "azure.ai.agent": {}, - "azure.ai.project": {}, - "azure.ai.connection": {}, - "azure.ai.toolbox": {}, - "microsoft.foundry": {}, -} - -// looksLikeFoundryAzureYaml reports whether the given YAML content is a unified -// Foundry `azure.yaml` project manifest rather than an agent manifest. +// looksLikeFoundryAzureYaml reports whether the content is a unified +// Foundry azure.yaml project manifest rather than an agent manifest. // -// It returns true when the document has a top-level `services:` map in which at -// least one service declares a Foundry `host:`. Agent manifests have a top-level -// `template:` and no `services:`, so they never match. This lets `azd ai agent -// init -m ` route a unified `azure.yaml` to the adoption path and an -// agent manifest to the legacy generate path unambiguously. +// It returns true when services contains host: azure.ai.agent. +// Agent manifests have a top-level template and no services, so they +// never match. This routes Agent project manifests to adoption and agent +// manifests to legacy generation. func looksLikeFoundryAzureYaml(content []byte) bool { var top map[string]any if err := yaml.Unmarshal(content, &top); err != nil { @@ -73,7 +61,7 @@ func looksLikeFoundryAzureYaml(content []byte) bool { if !ok { continue } - if _, isFoundry := foundryServiceHosts[host]; isFoundry { + if host == AiAgentHost { return true } } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index aa697dbd69c..4732c3cd101 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -37,13 +37,30 @@ services: want: true, }, { - name: "legacy microsoft.foundry host", + name: "unsupported microsoft.foundry host", content: `name: foundry-legacy services: agents: host: microsoft.foundry `, - want: true, + want: false, + }, + { + name: "unified azure.yaml with only sibling Foundry hosts", + content: `name: foundry-resources +services: + ai-project: + host: azure.ai.project + search-connection: + host: azure.ai.connection + toolbox: + host: azure.ai.toolbox + summarize: + host: azure.ai.skill + daily-report: + host: azure.ai.routine +`, + want: false, }, { name: "agent manifest with top-level template", diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go index 54b87aeb4c5..1d15fd94cea 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go @@ -266,8 +266,7 @@ func foundryAzureYamlServiceHost(service *yaml.Node) (string, bool) { return "", false } - _, knownHost := foundryServiceHosts[host.Value] - if !knownHost && !strings.HasPrefix(host.Value, "azure.ai.") { + if !strings.HasPrefix(host.Value, "azure.ai.") { return "", false } return host.Value, true @@ -279,8 +278,7 @@ func supportsAzureYamlEnvironmentReferences(host string) bool { "azure.ai.connection", "azure.ai.project", "azure.ai.routine", - "azure.ai.toolbox", - "microsoft.foundry": + "azure.ai.toolbox": return true default: return false @@ -381,7 +379,7 @@ func collectAzureYamlServiceEnvironmentReferences( ); err != nil { return fmt.Errorf("scanning connection service %q metadata: %w", serviceName, err) } - case "azure.ai.project", "microsoft.foundry": + case "azure.ai.project": var config azureYamlProjectEnvironmentConfig if err := active.Decode(&config); err != nil { return fmt.Errorf("decoding project service %q: %w", serviceName, err) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go index 41018fcb2df..1d1c6ec3274 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go @@ -234,6 +234,18 @@ services: skill: host: azure.ai.skill $ref: ./missing-skill.yaml +`, + want: nil, + }, + { + name: "deprecated Foundry host refs are ignored", + content: `name: sample +services: + project: + host: microsoft.foundry + network: + agentSubnet: + vnet: ${UNSUPPORTED_FOUNDRY_HOST} `, want: nil, }, From b1107edc53e70074f5520056349ea3b60d34f91b Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 4 Aug 2026 22:13:46 +0800 Subject: [PATCH 2/5] fix: resolve local Agent service references --- .../azure.ai.agents/internal/cmd/init.go | 6 +++- .../internal/cmd/init_adopt.go | 31 +++++++++++++------ .../internal/cmd/init_adopt_test.go | 21 +++++++++++-- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index c3adfccb590..6ea2c1df43d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -1307,9 +1307,13 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, // (generate the project). For private GitHub URLs, the detector // falls back to the authenticated gh CLI download path before // deciding whether this is a unified azure.yaml. See #8798. + manifestRoot := "" + if isLocalFilePath(flags.manifestPointer) { + manifestRoot = filepath.Dir(flags.manifestPointer) + } if content, ok := readManifestContentForInitDetection( ctx, azdClient, flags.manifestPointer, httpClient, - ); ok && looksLikeFoundryAzureYaml(content) { + ); ok && declaresAgentService(content, manifestRoot) { if err := runInitFromAzureYaml(ctx, flags, azdClient, httpClient, content); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index c4bf04d04be..3d9e798d636 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -25,6 +25,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/foundry" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" @@ -34,14 +35,13 @@ import ( "gopkg.in/yaml.v3" ) -// looksLikeFoundryAzureYaml reports whether the content is a unified -// Foundry azure.yaml project manifest rather than an agent manifest. +// declaresAgentService reports whether a unified azure.yaml declares an +// azure.ai.agent service rather than an agent manifest. // -// It returns true when services contains host: azure.ai.agent. -// Agent manifests have a top-level template and no services, so they -// never match. This routes Agent project manifests to adoption and agent -// manifests to legacy generation. -func looksLikeFoundryAzureYaml(content []byte) bool { +// Local service-level $ref entries are resolved against projectRoot. +// Remote manifest references are left unresolved because sibling files +// are not available during the initial peek. +func declaresAgentService(content []byte, projectRoot string) bool { var top map[string]any if err := yaml.Unmarshal(content, &top); err != nil { return false @@ -57,10 +57,23 @@ func looksLikeFoundryAzureYaml(content []byte) bool { if !ok { continue } - host, ok := svcMap["host"].(string) - if !ok { + host, _ := svcMap["host"].(string) + if host == AiAgentHost { + return true + } + + if projectRoot == "" { + continue + } + if _, ok := svcMap["$ref"]; !ok { + continue + } + + resolved, err := foundry.ResolveFileRefs(svcMap, projectRoot) + if err != nil { continue } + host, _ = resolved["host"].(string) if host == AiAgentHost { return true } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index 4732c3cd101..c8e92857f83 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -18,7 +18,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -func TestLooksLikeFoundryAzureYaml(t *testing.T) { +func TestDeclaresAgentService(t *testing.T) { tests := []struct { name string content string @@ -113,11 +113,28 @@ services: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, looksLikeFoundryAzureYaml([]byte(tt.content))) + require.Equal(t, tt.want, declaresAgentService([]byte(tt.content), "")) }) } } +func TestDeclaresAgentService_LocalServiceRef(t *testing.T) { + root := t.TempDir() + refPath := filepath.Join(root, "services", "agent.yaml") + require.NoError(t, os.MkdirAll(filepath.Dir(refPath), 0700)) + require.NoError(t, os.WriteFile(refPath, []byte("host: azure.ai.agent\nkind: hosted\n"), 0600)) + + content := []byte(`name: foundry-ref +services: + ai-project: + host: azure.ai.project + assistant: + $ref: ./services/agent.yaml +`) + + require.True(t, declaresAgentService(content, root)) +} + func TestFoundryProjectName(t *testing.T) { tests := []struct { name string From 64d0309f6055f4916c2f1426cfd287d66bbdbac9 Mon Sep 17 00:00:00 2001 From: huimiu Date: Wed, 5 Aug 2026 13:54:15 +0800 Subject: [PATCH 3/5] fix: validate Agent service adoption refs --- .../azure.ai.agents/internal/cmd/init.go | 30 ++++- .../internal/cmd/init_adopt.go | 110 +++++++++++++---- .../internal/cmd/init_adopt_test.go | 116 +++++++++++++++--- .../azure.ai.agents/internal/cmd/init_env.go | 8 +- .../internal/cmd/init_env_test.go | 8 +- 5 files changed, 223 insertions(+), 49 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 6ea2c1df43d..155a40005fc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -1313,14 +1313,32 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } if content, ok := readManifestContentForInitDetection( ctx, azdClient, flags.manifestPointer, httpClient, - ); ok && declaresAgentService(content, manifestRoot) { - if err := runInitFromAzureYaml(ctx, flags, azdClient, httpClient, content); err != nil { - if exterrors.IsCancellation(err) { - return exterrors.Cancelled("initialization was cancelled") - } + ); ok { + manifestInfo, err := inspectAzureYaml(content, manifestRoot) + if err != nil { return err } - return ejectInfraAfterInit(infraProvider) + if manifestInfo.hasServices { + if manifestInfo.hasAgentService || + manifestInfo.hasUnresolvedRefs { + if err := runInitFromAzureYaml( + ctx, + flags, + azdClient, + httpClient, + content, + ); err != nil { + if exterrors.IsCancellation(err) { + return exterrors.Cancelled( + "initialization was cancelled", + ) + } + return err + } + return ejectInfraAfterInit(infraProvider) + } + return missingAgentServiceError(flags.manifestPointer) + } } // Resolve the agent name BEFORE creating the project folder diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index 3d9e798d636..0837a6bb350 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -35,53 +35,115 @@ import ( "gopkg.in/yaml.v3" ) -// declaresAgentService reports whether a unified azure.yaml declares an -// azure.ai.agent service rather than an agent manifest. +type azureYamlManifestInfo struct { + hasServices bool + hasAgentService bool + hasUnresolvedRefs bool +} + +// inspectAzureYaml identifies unified manifests and Agent services. // -// Local service-level $ref entries are resolved against projectRoot. -// Remote manifest references are left unresolved because sibling files -// are not available during the initial peek. -func declaresAgentService(content []byte, projectRoot string) bool { +// Local references are resolved against projectRoot. Remote references +// wait for the sample directory download. +func inspectAzureYaml(content []byte, projectRoot string) (azureYamlManifestInfo, error) { + var info azureYamlManifestInfo var top map[string]any if err := yaml.Unmarshal(content, &top); err != nil { - return false + return info, nil } services, ok := top["services"].(map[string]any) if !ok { - return false + return info, nil } + info.hasServices = true - for _, svc := range services { + for serviceName, svc := range services { svcMap, ok := svc.(map[string]any) if !ok { continue } + + if hasAzureYamlFileRef(svcMap) { + if projectRoot == "" { + info.hasUnresolvedRefs = true + } else { + resolved, err := foundry.ResolveFileRefs(svcMap, projectRoot) + if err != nil { + return info, fmt.Errorf( + "resolving $ref includes for service %q: %w", + serviceName, + err, + ) + } + svcMap = resolved + } + } + host, _ := svcMap["host"].(string) if host == AiAgentHost { - return true + info.hasAgentService = true } + } - if projectRoot == "" { - continue - } - if _, ok := svcMap["$ref"]; !ok { - continue - } + return info, nil +} - resolved, err := foundry.ResolveFileRefs(svcMap, projectRoot) - if err != nil { - continue - } - host, _ = resolved["host"].(string) - if host == AiAgentHost { +func hasAzureYamlFileRef(value any) bool { + switch typed := value.(type) { + case map[string]any: + if _, ok := typed["$ref"]; ok { return true } + for _, child := range typed { + if hasAzureYamlFileRef(child) { + return true + } + } + case []any: + for _, child := range typed { + if hasAzureYamlFileRef(child) { + return true + } + } } return false } +func missingAgentServiceError(manifestPointer string) error { + return exterrors.Validation( + exterrors.CodeInvalidManifestPointer, + fmt.Sprintf( + "manifest %q is a unified azure.yaml but does not declare an agent service", + manifestPointer, + ), + fmt.Sprintf( + "add a service with host: %s, or pass an agent manifest", + AiAgentHost, + ), + ) +} + +func validateStagedAzureYaml(stagingDir, manifestPointer string) error { + manifestPath := filepath.Join(stagingDir, "azure.yaml") + //nolint:gosec // stagingDir is created or selected by the init flow + content, err := os.ReadFile(manifestPath) + if err != nil { + return fmt.Errorf("reading staged azure.yaml: %w", err) + } + + info, err := inspectAzureYaml(content, stagingDir) + if err != nil { + return err + } + if !info.hasServices || !info.hasAgentService { + return missingAgentServiceError(manifestPointer) + } + + return nil +} + // foundryProjectName returns the top-level `name:` of a unified azure.yaml, used // to derive the project folder name. Returns "" when the name is absent or the // content cannot be parsed. @@ -858,6 +920,10 @@ func runInitFromAzureYaml( } defer cleanup() + if err := validateStagedAzureYaml(stagingDir, flags.manifestPointer); err != nil { + return err + } + fmt.Println(output.WithGrayFormat("Adopting the sample's azure.yaml as your project manifest...")) envName := deriveEnvName(flags, targetDir) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index c8e92857f83..c915a882f59 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "testing" + "azureaiagent/internal/exterrors" "azureaiagent/internal/project" "github.com/azure/azure-dev/cli/azd/pkg/azdext" @@ -18,11 +19,12 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -func TestDeclaresAgentService(t *testing.T) { +func TestInspectAzureYaml(t *testing.T) { tests := []struct { - name string - content string - want bool + name string + content string + wantServices bool + wantAgentService bool }{ { name: "unified azure.yaml with split foundry hosts", @@ -34,7 +36,8 @@ services: host: azure.ai.agent kind: hosted `, - want: true, + wantServices: true, + wantAgentService: true, }, { name: "unsupported microsoft.foundry host", @@ -43,7 +46,7 @@ services: agents: host: microsoft.foundry `, - want: false, + wantServices: true, }, { name: "unified azure.yaml with only sibling Foundry hosts", @@ -60,7 +63,7 @@ services: daily-report: host: azure.ai.routine `, - want: false, + wantServices: true, }, { name: "agent manifest with top-level template", @@ -71,7 +74,7 @@ template: parameters: {} resources: [] `, - want: false, + wantServices: false, }, { name: "azure.yaml with only non-foundry services", @@ -81,24 +84,21 @@ services: host: containerapp language: js `, - want: false, + wantServices: true, }, { name: "empty content", content: "", - want: false, }, { name: "malformed yaml", content: "name: [unterminated", - want: false, }, { name: "services present but not a map", content: `name: broken services: just-a-string `, - want: false, }, { name: "service without host", @@ -107,13 +107,16 @@ services: ai-project: deployments: [] `, - want: false, + wantServices: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, declaresAgentService([]byte(tt.content), "")) + info, err := inspectAzureYaml([]byte(tt.content), "") + require.NoError(t, err) + require.Equal(t, tt.wantServices, info.hasServices) + require.Equal(t, tt.wantAgentService, info.hasAgentService) }) } } @@ -132,7 +135,90 @@ services: $ref: ./services/agent.yaml `) - require.True(t, declaresAgentService(content, root)) + info, err := inspectAzureYaml(content, root) + require.NoError(t, err) + require.True(t, info.hasServices) + require.True(t, info.hasAgentService) + require.False(t, info.hasUnresolvedRefs) +} + +func TestInspectAzureYaml_LocalServiceRefValidation(t *testing.T) { + root := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(root, "services"), 0700)) + require.NoError(t, os.WriteFile( + filepath.Join(root, "services", "project.yaml"), + []byte("host: azure.ai.project\n"), + 0600, + )) + require.NoError(t, os.WriteFile( + filepath.Join(root, "services", "agent.yaml"), + []byte("host: azure.ai.agent\n"), + 0600, + )) + + t.Run("non-Agent ref", func(t *testing.T) { + info, err := inspectAzureYaml([]byte(`services: + project: + $ref: ./services/project.yaml +`), root) + require.NoError(t, err) + require.True(t, info.hasServices) + require.False(t, info.hasAgentService) + }) + + t.Run("inline host overrides referenced host", func(t *testing.T) { + info, err := inspectAzureYaml([]byte(`services: + project: + $ref: ./services/agent.yaml + host: azure.ai.project +`), root) + require.NoError(t, err) + require.True(t, info.hasServices) + require.False(t, info.hasAgentService) + }) + + t.Run("missing ref is returned", func(t *testing.T) { + _, err := inspectAzureYaml([]byte(`services: + agent: + $ref: ./services/missing.yaml +`), root) + require.ErrorContains(t, err, "cannot read") + }) + + t.Run("remote ref is returned", func(t *testing.T) { + _, err := inspectAzureYaml([]byte(`services: + agent: + $ref: https://example.com/agent.yaml +`), root) + require.ErrorContains(t, err, "remote includes are not supported") + }) +} + +func TestInspectAzureYaml_RemoteServiceRefIsDeferred(t *testing.T) { + info, err := inspectAzureYaml([]byte(`services: + agent: + $ref: ./services/agent.yaml +`), "") + require.NoError(t, err) + require.True(t, info.hasServices) + require.False(t, info.hasAgentService) + require.True(t, info.hasUnresolvedRefs) +} + +func TestValidateStagedAzureYamlRequiresAgentService(t *testing.T) { + root := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(root, "azure.yaml"), + []byte("services:\n project:\n host: azure.ai.project\n"), + 0600, + )) + + err := validateStagedAzureYaml(root, filepath.Join(root, "azure.yaml")) + require.Error(t, err) + var localErr *azdext.LocalError + require.ErrorAs(t, err, &localErr) + require.Equal(t, exterrors.CodeInvalidManifestPointer, localErr.Code) + require.Contains(t, localErr.Message, "does not declare an agent service") } func TestFoundryProjectName(t *testing.T) { diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go index 1d15fd94cea..de72c8cecab 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env.go @@ -266,7 +266,8 @@ func foundryAzureYamlServiceHost(service *yaml.Node) (string, bool) { return "", false } - if !strings.HasPrefix(host.Value, "azure.ai.") { + if host.Value != "microsoft.foundry" && + !strings.HasPrefix(host.Value, "azure.ai.") { return "", false } return host.Value, true @@ -278,7 +279,8 @@ func supportsAzureYamlEnvironmentReferences(host string) bool { "azure.ai.connection", "azure.ai.project", "azure.ai.routine", - "azure.ai.toolbox": + "azure.ai.toolbox", + "microsoft.foundry": return true default: return false @@ -379,7 +381,7 @@ func collectAzureYamlServiceEnvironmentReferences( ); err != nil { return fmt.Errorf("scanning connection service %q metadata: %w", serviceName, err) } - case "azure.ai.project": + case "azure.ai.project", "microsoft.foundry": var config azureYamlProjectEnvironmentConfig if err := active.Decode(&config); err != nil { return fmt.Errorf("decoding project service %q: %w", serviceName, err) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go index 1d1c6ec3274..53d25c3160e 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_env_test.go @@ -238,16 +238,18 @@ services: want: nil, }, { - name: "deprecated Foundry host refs are ignored", + name: "deprecated Foundry host refs are scanned", content: `name: sample services: project: host: microsoft.foundry network: agentSubnet: - vnet: ${UNSUPPORTED_FOUNDRY_HOST} + vnet: ${FOUNDRY_HOST_NETWORK} `, - want: nil, + want: []azureYamlEnvironmentReference{ + {Name: "FOUNDRY_HOST_NETWORK"}, + }, }, { name: "deprecated toolbox and routine config fields are scanned", From 3d50b055087534fe828ed2b65e983953ff81e7ab Mon Sep 17 00:00:00 2001 From: huimiu Date: Wed, 5 Aug 2026 13:58:26 +0800 Subject: [PATCH 4/5] test: cover staged Agent ref errors --- .../internal/cmd/init_adopt_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index c915a882f59..54f659132ff 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -221,6 +221,22 @@ func TestValidateStagedAzureYamlRequiresAgentService(t *testing.T) { require.Contains(t, localErr.Message, "does not declare an agent service") } +func TestValidateStagedAzureYamlReturnsRefErrors(t *testing.T) { + root := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(root, "azure.yaml"), + []byte(`services: + agent: + $ref: ./services/missing.yaml +`), + 0600, + )) + + err := validateStagedAzureYaml(root, filepath.Join(root, "azure.yaml")) + require.ErrorContains(t, err, "cannot read") + require.ErrorContains(t, err, "missing.yaml") +} + func TestFoundryProjectName(t *testing.T) { tests := []struct { name string From a6ea58d4305d2a843513b14559bc08b853806c64 Mon Sep 17 00:00:00 2001 From: huimiu Date: Wed, 5 Aug 2026 14:06:03 +0800 Subject: [PATCH 5/5] fix: modernize Agent ref detection --- .../extensions/azure.ai.agents/internal/cmd/init_adopt.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index 0837a6bb350..24eddacfc7c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -101,10 +101,8 @@ func hasAzureYamlFileRef(value any) bool { } } case []any: - for _, child := range typed { - if hasAzureYamlFileRef(child) { - return true - } + if slices.ContainsFunc(typed, hasAzureYamlFileRef) { + return true } }