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
4 changes: 4 additions & 0 deletions cli/azd/extensions/azure.ai.agents/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release History

## Unreleased

- 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/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/raiPolicies/<policyName>`). `azd deploy` forwards it to the Foundry data plane as `rai_config.rai_policy_name`.

## 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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ func constructBuildConfig(options ...AgentBuildOption) *AgentBuildConfig {
return config
}

// 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 nil
}

// 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.
Expand Down Expand Up @@ -396,7 +408,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,
Expand All @@ -420,7 +433,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1534,3 +1534,108 @@ 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: []Policy{
{Type: PolicyTypeRai, 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: []Policy{
{Type: PolicyTypeRai, 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([]Policy{}); got != nil {
t.Errorf("mapRaiConfig(empty policies) = %+v, want nil", got)
}
if got := mapRaiConfig([]Policy{{Type: PolicyTypeRai}}); got != nil {
t.Errorf("mapRaiConfig(empty policy name) = %+v, want nil", got)
}
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,23 @@ func ValidateAgentDefinition(templateBytes []byte) error {
case AgentKindHosted:
var agent ContainerAgent
if err := yaml.Unmarshal(templateBytes, &agent); err == nil {
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 {
// errors = append(errors, "template.models is required and must not be empty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1099,3 +1099,90 @@ 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_policy",
yaml: `kind: hosted
name: rai-agent
policies:
- 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
version: "1.0.0"
`,
},
{
name: "rai_policy missing policy name",
yaml: `kind: hosted
name: rai-agent
policies:
- type: rai_policy
protocols:
- protocol: responses
version: "1.0.0"
`,
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",
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())
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
template:
kind: hosted
name: hosted-rai-agent
description: A hosted container agent with a content-safety guardrail policy
policies:
- 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:
- protocol: responses
version: "1.0.0"
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,24 @@ type CodeConfiguration struct {
DependencyResolution *string `json:"dependencyResolution,omitempty" yaml:"dependency_resolution,omitempty"`
}

// PolicyType identifies the kind of governance policy attached to a hosted agent.
type PolicyType string

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/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/raiPolicies/<policyName>".
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.
// The intent is to represent a container application that the user wants to run
// in a hosted environment that the provider manages.
Expand All @@ -210,6 +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 []Policy `json:"policies,omitempty" yaml:"policies,omitempty"`
}

// AgentManifest The following represents a manifest that can be used to create agents dynamically.
Expand Down
Loading