feat(azure.ai.agents): add invocationsModeration to RAI policies - #9596
feat(azure.ai.agents): add invocationsModeration to RAI policies#9596Amit Bhave (amitbhave10) wants to merge 5 commits into
Conversation
Hosted agents on the invocations protocol could attach a RAI policy through azd but had no way to tell the content-safety proxy where the moderatable text lives in their request and response bodies. Without that the proxy has nothing to submit to the policy, so the guardrail is attached but inert. Add an optional `invocationsModeration` block to `rai_policy` policies covering responseMode, input/output content types, input/output JSONPaths, and SSE stream selectors, mapped onto `rai_config.invocations_moderation` on the wire. Validation mirrors the service's own create-time rules so misconfiguration is caught locally instead of surfacing as an opaque invalid_payload response, and declaring the block on an agent that does not expose the `invocations` protocol is rejected rather than silently deploying a policy that never runs. Agents that omit the block serialize exactly as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b
- Report the invocations-protocol error alone instead of cascading field-level errors that are irrelevant on a non-invocations agent. - Encode the conditional-required rules in the JSON schema via if/then, so editors match azd's validation instead of falsely reporting an incomplete block as valid. Adds minItems and a non-blank eventType pattern. - Drop omitempty from the required response_mode / eventType fields so the wire and YAML models state the contract accurately. - Cover the inline azure.yaml (camelCase) path end to end, which the previous tests reached only through the snake_case agent.yaml surface. - Add the missing validation cases: `both` missing either output array, mixed text/json directions, explicit json, whitespace-only eventType, per-policy error indexing, and non-cascade assertions. - Document that inputPaths is required when inputContentType is omitted, and note the new protocol validation error in the changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b
|
Azure Pipelines: Successfully started running 1 pipeline(s). 18 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
"moderatable" is not in the cspell dictionary; use "the text to moderate" instead of extending the shared word list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b
There was a problem hiding this comment.
Pull request overview
Adds invocations-protocol moderation configuration to Azure AI Agents RAI policies.
Changes:
- Adds API/YAML models and mapping.
- Adds validation, schema support, and tests.
- Documents configuration and usage.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
schemas/azure.ai.agent.json |
Defines moderation schema. |
README.md |
Documents configuration. |
internal/project/agent_policies_test.go |
Tests inline round-tripping. |
internal/pkg/agents/agent_yaml/yaml.go |
Adds YAML models. |
internal/pkg/agents/agent_yaml/testdata/hosted-agent-with-invocations-moderation.yaml |
Adds fixture. |
internal/pkg/agents/agent_yaml/testdata_test.go |
Registers fixture. |
internal/pkg/agents/agent_yaml/parse.go |
Validates moderation settings. |
internal/pkg/agents/agent_yaml/parse_test.go |
Tests validation rules. |
internal/pkg/agents/agent_yaml/map.go |
Maps settings to API models. |
internal/pkg/agents/agent_yaml/map_test.go |
Tests mapping and serialization. |
internal/pkg/agents/agent_api/models.go |
Adds wire-format models. |
CHANGELOG.md |
Adds an unreleased entry. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| errors = append(errors, | ||
| validateInvocationsModeration(i, policy.InvocationsModeration, agent.Protocols)...) |
| "type": { "type": "string", "description": "Policy type (e.g., 'rai_policy')." }, | ||
| "raiPolicyName": { "type": "string", "description": "ARM resource ID of the RAI policy (for type 'rai_policy')." } | ||
| "raiPolicyName": { "type": "string", "description": "ARM resource ID of the RAI policy (for type 'rai_policy')." }, | ||
| "invocationsModeration": { "$ref": "#/definitions/InvocationsModeration" } |
There was a problem hiding this comment.
Agreed that the gap is real, and thanks for pinning down the exact conditional.
Deferring this one to a separate work item rather than folding it in here. The same class of gap exists one level up and is worth fixing together: Policy.type is a bare {"type": "string"} with no enum, so the schema also accepts policy types that Go rejects in its default: branch, and it does not require raiPolicyName for rai_policy. Fixing the protocol conditional alone would leave the sibling holes open, so a single follow-up that hardens the whole Policy definition (type enum + if/then per type + the protocol/kind conditional you describe) is easier to review and validate as one piece.
Worth noting the scope is editor-only: azd already rejects every configuration listed here at validation time, including the non-hosted kinds, which this push now covers in Go with regression tests. So nothing invalid deploys today — the cost is that editors stay green on config azd will refuse.
| | `outputPaths` | when `responseMode` includes non-streaming and `outputContentType` is `json` | JSONPath expressions selecting the buffered response text. | | ||
| | `streamSelectors` | when `responseMode` includes streaming and `outputContentType` is `json` | `eventType` (required) and `textField` per server-sent event frame. | |
| ## 1.0.0-beta.11 (Unreleased) | ||
|
|
||
| ### Features Added | ||
|
|
||
| - [[#9596]](https://github.com/Azure/azure-dev/pull/9596) Add `invocationsModeration` to `rai_policy` policies so hosted agents on the `invocations` protocol can tell the content-safety proxy where the moderatable text lives in their request and response bodies. Supports buffered and server-sent-event responses via `responseMode`, `inputPaths`, `outputPaths`, and `streamSelectors`, and is validated locally before deploy. Declaring `invocationsModeration` on an agent that does not expose the `invocations` protocol is now a validation error, since the block would otherwise be silently ignored. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:98
- This maps moderation only from the first named RAI policy. Validation currently accepts every RAI policy independently, so two valid RAI entries with
invocationsModerationon the second pass validation but serialize the first policy with no moderation, recreating the inert guardrail this change is intended to prevent. Reject duplicate RAI policies or reject moderation on any policy that will not be selected. (azd-code-reviewer)
return &agent_api.RaiConfig{
RaiPolicyName: policy.RaiPolicyName,
InvocationsModeration: mapInvocationsModeration(policy.InvocationsModeration),
}
cli/azd/extensions/azure.ai.agents/CHANGELOG.md:8
- Remove this release entry from the feature PR. The extension's release instructions reserve
CHANGELOG.mdupdates for the dedicated version-bump PR (cli/azd/extensions/azure.ai.agents/AGENTS.md:156-172), which avoids release-section conflicts and keeps release preparation atomic.
## 1.0.0-beta.11 (Unreleased)
### Features Added
- [[#9596]](https://github.com/Azure/azure-dev/pull/9596) Add `invocationsModeration` to `rai_policy` policies so hosted agents on the `invocations` protocol can tell the content-safety proxy where the text to moderate lives in their request and response bodies. Supports buffered and server-sent-event responses via `responseMode`, `inputPaths`, `outputPaths`, and `streamSelectors`, and is validated locally before deploy. Declaring `invocationsModeration` on an agent that does not expose the `invocations` protocol is now a validation error, since the block would otherwise be silently ignored.
cli/azd/extensions/azure.ai.agents/schemas/azure.ai.agent.json:216
- The new conditional requirements are not exercised by the schema tests: the README contributes only one fully valid example, while the Go validator tests cannot catch drift in this JSON Schema. Add table-driven
schema.validatecases for omitted/default content types and each missinginputPaths,outputPaths, andstreamSelectorsbranch. (azd-code-reviewer)
"allOf": [
{
"if": { "$ref": "#/definitions/InvocationsInputIsJson" },
"then": { "required": ["inputPaths"] }
- Reject invocationsModeration on non-hosted agent kinds. Only hosted agents carry policies to the service, so a block on a prompt-voice or workflow definition parsed cleanly and was dropped silently -- even with an invalid response_mode -- which contradicted the documented fail-fast guarantee. Validated from a minimal policies envelope, since the other kinds have no policies field of their own. - Drop the CHANGELOG entry. Per AGENTS.md "Release preparation", release sections are added by the dedicated version-bump PR that touches only version.txt, extension.yaml and CHANGELOG.md; feature PRs leave it alone to avoid merge-conflict churn. - State the json-or-omitted defaulting in the outputPaths and streamSelectors rows, matching the inputPaths row, and note the hosted requirement alongside the protocol requirement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go:503
- [azd-code-reviewer] This check misses the camelCase
invocationsModerationkey used by inlineazure.yaml. The non-hosted inline path passes the raw property map here (internal/project/agent_definition.go:748-751), butyaml.UnmarshalbindsPolicyusing only itsyaml:"invocations_moderation"tag. A prompt-voice/workflow service can therefore declare the block, pass validation, and silently drop it. Recognize both key shapes here (or validate the already JSON-decoded inline policies), and add an inline non-hosted regression test.
if policy.InvocationsModeration != nil {
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:98
- [azd-code-reviewer] Only the first named RAI policy reaches
rai_config, so a validinvocationsModerationblock on a later RAI policy is silently discarded. Validation currently accepts multiple RAI policies and validates every block, making this easy to configure successfully while leaving moderation inert. Reject duplicate RAI policies or otherwise define and enforce which single policy owns the moderation block before mapping.
if policy.Type == PolicyTypeRai && policy.RaiPolicyName != "" {
return &agent_api.RaiConfig{
RaiPolicyName: policy.RaiPolicyName,
InvocationsModeration: mapInvocationsModeration(policy.InvocationsModeration),
}
cli/azd/extensions/azure.ai.agents/schemas/azure.ai.agent.json:216
- [azd-code-reviewer] The new
if/thenrules have no automated schema-level coverage: the README example exercises only one validboth/JSON configuration, whileTestDocSchemaValidatesConstraintscontains no invalid moderation cases. Add cases that validate omitted content-type defaults and reject missinginputPaths,outputPaths, andstreamSelectors, so editor validation cannot drift from the Go validator.
"allOf": [
{
"if": { "$ref": "#/definitions/InvocationsInputIsJson" },
"then": { "required": ["inputPaths"] }
Summary
Fixes #9597
Adds
invocationsModerationtorai_policypolicies in theazure.ai.agentsextension, so hosted agents on the invocations protocol can tell the Foundry content-safety proxy where the moderatable text lives in their request and response bodies.Today the extension can attach an RAI policy, but only as a bare
rai_config.rai_policy_name. On the invocations path the proxy has no way to know which part of an arbitrary JSON body is user text, so the attached policy has nothing to moderate and the guardrail is effectively inert. The service accepts aninvocations_moderationblock to close that gap; this PR makes it expressible fromazure.yaml/agent.yaml.Usage
which is sent as:
responseModedeclares the shapes the container can produce. It is a capability declaration, not an input/output switch: the proxy runs exactly one output gate per response, chosen from the actual responseContent-Type. This is called out in the README sobothis not mislabelled.Notes
json:"camelCase"for the unifiedazure.yamlsurface,yaml:"snake_case"for the deprecated on-diskagent.yaml. Enum values (non_streaming,json, ...) stay snake_case in both, since they are wire values rather than keys.azdthat they would get back from the API. JSONPath syntax is deliberately not re-implemented — malformed paths still surface as the service'sinvalid_payload.invocationsModerationon an agent that does not expose theinvocationsprotocol is an error rather than a silent no-op.if/then), so editors agree withazdinstead of green-lighting an incomplete block. This was mandatory, not cosmetic: thePolicydefinition isadditionalProperties: false, so shipping the Go change alone would red-squiggle valid config.Verification
go build ./...,go vet,gofmtclean; full suite green (21/21 packages).azure.yamlcamelCase round trip throughstructpb, not just the snake_caseagent.yamlsurface.agent_yaml→map.gopipeline came back withinvocations_moderationechoed verbatim, and 7/7 deliberately-invalid blocks were rejected by the service with messages matching this PR's local validation one-for-one.Size
~450 net new lines, but a single concern (one feature, no refactor mixed in) across the extension's standard pipeline: API model → YAML model → mapper → validation → schema → docs → tests. Splitting it would produce non-functional intermediate commits.