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’sPurviewClient.get_user_info_from_tokendecodes the bearer token’s JWT payload (tid/oid/appidclaims, unverified — no signature check either side) to fill intenant_idwhenPurviewSettings.tenant_idis unset, and an application-location fallback whenPurviewSettings.purview_app_locationis unset. This port requires both to be set explicitly onPurviewSettings— attempting to evaluate without them is aagent_framework_core::error::Error::Configurationerror (itself subject toPurviewSettings::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-shapedauthor_name— has nothing to do with the bearer token and is faithfully ported; seeprocessor::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
settings—PurviewSettings,PurviewAppLocation,PurviewLocationType.auth—TokenProvider: bring-your-own bearer token (see “Auth burden” below).models— theprocessContentrequest/response wire shapes.client—PurviewClient: the singleprocessContentHTTP call.processor—processor::ContentProcessor: message → request mapping, user-id resolution, and the per-message evaluate-until-blocked loop.middleware—PurviewAgentMiddleware/PurviewChatMiddleware: the two middleware hook points.
§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)
- Calls only
processContent— noprotectionScopes/computeprecheck, no caching, no backgroundcontentActivitieslogging (see “Scope” above). - No JWT-derived
tenant_id/ app-location fallback — both must be set explicitly onPurviewSettings(see “Scope” above). - No distinct exception type hierarchy
(
PurviewAuthenticationError/PurviewRateLimitError/…): every HTTP failure becomesagent_framework_core::error::Error::ServiceStatus, carrying the real status code; callers (namely this crate’s own middleware) branch onagent_framework_core::error::Error::statusthe same way Python’sexcept PurviewPaymentRequiredErrorbranches on exception type — status 402 is still special-cased identically (seeclient::PurviewClient::process_contentandmiddleware::PurviewAgentMiddleware).
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 GraphprocessContentendpoint.- middleware
PurviewAgentMiddlewareandPurviewChatMiddleware: enforce Purview policy on both the outgoing prompt and the model/agent response. Mirrors Python’sPurviewPolicyMiddleware(agent-level) andPurviewChatPolicyMiddleware(chat-client-level) — see the crate docs for which hook points these attach to and why both directions evaluate withActivity::UploadText.- models
- Wire types for the Microsoft Graph
dataSecurityAndGovernanceprocessContentAPI. - processor
ContentProcessor: mapsMessages toprocessContentrequests, resolves the acting user id, and evaluates the resulting verdicts.- settings
PurviewSettingsand the location-scoping types it carries. Mirrors Python’sagent_framework_purview._settings.