Repository navigation
Add Purview DLP integration skill for Agent 365 - #80
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
It includes a few concrete policy/convention issues (hardcoded tenant identifier in docs/comments, unexpected default permission scope, and a confusing validator exclusion path) that should be corrected before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a new purview-dlp-integration skill to the Agent 365 skills plugin, providing a drop-in Microsoft Purview DLP “gate” around an agent’s LLM call (prompt input, and optional response output) using Microsoft Graph processContent, along with scripts, docs, evals, and a report-first validator.
Changes:
- Introduces Purview DLP guard templates for Node.js, Python, and .NET (plus an S2S Node.js guard) and wiring snippets for input/output gating.
- Adds PowerShell automation for delegated scope grant and AI-app DLP policy creation, plus portal/troubleshooting documentation.
- Adds a report-first stop-hook validator, unit tests, eval scenarios, and updates plugin/docs metadata to include the new skill.
File summaries
| File | Description |
|---|---|
| tests/validate-purview-dlp-integration.test.js | Adds unit tests for the report-first Purview DLP validator behavior. |
| README.md | Documents the new standalone skill and its trigger phrases. |
| plugins/agent365/skills/purview-dlp-integration/SKILL.md | Defines the new skill workflow, constraints, and user prompts. |
| plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1 | Adds script to create/list Purview AI-app DLP policies and rules. |
| plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1 | Adds script to append delegated Graph scopes needed for DLP evaluation. |
| plugins/agent365/skills/purview-dlp-integration/scripts/Grant-ContentProcessAppRole.ps1 | Adds script for S2S/app-only scenario to grant Content.Process.All to the agent identity SP. |
| plugins/agent365/skills/purview-dlp-integration/references/troubleshooting.md | Adds troubleshooting guide keyed off [purview] log output. |
| plugins/agent365/skills/purview-dlp-integration/references/purview-portal-guide.md | Adds tenant prerequisites + portal steps for enabling/configuring AI-app DLP. |
| plugins/agent365/skills/purview-dlp-integration/README.md | Adds user-facing skill documentation and conceptual overview. |
| plugins/agent365/skills/purview-dlp-integration/assets/wiring-snippet.ts | Provides reference wiring snippet for Node.js agents (input/output gate). |
| plugins/agent365/skills/purview-dlp-integration/assets/wiring-snippet.py | Provides reference wiring snippet for Python agents (input/output gate). |
| plugins/agent365/skills/purview-dlp-integration/assets/wiring-snippet.cs | Provides reference wiring snippet for .NET agents (input/output gate). |
| plugins/agent365/skills/purview-dlp-integration/assets/purview.ts | Adds Node.js guard implementation calling Graph processContent with agentic delegated auth. |
| plugins/agent365/skills/purview-dlp-integration/assets/purview.py | Adds Python guard implementation calling Graph processContent with agentic delegated auth. |
| plugins/agent365/skills/purview-dlp-integration/assets/purview.env.example | Documents environment variables shared across language variants. |
| plugins/agent365/skills/purview-dlp-integration/assets/purview.cs | Adds .NET guard (best-effort port) calling Graph processContent. |
| plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts | Adds Node.js S2S/app-only guard using FMI chain + app-only processContent endpoint. |
| plugins/agent365/hooks/stop/validate-purview-dlp-integration.js | Adds report-first static validator to detect guard presence, wiring, and enablement. |
| plugins/agent365/.claude-plugin/plugin.json | Updates plugin metadata/keywords to include Purview DLP. |
| package.json | Updates repository package metadata and validate script to include the new validator. |
| evals/README.md | Documents the new skill’s eval folder and prerequisites. |
| evals/agent365/purview-dlp-integration/evals.json | Adds eval scenarios and trigger-boundary tests for the new skill. |
| CLAUDE.md | Updates repository structure/docs to include the new skill and validator. |
| AGENTS.md | Updates contributor guidance and skill inventory to include Purview DLP integration. |
| .github/copilot-instructions.md | Adds the new skill to Copilot instructions and documents its constraints/markers. |
Review details
- Files reviewed: 25/25 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new Node.js S2S Purview guard does not align with the repo’s existing S2S env-var conventions and the delegated-scope grant script appears to over-grant by default.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:36
- Grant-DelegatedGraphScope.ps1 claims to grant the delegated Graph scope(s) the DLP guard needs (
Content.Process.User), but the default$Scopearray also includesProtectionScopes.Compute.User, which isn’t referenced anywhere else in this repo and broadens permissions beyond what the skill documents. If the extra scope isn’t required forprocessContent, it should be removed from the default (users can still pass it explicitly if needed).
[string[]] $Scope = @('Content.Process.User', 'ProtectionScopes.Compute.User'),
- Files reviewed: 25/25 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The PR introduces at least one over-permission default in the grant script and a timestamp formatting issue in the S2S guard that can break Graph requests.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:194
createdDateTime/modifiedDateTimeare generated by stripping the trailingZfromtoISOString(), producing a timestamp without an explicit timezone. Keep the full ISO-8601 UTC timestamp (includingZ) to avoid the Graph API rejecting or misinterpreting the datetime fields.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:144- In the auto-discovery table,
PURVIEW_APP_IDandPURVIEW_BLUEPRINT_IDboth point toagentBlueprintId, which can read like two different values even though they default to the same thing for A365 projects. Clarify thatPURVIEW_BLUEPRINT_IDdefaults toPURVIEW_APP_ID(set only to override) so users don’t mistakenly hunt for a second identifier.
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:37
- The default scope list grants
ProtectionScopes.Compute.Userin addition toContent.Process.User, but the Purview DLP guard and docs only requireContent.Process.User. Granting extra delegated permissions by default is unnecessary and weakens least-privilege; keep the default minimal and let users opt-in to additional scopes via-Scopeif needed.
param(
[string] $AppId, # auto-discovered from a365 config if omitted
[string[]] $Scope = @('Content.Process.User', 'ProtectionScopes.Compute.User'),
[string] $ConfigDir = '.' # folder containing a365.config.json / a365.generated.config.json
- Files reviewed: 25/25 changed files
- Comments generated: 0 new
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate issues affect S2S configuration, guard behavior, language assets, permission automation, and workflow correctness.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (40)
Previously missed (4) — in code that hasn't changed since the last review.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:161
- The documented non-A365 flow asks for a tenant ID and says to pass it to the scripts/environment, but the guards do not read a
PURVIEW_TENANT_IDvalue and neither script accepts a tenant parameter. A user following this path cannot actually provide the requested tenant value; remove it from the required inputs or add a real consumption path.
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:194 - This uses the blueprint/client app id as
aiAgentInfo.identifierfor every .NET turn. Existing A365 conventions resolve the runtime agent identity fromturnContext.Activity.GetAgenticInstanceId(); using_appIdcan attribute DLP events to the blueprint instead of the active agent instance. Resolve the runtime identity here and only use the app id as a deliberate fallback when no agentic identity is available.
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:69 - This lookup selects the first Graph grant for the service principal without requiring
consentType = 'AllPrincipals'. In a tenant with both principal-specific and app-wide grants, the script can patch the wrong grant and leave the agent-wide permission unchanged. Filter the lookup to the AllPrincipals grant.
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:116 - When a policy with the generated name already exists, this branch skips it without checking that its Applications location contains this app, that its enforcement plane is
Application, or that it is enabled. A name collision or partially configured prior run can therefore produce false success with no matching policy; validate or reconcile the existing policy before reporting success.
.github/copilot-instructions.md:330
- These generated Copilot instructions describe only the delegated
/mepath and say app-only client credentials are never valid, but this PR adds the S2Spurview-s2s.tspath that deliberately uses the agent identity's FMI-minted app-only token and a/users/{sponsor}endpoint. Copilot users following this instruction will reject or misconfigure the advertised S2S feature; document the explicit blueprint-vs-agent-identity exception here.
**Non-negotiable constraints:** agentic delegated token + `/me` (never app-only client credentials on a blueprint app — Graph strips data-plane roles); `contentEntry.name` always set + `processingErrors` fail-closed; dedicated AI-app policy; `RestrictAccess` block on `UploadText`; never `admin-consent` the blueprint app; trust the `[purview]` log, not `DistributionStatus`.
AGENTS.md:58
- The contributor guide has the same delegated-only description even though the new skill includes an S2S guard and separate application-role grant. This makes the repository documentation internally inconsistent and can cause future contributors to remove or bypass the supported S2S path; add the agent-identity FMI/app-only exception and the S2S script to this mirrored entry.
`purview-dlp-integration` is **additive** and independent of observability / WorkIQ. It auto-discovers the app (client) id, display name, blueprint id, and current Graph scopes from `a365.config.json` + `a365.generated.config.json`, then asks only for what's missing (DLP policy choice, sensitive info type, admin UPN, agentic auth handler name). It detects the agent language and copies ONE generic env-driven guard (`assets/purview.ts` / `purview.py` / `purview.cs` — the .NET guard is a best-effort port), then applies minimal wiring: an **INPUT gate** that calls the Graph `processContent` API before the LLM (blocks the turn → the LLM is never called) and an optional **OUTPUT gate** before the reply. The guard uses the agent's own **agentic delegated** token evaluated as `/me` (never app-only client credentials on a blueprint app), always sets `contentEntry.name`, and fails closed. It guides the delegated `Content.Process.User` scope grant (`scripts/Grant-DelegatedGraphScope.ps1` — appends, never `admin-consent` on the blueprint app) and a DLP-policy choice (new via `scripts/New-AiAppDlpPolicy.ps1`, existing via `-ListExisting`, or skip). Success is judged by the `[purview] uploadText -> BLOCKED (… errors=0)` log, not `DistributionStatus`. The stop-hook validator (`validate-purview-dlp-integration.js`) is report-first (advisory findings, never blocks) because the skill supports a legitimate "start disabled / skip policy" bring-up state.
CLAUDE.md:120
- These instructions describe only the delegated
/mepath and say app-only client credentials are never valid, but this PR adds the S2Spurview-s2s.tspath that deliberately uses the agent identity's FMI-minted app-only token and/users/{sponsor}. Users following this instruction will reject or misconfigure S2S; document the explicit blueprint-vs-agent-identity exception here.
the LLM + an optional OUTPUT gate. The guard uses the agent's **agentic delegated** token
evaluated as `/me` (never app-only client credentials on a blueprint app), always sets
`contentEntry.name`, and fails closed. Never run `az ad app permission admin-consent` on the
blueprint app — append the `Content.Process.User` scope instead. Its validator
evals/README.md:54
- This new exception explicitly allows a code-editing skill to bypass
.a365-workspace-detection.local.json, contradicting the repository-wide requirement that the cache exists before any code edit. Either make the skill cache-aware (and update this exception) or formally update the shared invariant and validators; leaving only this eval note creates an inconsistent contract.
- `purview-dlp-integration` can run without the cache — it reads `a365.config.json` /
`a365.generated.config.json` directly (or takes the app id / display name / tenant id from you).
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:103
- The validator treats any handler file containing the guard symbol as wired, so a bare import or unused reference passes without an input or output gate. That makes the advisory
purview-guard-not-wiredfinding miss the common case where the asset was copied and imported but never called; require anevaluatePrompt/evaluateResponse(or language-equivalent) invocation instead of only the symbol.
const wiredFiles = codeFiles.filter(f => {
if (guardFiles.includes(f)) return false;
const c = read(f);
return GUARD_SYMBOL.test(c);
});
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:46
appSettingsFilesis collected here but never consulted byenvHasAny, so the .NET validator reportspurview-env-flag-missing-dotneteven whenPURVIEW_DLP_ENABLEDis configured inappsettings.json, which this diagnostic message explicitly treats as a supported location. Either scan the supported settings files or remove them from the guidance.
const envFiles = filterByName(allFiles, '.env', '.env.local', '.env.production', '.env.development');
const appSettingsFiles = filterByName(allFiles, 'appsettings.json', 'appsettings.Development.json', 'appsettings.Production.json');
plugins/agent365/skills/purview-dlp-integration/README.md:53
- A blocked message is still sent from the agent to Microsoft Purview through Graph for evaluation and auditing; only the LLM call is skipped. Saying the sensitive text “never leaves your agent” is therefore false and conflicts with the documented
processContentflow. Limit this claim to “never sent to the AI model” (or equivalent).
> **Key point:** when a message is blocked, **the AI model is never called** — the
> sensitive text never leaves your agent. Every checked message is also written to
> Microsoft Purview's audit log, so you get compliance records automatically.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:209
- The statement that
PURVIEW_DLP_ENABLEDis the only required key is true for the delegated AgentApplication path, but not for the S2S guard described above: it also needs tenant, runtime agent identity, blueprint/client credentials or managed identity, and sponsor user configuration. This guidance causes S2S users to start with incomplete configuration and fail closed; split the prerequisites by guard type.
### 4. Add environment variables
Append the keys from [`assets/purview.env.example`](./assets/purview.env.example) to the agent's
`.env`. On an A365 project the only key you **must** set 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.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:169
- This new multi-phase skill enters the procedure without the visible TODO/task checklist required for every skill in this repository. In Copilot/CLI mode there is no TaskCreate fallback, so users cannot see or track phase completion; add the checklist before the procedure and mark each phase complete as it runs.
## Procedure
### 0. Detect the agent language
Pick the guard/wiring by the agent's stack (all three share the SAME env vars, PowerShell scripts,
policy, and `[purview]` log format — only the guard file + wiring differ):
plugins/agent365/skills/purview-dlp-integration/SKILL.md:138
- This skill proceeds directly to config discovery and code-edit steps without the repository-mandated Phase 0A cache guard. On a project with code it can bypass
a365-setupand leave no.a365-workspace-detection.local.json, violating the shared invariant that must hold before any code edit. Add the hard-stop/cache-writing phase or update the repository-wide contract consistently.
## 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`:
plugins/agent365/skills/purview-dlp-integration/SKILL.md:201
- This procedure is written only for AgentApplication turns and always passes authorization, handler name, and TurnContext. For the S2S variant introduced above, the required wiring is direct
evaluatePrompt(text)/evaluateResponse(text)calls frompurview-s2s.ts; following this step wires the wrong contract and fails. Add an explicit S2S branch here.
### 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`](./assets/wiring-snippet.ts) — passes `this.authorization`.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:215
- This permission step unconditionally instructs the user to grant delegated
Content.Process.User. For S2S, the guard requiresContent.Process.Allon the agent identity andGrant-ContentProcessAppRole.ps1; following this step leaves the S2S token without its required role and every check fails closed. Branch the permission step on auth mode.
### 5. Grant the delegated Graph scope
Run [`scripts/Grant-DelegatedGraphScope.ps1`](./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
plugins/agent365/skills/purview-dlp-integration/SKILL.md:110
- The PR description says live-tenant integration was not tested during preparation, but this new section claims S2S app-only DLP was “Verified 2026-08-26 in a live tenant.” Clarify the provenance or remove the live-tenant claim; otherwise the skill presents an unverified permission/token behavior as established fact.
**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**:
plugins/agent365/skills/purview-dlp-integration/SKILL.md:193
- The procedure's guard-copy step lists only the delegated
purview.ts/.py/.csassets, while the S2S section requirespurview-s2s.tsinstead ofpurview.ts. An S2S user following the numbered workflow will copy the incompatible guard; add the S2S asset as an explicit branch here.
### 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:
- **Node.js:** [`assets/purview.ts`](./assets/purview.ts)
- **Python:** [`assets/purview.py`](./assets/purview.py)
plugins/agent365/skills/purview-dlp-integration/SKILL.md:175
- The skill's top-level language table omits the S2S guard even though S2S is advertised as supported. This makes detection select the AgentApplication wiring assets for an autonomous Node.js agent; include the S2S variant and its direct-call wiring in the selection table.
| 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` |
plugins/agent365/skills/purview-dlp-integration/SKILL.md:264
- This conflicts with the policy guidance below: the policy created here only supports
RestrictAccessonUploadText, and the skill saysDownloadTextblocking is unsupported, yet the guard defaultsPURVIEW_CHECK_OUTPUTtotrueand the documented flow promises output withholding. The default therefore performs an unenforceable output check and can withhold responses on output errors; default it off or document the output path as audit-only.
| # | 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. |
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:143
- Hop 1+2 is hard-coded to client-secret authentication. The repository's canonical Node.js S2S configuration defaults
AGENT365_USE_MANAGED_IDENTITY=trueand permits no secret in production, so this guard cannot work in the documented Azure deployment and every turn fails closed. Add the managed-identity branch, or explicitly require a client secret and remove the MSI-compatible S2S claim.
// Hop 1+2: Blueprint (client secret) → T1 via FMI path. MSAL doesn't serialize fmi_path,
// so use a direct form POST (same workaround as the observability token service).
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:100
- The fallback
agent365Observability__agentIdis not the runtime agent identity: the repository documents that setup-all stamps the blueprint ID into this variable. This value is used both as FMIfmi_pathand asaiAgentInfo.identifier, so S2S can request a token for the wrong principal and misattribute the DLP event. Only accept the runtimeAGENT365_AGENT_ID/agenticAppIdvalue and fail closed when it is absent.
this.agentId = process.env.agent365Observability__agentId ?? process.env.AGENT365_AGENT_ID ?? "";
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:205
this.appIdis the DLP application scope and can be overridden byPURVIEW_APP_ID, but the request'saiAgentInfo.blueprintIdmust remain the blueprint ID. With the documented app-id override, this sends the policy app ID asblueprintId, producing an inconsistent S2S request; keep a separate resolved blueprint ID and use it here.
"@odata.type": "microsoft.graph.aiAgentInfo",
blueprintId: this.appId,
identifier: this.agentId,
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:253
- Unlike both delegated guards, this path parses JSON for every successful response.
processContentmay return 202/204 without a body;res.json()then throws, and fail-closed mode blocks a valid request. Handle 202/204 as accepted before parsing, as the other guards do.
const json = (await res.json()) as ProcessContentResponse;
if (this.debug) console.log(`[purview] ${activity} raw:`, JSON.stringify(json));
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:12
- This asset repeats a “verified in a live tenant” claim even though the PR validation notes say live-tenant integration was not tested. That claim is material because the token roles and endpoint behavior determine whether the new S2S path is safe; reconcile the evidence or label this as unverified/best effort.
// ── WHY A SEPARATE GUARD (verified 2026-08-26 in a live tenant) ─────────────
// The delegated guard (purview.ts) needs an agentic `/me` token, which an S2S agent does
// not have. The blocker in the base skill — "blueprint app-only tokens get Content.Process
// stripped" — is REAL for the *blueprint* app, but NOT for the *agent identity*:
// • Blueprint app-only Graph token → roles: AgentIdentity.CreateAsManager (STRIPPED)
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:94
- The procedure documents
DownloadTextrestriction as unsupported and creates only anUploadTextblock rule, yet this defaults response checking on for every turn. SinceprocessingErrorsare fail-closed, an unsupported or invalid response check can withhold every otherwise-allowed answer even though output checking is described as optional. Default this tofalseand require explicit opt-in.
this.checkOutput = envBool("PURVIEW_CHECK_OUTPUT", true);
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:274
- The guard's config loader never reads
blueprintId, even thougha365.config.jsonuses that field in this repository. When the generated config is absent or incomplete, the documented A365 auto-discovery path leavesappId/blueprintIdempty and the guard fails closed; include theblueprintIdfallback for the second config file.
outp["appId"] = outp.GetValueOrDefault("appId") ?? Str(r, "agentBlueprintId") ?? Str(r, "botMsaAppId") ?? Str(r, "botId");
outp["blueprintId"] = outp.GetValueOrDefault("blueprintId") ?? Str(r, "agentBlueprintId");
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:78
- The procedure documents
DownloadTextrestriction as unsupported and creates only anUploadTextblock rule, yet this defaults response checking on for every turn. SinceprocessingErrorsare fail-closed, an unsupported or invalid response check can withhold every otherwise-allowed answer even though output checking is described as optional. Default this tofalseand require explicit opt-in.
_checkOutput = EnvBool("PURVIEW_CHECK_OUTPUT", true);
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:95
int(...)runs while the module is imported, so a malformedPURVIEW_TIMEOUT_MScrashes the entire Python agent before it can start or fall back to the documented safe default. Parse the value defensively and use the default for non-numeric or non-positive input, matching the Node.js and .NET guards.
self.timeout_ms = int(os.getenv("PURVIEW_TIMEOUT_MS") or 2000)
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:85
- The Python loader has two discovery problems:
setdefaultkeeps empty values from the generated file, preventing fallback toa365.config.json, and it never reads that file's repository-standardblueprintIdfield. A partial/generated config can therefore hide valid configuration and make the guard fail closed; retain only non-empty values and includeblueprintIdin the candidates.
out.setdefault("appId", j.get("agentBlueprintId") or j.get("botMsaAppId") or j.get("botId"))
out.setdefault("blueprintId", j.get("agentBlueprintId"))
out.setdefault("appName", j.get("agentBlueprintDisplayName") or j.get("agentDescription") or j.get("agentIdentityDisplayName"))
out.setdefault("tenantId", j.get("tenantId"))
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:94
- The procedure documents
DownloadTextrestriction as unsupported and creates only anUploadTextblock rule, yet this defaults response checking on for every turn. SinceprocessingErrorsare fail-closed, an unsupported or invalid response check can withhold every otherwise-allowed answer even though output checking is described as optional. Default this tofalseand require explicit opt-in.
self.check_output = _env_bool("PURVIEW_CHECK_OUTPUT", True)
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:112
- The guard's config loader never reads
blueprintId, even thougha365.config.jsonuses that field in this repository. When the generated config is absent or incomplete, the documented A365 auto-discovery path leavesappId/blueprintIdempty and the guard fails closed; include theblueprintIdfallback for the second config file.
out.appId ??= (j.agentBlueprintId ?? j.botMsaAppId ?? j.botId) as string | undefined;
out.blueprintId ??= j.agentBlueprintId as string | undefined;
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:137
- The procedure documents
DownloadTextrestriction as unsupported and creates only anUploadTextblock rule, yet this defaults response checking on for every turn. SinceprocessingErrorsare fail-closed, an unsupported or invalid response check can withhold every otherwise-allowed answer even though output checking is described as optional. Default this tofalseand require explicit opt-in.
this.checkOutput = envBool('PURVIEW_CHECK_OUTPUT', true);
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:98
- The verification lookup repeats the same unfiltered selection, so it can report
MISSINGfor a principal-specific grant even after the correct AllPrincipals grant was updated. Apply the sameconsentType -eq 'AllPrincipals'filter here.
$after = (az rest --method GET --url "https://graph.microsoft.com/v1.0/servicePrincipals/$bpSpId/oauth2PermissionGrants" | ConvertFrom-Json).value |
Where-Object { $_.resourceId -eq $graphSpId } | Select-Object -First 1
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:104
- The script prints
MISSINGbut still exits successfully when a requested scope is absent after the grant operation. That lets the skill continue as if permission setup succeeded, while the guard will fail closed on every token request. Make verification throw or return a non-zero exit code when any scope is missing.
foreach ($s in $Scope) {
$has = ($after.scope -split '\s+') -contains $s
Write-Host ("{0,-32} -> {1}" -f $s, ($(if ($has) { 'PRESENT' } else { 'MISSING' })))
}
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:55
- The documented config discovery includes
a365.config.json, whose repository schema stores the blueprint underblueprintId, but this key is not searched. A project with only that config reaches the explicit AppId error despite following the documented A365 setup path; includeblueprintIdin the key list.
if (-not $AppId) { $AppId = Get-A365Value $ConfigDir @('a365.generated.config.json','a365.config.json') @('agentBlueprintId','botMsaAppId','botId') }
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:36
- The script's documented contract and all guard code require only
Content.Process.User, but the default also appendsProtectionScopes.Compute.Userto the agentic delegated grant. That silently broadens consent beyond this integration and contradicts the README's claim that one permission is added; remove the extra scope from the default unless the guard actually uses it and document the reason.
[string[]] $Scope = @('Content.Process.User', 'ProtectionScopes.Compute.User'),
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:65
- The documented config discovery includes
a365.config.json, whose repository schema stores the blueprint underblueprintId, but this key is not searched. A project with only that config can create no policy and fails at the explicit AppId check even though it has valid A365 configuration; includeblueprintIdin the key list.
if (-not $AppId) { $AppId = Get-A365Value $ConfigDir @('a365.generated.config.json','a365.config.json') @('agentBlueprintId','botMsaAppId','botId') }
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:128
- This idempotency shortcut accepts any existing rule with this name without verifying the required SIT condition or
RestrictAccess=BlockonUploadText. A pre-existing or partially created rule can make the script claim success while the agent remains unprotected; validate or reconcile the existing rule before skipping creation.
if (Get-DlpComplianceRule -Identity $RuleName -ErrorAction SilentlyContinue) {
Write-Host "Rule already exists: $RuleName"
} else {
$sit = @($SensitiveInfoType | ForEach-Object { @{ Name = $_ } })
$ruleParams = @{
tests/validate-purview-dlp-integration.test.js:54
- The PR description says this validator has eight unit tests, but this file currently defines seven
test(...)cases. Add the missing case or correct the description so the validation claim matches the committed test suite.
describe('validate-purview-dlp-integration', () => {
- Files reviewed: 25/25 changed files
- Comments generated: 4
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate issues affect fail-closed enforcement, identity handling, timeouts, policy provisioning, and validation behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (28)
Previously missed (12) — in code that hasn't changed since the last review.
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:95
- The S2S variant has the same mismatch: it enables output evaluation by default although the policy guidance says
DownloadTextblocking is unsupported. This adds unnecessary per-response latency and can fail closed on every reply if the endpoint rejects output evaluation; default to false and require explicit opt-in.
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:205 - When
PURVIEW_APP_IDis overridden, this writes the policy-scope app ID intoaiAgentInfo.blueprintId. Those are separate fields, so the supported override sends a non-blueprint identifier as the blueprint metadata and can misattribute or reject the request. Keep the blueprint ID separate and useappIdonly forprotectedAppMetadata.applicationLocation.
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:78 - This .NET guard defaults the optional output path on even though the skill documents that
DownloadTextblocking is unsupported. Every reply will therefore make an extra Graph call and can be withheld by fail-closed error handling; use false as the default and require explicit opt-in.
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:194 _appIdis the configured application/blueprint ID, not the per-turn agent instance ID. This causes every .NET request to identify the blueprint even though the repository's A365 contract requiresGetAgenticInstanceId()for agentic turns andUtility.ResolveAgentIdentity(turnContext, token)for OBO turns; resolve that runtime identity before building the request.
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:97- The documented workload only supports blocking
UploadText, yet this guard enables theDownloadTextcheck by default. That adds a second call on every turn and, under fail-closed error handling, can suppress all replies when output processing is unsupported; default this option to false and require explicit opt-in.
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:98 - An invalid
PURVIEW_TIMEOUT_MSvalue raises during module import, before the agent can start and before the guard's fail-closed path is available. Match the other implementations by catching parse errors and falling back to the 2000 ms default (also reject non-positive values).
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:162 - A new
httpx.AsyncClientis created and closed for every DLP evaluation. Since this runs for both input and optional output on every turn, the agent loses connection pooling and repeats TCP/TLS setup, adding avoidable latency and load; keep a client on the guard instance and close it during application shutdown.
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:225 - When
recipient.agentic_app_idis absent on a non-agentic/OBO or Playground turn, this fallback sends the configured app/blueprint ID asaiAgentInfo.identifier. Purview then receives the wrong runtime identity; resolve the per-turn identity from the activity or acquired token and pass it into_build_bodyinstead of usingself.app_idas a fallback.
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:137 - Unlike the delegated guards, this path defaults output checking on, but the skill's policy guidance says
DownloadTextblocking is unsupported and describes output checking as optional. This makes every response incur an unsupported second Graph evaluation and can withhold all responses on errors; default to false and require explicit opt-in for output auditing.
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:297 recipient.agenticAppIdis not guaranteed on non-agentic/OBO or Playground turns, so this fallback sends the configured blueprint/client app ID asaiAgentInfo.identifier. That misattributes the Purview record to the blueprint instead of the runtime agent identity; resolve the ID fromGetAgenticInstanceId()or the acquired token and pass it intobuildBodyrather than falling back tothis.appId.
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:109$AppNameis interpolated directly into a JSON string. A valid display name containing a quote or backslash produces malformed-LocationsJSON and prevents policy creation; build the location as a PowerShell object and serialize it withConvertTo-Jsoninstead of manual escaping.
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:117- The idempotency check reuses any policy with the requested name without verifying that its Applications location contains
$AppIdor that it uses the Application enforcement plane. A same-named policy for another agent will therefore be reported as ready while this agent remains uncovered; validate the existing policy's scope before reusing it or generate a unique name.
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:72
- The validator accepts
onas a truthy value in the guards, but this regex does not. A valid configuration such asPURVIEW_DLP_ENABLED=onwill therefore produce a falsepurview-env-flag-missingfinding; includeonin the accepted values (and keep the parser consistent with the guard).
const truthy = new RegExp(`^\\s*${name}\\s*=\\s*(true|1|yes)\\s*$`, 'im');
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:137
appSettingsFilesis collected but never consulted, so a .NET agent that correctly configuresPURVIEW_DLP_ENABLEDinappsettings.jsonorlaunchSettings.jsonstill receivespurview-env-flag-missing-dotnet. That contradicts the validator's own message and creates a false advisory finding; inspect the documented .NET settings sources before reporting the flag missing.
if (!envHasAny('PURVIEW_DLP_ENABLED')) {
if (isDotnet) {
add(
'medium',
'purview-env-flag-missing-dotnet',
plugins/agent365/skills/purview-dlp-integration/SKILL.md:170
- This skill explicitly allows proceeding in a project without
.a365-workspace-detection.local.json, but repository guidance requires every code-editing skill to perform the cache triage and route througha365-setupbefore copying a guard or modifying a handler. Add the Phase 0A hard-stop/a365-setuppath instead of proceeding directly from config discovery.
> 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.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:179
- Before the first code-editing procedure, this skill has no visible TODO checklist or task creation/update flow. Repository skill conventions require every skill to show its task list before Phase 1 work and mark phases complete as they finish; add that checklist before the detection/edit steps.
## Procedure
### 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.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:94
- This flow says the output gate can block/withhold a response, but the policy this skill creates only has
RestrictAccessonUploadTextand the procedure explicitly saysDownloadTextrestriction is unsupported. With the generated policy,evaluateResponsecan audit but will never return a block; clarify the output path as audit-only or provide a supported output-policy path.
└─ allow ─► LLM ─► [OUTPUT gate] processContent(downloadText) ─► block? ─► withhold
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:214
- The S2S path sends the full prompt/response and hard-codes
isTruncated: false, while the delegated guards cap content atMAX_CONTENT_CHARS. Large agent messages can exceed the Graph payload limit and fail closed unnecessarily; apply the same bounded truncation and setisTruncatedfrom the actual request data.
content: { "@odata.type": "microsoft.graph.textContent", data: text },
agents: [
{
"@odata.type": "microsoft.graph.aiAgentInfo",
blueprintId: this.appId,
identifier: this.agentId,
name: this.agentName,
version: "1.0",
},
],
name: `${this.agentName} ${activity}`, // REQUIRED non-empty name
correlationId: randomUUID(),
sequenceNumber: activity === "uploadText" ? 0 : 1,
isTruncated: false,
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:145
- Both FMI token requests run before the request-level abort timer is created at line 233. If either token endpoint hangs,
PURVIEW_TIMEOUT_MSdoes not bound the DLP check and the autonomous worker can remain stuck before reaching Graph; apply one deadline/signal to all three token and Graph requests.
const r12 = await fetch(authority, {
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:120
PURVIEW_TIMEOUT_MSis documented as a per-call hard timeout, butGetTokenAsyncruns before the linked cancellation source is created. A hung auth/token exchange can therefore hold the turn indefinitely; create the deadline before token acquisition and pass it through the whole operation.
var token = await GetTokenAsync(authorization, authHandlerName, turnContext, ct);
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:87
setdefaultstoresNonewhen the generated config has no display-name fields (the normal case), so the latera365.config.jsonpass cannot populateappName; the guard then silently usesAI Agentdespite the documented auto-discovery. Assign the first non-empty value instead of treatingNoneas final.
out.setdefault("appName", j.get("agentBlueprintDisplayName") or j.get("agentDescription") or j.get("agentIdentityDisplayName"))
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:160
PURVIEW_TIMEOUT_MSis documented as a per-call hard timeout, but token acquisition occurs before the HTTP client's timeout is applied. A hung auth/token exchange can therefore hold the turn indefinitely; wrap token acquisition and the Graph request in one deadline or pass a cancellation signal.
token = await self._get_token(authorization, auth_handler_name, context)
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:145
- Because
??treats an empty string as a value, a commonPURVIEW_APP_ID=environment entry overrides the discovered A365 config and leavesappIdempty, causing every enabled turn to fail closed. Ignore blank/whitespace overrides when selecting the configured app id, as the Python and .NET guards already do.
process.env.PURVIEW_APP_ID ??
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:213
PURVIEW_TIMEOUT_MSis documented as a per-call hard timeout, but token acquisition occurs beforepostWithTimeoutcreates itsAbortController. A hung auth/token exchange can therefore hold the turn indefinitely; wrap token acquisition and the Graph request in one deadline or pass a cancellation signal.
const token = await this.getToken(authorization, ctx);
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:235
postWithTimeoutclears its timer as soon asfetch()resolves, beforeres.json()consumes the response body. A server that sends headers and then stalls can therefore hang this fail-closed gate indefinitely; keep the abort signal active through body parsing or use a whole-operation deadline.
const json = (await res.json()) as ProcessContentResponse;
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:36
- The skill and reference docs say this script grants only
Content.Process.User, but the default also requestsProtectionScopes.Compute.User. That extra delegated permission is not needed for the documentedprocessContentcall and can cause unnecessary consent or violate least privilege; keep the default to the single required scope.
[string[]] $Scope = @('Content.Process.User', 'ProtectionScopes.Compute.User'),
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:69
- The script says it is finding the existing agentic
AllPrincipalsgrant, but this filter checks only the Graph resource. If a user/principal grant is returned first, the PATCH adds the DLP scopes to the wrong consent and leaves the agentic grant unchanged. Filter the selection byconsentType -eq 'AllPrincipals'.
# Find the existing delegated (AllPrincipals) grant to Microsoft Graph.
$grants = (az rest --method GET --url "https://graph.microsoft.com/v1.0/servicePrincipals/$bpSpId/oauth2PermissionGrants" | ConvertFrom-Json).value
$g = $grants | Where-Object { $_.resourceId -eq $graphSpId } | Select-Object -First 1
tests/validate-purview-dlp-integration.test.js:55
- The PR description says this change adds eight unit tests, but this file defines seven
test(...)cases. Please either add the missing scenario or correct the validation summary so the PR metadata matches the submitted tests.
describe('validate-purview-dlp-integration', () => {
test('always returns ok:true (report-first)', () => {
- Files reviewed: 25/25 changed files
- Comments generated: 6
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved implementation issues affect S2S identity binding, configuration discovery, validation, and permission automation.
Review details
Suppressed comments (24)
Previously missed (1) — in code that hasn't changed since the last review.
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:196
- The comment says to resolve the per-instance identity with
GetAgenticInstanceId(), but the implementation always assigns_appIdinstead. This sends a configuration/blueprint value asaiAgentInfo.identifierfor every .NET request, so runtime identity binding is lost; resolveturnContext.Activity.GetAgenticInstanceId()and fail closed if it is missing.
.github/copilot-instructions.md:326
- This Copilot-facing summary omits the S2S branch described in
SKILL.md: it always selects the delegatedpurview.tspath,Content.Process.User, and/me, while autonomous S2S agents requirepurview-s2s.ts, the FMI chain, andContent.Process.All. A Copilot invocation for an S2S agent can therefore follow this summary and wire a guard that cannot authenticate. Add the S2S branch here and keep the duplicated AGENTS.md/CLAUDE.md summaries consistent.
2. Detects the agent language and picks the matching guard + wiring: Node.js → `assets/purview.ts`, Python → `assets/purview.py`, .NET → `assets/purview.cs` (best-effort port). All three share the same env vars, PowerShell scripts, DLP policy, and `[purview]` log format.
3. Copies ONE generic, env-driven guard file into the agent source (no edits needed).
4. Wires two gates into the message handler: the **INPUT gate** calls `processContent(uploadText)` before the LLM (blocks the turn → the LLM is never called), and the optional **OUTPUT gate** calls `processContent(downloadText)` before the reply. Passes the agent's own **agentic delegated** auth handler + handler name + turn context — evaluated as `/me` (the agent identity).
5. Appends env vars to `.env` — `PURVIEW_DLP_ENABLED` is the only required key on an A365 project (app id / display name / blueprint id are auto-read from the a365 config).
6. Guides the delegated scope grant via `scripts/Grant-DelegatedGraphScope.ps1` — **appends** `Content.Process.User` to the agent's agentic consent (never `az ad app permission admin-consent` on the blueprint app).
README.md:296
- This description is now incomplete for the S2S path added by this PR:
purview-s2s.tsuses the agent identity's FMI app-only token and/users/{sponsor}/...withContent.Process.All, rather than a delegated/metoken. Distinguish the AgentApplication and S2S variants here so users are not directed to the wrong guard and permission flow.
gate wiring. The guard uses the agent's own **agentic delegated** token evaluated as `/me` (never
app-only client credentials), always sets `contentEntry.name`, and fails closed by default.
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:103
wiredFilesis considered non-empty when any non-guard file merely mentionspurviewGuard/purview_guard/PurviewGuard. An unused import or a diagnostic reference therefore suppressespurview-guard-not-wiredeven when noevaluatePrompt/evaluateResponsegate exists or the gate is after the LLM call. Inspect the language-specific gate calls and ordering rather than treating a symbol mention as proof of wiring.
const wiredFiles = codeFiles.filter(f => {
if (guardFiles.includes(f)) return false;
const c = read(f);
return GUARD_SYMBOL.test(c);
});
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:46
launchSettings.jsonis an explicitly supported .NET location forPURVIEW_DLP_ENABLED, but it is neither included inenvFilesnor read here;appSettingsFilesis also never used. A project using the documented launch profile will still receivepurview-env-flag-missing-dotneton every run. Parse the launch profile's environment variables or suppress this finding when that supported location contains the key.
const envFiles = filterByName(allFiles, '.env', '.env.local', '.env.production', '.env.development');
const appSettingsFiles = filterByName(allFiles, 'appsettings.json', 'appsettings.Development.json', 'appsettings.Production.json');
plugins/agent365/skills/purview-dlp-integration/README.md:140
- The user-facing file still says every integration adds
Content.Process.User, but the S2S variant introduced here requiresContent.Process.Allassigned to the agent-identity service principal. Please qualify this row for delegated AgentApplication versus Node.js S2S agents so the setup guidance does not grant the wrong permission.
| **1 Microsoft Graph permission** | `Content.Process.User`, added to your agent's existing permissions (nothing broadened). |
plugins/agent365/skills/purview-dlp-integration/SKILL.md:176
- The no-config path asks for a tenant ID and says to pass it to the scripts/env, but the delegated guards do not consume
tenantIdand neither PowerShell script accepts or verifies a tenant. Both scripts operate on whichever tenant the current Azure CLI/IPPSSession targets, so a user can grant consent or create the policy in a different tenant than the one they entered. Make tenant selection explicit and verify it before running the scripts, or remove this unsupported input path.
> 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.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:184
- This procedure starts code edits without the repository-required Phase 0A cache triage.
AGENTS.md:324-332requires every skill to create and load.a365-workspace-detection.local.jsonbefore editing, but this skill neither checks nor writes it and its stop hook does not enforce that requirement. Add the cache guard/write before Step 0 or route this workflow througha365-setup.
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.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:158
- The discovery table only documents
agentBlueprintIdina365.generated.config.json, but reuse projects useblueprintIdina365.config.json(validate-make-a365-agent.js:66-69). This makes the documented auto-discovery incomplete and leads users to supply an unnecessary manual override. Document both fields and their file-specific names.
| App (client) id → `PURVIEW_APP_ID` | `a365.generated.config.json` → `agentBlueprintId` (or `botMsaAppId`) |
| Blueprint id → `PURVIEW_BLUEPRINT_ID` | `a365.generated.config.json` → `agentBlueprintId` |
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:100
- The canonical S2S configuration keeps
AGENT365_CLIENT_ID(the blueprint client credential used for FMI Hop 1) separate fromAGENT365_BLUEPRINT_ID(the blueprint identifier). Assigning the former toaiAgentInfo.blueprintIdsends the client ID in the blueprint field whenever the optional blueprint env var is absent, so Purview cannot associate the request with the intended blueprint. ReadAGENT365_BLUEPRINT_ID/agent365Observability__agentBlueprintIdfor this field and fail closed if it is missing.
this.blueprintId = process.env.AGENT365_CLIENT_ID || process.env.agent365Observability__clientId || "";
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:105
appIdis emitted asprotectedAppMetadata.applicationLocation, which must be the Entra application/client ID used to scope the DLP policy. The canonical S2S settings keep that value inAGENT365_CLIENT_ID; this fallback instead usesAGENT365_BLUEPRINT_ID, so a default-configured S2S agent sends the blueprint identifier and no policy matches. Default this field fromAGENT365_CLIENT_ID(or the lowercase client-id setting), leaving the blueprint ID only inaiAgentInfo.blueprintId.
const blueprintIdForPolicyScope = process.env.AGENT365_BLUEPRINT_ID ?? process.env.agent365Observability__agentBlueprintId;
this.appId = process.env.PURVIEW_APP_ID ?? blueprintIdForPolicyScope ?? this.blueprintId;
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:221
- Every S2S request is recorded with
ipAddress: 127.0.0.1and Linux/unknown metadata, even when the worker is deployed elsewhere. That makes the Purview audit record inaccurate for the autonomous-agent scenario this guard is intended to support. OmitdeviceMetadataif it is optional, or derive actual host metadata instead of emitting a fixed loopback address.
deviceMetadata: {
operatingSystemSpecifications: { operatingSystemPlatform: "Linux", operatingSystemVersion: "unknown" },
ipAddress: "127.0.0.1",
},
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:102
- These fallbacks are not valid A365 S2S identity values:
agent365Observability__agentIdis the blueprint ID, while this field is used asfmi_path/the Hop 3 client;agent365Observability__clientIdand__clientSecretare not written by the CLI. A normal project using the documented environment therefore sends the blueprint ID as the target agent and the token exchange fails or targets the wrong principal. Use the canonicalAGENT365_*variables here (or fail fast when they are missing).
this.blueprintId = process.env.AGENT365_CLIENT_ID || process.env.agent365Observability__clientId || "";
this.agentId = process.env.AGENT365_AGENT_ID || process.env.agent365Observability__agentId || "";
this.clientSecret = process.env.AGENT365_CLIENT_SECRET || process.env.agent365Observability__clientSecret || "";
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:274
- Reuse-mode projects can contain only
a365.config.json, whose schema usesblueprintId(as documented byvalidate-make-a365-agent.js:66-69). This loader checks onlyagentBlueprintId/bot*, so_appIdfalls through to an empty value and an enabled guard fails closed even with a valid config. IncludeblueprintIdin both fallbacks.
outp["appId"] = outp.GetValueOrDefault("appId") ?? Str(r, "agentBlueprintId") ?? Str(r, "botMsaAppId") ?? Str(r, "botId");
outp["blueprintId"] = outp.GetValueOrDefault("blueprintId") ?? Str(r, "agentBlueprintId");
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:240
- This fallback has the same identity-binding problem as the TypeScript guard: when the activity has no
agentic_app_id, the request identifies the agent withself.app_idfrom configuration. Use the runtime Agent Identity from the recipient and fail closed when it is unavailable; do not send a blueprint/config ID asaiAgentInfo.identifier.
agent_id = getattr(recipient, "agentic_app_id", None) or self.app_id
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:89
- Reuse-mode projects can contain only
a365.config.json, whose schema usesblueprintId(as documented byvalidate-make-a365-agent.js:66-69). This loader checks onlyagentBlueprintId/bot*, soappIdandblueprintIdremain empty and an enabled guard fails closed withmissing PURVIEW_APP_IDdespite having a valid config. IncludeblueprintIdin both fallbacks.
out["appId"] = out.get("appId") or j.get("agentBlueprintId") or j.get("botMsaAppId") or j.get("botId")
out["blueprintId"] = out.get("blueprintId") or j.get("agentBlueprintId")
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:216
- Creating and closing an
AsyncClientfor every input/output check defeats HTTP connection pooling; with output checks enabled each turn pays for two new client/transport setups and creates avoidable socket churn. Keep a sharedAsyncClientfor the guard singleton (with an explicit shutdown path) or otherwise reuse a transport.
async with httpx.AsyncClient(timeout=self.timeout_ms / 1000.0) as client:
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:298
- When
recipient.agenticAppIdis unavailable, this falls back tothis.appId, which is configuration-derived rather than the runtime Agent Identity. That makesaiAgentInfo.identifiercontain the blueprint/application value and breaks the identity binding for requests that do not carry the recipient field; resolve the runtime ID from the activity and fail closed if it is absent instead of substituting config.
const agentId = (recipient?.agenticAppId as string) ?? this.appId;
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:112
- Reuse-mode projects can contain only
a365.config.json, whose schema usesblueprintId(as documented byvalidate-make-a365-agent.js:66-69). This loader checks onlyagentBlueprintId/bot*, soappIdandblueprintIdremain empty and an enabled guard fails closed withmissing PURVIEW_APP_IDdespite having a valid config. IncludeblueprintIdin both fallbacks.
out.appId ??= (j.agentBlueprintId ?? j.botMsaAppId ?? j.botId) as string | undefined;
out.blueprintId ??= j.agentBlueprintId as string | undefined;
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:279
timeris explicitly typed asReturnType<typeof setTimeout> | undefinedabove. With the Node TypeScript typings used by strict customer projects, passing that union directly toclearTimeoutdoes not match the overload, so copying this guard can failtscbefore it runs. Guard the cleanup when the timer was created.
clearTimeout(timer);
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-ContentProcessAppRole.ps1:69
az restuses--urlfor the request URI (the delegated-scope script in this same skill uses that spelling);--uriis not a valid Azure CLI option. The S2S permission script therefore fails before it can inspect the existing assignment. Replace this option with--url(and make the same correction on the POST below).
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$agentSpId/appRoleAssignments" `
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-ContentProcessAppRole.ps1:84
- This POST repeats the invalid
--urioption, so even after the read path is fixed the role assignment cannot be created. Use Azure CLI's--urlparameter here as well.
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$agentSpId/appRoleAssignments" `
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:55
- The consent script also omits
blueprintId, even though that is the ID field ina365.config.json. Running the documented command from a reuse-mode project therefore fails discovery before it can grantContent.Process.User; include the same fallback used by the other A365 readers.
if (-not $AppId) { $AppId = Get-A365Value $ConfigDir @('a365.generated.config.json','a365.config.json') @('agentBlueprintId','botMsaAppId','botId') }
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:65
- The policy script has the same reuse-config gap:
a365.config.jsonstores the blueprint underblueprintId, but this lookup omits that key. In a reuse-mode project the documented no-argument command therefore throwsAppId not providedand cannot create or list the policy. AddblueprintIdto the discovery keys.
if (-not $AppId) { $AppId = Get-A365Value $ConfigDir @('a365.generated.config.json','a365.config.json') @('agentBlueprintId','botMsaAppId','botId') }
- Files reviewed: 25/25 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect permission scripts, identity handling, configuration discovery, and validation.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (19)
Previously missed (7) — in code that hasn't changed since the last review.
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:103
- The scanner treats any non-guard file containing the guard symbol as wired, including a file that only imports
purviewGuardand never calls an evaluation method. That produces a clean report for an actually unprotected handler; require anevaluatePrompt/evaluateResponse(language-appropriate) call and add a regression fixture for import-only code.
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:100 AGENT365_CLIENT_IDis the blueprint client credential used for the FMI request, but this property is later serialized asagents[0].blueprintId(line 203). The repository keepsAGENT365_BLUEPRINT_ID(the blueprint identifier) distinct fromAGENT365_CLIENT_ID; when those values differ, Purview receives the wrong blueprint metadata. Keep a separate client-ID property for Hop 1+2 and populate this metadata field fromAGENT365_BLUEPRINT_ID.
This issue also appears on line 101 of the same file.
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:273
- This loader does not read
blueprintId, the field used bya365.config.json; onlyagentBlueprintIdfrom the generated config is handled. When the generated file is absent, a valid config-only .NET agent falls back to an empty app/blueprint ID and cannot make a valid Purview request.
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:89 - The loader omits
blueprintId, which is the field used bya365.config.json; it only understandsagentBlueprintIdfrom the generated config. With a valid config-only project, the guard cannot discover the policy app/blueprint IDs and every enabled check fails closed.
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:112 - This discovery path never reads
blueprintId, even thougha365.config.jsonuses that field (the generated file usesagentBlueprintId). A project with onlya365.config.jsontherefore loses both the app ID and blueprint ID and the enabled guard fails closed instead of using the documented auto-discovery. Include the config-file field in both fallbacks.
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:55 - The auto-discovery key list omits
blueprintId, although that is the supported field ina365.config.json. As a result, this permission script throwsAppId not providedfor config-only projects unless the operator manually supplies an ID.
plugins/agent365/skills/purview-dlp-integration/scripts/New-AiAppDlpPolicy.ps1:65 - This policy script has the same config-discovery gap:
a365.config.jsonstores the blueprint underblueprintId, but the lookup only checksagentBlueprintIdand bot IDs. It therefore fails to create a policy automatically for a valid config-only A365 project.
plugins/agent365/skills/purview-dlp-integration/README.md:152
- This absolute privacy statement is misleading: the DLP check goes to Microsoft Purview, but allowed text still follows the existing agent's LLM/model-provider path and may go to a third party. Clarify that only the DLP check is sent to Microsoft so users do not infer that this integration controls the agent's model destination.
- It does **not** change your AI's answers — it only allows or blocks a turn.
- It does **not** send your data to any third party — the check goes to **Microsoft
Purview** through Microsoft Graph.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:184
- This mutating skill goes straight from the checklist into code discovery and editing without the required workspace-cache preflight. The repository convention in
CLAUDE.md:88-94requires.a365-workspace-detection.local.jsonbefore any code-edit phase; add a Phase 0A that invokesa365-setup/writes the cache and hard-stops before copying or wiring when it is absent.
## 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.
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:101
agent365Observability__agentIdis the blueprint ID stamped by this repository, not the runtime agent identity. Falling back to it makes the FMIfmi_path/Hop 3 client andaiAgentInfo.identifiertarget the blueprint wheneverAGENT365_AGENT_IDis absent, so S2S authentication and attribution are wrong. Require the canonicalAGENT365_AGENT_IDinstead of using this fallback.
this.agentId = process.env.AGENT365_AGENT_ID || process.env.agent365Observability__agentId || "";
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:105
- When
AGENT365_BLUEPRINT_IDis omitted, this fallback usesthis.blueprintId, which is actuallyAGENT365_CLIENT_ID(the blueprint client credential). That value is then used as the DLP policy'sapplicationLocation, so the guard can scope policy evaluation to the wrong app. Resolve the blueprint/app ID fromAGENT365_BLUEPRINT_IDor the repository's blueprint metadata and fail before requests if it is unavailable; never substitute the client credential.
const blueprintIdForPolicyScope = process.env.AGENT365_BLUEPRINT_ID ?? process.env.agent365Observability__agentBlueprintId;
this.appId = process.env.PURVIEW_APP_ID ?? blueprintIdForPolicyScope ?? this.blueprintId;
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:113
AGENT365_CLIENT_SECRETis required by the FMI form post below, but it is omitted from this completeness check. A missing secret therefore produces no startup warning and causes a doomed token request on every turn before failing closed; include!this.clientSecretand name it in the warning.
if (this.enabled && (!this.tenantId || !this.agentId || !this.blueprintId || !this.sponsorUserId)) {
console.warn(
"[purview] PURVIEW_DLP_ENABLED=true but S2S config incomplete (need tenantId, agentId, " +
"blueprintId, sponsorUserId). Every turn will fail-closed.",
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:195
- This fallback has the same identity mix-up as the other guards:
GetAgenticInstanceId()is the runtime identity, while_appIdis the blueprint/policy app ID. Using_appIdwhen no instance ID is present sends blueprint metadata asaiAgentInfo.identifier; resolve the runtime identity or fail closed instead.
var agentId = FirstNonEmpty(turnContext?.Activity?.GetAgenticInstanceId()) ?? _appId;
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:168
- Because the cancellation token is linked into
cts, this catches both the guard's own timeout and cancellation of the host request, converting cancellation into an error verdict. The handler may then try to send a block/withhold activity on an already-cancelled turn; only handle the internal timeout and rethrow caller cancellation.
catch (OperationCanceledException)
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:240
- If
recipient.agentic_app_idis absent, the guard usesself.app_idasagents[0].identifier. That is the blueprint/policy app ID, not the runtime agent identity required by the A365 metadata contract, so OBO/non-agentic turns can be misattributed or rejected. Resolve the runtime identity or fail closed instead of using this fallback.
agent_id = getattr(recipient, "agentic_app_id", None) or self.app_id
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:298
- When an inbound activity has no
recipient.agenticAppId, this substitutes the blueprint/policy app ID for the runtime agent identifier. A365 keeps the runtime AUID and blueprint ID separate; sending the blueprint here misattributes OBO/non-agentic turns and can make Purview reject or misapply the agent metadata. Resolve a runtime identity for the turn or fail closed rather than falling back tothis.appId.
const agentId = (recipient?.agenticAppId as string) ?? this.appId;
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-ContentProcessAppRole.ps1:86
- The create call repeats the invalid
--urioption. Even after fixing the lookup above, Azure CLI will reject this POST instead of assigningContent.Process.All; use the supported--urloption here as well.
$response = az rest --method POST `
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$agentSpId/appRoleAssignments" `
--headers "Content-Type=application/json" `
--body "@$tmp" -o json
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-DelegatedGraphScope.ps1:85
- If no existing Graph
AllPrincipalsgrant is found, this path creates a grant containing onlyContent.Process.User, despite the script's stated contract of appending to the agent's existing agentic consent. Running it before A365 setup/admin consent can leave the app with a partial grant and break agentic authentication. For the supported flow, fail and direct the user to complete setup instead of creating a partial grant.
if (-not $grant) {
Write-Host "No existing Graph delegated grant found — creating a new AllPrincipals grant."
$body = @{ clientId = $bpSpId; consentType = 'AllPrincipals'; resourceId = $graphSpId; scope = ($Scope -join ' ') } | ConvertTo-Json -Compress
$method = 'POST'
$url = 'https://graph.microsoft.com/v1.0/oauth2PermissionGrants'
tests/validate-purview-dlp-integration.test.js:55
- The PR description says this change adds eight unit tests, but this file defines seven
test(...)cases. Please add the missing case or correct the validation summary so the stated coverage is accurate.
describe('validate-purview-dlp-integration', () => {
test('always returns ok:true (report-first)', () => {
- Files reviewed: 25/25 changed files
- Comments generated: 2
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
Fix incorrect usage of az rest command by removing duplicate --uri option. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: dbezic <50911161+dbezic@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect configuration discovery, identity handling, validation, and S2S setup.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (12)
Previously missed (3) — in code that hasn't changed since the last review.
plugins/agent365/hooks/stop/validate-purview-dlp-integration.js:139
- The validator declares
appSettingsFilesbut never consults it, and it does not scanProperties/launchSettings.json. Consequently a .NET agent configured through the documented launch-settings profile still receivespurview-env-flag-missing-dotnet; the diagnostic is not checking the supported static configuration source. Either inspect launchSettings consistently or make the finding explicitly limited to projects where no supported configuration source is present.
plugins/agent365/skills/purview-dlp-integration/SKILL.md:193 - This skill edits an existing agent but starts directly at the checklist/detection steps without loading or enforcing
.a365-workspace-detection.local.json. The other code-edit skills require that cache before mutations so language/authentication routing and reruns are stateful; this skill's always-true validator also cannot catch a skipped triage. Add the same workspace-triage/cache guard before Step 0, or route through setup before allowing source edits.
plugins/agent365/skills/purview-dlp-integration/assets/purview-s2s.ts:114 - This variant explicitly requires a client-secret FMI configuration, but the startup completeness check does not include
this.clientSecret. A missingAGENT365_CLIENT_SECRETtherefore produces no warning and only fails later on every token request, making configuration troubleshooting unnecessarily opaque. Include the secret in the predicate and diagnostic text.
AGENTS.md:104
- The contributor-tree entry omits the newly added
Grant-ContentProcessAppRole.ps1, even though the skill now documents and depends on it for S2S. This leaves the repository inventory incomplete; include the S2S grant helper in the listed scripts.
│ │ └── scripts/ # Grant-DelegatedGraphScope.ps1, New-AiAppDlpPolicy.ps1
README.md:140
- This summary lists only the delegated
Content.Process.Userpermission, but the same PR documents an S2S path that requires theContent.Process.Allapplication role on the agent identity. A user following this table for the S2S variant could run the wrong grant script; distinguish the delegated scope from the S2S app role here.
plugins/agent365/skills/purview-dlp-integration/README.md:141 - This requirements table is written as universal, but the skill also supports the S2S variant, which requires
Content.Process.Allon the agent-identity service principal andGrant-ContentProcessAppRole.ps1, not delegatedContent.Process.User. As written, an S2S user following this README is directed toward the wrong permission and may wire the delegated path; scope these rows to delegated agents or add the S2S alternative.
| Change | What it is |
|---|---|
| **1 new file** | The DLP guard — a drop-in file, no editing needed. |
| **A few lines in your message handler** | The two checkpoints (before the AI, and optionally before the reply). |
| **A few settings** | Mainly a single on/off switch: `PURVIEW_DLP_ENABLED`. |
| **1 Microsoft Graph permission** | `Content.Process.User`, added to your agent's existing permissions (nothing broadened). |
| **1 Purview DLP policy** | Created for you, or reuse one you already have, or skip and add it later. |
plugins/agent365/skills/purview-dlp-integration/SKILL.md:200
- The documented S2S variant explicitly has no
@microsoft/agents-hostingAgentApplication, but this is the only Node.js detection rule. A worker with the documented canonicalAGENT365_*credentials can therefore be missed or rejected beforepurview-s2s.tsis selected. Add an explicit Node.js S2S detection/selection branch separate from the delegated hosting check.
| **Node.js / TypeScript** | `package.json` (`@microsoft/agents-hosting`) | `assets/purview.ts` | `assets/wiring-snippet.ts` |
plugins/agent365/skills/purview-dlp-integration/assets/purview.cs:195
- When
GetAgenticInstanceId()returns no value (for example on a non-agentic/OBO turn), this substitutes the configured app/blueprint ID foraiAgentInfo.identifier. That is not the runtime agent identity; the repository's A365 contract requires resolving agent IDs from the turn context/token rather than configuration (shared/agent-detection.md:602 and instrument-observability/references/dotnet-observability.md:596-610). This can attribute DLP activity to the wrong identity; use the same runtime resolver or fail rather than sending the blueprint ID as the instance identifier.
var agentId = FirstNonEmpty(turnContext?.Activity?.GetAgenticInstanceId()) ?? _appId;
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:216
- This creates a new
AsyncClientfor every Purview check. The guard is on the per-turn hot path and can make two checks per turn, so each call discards its connection pool and loses HTTP keep-alive/TLS reuse. Keep a shared client onPurviewGuard(and close it with the host lifecycle) or otherwise reuse a client to avoid unnecessary connection and latency overhead.
async with httpx.AsyncClient(timeout=self.timeout_ms / 1000.0) as client:
plugins/agent365/skills/purview-dlp-integration/assets/purview.py:240
- When
recipient.agentic_app_idis absent (for example on a non-agentic/OBO or Playground turn), this substitutes the configured app/blueprint ID foraiAgentInfo.identifier. That is not the runtime agent identity; the repository's A365 contract requires resolving agent IDs from the turn context/token rather than configuration (shared/agent-detection.md:602 and instrument-observability/references/python-observability.md:434-456). This can attribute DLP activity to the wrong identity; use the same runtime resolver or fail rather than sending the blueprint ID as the instance identifier.
agent_id = getattr(recipient, "agentic_app_id", None) or self.app_id
plugins/agent365/skills/purview-dlp-integration/assets/purview.ts:298
- When
recipient.agenticAppIdis absent (for example on a non-agentic/OBO or Playground turn), this substitutes the configured app/blueprint ID foraiAgentInfo.identifier. That is not the runtime agent identity; the repository's A365 contract requires resolving agent IDs from the turn context/token rather than configuration (shared/agent-detection.md:602 and instrument-observability/references/nodejs-observability.md:168-175). This can attribute DLP activity to the wrong identity; use the same runtime resolver or fail rather than sending the blueprint ID as the instance identifier.
const agentId = (recipient?.agenticAppId as string) ?? this.appId;
plugins/agent365/skills/purview-dlp-integration/scripts/Grant-ContentProcessAppRole.ps1:84
az restuses--urlfor the endpoint;--uriis not a supported option for this command. As written, the S2S permission-grant path exits before making the app-role assignment, so the documented setup cannot work.
--uri "https://graph.microsoft.com/v1.0/servicePrincipals/$agentSpId/appRoleAssignments" `
- Files reviewed: 25/25 changed files
- Comments generated: 7
- Review effort level: Lite
Summary
Adds
/agent365:purview-dlp-integrationto protect agent prompts and responses using Microsoft Purview DLP through Microsoft GraphprocessContent.Changes
Validation
npm test: 132 passed, zero failures.