From 42202d7d91240b4aa12aa9a2af3c6cff75397f16 Mon Sep 17 00:00:00 2001 From: amitbhave10 Date: Tue, 2 Jun 2026 05:26:35 +0000 Subject: [PATCH 1/2] Expose policies.rai_config in agent.yaml for hosted agents Add support for the policies.rai_config.rai_policy_name field in the azure.ai.agents extension's agent.yaml manifest so users can attach a Responsible AI (content safety) guardrail policy to hosted agents. The value is the full ARM resource ID of the RAI policy, which azd deploy forwards to the Foundry data plane via the existing rai_config field. - yaml.go: add RaiConfig and AgentPolicies manifest structs and a Policies field on ContainerAgent - map.go: add mapRaiConfig helper and wire it into both the container/image and code-deploy request builders - parse.go: validate rai_policy_name is non-empty when present - tests + testdata fixture covering image, code, and validation paths - CHANGELOG entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../extensions/azure.ai.agents/CHANGELOG.md | 4 + .../internal/pkg/agents/agent_yaml/map.go | 15 ++- .../pkg/agents/agent_yaml/map_test.go | 102 ++++++++++++++++++ .../internal/pkg/agents/agent_yaml/parse.go | 5 + .../pkg/agents/agent_yaml/parse_test.go | 63 +++++++++++ .../testdata/hosted-agent-with-rai.yaml | 11 ++ .../pkg/agents/agent_yaml/testdata_test.go | 6 ++ .../internal/pkg/agents/agent_yaml/yaml.go | 13 +++ 8 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml diff --git a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md index a52946615c8..d60f112de30 100644 --- a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## Unreleased + +- Add support for the `policies.rai_config.rai_policy_name` field in `agent.yaml` to attach a Responsible AI (content safety) guardrail policy to hosted agents. The value is the full ARM resource ID of the RAI policy (for example, `/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/`). `azd deploy` forwards it to the Foundry data plane. + ## 0.1.37-preview (2026-06-01) - [[#8512]](https://github.com/Azure/azure-dev/pull/8512) Normalize connection auth `AgenticIdentity` values to the ARM-required `AgenticIdentityToken`. diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go index 6f0dbf206fa..a601323cc33 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go @@ -84,6 +84,15 @@ func constructBuildConfig(options ...AgentBuildOption) *AgentBuildConfig { return config } +// mapRaiConfig flattens the manifest-level policies.rai_config into the data-plane +// rai_config field. It returns nil when no policy name is configured. +func mapRaiConfig(policies *AgentPolicies) *agent_api.RaiConfig { + if policies == nil || policies.RaiConfig == nil || policies.RaiConfig.RaiPolicyName == "" { + return nil + } + return &agent_api.RaiConfig{RaiPolicyName: policies.RaiConfig.RaiPolicyName} +} + // MapEndpointAndCard maps YAML-layer endpoint and card fields to API model types // without requiring or validating the full agent definition. This is used by the // endpoint update command where only endpoint/card patching is needed. @@ -396,7 +405,8 @@ func CreateHostedAgentAPIRequest(hostedAgent ContainerAgent, buildConfig *AgentB codeDef := agent_api.HostedAgentDefinition{ AgentDefinition: agent_api.AgentDefinition{ - Kind: agent_api.AgentKindHosted, + Kind: agent_api.AgentKindHosted, + RaiConfig: mapRaiConfig(hostedAgent.Policies), }, ProtocolVersions: protocolVersions, CPU: cpu, @@ -420,7 +430,8 @@ func CreateHostedAgentAPIRequest(hostedAgent ContainerAgent, buildConfig *AgentB imageDef := agent_api.HostedAgentDefinition{ AgentDefinition: agent_api.AgentDefinition{ - Kind: agent_api.AgentKindHosted, + Kind: agent_api.AgentKindHosted, + RaiConfig: mapRaiConfig(hostedAgent.Policies), }, ProtocolVersions: protocolVersions, CPU: cpu, diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go index 8961b86c8bd..ae52b21ed21 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go @@ -1534,3 +1534,105 @@ func TestCreateHostedAgentAPIRequest_WithVersionSelectorAndAuthSchemes(t *testin t.Errorf("schemes[1].IsolationKeySource should be nil, got %v", schemes[1].IsolationKeySource) } } + +func TestCreateHostedAgentAPIRequest_WithRaiConfig(t *testing.T) { + t.Parallel() + const raiPolicyID = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/" + + "my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2" + agent := ContainerAgent{ + AgentDefinition: AgentDefinition{ + Kind: AgentKindHosted, + Name: "rai-agent", + }, + Policies: &AgentPolicies{ + RaiConfig: &RaiConfig{RaiPolicyName: raiPolicyID}, + }, + } + buildConfig := &AgentBuildConfig{ImageURL: "img:latest"} + + req, err := CreateHostedAgentAPIRequest(agent, buildConfig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + imgDef := req.Definition.(agent_api.HostedAgentDefinition) + if imgDef.RaiConfig == nil { + t.Fatal("expected RaiConfig to be set, got nil") + } + if imgDef.RaiConfig.RaiPolicyName != raiPolicyID { + t.Errorf("RaiPolicyName = %q, want %q", imgDef.RaiConfig.RaiPolicyName, raiPolicyID) + } +} + +func TestCreateAgentAPIRequest_CodeDeploy_WithRaiConfig(t *testing.T) { + t.Parallel() + const raiPolicyID = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/" + + "my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2" + agent := ContainerAgent{ + AgentDefinition: AgentDefinition{ + Kind: AgentKindHosted, + Name: "rai-code-agent", + }, + Protocols: []ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + }, + CodeConfiguration: &CodeConfiguration{ + Runtime: "python_3_12", + EntryPoint: "agent.py", + }, + Policies: &AgentPolicies{ + RaiConfig: &RaiConfig{RaiPolicyName: raiPolicyID}, + }, + } + + req, err := CreateAgentAPIRequestFromDefinition(agent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + codeDef := req.Definition.(agent_api.HostedAgentDefinition) + if codeDef.RaiConfig == nil { + t.Fatal("expected RaiConfig to be set, got nil") + } + if codeDef.RaiConfig.RaiPolicyName != raiPolicyID { + t.Errorf("RaiPolicyName = %q, want %q", codeDef.RaiConfig.RaiPolicyName, raiPolicyID) + } +} + +func TestCreateHostedAgentAPIRequest_NoRaiConfig(t *testing.T) { + t.Parallel() + agent := ContainerAgent{ + AgentDefinition: AgentDefinition{ + Kind: AgentKindHosted, + Name: "no-rai-agent", + }, + } + buildConfig := &AgentBuildConfig{ImageURL: "img:latest"} + + req, err := CreateHostedAgentAPIRequest(agent, buildConfig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + imgDef := req.Definition.(agent_api.HostedAgentDefinition) + if imgDef.RaiConfig != nil { + t.Errorf("expected RaiConfig to be nil, got %+v", imgDef.RaiConfig) + } +} + +func TestMapRaiConfig(t *testing.T) { + t.Parallel() + if got := mapRaiConfig(nil); got != nil { + t.Errorf("mapRaiConfig(nil) = %+v, want nil", got) + } + if got := mapRaiConfig(&AgentPolicies{}); got != nil { + t.Errorf("mapRaiConfig(empty policies) = %+v, want nil", got) + } + if got := mapRaiConfig(&AgentPolicies{RaiConfig: &RaiConfig{}}); got != nil { + t.Errorf("mapRaiConfig(empty policy name) = %+v, want nil", got) + } + got := mapRaiConfig(&AgentPolicies{RaiConfig: &RaiConfig{RaiPolicyName: "p1"}}) + if got == nil || got.RaiPolicyName != "p1" { + t.Errorf("mapRaiConfig(p1) = %+v, want RaiPolicyName=p1", got) + } +} 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 d5189045574..6758fef85f6 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 @@ -384,6 +384,11 @@ func ValidateAgentDefinition(templateBytes []byte) error { case AgentKindHosted: var agent ContainerAgent if err := yaml.Unmarshal(templateBytes, &agent); err == nil { + if agent.Policies != nil && agent.Policies.RaiConfig != nil && + agent.Policies.RaiConfig.RaiPolicyName == "" { + errors = append(errors, + "policies.rai_config requires a policy name (rai_policy_name)") + } // TODO: Do we need this? // if len(agent.Models) == 0 { // errors = append(errors, "template.models is required and must not be empty") diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go index 46a5d454666..2d507265a2f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go @@ -1099,3 +1099,66 @@ resources: t.Error("Expected a non-empty suggestion") } } + +func TestValidateAgentDefinition_RaiConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yaml string + wantErrSubst string + }{ + { + name: "valid rai_config", + yaml: `kind: hosted +name: rai-agent +policies: + rai_config: + rai_policy_name: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2 +protocols: + - protocol: responses + version: "1.0.0" +`, + }, + { + name: "rai_config missing policy name", + yaml: `kind: hosted +name: rai-agent +policies: + rai_config: {} +protocols: + - protocol: responses + version: "1.0.0" +`, + wantErrSubst: "policies.rai_config requires a policy name", + }, + { + name: "no policies", + yaml: `kind: hosted +name: rai-agent +protocols: + - protocol: responses + version: "1.0.0" +`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := ValidateAgentDefinition([]byte(tc.yaml)) + if tc.wantErrSubst == "" { + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErrSubst) + } + if !strings.Contains(err.Error(), tc.wantErrSubst) { + t.Fatalf("expected error containing %q, got %q", tc.wantErrSubst, err.Error()) + } + }) + } +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml new file mode 100644 index 00000000000..5dd21575ce9 --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml @@ -0,0 +1,11 @@ +template: + kind: hosted + name: hosted-rai-agent + description: A hosted container agent with a content-safety guardrail policy + policies: + rai_config: + # Full ARM resource ID of the RAI policy on the Cognitive Services account. + rai_policy_name: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2 + protocols: + - protocol: responses + version: "1.0.0" diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata_test.go index 569badd1e1b..7ab78d58717 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata_test.go @@ -31,6 +31,12 @@ func TestFixtures_ValidYAML(t *testing.T) { wantKind: AgentKindHosted, wantName: "hosted-test-agent", }, + { + name: "hosted agent with rai policy", + file: filepath.Join("testdata", "hosted-agent-with-rai.yaml"), + wantKind: AgentKindHosted, + wantName: "hosted-rai-agent", + }, } for _, tc := range tests { diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go index 988e3294707..250cf6bac50 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go @@ -191,6 +191,18 @@ type CodeConfiguration struct { DependencyResolution *string `json:"dependencyResolution,omitempty" yaml:"dependency_resolution,omitempty"` } +// RaiConfig represents the Responsible AI (content safety) policy applied to a hosted agent. +// RaiPolicyName is the full ARM resource ID of the RAI policy, for example +// "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/". +type RaiConfig struct { + RaiPolicyName string `json:"raiPolicyName" yaml:"rai_policy_name"` +} + +// AgentPolicies represents safety and governance policies for a hosted agent. +type AgentPolicies struct { + RaiConfig *RaiConfig `json:"raiConfig,omitempty" yaml:"rai_config,omitempty"` +} + // ContainerAgent This represents a container based agent hosted by the provider/publisher. // The intent is to represent a container application that the user wants to run // in a hosted environment that the provider manages. @@ -210,6 +222,7 @@ type ContainerAgent struct { AgentEndpoint *AgentEndpoint `json:"agentEndpoint,omitempty" yaml:"agent_endpoint,omitempty"` AgentCard *AgentCard `json:"agentCard,omitempty" yaml:"agent_card,omitempty"` CodeConfiguration *CodeConfiguration `json:"codeConfiguration,omitempty" yaml:"code_configuration,omitempty"` + Policies *AgentPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` } // AgentManifest The following represents a manifest that can be used to create agents dynamically. From 3b3750295ae4afd09cb9dce7bfb4e695aca3de91 Mon Sep 17 00:00:00 2001 From: Amit Bhave Date: Wed, 3 Jun 2026 08:48:46 +0000 Subject: [PATCH 2/2] Make policies a generic typed list in agent.yaml Replace the nested `policies.rai_config.rai_policy_name` object with a generic `policies` list of typed entries. Each entry has a `type` discriminator; `type: rai_policy` attaches a Responsible AI (content safety) guardrail via `rai_policy_name` (full ARM resource ID of the RAI policy). This keeps the manifest authoring shape extensible to future policy types while the mapping layer continues to flatten a `rai_policy` entry into the data-plane `rai_config.rai_policy_name` that the Foundry hosted-agent API expects (unchanged on the wire). Updates the yaml model (Policy / PolicyType), mapRaiConfig, manifest validation (known type + required rai_policy_name), the test fixture, CHANGELOG, and unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../extensions/azure.ai.agents/CHANGELOG.md | 2 +- .../internal/pkg/agents/agent_yaml/map.go | 15 ++++---- .../pkg/agents/agent_yaml/map_test.go | 17 ++++++---- .../internal/pkg/agents/agent_yaml/parse.go | 20 ++++++++--- .../pkg/agents/agent_yaml/parse_test.go | 34 ++++++++++++++++--- .../testdata/hosted-agent-with-rai.yaml | 2 +- .../internal/pkg/agents/agent_yaml/yaml.go | 26 ++++++++------ 7 files changed, 82 insertions(+), 34 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md index d60f112de30..d07f9cc4f36 100644 --- a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Add support for the `policies.rai_config.rai_policy_name` field in `agent.yaml` to attach a Responsible AI (content safety) guardrail policy to hosted agents. The value is the full ARM resource ID of the RAI policy (for example, `/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/`). `azd deploy` forwards it to the Foundry data plane. +- Add support for a generic `policies` list in `agent.yaml` to attach governance policies to hosted agents. Each entry has a `type` discriminator; `type: rai_policy` attaches a Responsible AI (content safety) guardrail via `rai_policy_name`, the full ARM resource ID of the RAI policy (for example, `/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/`). `azd deploy` forwards it to the Foundry data plane as `rai_config.rai_policy_name`. ## 0.1.37-preview (2026-06-01) diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go index a601323cc33..e9720dabcb5 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go @@ -84,13 +84,16 @@ func constructBuildConfig(options ...AgentBuildOption) *AgentBuildConfig { return config } -// mapRaiConfig flattens the manifest-level policies.rai_config into the data-plane -// rai_config field. It returns nil when no policy name is configured. -func mapRaiConfig(policies *AgentPolicies) *agent_api.RaiConfig { - if policies == nil || policies.RaiConfig == nil || policies.RaiConfig.RaiPolicyName == "" { - return nil +// mapRaiConfig flattens the manifest-level policies list into the data-plane +// rai_config field. It returns the RAI config derived from the first policy of +// type "rai_policy" that has a policy name, or nil when none is configured. +func mapRaiConfig(policies []Policy) *agent_api.RaiConfig { + for _, policy := range policies { + if policy.Type == PolicyTypeRai && policy.RaiPolicyName != "" { + return &agent_api.RaiConfig{RaiPolicyName: policy.RaiPolicyName} + } } - return &agent_api.RaiConfig{RaiPolicyName: policies.RaiConfig.RaiPolicyName} + return nil } // MapEndpointAndCard maps YAML-layer endpoint and card fields to API model types diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go index ae52b21ed21..c07fb7a10cb 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go @@ -1544,8 +1544,8 @@ func TestCreateHostedAgentAPIRequest_WithRaiConfig(t *testing.T) { Kind: AgentKindHosted, Name: "rai-agent", }, - Policies: &AgentPolicies{ - RaiConfig: &RaiConfig{RaiPolicyName: raiPolicyID}, + Policies: []Policy{ + {Type: PolicyTypeRai, RaiPolicyName: raiPolicyID}, }, } buildConfig := &AgentBuildConfig{ImageURL: "img:latest"} @@ -1580,8 +1580,8 @@ func TestCreateAgentAPIRequest_CodeDeploy_WithRaiConfig(t *testing.T) { Runtime: "python_3_12", EntryPoint: "agent.py", }, - Policies: &AgentPolicies{ - RaiConfig: &RaiConfig{RaiPolicyName: raiPolicyID}, + Policies: []Policy{ + {Type: PolicyTypeRai, RaiPolicyName: raiPolicyID}, }, } @@ -1625,13 +1625,16 @@ func TestMapRaiConfig(t *testing.T) { if got := mapRaiConfig(nil); got != nil { t.Errorf("mapRaiConfig(nil) = %+v, want nil", got) } - if got := mapRaiConfig(&AgentPolicies{}); got != nil { + if got := mapRaiConfig([]Policy{}); got != nil { t.Errorf("mapRaiConfig(empty policies) = %+v, want nil", got) } - if got := mapRaiConfig(&AgentPolicies{RaiConfig: &RaiConfig{}}); got != nil { + if got := mapRaiConfig([]Policy{{Type: PolicyTypeRai}}); got != nil { t.Errorf("mapRaiConfig(empty policy name) = %+v, want nil", got) } - got := mapRaiConfig(&AgentPolicies{RaiConfig: &RaiConfig{RaiPolicyName: "p1"}}) + if got := mapRaiConfig([]Policy{{Type: "other", RaiPolicyName: "p1"}}); got != nil { + t.Errorf("mapRaiConfig(non-rai type) = %+v, want nil", got) + } + got := mapRaiConfig([]Policy{{Type: PolicyTypeRai, RaiPolicyName: "p1"}}) if got == nil || got.RaiPolicyName != "p1" { t.Errorf("mapRaiConfig(p1) = %+v, want RaiPolicyName=p1", got) } 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 6758fef85f6..2a9dd971239 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 @@ -384,10 +384,22 @@ func ValidateAgentDefinition(templateBytes []byte) error { case AgentKindHosted: var agent ContainerAgent if err := yaml.Unmarshal(templateBytes, &agent); err == nil { - if agent.Policies != nil && agent.Policies.RaiConfig != nil && - agent.Policies.RaiConfig.RaiPolicyName == "" { - errors = append(errors, - "policies.rai_config requires a policy name (rai_policy_name)") + for i, policy := range agent.Policies { + switch policy.Type { + case PolicyTypeRai: + if policy.RaiPolicyName == "" { + errors = append(errors, fmt.Sprintf( + "policies[%d] of type '%s' requires a policy name (rai_policy_name)", + i, policy.Type)) + } + case "": + errors = append(errors, fmt.Sprintf( + "policies[%d] requires a type", i)) + default: + errors = append(errors, fmt.Sprintf( + "policies[%d] has an unsupported type '%s' (supported: %s)", + i, policy.Type, PolicyTypeRai)) + } } // TODO: Do we need this? // if len(agent.Models) == 0 { diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go index 2d507265a2f..6129008d05b 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go @@ -1109,11 +1109,11 @@ func TestValidateAgentDefinition_RaiConfig(t *testing.T) { wantErrSubst string }{ { - name: "valid rai_config", + name: "valid rai_policy", yaml: `kind: hosted name: rai-agent policies: - rai_config: + - type: rai_policy rai_policy_name: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2 protocols: - protocol: responses @@ -1121,16 +1121,40 @@ protocols: `, }, { - name: "rai_config missing policy name", + name: "rai_policy missing policy name", yaml: `kind: hosted name: rai-agent policies: - rai_config: {} + - type: rai_policy protocols: - protocol: responses version: "1.0.0" `, - wantErrSubst: "policies.rai_config requires a policy name", + wantErrSubst: "policies[0] of type 'rai_policy' requires a policy name", + }, + { + name: "policy missing type", + yaml: `kind: hosted +name: rai-agent +policies: + - rai_policy_name: /subscriptions/x/raiPolicies/p +protocols: + - protocol: responses + version: "1.0.0" +`, + wantErrSubst: "policies[0] requires a type", + }, + { + name: "unsupported policy type", + yaml: `kind: hosted +name: rai-agent +policies: + - type: network_policy +protocols: + - protocol: responses + version: "1.0.0" +`, + wantErrSubst: "policies[0] has an unsupported type 'network_policy'", }, { name: "no policies", diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml index 5dd21575ce9..ccab564a4b9 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-rai.yaml @@ -3,7 +3,7 @@ template: name: hosted-rai-agent description: A hosted container agent with a content-safety guardrail policy policies: - rai_config: + - type: rai_policy # Full ARM resource ID of the RAI policy on the Cognitive Services account. rai_policy_name: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg/providers/Microsoft.CognitiveServices/accounts/my-account/raiPolicies/Microsoft.DefaultV2 protocols: diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go index 250cf6bac50..8038d30ba06 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go @@ -191,16 +191,22 @@ type CodeConfiguration struct { DependencyResolution *string `json:"dependencyResolution,omitempty" yaml:"dependency_resolution,omitempty"` } -// RaiConfig represents the Responsible AI (content safety) policy applied to a hosted agent. -// RaiPolicyName is the full ARM resource ID of the RAI policy, for example -// "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/". -type RaiConfig struct { - RaiPolicyName string `json:"raiPolicyName" yaml:"rai_policy_name"` -} +// PolicyType identifies the kind of governance policy attached to a hosted agent. +type PolicyType string -// AgentPolicies represents safety and governance policies for a hosted agent. -type AgentPolicies struct { - RaiConfig *RaiConfig `json:"raiConfig,omitempty" yaml:"rai_config,omitempty"` +const ( + // PolicyTypeRai is a Responsible AI (content safety) policy. + PolicyTypeRai PolicyType = "rai_policy" +) + +// Policy represents a single safety or governance policy attached to a hosted agent. +// Type discriminates the policy kind; the remaining fields are interpreted based on Type. +// +// For Type "rai_policy", RaiPolicyName is the full ARM resource ID of the RAI policy, for example +// "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//raiPolicies/". +type Policy struct { + Type PolicyType `json:"type" yaml:"type"` + RaiPolicyName string `json:"raiPolicyName,omitempty" yaml:"rai_policy_name,omitempty"` } // ContainerAgent This represents a container based agent hosted by the provider/publisher. @@ -222,7 +228,7 @@ type ContainerAgent struct { AgentEndpoint *AgentEndpoint `json:"agentEndpoint,omitempty" yaml:"agent_endpoint,omitempty"` AgentCard *AgentCard `json:"agentCard,omitempty" yaml:"agent_card,omitempty"` CodeConfiguration *CodeConfiguration `json:"codeConfiguration,omitempty" yaml:"code_configuration,omitempty"` - Policies *AgentPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` + Policies []Policy `json:"policies,omitempty" yaml:"policies,omitempty"` } // AgentManifest The following represents a manifest that can be used to create agents dynamically.