SuperAPI AI Directory

purview-dlp-integration

Agents & models

View repository

Updated 10-16 09:32

About this skill

Microsoft Purview DLP Integration for Agent 365

Trigger phrases — any of these will activate this skill automatically:

  • "add Purview DLP to my agent"
  • "add data loss prevention to my A365 agent"
  • "block credit cards / SSNs / PII before my agent's LLM sees them"
  • "enforce compliance / audit on agent prompts and responses"
  • "wire a DLP gate into my agent code"
  • "call the Graph processContent API from my agent"
  • "add Purview blocking to my Node.js / Python / .NET agent"

Add a Purview data-loss-prevention gate before an existing A365 agent's LLM call: each prompt is evaluated via the Graph processContent API and blocked when an input-blocking policy matches. Optional response checks provide audit, not response blocking, for the supplied Applications policy. processContent sends the checked text to Microsoft Purview for evaluation and audit. Designed for minimal, reversible changes to the customer's agent.

When to use

  • "Add Purview DLP / data loss prevention to my agent."
  • "Block credit cards / SSNs / PII before my agent's LLM sees them."
  • "Enforce compliance / audit on agent prompts and responses."
  • Any A365 agent — Node.js, Python, or .NET — that needs inline DLP.

When NOT to use

  • Adding OpenTelemetry tracing / MAC Activity telemetry → use instrument-observability.
  • Diagnosing why telemetry isn't reaching MAC Activity / Defender → use a365-code-validator.
  • Provisioning the blueprint / Entra permissions from scratch → use a365-setup / make-a365-agent first, then return here.

How it works

user turn ─► [INPUT gate] processContent(uploadText, /me)  ─► block? ─► reply "blocked", STOP (LLM never called)
                                                            └─ allow ─► LLM ─► optional output audit ─► reply
  • Token: the agent's agentic delegated Graph token, evaluated as /me (the agent identity) — acquired via the agent's own auth handler: Node.js GetAgenticUserToken, Python authorization.exchange_token(…), .NET UserAuthorization.GetTurnTokenAsync(…). A365 blueprint apps cannot use app-only client-credentials here — Graph strips data-plane roles from that token.
  • Fail-closed by default: any Purview error/timeout blocks the turn.
  • Content limit: delegated guards reject messages over 100,000 characters before sending them to Graph, even in fail-open mode. They never approve a full message based on a prefix.
  • Output: PURVIEW_CHECK_OUTPUT=true submits responses for audit. The supplied workload does not support DownloadText blocking; do not promise that it filters sensitive responses. With fail-closed mode, a failed output audit still withholds the reply.
  • Policy: a dedicated Purview DLP policy scoped to the agent's Entra app id (portal: "Managed cloud apps" / Applications workload) with a RestrictAccess=Block rule.

S2S / app-only agents (autonomous, no AgentApplication)

For an agent that authenticates service-to-service (no signed-in user, no @microsoft/agents-hosting AgentApplication — e.g. an Express / worker-loop agent using the A365 FMI client-credentials chain), use the S2S guard assets/purview-s2s.ts instead of purview.ts.

Verified 2026-08-26: app-only DLP is supported — the "blueprint app-only tokens get stripped" rule is true for the blueprint app but not for the agent identity:

Token source roles in the Graph token
Blueprint app-only (client-credentials) AgentIdentity.CreateAsManager — Content.Process. STRIPPED*
Agent identity via the FMI 3-hop chain Content.Process.All RETAINED ✅

So the S2S guard (1) mints the agent identity's Graph token through the FMI chain (Blueprint → fmi_path → Agent Identity → Graph), (2) calls the app-only endpoint POST /beta/users/{sponsorUserId}/dataSecurityAndGovernance/processContent (app-only can't use /me), and (3) passes an agents:[{"@odata.type":"aiAgentInfo", blueprintId, identifier, name}] entry plus protectedAppMetadata.applicationLocation = the agent app id (the DLP policy scope).

Two steps differ from the delegated path:

  • Permission: grant Content.Process.All (Application) to the agent identity SP (not the blueprint) — run scripts/Grant-ContentProcessAppRole.ps1 instead of Grant-DelegatedGraphScope.ps1.
  • Wiring: import purviewGuard from ./purview-s2s.js and call evaluatePrompt(text) / evaluateResponse(text) directly around the LLM call (no TurnContext / Authorization args). The guard prefers the existing AGENT365_TENANT_ID, AGENT365_AGENT_ID, AGENT365_CLIENT_ID, and AGENT365_CLIENT_SECRET credentials; AGENT365_BLUEPRINT_ID sets the default policy app ID. Confirm these are configured: a365 setup all does not populate the lowercase agent365Observability__clientId / agent365Observability__clientSecret credentials. Use a real directory user object ID for PURVIEW_SPONSOR_USER_ID. Configure secrets directly in the environment or secret store, never in chat. This variant requires client-secret FMI; do not switch managed-identity-only agents to it.

Everything else — the DLP policy (Step 6), the [purview] log format, fail-closed semantics — is identical.


Before you start — read the A365 config, then ask for the rest

First, auto-discover from the project's A365 config. The guard and both scripts read these automatically, but read them yourself to confirm values and drive the workflow. From the agent project root, read a365.config.json and a365.generated.config.json:

Value Config source
App (client) id → PURVIEW_APP_ID a365.generated.config.json → agentBlueprintId (or botMsaAppId)
Blueprint id → PURVIEW_BLUEPRINT_ID a365.generated.config.json → agentBlueprintId
Agent SP object id (for the consent script) Resolved by the script for the requested app ID in the signed-in tenant; cached SP IDs are not trusted.
Display name → PURVIEW_APP_NAME a365.config.json → agentBlueprintDisplayName / agentDescription
Tenant id a365.config.json → tenantId
Current Graph scopes (is Content.Process.User already granted?) a365.generated.config.json → resourceConsents[] where resourceName == "Microsoft Graph" → scopes

Then ask the customer only for what's not in config (use the ask-questions tool):

Ask Notes / default
DLP policy — new, existing, or skip Always ask (unless the user specified it when invoking the skill): create a new policy (default), use an existing one, or skip (they'll add it manually / already have it). Drives Step 6.
Sensitive info type(s) to block Default Credit Card Number. Any Purview SIT. Only needed when creating a new policy.
Admin UPN for alerts Optional; recommended (agents are unaware of blocks).
Agentic auth handler name The static authHandlerName on their AgentApplication (usually agentic).
Entry/handler file + LLM call site Where the message handler + LLM call live (locate in code when possible).
Who does Purview admin? Confirm they (or an admin) can enable billing/DSPM-for-AI + run the scripts.

If the project has no a365 config (non-A365 or not yet provisioned), ask for the app id, display name, and tenant id directly and pass them explicitly to the scripts/env.


Procedure

Before Step 1, show the following checklist. In Claude Code, use TaskCreate and TaskUpdate; in Copilot, show and update the Markdown checkboxes. Keep one item in progress and mark it complete as soon as its numbered steps finish. Do not mark a permission or policy failure complete.

  • Locate the handler and confirm its authentication (Steps 0-1)
  • Copy the guard and wire the checks (Steps 2-3)
  • Configure and verify enablement (Step 4)
  • Grant and verify Graph permission (Step 5)
  • Resolve policy choice and tenant prerequisites (Steps 6-7)
  • Build and verify a blocked turn (Step 8)

0. Detect the agent language

For delegated agents, pick the guard/wiring by the agent's stack (all three share the SAME env vars, PowerShell scripts, policy, and [purview] log format). For Node.js S2S, follow the S2S variant above instead of the delegated guard/wiring steps below.

Language Detect by Guard asset Wiring
Node.js / TypeScript package.json (@microsoft/agents-hosting) assets/purview.ts assets/wiring-snippet.ts
Python pyproject.toml / requirements.txt (microsoft-agents-hosting-*) assets/purview.py assets/wiring-snippet.py
.NET *.csproj (Microsoft.Agents.*) assets/purview.cs assets/wiring-snippet.cs

Node.js and Python guards are verified against a live agent; the .NET guard is a best-effort port — verify the ITurnContext / UserAuthorization namespaces against your SDK version.

1. Locate the agent code

Find the message handler (the method that calls the LLM) and the LLM call:

  • Node.js: the AgentApplication subclass, onActivity(ActivityTypes.Message, …).
  • Python: the AgentInterface implementation's process_user_message (or the aiohttp host's on_message).
  • .NET: the AgentApplication subclass's OnMessageAsync.

Confirm the A365 runtime/hosting package is present (Node.js @microsoft/agents-a365-runtime, Python microsoft-agents-hosting-core, .NET Microsoft.Agents.*) — standard for A365 agents. Confirm the selected handler can obtain a delegated Graph token; for .NET, its configured scopes must include https://graph.microsoft.com/.default. A plain bot without a supported hosting/authentication path cannot be integrated just by supplying app IDs: stop before copying the guard and route to a365-setup. Do not switch an existing S2S agent to delegated auth.

2. Add the guard (1 new file)

Copy the guard for your language into the agent's source folder (next to the agent class) — it is generic and env-driven, no edits needed:

3. Wire the two gates (minimal edits)

Apply the wiring snippet for your language to the message handler (and any notification/email handler that calls the LLM): import the guard, add the input gate before the LLM and the output gate before the reply is sent. Pass the agent's authorization handler + auth-handler name + turn context/id.

  • Node.js: assets/wiring-snippet.ts — passes this.authorization.
  • Python: assets/wiring-snippet.py — for response-returning process_user_message handlers; passes auth, auth_handler_name, context. The caller sends the returned text. If wiring directly into a host on_message callback instead, send the block/withhold message with await context.send_activity(...) and then return; returning a string alone does not send a reply.
  • .NET: assets/wiring-snippet.cs — passes UserAuthorization, the agentic handler name, turnContext.

4. Add environment variables

Use the keys from assets/purview.env.example, preserving existing values rather than appending duplicate keys:

  • Node.js / Python: add missing keys to .env, which their guards load.
  • .NET: set PURVIEW_DLP_ENABLED in the selected Properties/launchSettings.json profile's environmentVariables, or in the cloud host's process environment. A .env file or appsettings.json key alone is not loaded by this guard. For a one-session local run, PowerShell can set $env:PURVIEW_DLP_ENABLED = 'true' before dotnet run.

On a delegated A365 project the only required guard key is PURVIEW_DLP_ENABLED=true — the guard auto-reads PURVIEW_APP_ID / PURVIEW_APP_NAME / blueprint id from a365.config.json + a365.generated.config.json. Set them explicitly only to override or for a non-A365 project. Confirm the guard's isEnabled / is_enabled / IsEnabled is true in the running agent.

For Node.js S2S, confirm the canonical credentials listed above instead of relying on config auto-discovery.

5. Grant the delegated Graph scope

For Node.js S2S, use Grant-ContentProcessAppRole.ps1 as described above and skip this delegated step.

Run scripts/Grant-DelegatedGraphScope.ps1 from the agent project folder (needs az login). -AppId is auto-discovered from a365.generated.config.json (pass -AppId <app-id> to override, or -ConfigDir <path> if the config lives elsewhere). It appends Content.Process.User to the agent's existing agentic consent. Never run az ad app permission admin-consent on the blueprint app. The scripts stop on failed CLI commands or failed verification. Surface that failure and resolve the tenant/admin issue before claiming permission setup is complete; do not broaden permissions.

6. Choose the DLP policy — new, existing, or skip

Ask the user which policy to use (skip the prompt only if they already told you when invoking the skill, e.g. "use my existing 'Corp PII' policy", "create a new one", or "I'll add it manually"):

"Which Purview DLP policy should gate this agent?

  1. Create a new dedicated AI-app policy (default),
  2. Use an existing DLP policy you already have, or
  3. Skip — don't create one now; I'll add it manually (or already have one)."

The guard is policy-name-agnostic — it calls processContent with the agent's app id as the applicationLocation, and Purview evaluates every policy whose Applications location includes that app id. So this choice affects only this step, never the guard code.

Option A — Create a new policy (default): Run scripts/New-AiAppDlpPolicy.ps1 from the agent project folder (needs the ExchangeOnlineManagement module). -AppId / -AppName are auto-discovered from the a365 config; typically you only pass -SensitiveInfoType "Credit Card Number" -NotifyUser <admin-upn>. Override with -AppId / -AppName for a non-A365 project. Or follow references/purview-portal-guide.md to do it in the portal. The script reuses existing names only when the app, enforcement mode, and enabled input-blocking rule match. On a mismatch, it stops without modifying those objects. Ask the admin to review them or use distinct names; do not silently rewrite an existing policy.

Option B — Use an existing policy:

  1. List candidates (read-only) to see which policies already cover this agent's app id:
    ./scripts/New-AiAppDlpPolicy.ps1 -ListExisting
    
    It connects to Security & Compliance and prints every DLP policy, flagging whether the agent's app id is already COVERS THIS APP.
  2. If the chosen policy already covers the app id — nothing to create; the guard will match it. Confirm its Mode is Enable and it has a RestrictAccess/Block rule on UploadText.
  3. If the chosen policy does not cover the app id — add this agent's app id to that policy's Applications (Managed cloud apps) location in the portal (purview-portal-guide.md), or fall back to Option A.

Option C — Skip (add the policy manually, or handle it later): Create nothing now. The guard stays wired and enabled — it just won't block until a matching policy exists (until then it logs allowed … 0 policyAction(s)). To add the policy yourself, it must have ALL of the following or the guard won't block (full portal walkthrough: purview-portal-guide.md):

# Requirement Value / note
1 Location = Applications (portal: "Managed cloud apps") scoped to the agent's Entra app (client) id (PURVIEW_APP_ID). Do not combine Exchange/SharePoint/OneDrive/Teams in the same policy — Purview rejects that mix for this workload.
2 Enforcement plane = Application NOT "Copilot experiences" (that's first-party Copilot).
3 Rule condition = Content contains sensitive info type your SIT(s), e.g. Credit Card Number.
4 Rule action = Restrict access → Block on the prompt (UploadText) blocking the response (DownloadText) isn't supported for this workload → prompt/input gate only.
5 Mode = Enable turn the policy on.
6 (optional) Notify an admin UPN agents don't surface a block to a human.

Equivalent Security & Compliance PowerShell (exactly what Option A automates — run it by hand if you prefer):

Connect-IPPSSession
$loc = '[{"Workload":"Applications","Location":"<APP_ID>","LocationDisplayName":"<name>","LocationSource":"Entra","LocationType":"Individual","Inclusions":[{"Type":"Tenant","Identity":"All"}]}]'
New-DlpCompliancePolicy -Name "<policy>" -Mode Enable -Locations $loc -EnforcementPlanes @('Application')
New-DlpComplianceRule   -Name "<rule>" -Policy "<policy>" `
  -ContentContainsSensitiveInformation @(@{ Name = 'Credit Card Number' }) `
  -RestrictAccess @(@{ setting = 'UploadText'; value = 'Block' })

Judge success only by the agent's [purview] uploadText -> BLOCKED (… 1 policyAction(s) … errors=0) log line — not by DistributionStatus (shows Pending even for live policies). Allow up to ~1 hour to propagate.

7. Confirm tenant prerequisites

DLP-for-AI is metered. If everything is configured but nothing blocks (with errors=0), verify pay-as-you-go billing + DSPM-for-AI onboarding + licensing — see references/purview-portal-guide.md §0.

8. Build, run, verify

Rebuild/restart the agent so it reloads .env (Node.js: npm run build && npm start; Python: restart the existing Python command; .NET: dotnet run with the configured process environment from Step 4). Send a message containing a Luhn-valid test value, e.g. My credit card is 4111 1111 1111 1111. Watch the console. Verify that a blocked prompt never reaches the LLM call, not just that a block log appears. Also send an ordinary prompt and confirm it reaches the LLM. Output audit is not proof of response blocking for this workload.


Verify — expected logs

[turn] in  <- ... text="My credit card is 4111 1111 1111 1111"
[purview] uploadText -> BLOCKED (HTTP 200, 1 policyAction(s), scopeState=modified, errors=0)
  • ✅ BLOCKED + 1 policyAction(s) + errors=0 → working; the agent replies with the block message and the LLM is never called.
  • allowed (… 0 policyAction(s) … errors=0) → request fine, but no policy matched yet (scope/propagation/tenant enablement).
  • REQUEST ERROR (… errors=1 …) → malformed request (usually the name field) — see troubleshooting.

Full symptom→fix table: references/troubleshooting.md.


Dependencies (customer agent)

No extra Graph SDK / @azure/identity / MSAL needed for any language — the guard uses the agent's own agentic auth handler + a plain HTTPS POST.

  • Node.js: @microsoft/agents-hosting + @microsoft/agents-a365-runtime, Node 18+ (global fetch), dotenv.
  • Python: microsoft-agents-hosting-core + httpx (both standard in A365 Python agents), python-dotenv.
  • .NET: Microsoft.Agents.* hosting packages (standard); uses System.Net.Http + System.Text.Json (no extra NuGet).

Non-negotiable constraints (why it's built this way)

  1. Agentic delegated token + /me (AgentApplication agents); agent-identity FMI token (S2S agents) — a blueprint app-only token is stripped of data-plane roles (Content.Process.*) by Graph, so an AgentApplication agent must use its agentic delegated token (Node.js AgenticAuthenticationService.GetAgenticUserToken, Python authorization.exchange_token(…), .NET UserAuthorization.GetTurnTokenAsync(…)). But an S2S / autonomous agent CAN do app-only DLP — using the agent identity's FMI-minted Graph token (not the blueprint's) against /users/{sponsor}/… instead of /me. See "S2S / app-only agents" above. Do not "fix" the AgentApplication path by switching it to client-credentials.
  2. contentEntry.name is required — omitting it returns a permanent BadRequest that looks like a clean allow. The guard always sets it and treats processingErrors as fail-closed.
  3. Dedicated AI-app policy — the Applications/Managed-cloud-apps location can't be combined with Exchange/SharePoint/OneDrive/Teams in one policy.
  4. RestrictAccess block, EnforcementPlanes=Application — not BlockAccess, not CopilotExperiences (that's first-party Copilot).
  5. Never admin-consent the blueprint app — it can narrow the agentic consent and break the agent's sign-in. Append the scope instead.
  6. Trust the [purview] log, not DistributionStatus (which reads Pending even for live policies).

Files in this skill

File Purpose
assets/purview.ts · purview.py · purview.cs Drop-in DLP guard (Node.js / Python / .NET — generic, env-driven).
assets/purview-s2s.ts S2S (app-only) DLP guard for autonomous Node.js agents — agent-identity FMI Graph token, /users/{sponsor} endpoint.
assets/wiring-snippet.ts · .py · .cs The minimal handler edits (Node.js / Python / .NET).
assets/purview.env.example Environment variables (same for all three languages).
scripts/Grant-DelegatedGraphScope.ps1 Append the delegated Graph scope (surgical, no admin-consent).
scripts/Grant-ContentProcessAppRole.ps1 S2S: grant Content.Process.All (Application) to the agent identity SP.
scripts/New-AiAppDlpPolicy.ps1 Create the AI-app DLP policy + block rule.
references/purview-portal-guide.md Tenant enablement (billing/DSPM) + manual portal policy steps.
references/troubleshooting.md Log reference + symptom→cause→fix table.