Skip to main content

Crate agent_framework_purview

Crate agent_framework_purview 

Source
Expand description

§agent-framework-purview

Microsoft Purview compliance middleware for agent-framework-rs: evaluate an agent’s/chat client’s outgoing prompt and incoming response against Microsoft Graph dataSecurityAndGovernance data-loss-prevention policy, blocking either direction when policy says to.

This is the Rust equivalent of agent_framework_purview (PurviewPolicyMiddleware / PurviewChatPolicyMiddleware) in the Python reference implementation, hand-rolled against reqwest rather than wrapping httpx + azure-identity (which have no Rust equivalents in this workspace).

§Scope: what this port narrows down to, and why

The Python package’s ScopedContentProcessor orchestrates three Graph endpoints per evaluated message: it first computes applicable protection scopes (protectionScopes/compute, cached with ETag-based invalidation and a 4-hour default TTL), only calls processContent inline when a scope actually applies and demands inline evaluation (queuing it in the background otherwise), and — when no scope applies at all — fires a background activities/contentActivities audit log entry instead. This work package’s brief scopes this crate down to the single call that actually produces a block/allow verdict:

“PurviewClient: POST the processContent route Python uses … parse verdict (policy actions / restrict/block per Python).”

So this crate calls processContent directly, on every evaluated message, and parses its policyActions for a block verdict. It does not call protectionScopes/compute or activities/contentActivities, does not cache anything, and has no background-task queuing. A consequence worth calling out: since there’s no scopes precheck to determine “does any policy even apply here”, this port evaluates inline (and synchronously) on every call, which is both simpler and more conservative (never silently skips evaluation the way Python’s “no applicable scope → log-only, don’t block” fallback can) but forgoes the offline/background execution mode and the performance benefit of Python’s caching layer. PurviewSettings::cache_ttl_seconds / PurviewSettings::max_cache_size_bytes are still present on the settings struct (parity with Python’s configuration surface) but are currently unused by this port.

Two further, independent scope cuts, both driven by this crate’s self-contained TokenProvider (see the auth module docs — there’s no azure-identity-equivalent credential to introspect a JWT with here):

  • No token-derived tenant_id/user_id/app-location fallback. Python’s PurviewClient.get_user_info_from_token decodes the bearer token’s JWT payload (tid/oid/appid claims, unverified — no signature check either side) to fill in tenant_id when PurviewSettings.tenant_id is unset, and an application-location fallback when PurviewSettings.purview_app_location is unset. This port requires both to be set explicitly on PurviewSettings — attempting to evaluate without them is a agent_framework_core::error::Error::Configuration error (itself subject to PurviewSettings::ignore_exceptions, same as any other evaluation failure).
  • User id resolution from messages is still ported. The other half of Python’s user-id resolution — scanning Message::additional_properties["user_id"], falling back to a GUID-shaped author_name — has nothing to do with the bearer token and is faithfully ported; see processor::resolve_user_id.

§A curious fidelity note: both directions check UploadText

Python’s Activity enum has both UPLOAD_TEXT and DOWNLOAD_TEXT variants, and it would be reasonable to expect the response-direction (egress) check to use DOWNLOAD_TEXT. It does not: both PurviewPolicyMiddleware.process’s pre- and post-check call self._processor.process_messages(messages, Activity.UPLOAD_TEXT, ...) — the exact same activity constant, confirmed against the Python package’s own test suite (tests/test_middleware.py::test_middleware_processor_receives_correct_activity asserts Activity.UPLOAD_TEXT for both calls). This port mirrors that exactly rather than “fixing” it — see processor::ContentProcessor::evaluate’s caller in middleware.

§Layout

§Example

use std::sync::Arc;
use agent_framework_core::prelude::*;
use agent_framework_purview::{
    PurviewAgentMiddleware, PurviewAppLocation, PurviewLocationType, PurviewSettings,
    StaticTokenProvider,
};

let settings = PurviewSettings::new("My App")
    .with_tenant_id("00000000-0000-0000-0000-000000000000")
    .with_purview_app_location(PurviewAppLocation::new(
        PurviewLocationType::Application,
        "00000000-0000-0000-0000-000000000001", // the app registration's client id
    ));
// Bring your own Microsoft Graph bearer token (see the `auth` module docs).
let token_provider = StaticTokenProvider::new("<graph-bearer-token>");
let middleware = PurviewAgentMiddleware::new(token_provider, settings);

let agent = Agent::builder(client)
    .instructions("You are a helpful assistant.")
    .middleware(Arc::new(middleware))
    .build();

// A `Message` needs a GUID-shaped `additional_properties["user_id"]`
// (or `author_name`) for policy evaluation to run at all -- see the
// crate docs' "Scope" section.
let mut message = Message::user("Summarize this quarter's roadmap.");
message
    .additional_properties
    .insert("user_id".into(), serde_json::json!("00000000-0000-0000-0000-0000000000aa"));

let response = agent.run(vec![message], None).await?;
println!("{}", response.text());

§Auth burden

The Python reference accepts an azure-identity TokenCredential / AsyncTokenCredential directly, inheriting whatever credential chain the caller already has configured. This crate has no such dependency and is deliberately self-contained per this work package’s brief: implement TokenProvider to bring a Microsoft Graph bearer token (scope https://graph.microsoft.com/.default, or the equivalent for a custom PurviewSettings::graph_base_uri — see PurviewSettings::get_scopes), carrying the dataSecurityAndGovernance Graph permission. See the auth module docs for the full rationale; StaticTokenProvider is provided for a fixed/pre-fetched token (tests, short scripts, externally-managed refresh).

§Divergences from the Python reference (summary)

Re-exports§

pub use auth::StaticTokenProvider;
pub use auth::TokenProvider;
pub use client::PurviewClient;
pub use middleware::PurviewAgentMiddleware;
pub use middleware::PurviewChatMiddleware;
pub use models::Activity;
pub use models::DlpAction;
pub use models::DlpActionInfo;
pub use models::ProcessContentRequest;
pub use models::ProcessContentResponse;
pub use models::ProtectionScopeState;
pub use models::RestrictionAction;
pub use processor::ContentProcessor;
pub use settings::PurviewAppLocation;
pub use settings::PurviewLocationType;
pub use settings::PurviewSettings;

Modules§

auth
Authentication: a self-contained, bring-your-own bearer token trait.
client
PurviewClient: calls the Microsoft Graph processContent endpoint.
middleware
PurviewAgentMiddleware and PurviewChatMiddleware: enforce Purview policy on both the outgoing prompt and the model/agent response. Mirrors Python’s PurviewPolicyMiddleware (agent-level) and PurviewChatPolicyMiddleware (chat-client-level) — see the crate docs for which hook points these attach to and why both directions evaluate with Activity::UploadText.
models
Wire types for the Microsoft Graph dataSecurityAndGovernance processContent API.
processor
ContentProcessor: maps Messages to processContent requests, resolves the acting user id, and evaluates the resulting verdicts.
settings
PurviewSettings and the location-scoping types it carries. Mirrors Python’s agent_framework_purview._settings.