Repository navigation
Add Microsoft Purview DLP real-time protection on agent-hooks - #267
Open
Krishnadheeraj (DheerajPannala) wants to merge 2 commits into
Conversation
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
Krishnadheeraj (DheerajPannala)
requested review from
ajmfehr and
Grant Harris (gwharris7)
and
a balanced review from Copilot
October 9, 2026 12:01
Contributor
There was a problem hiding this comment.
🟡 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.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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 GraphprocessContentAPI asuploadText(content entering the agent) ordownloadText(its reply), and returns aPurviewDlpEvaluationResult.A365PurviewInterceptor(Microsoft.Agents.A365.Tooling.Extensions.AgentHooks,AddA365Purview) gates the user's message atinputand audits the reply atoutput.parallel/strictest: a deny from either wins).A365AgentHooks.CreateProtectionEmittertakes an optionalPurviewDlpOptions.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
inputuploadText: the user's messagepurview:blockoutputdownloadText: the replyenforce, denypurview:blockprocessConversationMetadataentry 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-hookssession.id,sequenceNumber= the agent-hookssequence,isTruncated, UTC timestamps andcontentCategory: ai(v1.0 acceptsagentsandcontentCategory, verified live).protectedAppMetadata.applicationLocationisApplicationId, defaulting to the blueprint id (what DLP policies are scoped to), then the agent identity id. Every call uses one new GUID as both theclient-request-idheader and the entryidentifier, returned asCorrelationId.policyActions[]entry whoserestrictionActionisblockor whoseactionisblockAccess(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-emptyprocessingErrors, non-2xx, timeouts, transport errors, redirects, duplicate properties and bodies of an unexpected shape (including a 200 withoutpolicyActions, 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.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 evidenceurn:a365:purview:<client-request-id>. No verdict: fail open allows with apurview:unverifiedwarning, fail closed denies withruntime_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").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.enforceawaits and maps likeinput.\n, read only up toMaxContentCharacters(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.MaxContentCharactersis cut on a surrogate-safe boundary and sent withisTruncated, and an allow of a cut copy follows the fail mode; lone surrogates become U+FFFD; strict configuration parsing;Errorcarries 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.PurviewDlpTokenResolvers.FromAgenticUser(IAgenticTokenProvider)gets the agentic user's delegated Microsoft Graph token (Content.Process.User) throughGetAgenticUserTokenAsync(tenant, agent identity, agentic user, [scope])and evaluates as/me; the agentic user comes from the incoming activity (GetAgenticUser(), withGetAgenticInstanceId()andGetAgenticTenantId()).FromAccessTokenProvider(getToken, userId)takes a host-supplied Graph token, for example on-behalf-of for the signed-in user (/me) or app-only withContent.Process.Allfor a given user (/users/{id}). OnlyFromAgenticUsercaches: 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 fromFromAccessTokenProviderare never cached, since they may be for a user the agent context does not identify.protectionScopes/computeis not used.CreateProtectionEmitter(interceptorTimeout, defender, purview)gains the optionalpurviewparameter (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
ENABLE_A365_PURVIEW_DLPtrue/false(1/0,yes/no,on/off); unset is off; anything else fails at startupA365_PURVIEW_DLP_GRAPH_BASE_URLhttps://graph.microsoft.com/v1.0; absolute HTTPS onlyA365_PURVIEW_DLP_AUTHENTICATION_SCOPEhttps://graph.microsoft.com/.defaultA365_PURVIEW_DLP_FAIL_MODEopen(default) orclosedA365_PURVIEW_DLP_TIMEOUT_MILLISECONDSA365_PURVIEW_DLP_MAX_CONTENT_CHARACTERSA365_PURVIEW_DLP_RESPONSE_MODEaudit(default) orenforceTenant prerequisites (documented in the AgentHooks README):
uploadTextwith Block.Content.Process.Userappended to the blueprint's tenant-wide (AllPrincipals) delegated Microsoft Graph grant, never replacing its existing scopes; the blueprint'sallAllowedGraph inheritable permissions (configured bya365 setup) pass it to agentic users.Testing
PurviewDlpClientTestsandPurviewDlpTokenResolversTests(request shape, sequencing, verdict parsing includingblockAccess, processing errors, shape checks, truncation, deadline, redirects, HTTPS rechecks, error hygiene, options parsing, resolvers and the agentic user token cache) against a mocked Graph;A365PurviewInterceptorTestsunder 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) anddotnet testfor the whole solution pass.Limitations and follow-ups
/users/{id}withContent.Process.All) is supported throughFromAccessTokenProviderbut not validated end to end.protectionScopes/compute(and caching its result) is not used: every evaluation callsprocessContent.downloadTextfor custom AI apps, so replies are audited by default;enforceis available for when that changes.pre_tool_call/post_tool_call) is not sent to Purview yet.Credit
Ports Dominik Bezic's Purview POC (
users/dominikbezic/purview-pocin microsoft/Agent365-nodejs).