From fac04198db709494e5a62b3c9b51fc984006634d Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 28 Jul 2026 14:24:40 +0800 Subject: [PATCH 1/4] fix: name the azure.yaml key in the RAI policy validation error --- cli/azd/extensions/azure.ai.agents/README.md | 36 ++++ .../internal/pkg/agents/agent_yaml/parse.go | 8 +- .../internal/project/agent_policies_test.go | 202 ++++++++++++++++++ 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go diff --git a/cli/azd/extensions/azure.ai.agents/README.md b/cli/azd/extensions/azure.ai.agents/README.md index daeffbe3ef3..022defd64a8 100644 --- a/cli/azd/extensions/azure.ai.agents/README.md +++ b/cli/azd/extensions/azure.ai.agents/README.md @@ -48,6 +48,42 @@ services: description: My hosted agent ``` +## Content safety policies + +A hosted agent can be bound to an Azure AI Content Safety (RAI) policy so every +request and response it handles is screened by that policy. Declare it with a +`policies` list on the `azure.ai.agent` service entry in `azure.yaml`: + +```yaml +services: + my-agent: + host: azure.ai.agent + project: . + kind: hosted + policies: + - type: rai_policy + raiPolicyName: /subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/ +``` + +`policies` applies to both deploy modes — container images and code deploys +(`codeConfiguration`) alike. It is optional; agents without it deploy exactly as +before. + +Details: + +- `type` is required. `rai_policy` is currently the only supported value. +- `raiPolicyName` is required for `rai_policy` and takes the **full ARM resource + ID** of the policy, not its short name. Built-in policies such as + `Microsoft.DefaultV2` still need the full ID, with the account that hosts them + in the path. +- Create or list policies on the Foundry account first — azd does not create the + policy, it only associates the agent with an existing one. + +> **Note:** In the deprecated on-disk `agent.yaml` shape the key is snake_case +> (`rai_policy_name`). In `azure.yaml` it is camelCase (`raiPolicyName`), like +> the other inline agent properties such as `codeConfiguration` and +> `environmentVariables`. + ## Session carry-over across deploys When a hosted agent is redeployed, Foundry assigns the agent a **new version** and diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go index 2a9dd971239..ae9f46f3087 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go @@ -388,8 +388,14 @@ func ValidateAgentDefinition(templateBytes []byte) error { switch policy.Type { case PolicyTypeRai: if policy.RaiPolicyName == "" { + // This validator runs for both the unified + // azure.yaml shape and the deprecated on-disk + // agent.yaml, which spell the key differently. + // Name both so the message matches whichever + // file the author is editing. errors = append(errors, fmt.Sprintf( - "policies[%d] of type '%s' requires a policy name (rai_policy_name)", + "policies[%d] of type '%s' requires a policy name "+ + "('raiPolicyName' in azure.yaml, 'rai_policy_name' in agent.yaml)", i, policy.Type)) } case "": diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go new file mode 100644 index 00000000000..9f968c814a6 --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import ( + "testing" + + "azureaiagent/internal/pkg/agents/agent_api" + "azureaiagent/internal/pkg/agents/agent_yaml" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +// raiPolicyID is a representative RAI policy ARM resource ID, the value users +// put in `raiPolicyName`. +const raiPolicyID = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/" + + "my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2" + +// inlineAgentService builds an azure.ai.agent service entry whose inline +// (service-level) properties are exactly what a user would author under the +// unified azure.yaml shape. +func inlineAgentService(t *testing.T, values map[string]any) *azdext.ServiceConfig { + t.Helper() + + props, err := structpb.NewStruct(values) + require.NoError(t, err) + + return &azdext.ServiceConfig{ + Name: "rai-agent", + Host: "azure.ai.agent", + AdditionalProperties: props, + } +} + +// TestAgentPoliciesRoundTrip verifies governance policies survive a marshal into +// the inline service properties and back, and that they are persisted under the +// camelCase `raiPolicyName` key that azure.yaml authors use — not the +// `rai_policy_name` key of the deprecated on-disk agent.yaml. +func TestAgentPoliciesRoundTrip(t *testing.T) { + t.Parallel() + + ca := sampleContainerAgent() + ca.Policies = []agent_yaml.Policy{ + {Type: agent_yaml.PolicyTypeRai, RaiPolicyName: raiPolicyID}, + } + + props, err := AgentDefinitionToServiceProperties(ca, nil) + require.NoError(t, err) + + policies := props.GetFields()["policies"].GetListValue().GetValues() + require.Len(t, policies, 1) + policy := policies[0].GetStructValue().GetFields() + require.Equal(t, "rai_policy", policy["type"].GetStringValue()) + require.Equal(t, raiPolicyID, policy["raiPolicyName"].GetStringValue()) + require.NotContains(t, policy, "rai_policy_name", + "azure.yaml uses the camelCase raiPolicyName key") + + svc := &azdext.ServiceConfig{ + Name: "rai-agent", + Host: "azure.ai.agent", + AdditionalProperties: props, + } + + got, isHosted, found, source, err := AgentDefinitionFromService(svc) + require.NoError(t, err) + require.True(t, found) + require.True(t, isHosted) + require.Equal(t, AgentDefinitionSourceInline, source) + require.Equal(t, ca.Policies, got.Policies) +} + +// TestAgentPoliciesReachRaiConfig is the end-to-end regression test for +// https://github.com/Azure/azure-dev/issues/8709: a `policies` entry authored on +// the unified azure.yaml service entry must reach the Foundry data plane as +// `rai_config.rai_policy_name`. It covers both deploy modes, mirroring the two +// mapRaiConfig call sites in agent_yaml/map.go. +func TestAgentPoliciesReachRaiConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + extra map[string]any + options []agent_yaml.AgentBuildOption + }{ + { + name: "container deploy", + options: []agent_yaml.AgentBuildOption{agent_yaml.WithImageURL("myregistry.azurecr.io/img:v1")}, + }, + { + name: "code deploy", + extra: map[string]any{ + "codeConfiguration": map[string]any{ + "runtime": "python_3_12", + "entryPoint": "agent.py", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + values := map[string]any{ + "kind": "hosted", + "name": "rai-agent", + "policies": []any{ + map[string]any{ + "type": "rai_policy", + "raiPolicyName": raiPolicyID, + }, + }, + } + for key, value := range test.extra { + values[key] = value + } + + agentDef, isHosted, found, _, err := AgentDefinitionFromService( + inlineAgentService(t, values), + ) + require.NoError(t, err) + require.True(t, found) + require.True(t, isHosted) + + request, err := agent_yaml.CreateAgentAPIRequestFromDefinition(agentDef, test.options...) + require.NoError(t, err) + + definition, ok := request.Definition.(agent_api.HostedAgentDefinition) + require.True(t, ok) + require.NotNil(t, definition.RaiConfig) + require.Equal(t, raiPolicyID, definition.RaiConfig.RaiPolicyName) + }) + } +} + +// TestAgentPoliciesNoRaiConfigWhenAbsent verifies an agent without policies +// sends no rai_config, so existing projects are unaffected. +func TestAgentPoliciesNoRaiConfigWhenAbsent(t *testing.T) { + t.Parallel() + + agentDef, _, found, _, err := AgentDefinitionFromService(inlineAgentService(t, map[string]any{ + "kind": "hosted", + "name": "rai-agent", + })) + require.NoError(t, err) + require.True(t, found) + + request, err := agent_yaml.CreateAgentAPIRequestFromDefinition( + agentDef, + agent_yaml.WithImageURL("myregistry.azurecr.io/img:v1"), + ) + require.NoError(t, err) + + definition, ok := request.Definition.(agent_api.HostedAgentDefinition) + require.True(t, ok) + require.Nil(t, definition.RaiConfig) +} + +// TestAgentPoliciesValidation verifies malformed policies authored inline in +// azure.yaml are rejected, and that the missing-name error names the +// azure.yaml key (raiPolicyName) rather than only the agent.yaml one. +func TestAgentPoliciesValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + policy map[string]any + wantErrSubst string + }{ + { + name: "missing policy name", + policy: map[string]any{"type": "rai_policy"}, + wantErrSubst: "'raiPolicyName' in azure.yaml", + }, + { + name: "missing type", + policy: map[string]any{"raiPolicyName": raiPolicyID}, + wantErrSubst: "policies[0] requires a type", + }, + { + name: "unsupported type", + policy: map[string]any{"type": "network_policy"}, + wantErrSubst: "policies[0] has an unsupported type 'network_policy'", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + _, _, _, _, err := AgentDefinitionFromService(inlineAgentService(t, map[string]any{ + "kind": "hosted", + "name": "rai-agent", + "policies": []any{test.policy}, + })) + require.ErrorContains(t, err, test.wantErrSubst) + }) + } +} From 58e0d69b27d02f77ab7fe2618105ca4ab152cb87 Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 28 Jul 2026 14:28:11 +0800 Subject: [PATCH 2/4] fix: remove redundant RAI policy validation comments --- .../azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go index ae9f46f3087..9d3cf3ab439 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go @@ -388,11 +388,6 @@ func ValidateAgentDefinition(templateBytes []byte) error { switch policy.Type { case PolicyTypeRai: if policy.RaiPolicyName == "" { - // This validator runs for both the unified - // azure.yaml shape and the deprecated on-disk - // agent.yaml, which spell the key differently. - // Name both so the message matches whichever - // file the author is editing. errors = append(errors, fmt.Sprintf( "policies[%d] of type '%s' requires a policy name "+ "('raiPolicyName' in azure.yaml, 'rai_policy_name' in agent.yaml)", From d0d16ca0cd9cb8c70786825e35fb464e4e3534b6 Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 28 Jul 2026 15:49:21 +0800 Subject: [PATCH 3/4] test: use maps.Copy for policy test case overrides --- .../azure.ai.agents/internal/project/agent_policies_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go index 9f968c814a6..5b60bd73513 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agent_policies_test.go @@ -4,6 +4,7 @@ package project import ( + "maps" "testing" "azureaiagent/internal/pkg/agents/agent_api" @@ -114,9 +115,7 @@ func TestAgentPoliciesReachRaiConfig(t *testing.T) { }, }, } - for key, value := range test.extra { - values[key] = value - } + maps.Copy(values, test.extra) agentDef, isHosted, found, _, err := AgentDefinitionFromService( inlineAgentService(t, values), From 3f15ab72f6b16cb9eb5507d776d8c88a4bf7db70 Mon Sep 17 00:00:00 2001 From: huimiu Date: Tue, 28 Jul 2026 15:57:07 +0800 Subject: [PATCH 4/4] fix: add required agent name to README azure.yaml examples --- cli/azd/extensions/azure.ai.agents/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/azd/extensions/azure.ai.agents/README.md b/cli/azd/extensions/azure.ai.agents/README.md index 022defd64a8..e84a4f60c99 100644 --- a/cli/azd/extensions/azure.ai.agents/README.md +++ b/cli/azd/extensions/azure.ai.agents/README.md @@ -34,6 +34,7 @@ services: project: . config: kind: hosted + name: my-agent description: My hosted agent ``` @@ -45,6 +46,7 @@ services: host: azure.ai.agent project: . kind: hosted + name: my-agent description: My hosted agent ``` @@ -60,6 +62,7 @@ services: host: azure.ai.agent project: . kind: hosted + name: my-agent policies: - type: rai_policy raiPolicyName: /subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/