Skip to content

feat(azure.ai.agents): add invocationsModeration to RAI policies - #9596

Open
Amit Bhave (amitbhave10) wants to merge 5 commits into
Azure:mainfrom
amitbhave10:amitbhave/agents-invocations-moderation
Open

feat(azure.ai.agents): add invocationsModeration to RAI policies#9596
Amit Bhave (amitbhave10) wants to merge 5 commits into
Azure:mainfrom
amitbhave10:amitbhave/agents-invocations-moderation

Conversation

@amitbhave10

@amitbhave10 Amit Bhave (amitbhave10) commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #9597

Adds invocationsModeration to rai_policy policies in the azure.ai.agents extension, 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 an invocations_moderation block to close that gap; this PR makes it expressible from azure.yaml / agent.yaml.

Usage

services:
  my-agent:
    host: azure.ai.agent
    kind: hosted
    protocols:
      - protocol: invocations
        version: "1.0.0"
    policies:
      - type: rai_policy
        raiPolicyName: /subscriptions/.../raiPolicies/MyPolicy
        invocationsModeration:
          responseMode: both
          inputPaths: ["$.input"]
          outputPaths: ["$.output"]
          streamSelectors:
            - eventType: response.output_text.delta
              textField: $.delta

which is sent as:

"rai_config": {
  "rai_policy_name": "...",
  "invocations_moderation": {
    "response_mode": "both",
    "input_paths": ["$.input"],
    "output_paths": ["$.output"],
    "stream_selectors": [{ "event_type": "...", "text_field": "$.delta" }]
  }
}

responseMode declares 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 response Content-Type. This is called out in the README so both is not mislabelled.

Notes

  • Follows the extension's dual-tag conventionjson:"camelCase" for the unified azure.yaml surface, yaml:"snake_case" for the deprecated on-disk agent.yaml. Enum values (non_streaming, json, ...) stay snake_case in both, since they are wire values rather than keys.
  • Validated locally, offline, mirroring the service rules so users see the same message from azd that they would get back from the API. JSONPath syntax is deliberately not re-implemented — malformed paths still surface as the service's invalid_payload.
  • Fails fast on a dead config: declaring invocationsModeration on an agent that does not expose the invocations protocol is an error rather than a silent no-op.
  • JSON schema encodes the conditional-required rules (if/then), so editors agree with azd instead of green-lighting an incomplete block. This was mandatory, not cosmetic: the Policy definition is additionalProperties: false, so shipping the Go change alone would red-squiggle valid config.
  • Backward compatible and purely additive. An agent without the block serializes byte-identically to before — asserted by a test.

Verification

  • go build ./..., go vet, gofmt clean; full suite green (21/21 packages).
  • New tests cover the mapper, the validation matrix, and — importantly — the inline azure.yaml camelCase round trip through structpb, not just the snake_case agent.yaml surface.
  • The 18 schema conditionals were checked against a real draft-07 validator, including the "omitted content type defaults to json" and mixed text/json cases.
  • Live round-trip against a running Agents service: an agent created through the real agent_yamlmap.go pipeline came back with invocations_moderation echoed 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.

AmitB (Amitbhave) and others added 2 commits August 15, 2026 05:15
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

Copy link
Copy Markdown
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +405 to +406
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" }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +178 to +179
| `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. |
Comment on lines +3 to +7
## 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.
Copilot AI review requested due to automatic review settings August 15, 2026 05:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 invocationsModeration on 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.md updates 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.validate cases for omitted/default content types and each missing inputPaths, outputPaths, and streamSelectors branch. (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
Copilot AI review requested due to automatic review settings August 15, 2026 12:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 invocationsModeration key used by inline azure.yaml. The non-hosted inline path passes the raw property map here (internal/project/agent_definition.go:748-751), but yaml.Unmarshal binds Policy using only its yaml:"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 valid invocationsModeration block 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/then rules have no automated schema-level coverage: the README example exercises only one valid both/JSON configuration, while TestDocSchemaValidatesConstraints contains no invalid moderation cases. Add cases that validate omitted content-type defaults and reject missing inputPaths, outputPaths, and streamSelectors, so editor validation cannot drift from the Go validator.
      "allOf": [
        {
          "if": { "$ref": "#/definitions/InvocationsInputIsJson" },
          "then": { "required": ["inputPaths"] }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

azure.ai.agents: cannot configure invocations_moderation, leaving RAI policies inert on the invocations protocol

3 participants