Skip to content

Add Microsoft Purview DLP real-time protection on agent-hooks - #267

Open
Krishnadheeraj (DheerajPannala) wants to merge 2 commits into
kpannala-microsoft-defender-rtp-agent-hooksfrom
kpannala-microsoft-purview-dlp-agent-hooks
Open

Krishnadheeraj (DheerajPannala) wants to merge 2 commits into
kpannala-microsoft-defender-rtp-agent-hooksfrom
kpannala-microsoft-purview-dlp-agent-hooks

Conversation

@DheerajPannala

@DheerajPannala Krishnadheeraj (DheerajPannala) commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Microsoft Purview data loss prevention (DLP) and audit to the Agent 365 .NET SDK on the agent-hooks control contract, next to the Microsoft Defender for AI interceptor:

  • PurviewDlpClient (Microsoft.Agents.A365.Tooling, Protection/Purview/, no agent-hooks dependency) evaluates a text with the Microsoft Graph processContent API as uploadText (content entering the agent) or downloadText (its reply), and returns a PurviewDlpEvaluationResult.
  • A365PurviewInterceptor (Microsoft.Agents.A365.Tooling.Extensions.AgentHooks, AddA365Purview) gates the user's message at input and audits the reply at output.
  • Defender and Purview compose on one emitter (parallel/strictest: a deny from either wins). A365AgentHooks.CreateProtectionEmitter takes an optional PurviewDlpOptions.

Stacked on #266 (Defender RTP on agent-hooks); retarget to main after #266 merges. Same design as the Node SDK's microsoft/Agent365-nodejs#311 and the Python SDK's microsoft/Agent365-python#280.

How it works

agent-hooks point Purview activity Default On a block
input uploadText: the user's message waits for the verdict deny purview:block
output downloadText: the reply audit: allowed at once, evaluated in the background with enforce, deny purview:block
other points none allowed without a call
  • Request. One processConversationMetadata entry per call: the text, agents (blueprint id when known, agent identity id, name), a non-empty entry name (<agent name> <activity>, the agent name defaulting to the agent identity id; an empty name is a permanent BadRequest that Graph reports as HTTP 200 with no actions), correlationId = the agent-hooks session.id, sequenceNumber = the agent-hooks sequence, isTruncated, UTC timestamps and contentCategory: ai (v1.0 accepts agents and contentCategory, verified live). protectedAppMetadata.applicationLocation is ApplicationId, defaulting to the blueprint id (what DLP policies are scoped to), then the agent identity id. Every call uses one new GUID as both the client-request-id header and the entry identifier, returned as CorrelationId.
  • Verdict. Any policyActions[] entry whose restrictionAction is block or whose action is blockAccess (case-insensitive, as in agent-framework's Purview processor) blocks; it stands even when processing errors, truncation or actions of another shape come with it. Other actions allow and are counted (Decision.ActionCount). Non-empty processingErrors, non-2xx, timeouts, transport errors, redirects, duplicate properties and bodies of an unexpected shape (including a 200 without policyActions, which Graph always returns) are not a verdict and follow the fail mode; 202/204 is an evaluated allow. Every node of the response is shape-checked.
  • Mapping. A block denies with reason purview:block, the message "The request was blocked by a Microsoft Purview data loss prevention policy." ("The response was blocked ..." for an enforced reply) and evidence urn:a365:purview:<client-request-id>. No verdict: fail open allows with a purview:unverified warning, fail closed denies with runtime_error:purview_unverified (mirrors Defender). A null factory result, or a factory or token resolver exception, follows the fail mode ("no agent identity was resolved").
  • Replies. Purview DLP cannot block replies of custom AI apps, so by default (audit) the interceptor allows the reply immediately and evaluates it in the background. The background evaluation is bounded by the client's own timeout, never uses the emitter's token, contains its exceptions and reports through the evaluation callback. enforce awaits and maps like input.
  • Structured content. Its string and number values are sent joined by \n, read only up to MaxContentCharacters (in values and containers as well as characters), so a large payload is never flattened past the limit; text cut there is sent as a truncated copy, and if reading stops at the limit while the text is still blank the fail mode applies instead of skipping Purview.
  • Hardening (as in Add Microsoft Defender for AI real-time protection on agent-hooks #266). One shared deadline covers token acquisition and the POST, and a resolver that ignores cancellation cannot hold it; HTTPS-only Graph base URL (rechecked at send time); redirects never followed; text over MaxContentCharacters is cut on a surrogate-safe boundary and sent with isTruncated, and an allow of a cut copy follows the fail mode; lone surrogates become U+FFFD; strict configuration parsing; Error carries only a status, identifier-like processing error codes or an exception type, never a response body or a token. The callback runs on the thread pool, off the timed path, and its exceptions are contained.
  • Authentication. PurviewDlpTokenResolvers.FromAgenticUser(IAgenticTokenProvider) gets the agentic user's delegated Microsoft Graph token (Content.Process.User) through GetAgenticUserTokenAsync(tenant, agent identity, agentic user, [scope]) and evaluates as /me; the agentic user comes from the incoming activity (GetAgenticUser(), with GetAgenticInstanceId() and GetAgenticTenantId()). FromAccessTokenProvider(getToken, userId) takes a host-supplied Graph token, for example on-behalf-of for the signed-in user (/me) or app-only with Content.Process.All for a given user (/users/{id}). Only FromAgenticUser caches: per tenant, agent identity, agentic user and scope until expiry, refreshed in the background within five minutes of it while the cached token stays in use, with one shared acquisition per key and failures never cached. Tokens from FromAccessTokenProvider are never cached, since they may be for a user the agent context does not identify. protectionScopes/compute is not used.
  • Emitter. CreateProtectionEmitter(interceptorTimeout, defender, purview) gains the optional purview parameter (source compatible); the default interceptor timeout is the longer of Defender's timeout and, only when Purview is enabled, Purview's, plus 2 s, so each client's fail mode applies before the emitter's timeout and Defender-only emitters are unchanged.

Configuration and setup

Variable Meaning
ENABLE_A365_PURVIEW_DLP true/false (1/0, yes/no, on/off); unset is off; anything else fails at startup
A365_PURVIEW_DLP_GRAPH_BASE_URL default https://graph.microsoft.com/v1.0; absolute HTTPS only
A365_PURVIEW_DLP_AUTHENTICATION_SCOPE default https://graph.microsoft.com/.default
A365_PURVIEW_DLP_FAIL_MODE open (default) or closed
A365_PURVIEW_DLP_TIMEOUT_MILLISECONDS default 10000, shared by token acquisition and the request
A365_PURVIEW_DLP_MAX_CONTENT_CHARACTERS default 100000
A365_PURVIEW_DLP_RESPONSE_MODE audit (default) or enforce

Tenant prerequisites (documented in the AgentHooks README):

  1. Purview licensing (Microsoft 365 E5 / E5 Compliance or equivalent), pay-as-you-go billing, and DSPM for AI onboarded.
  2. A DLP policy on the AI app location ("Managed cloud apps", Applications workload) scoped to the agent blueprint's application id, with a rule that restricts uploadText with Block.
  3. Content.Process.User appended to the blueprint's tenant-wide (AllPrincipals) delegated Microsoft Graph grant, never replacing its existing scopes; the blueprint's allAllowed Graph inheritable permissions (configured by a365 setup) pass it to agentic users.

Testing

  • Unit: 162 new test cases. PurviewDlpClientTests and PurviewDlpTokenResolversTests (request shape, sequencing, verdict parsing including blockAccess, processing errors, shape checks, truncation, deadline, redirects, HTTPS rechecks, error hygiene, options parsing, resolvers and the agentic user token cache) against a mocked Graph; A365PurviewInterceptorTests under the real agent-hooks emitter (native core), including background audit, enforce mode, fail modes, bounded reading of structured content, the callback contract, emitter timeout sizing, and Defender + Purview composed on one emitter.
  • dotnet build src/Microsoft.Agents.A365.Sdk.sln (Release, 0 warnings) and dotnet test for the whole solution pass.
  • Verified live: clean prompts allowed; a prompt with a credit card number blocked at input by a Purview DLP policy; replies audited. With Defender on the same emitter, a clean turn was allowed by both and a known-malicious test URL in a tool argument was denied by Defender.

Limitations and follow-ups

  • The app-only path (/users/{id} with Content.Process.All) is supported through FromAccessTokenProvider but not validated end to end.
  • protectionScopes/compute (and caching its result) is not used: every evaluation calls processContent.
  • Purview DLP cannot block downloadText for custom AI apps, so replies are audited by default; enforce is available for when that changes.
  • Tool-call content (pre_tool_call / post_tool_call) is not sent to Purview yet.

Credit

Ports Dominik Bezic's Purview POC (users/dominikbezic/purview-poc in microsoft/Agent365-nodejs).

PurviewDlpClient sends a text to the Microsoft Graph processContent API as
uploadText or downloadText: one conversation entry with the agent, the
session and its sequence, scoped to the agent's application, and a new
client-request-id per call. A policy action restricted with block blocks;
processing errors, failures and responses of another shape follow the fail
mode, and an allow of truncated text is not authoritative. One deadline
covers token acquisition and the request; HTTPS only, no redirects.

PurviewDlpTokenResolvers.FromAgenticUser evaluates as the agentic user (/me)
through IAgenticTokenProvider.GetAgenticUserTokenAsync; FromAccessTokenProvider
takes a host-supplied Graph token.

A365PurviewInterceptor gates the user's message at input and audits the reply
at output in the background (enforce on request). CreateProtectionEmitter
takes optional Purview options so Defender and Purview compose on one emitter.

Co-authored-by: Dominik Bezic <dominikbezic@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 94b4d34e-4cd4-4156-afd9-b486121d2b41

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.

🟡 Changes recommended

Structured-content flattening is unbounded before truncation, creating a memory and latency risk on the interception path.

2 open findings
What changed in this PR

Adds Microsoft Purview DLP evaluation and auditing alongside Defender protection for agent-hooks.

Changes:

  • Adds the Purview Graph client, configuration, token resolvers, and result models.
  • Adds prompt enforcement, reply auditing, and Defender/Purview emitter composition.
  • Adds comprehensive tests and documentation.
File Description
src/​Tooling/​Extensions/​AgentHooks/​README.md Documents Purview setup and usage.
src/​Tooling/​Extensions/​AgentHooks/​Microsoft.Agents.A365.Tooling.Extensions.AgentHooks.csproj Updates package metadata.
src/​Tooling/​Extensions/​AgentHooks/​A365PurviewInterceptor.cs Implements agent-hooks Purview interception.
src/​Tooling/​Extensions/​AgentHooks/​A365DefenderInterceptor.cs Adds Purview registration and timeout composition.
src/​Tooling/​Core/​Protection/​Purview/​PurviewDlpTokenResolvers.cs Provides Graph token resolvers.
src/​Tooling/​Core/​Protection/​Purview/​PurviewDlpTokenResolver.cs Defines token and agent context contracts.
src/​Tooling/​Core/​Protection/​Purview/​PurviewDlpOptions.cs Defines validated Purview configuration.
src/​Tooling/​Core/​Protection/​Purview/​PurviewDlpEvaluationResult.cs Models evaluation results.
src/​Tooling/​Core/​Protection/​Purview/​PurviewDlpClient.cs Implements Graph processContent evaluation.
src/​Tooling/​Core/​docs/​design.md Documents the core Purview design.
src/​Tests/​Microsoft.Agents.A365.Tooling.Extensions.AgentHooks.Tests/​A365PurviewInterceptorTests.cs Tests interceptor behavior and composition.
src/​Tests/​Microsoft.Agents.A365.Tooling.Core.Tests/​Protection/​PurviewDlpTokenResolversTests.cs Tests token resolution.
src/​Tests/​Microsoft.Agents.A365.Tooling.Core.Tests/​Protection/​PurviewDlpClientTests.cs Tests requests, verdicts, limits, and failures.
docs/​design.md Updates repository architecture documentation.
CHANGELOG.md Records the new Purview capabilities.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

- Block on restrictionAction "block" or action "blockAccess" (any case); a
  block stands next to processing errors or malformed actions.
- One GUID per call: the client-request-id and the content entry identifier.
- The agent name defaults to the agent identity id (then the application).
- Read structured content only up to MaxContentCharacters, in values as well
  as characters; text cut there is sent as a truncated copy, and content cut
  before any text was read follows the fail mode instead of being skipped.
- CreateProtectionEmitter counts Purview's timeout only when Purview is
  enabled, so Defender-only emitters are unchanged.
- FromAgenticUser caches the agentic user's token per tenant, agent, user and
  scope until it expires (background refresh, shared acquisition, failures
  never cached); host-supplied tokens are never cached.

Co-authored-by: Dominik Bezic <dominikbezic@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 94b4d34e-4cd4-4156-afd9-b486121d2b41

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.

🟡 Changes recommended

A 202 response is incorrectly treated as an authoritative allow before DLP processing completes.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

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.

2 participants