The companion piece on Prompt Shields and Spotlighting made the case that no single classifier is a boundary. You need layers, and on agents two of Azure’s own defenses don’t even fire. This post is the practical follow-on: which layers, and how you actually compose them around a Microsoft Agent Framework agent.
Here is the frustrating part I’ll get out of the way first. Microsoft ships solid, working code for every individual layer (Prompt Shields, Presidio, Content Safety, NeMo Guardrails, FIDES), but there is no official sample that wires all of them together around one MAF agent. The closest thing is a family of middleware classes in the langchain-azure package, and those target LangChain, not Agent Framework.
So the reference architecture below is one you have to assemble. Let me lay out where each piece fits, what it costs, and how it licenses.
What you’ll build#
- A reference stack that layers Presidio (PII), Azure Content Safety (harm categories), NeMo Guardrails (programmable rails), and FIDES (deterministic flow control) around one MAF agent.
- A decision on where enforcement runs: Foundry Guardrails, the standalone Content Safety API, or an APIM gateway policy.
- Where each tool attaches in the three-point pipeline (input, tool/RAG boundary, output), and the order that matters.
- The license and latency cost of each layer, so you stack only what the agent’s blast radius earns.
The map: three points, four tools#
Guardrails attach at three points around an agent (the input, the tool/RAG boundary, and the output), and different tools own different points.
detect + redact PII"]:::pii P1 --> S1["Prompt Shields
+ NeMo input rails"]:::shield S1 --> A["MAF agent
.AsBuilder().Use(...)"]:::core A --> TB["Tool / RAG boundary"]:::core TB --> F["FIDES labels
trusted vs untrusted"]:::det F --> P2["Presidio
redact retrieved docs"]:::pii P2 --> A A --> O1["Content Safety + PII filter
+ NeMo output rails"]:::rail O1 --> PM["Protected material
text / code"]:::shield PM --> R["Response"]
Nothing here is all-or-nothing: a read-only internal agent needs far less than one that emails customers. But the shape is the same: redact before the model sees data, shield and rail the input, label what crosses the tool boundary, and moderate the output.
Three ways to enforce it on Azure#
Before the individual tools, decide where the enforcement runs, because Azure gives you three architecturally distinct places to put a guardrail, and they are not interchangeable.
- Foundry Guardrails. A managed RAI policy you attach to a model deployment or hosted agent in the Foundry portal. The platform enforces it; your application writes no interception code. It’s the richest option (all four intervention points, Task Adherence, PII, protected material), but it requires a Foundry project, and, per the companion piece, Spotlighting and Groundedness don’t fire on agents through it.
- The standalone Content Safety API. Content Safety is a plain Cognitive Services resource. Provision it with
az cognitiveservices account create --kind ContentSafety --sku S0, and calltext:shieldPrompt,text:analyze, ortext:detectGroundednessdirectly from your own middleware, with no Foundry project and no model-deployment binding. This is the only path that works identically no matter what model or framework the agent runs, and it’s keyless:ContentSafetyClient(endpoint, DefaultAzureCredential())with theCognitive Services Userrole (a custom subdomain is required for Entra auth). - The APIM
llm-content-safetypolicy. Enforce at the gateway: one declarative XML policy checks Prompt Shields and harm categories for every model behind it, with the backend pointing at your standalone Content Safety resource. One catch worth knowing up front: the policy has no groundedness attribute, so for that you hand-write a<send-request>totext:detectGroundedness.
| Foundry Guardrails | Standalone Content Safety API | APIM policy | |
|---|---|---|---|
| Foundry project required | Yes | No | No |
| Works for a self-hosted / non-Azure agent | No | Yes | Yes (traffic routes through the gateway) |
| App integration code | None (platform-enforced) | You call the API and act on the result | None (XML policy only) |
| Prompt Shields | GA (models + agents) | GA | GA |
| Groundedness | Preview, models only | Preview (standalone) | Not supported |
| Best when | Foundry-hosted, central governance | Any model/framework, fine-grained control | A gateway is already in front |
Pick the enforcement point first; then place each tool below at the input, tool-boundary, or output stage.
Presidio: PII detection and redaction#
Presidio does one job well: find private entities in text (names, emails, credit cards, SSNs, phone numbers, and custom types you define) and anonymize them. The canonical pattern is three lines: analyze, then anonymize.
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
analyzer, anonymizer = AnalyzerEngine(), AnonymizerEngine()
results = analyzer.analyze(text=incoming, language="en")
clean = anonymizer.anonymize(
text=incoming, analyzer_results=results,
operators={"DEFAULT": OperatorConfig("replace", {"new_value": "<REDACTED>"})},
).textWhere it fits: run it on the incoming message and on any document you retrieve before it re-enters the model’s context, not just before you write logs. Domain-specific entities (claim numbers, policy IDs) come from a custom PatternRecognizer registered into the analyzer.
Three things the docs won’t tell you, from practitioners who run it:
- It’s community-owned now, and MIT-licensed. Presidio originated at Microsoft but has moved to a community project under the Data Privacy Stack org, so it is not an official, supported Microsoft product anymore. If you want a first-party, GA-supported PII service, that’s Azure AI Language PII detection (with
characterMask,entityMask,noMask, and a previewsyntheticReplacementmode), or the output-side PII content filter inside Foundry Guardrails. - The default recognizers are US/English-centric and throw false positives: a city named “Jordan” gets flagged as a person. Keep an allowlist of known-good terms.
- It’s CPU-heavy (the spaCy model), so initialize the engine once at startup, keep it warm, and set an explicit timeout so a slow analyze call can’t hang the whole model request.
Azure AI Content Safety: the harm categories#
Content Safety is the managed classifier for the four harm categories (hate, sexual, violence, self-harm), each scored Safe / Low / Medium / High, plus blocklists (exact-term or regex, including a prebuilt English profanity list) and custom categories in two flavors: standard (train your own ML classifier on samples, text only, takes hours) and rapid (LLM-based, fast, text and image, for emerging-incident response). It also covers protected material (copyrighted text and code) on the output side.
Where it fits: input and output. The cleanest wiring today is Microsoft’s own langchain-azure middleware, which is production-shaped even if you’re not on LangChain. It’s the port target for a MAF agent:
from langchain_azure_ai.agents.middleware import AzureContentModerationMiddleware
agent = create_agent(
model=model,
middleware=[
AzureContentModerationMiddleware(
categories=["Hate", "Violence", "SelfHarm"],
severity_threshold=4,
exit_behavior="error", # or "replace" / "continue"
)
],
)The same package ships AzurePromptShieldMiddleware, AzureGroundednessMiddleware, and AzureProtectedMaterialMiddleware. If you can front your models with API Management, the llm-content-safety policy does the same category and prompt-shield checks at the gateway, so every model behind it is covered without touching agent code.
NeMo Guardrails: programmable rails#
Presidio and Content Safety are classifiers. NeMo Guardrails (NVIDIA, Apache-2.0) is different. It’s a programmable layer that sits between your app and the model, with five rail types: input, dialog, retrieval, execution, and output. You write rails in Colang, a Python-like DSL, and it speaks Azure OpenAI natively since v0.22:
models:
- type: main
engine: azure
model: gpt-4
parameters:
azure_endpoint: https://my-resource.openai.azure.com/
azure_deployment: my-gpt4-deployment
api_version: "2024-02-15-preview"Where it fits: the programmable decisions the classifiers can’t express, like topic boundaries (“don’t discuss competitors”), conversation-flow constraints, and calling out to another check as a custom action.
Two honest caveats. NeMo’s bundled “content safety” example uses NVIDIA’s own NemoGuard model, not Azure Content Safety, so composing the two means writing a custom NeMo action that calls the Content Safety REST endpoint yourself, and no repo does this for you. And its LLM-based self-check rails add a full model round-trip each, which practitioners report doubles or triples input/output latency and cost; keep them off latency-sensitive paths or swap in a local model.
FIDES: the deterministic backstop#
Everything above is probabilistic. Agent Framework’s own answer to that is FIDES: information-flow control, shipped as an experimental, Python-only module. It labels every piece of content trusted or untrusted, propagates the label through tool calls (most-restrictive-wins), and lets a “sink” tool declare it refuses untrusted input:
class IntegrityLabel(str, Enum):
TRUSTED = "trusted"
UNTRUSTED = "untrusted"Where it fits: the trust boundary. A send_email tool can deterministically refuse to run on data that originated from an untrusted document, no matter what the model decided, and no matter whether a classifier caught the injection. It’s the structural layer the companion piece argued you can’t skip. Note it’s experimental and .NET is “coming soon,” so treat it as forward-looking for C# shops today.
Composing them around a MAF agent#
Native Agent Framework exposes middleware at three pipeline layers (agent run, function calling, chat client) via .AsBuilder().Use(...). The official sample chains PII and guardrail middleware in exactly this shape:
var guarded = agent
.AsBuilder()
.Use(PIIMiddleware, null) // redact before the model sees it
.Use(GuardrailMiddleware, null) // block/replace harmful content
.Build();PIIMiddleware and GuardrailMiddleware are hand-rolled regex and keyword filters, not calls into Presidio or Content Safety. The pipeline shape is real and supported; the bodies are illustrative.To get the production stack, you drop your Presidio call into the PII middleware and port the langchain-azure REST logic (the text:shieldPrompt and content-moderation calls) into the guardrail middleware. Order matters: redact (Presidio) → shield and rail the input (Prompt Shields, NeMo) → label the tool boundary (FIDES) → moderate the output (Content Safety, PII, protected material).
Licenses decide what you can ship, so keep them straight:
| Layer | License |
|---|---|
| Presidio | MIT (community-owned) |
| NeMo Guardrails | Apache-2.0 |
| FIDES | MIT (ships in Agent Framework) |
| Azure AI Content Safety | Paid Azure service |
| Azure AI Language PII | Paid Azure service |
The open-source layers are free to embed; the classifiers are metered API calls you budget for. Each layer also adds latency, so measure before you stack all four.
The portable core: if you’re not all-in on Azure#
Two of these layers don’t care about Azure at all. Presidio just formally became a vendor-neutral community project (still MIT), runs locally against any model, and is already the shared PII engine that Guardrails AI and NeMo Guardrails call under the hood. NeMo Guardrails (Apache-2.0) speaks any provider. So the redact-and-rail half of the stack ports as-is off Azure.
What you swap when you leave Azure is the classifiers. In place of Azure AI Content Safety, the open options are:
- Llama Guard: capable, but open-weight, not OSI open-source. A 700-million-MAU cutoff and a competitor clause matter for a commercial product.
- LLM-Guard (MIT).
- Guardrails AI (Apache-2.0).
Practitioners layer them cost-first: a cheap pattern/PII scan, then a small classifier, then the model, then output validators. Same defense-in-depth shape, self-hosted. That buys portability and shifts cost from per-token API calls to serving your own small models, at the price of the managed SLA and Microsoft’s continuously-updated classifiers.
The pragmatic call on an Azure-primary stack: keep the managed services as the default, and treat the open-source core as the hedge you already know how to reach for if you ever run multi-cloud or on-prem. Groundedness has its own set of portable substitutes, which the next post covers.
The bottom line#
- Microsoft ships every layer but no sample that composes them. The reference stack is yours to assemble, in a fixed order: redact, shield and rail the input, label the tool boundary, moderate the output.
- Decide where enforcement runs first. Foundry Guardrails, the standalone Content Safety API, and the APIM policy are not interchangeable, and only the standalone API is framework-agnostic.
- Presidio and NeMo are the portable, open-source half of the stack. The classifiers (Content Safety, Language PII) are the paid, Azure-locked half.
- The MAF middleware pipeline is real and supported, but the official sample’s filter bodies are placeholders. You supply the real Presidio and Content Safety calls.
- One layer must be deterministic. FIDES (or a least-privilege tool-call gate until .NET ships) is what holds when a probabilistic filter is bypassed.
Where to start#
- Redact first. Put Presidio (or Azure AI Language PII) on the input and on retrieved documents before they reach the model. This is the cheapest, highest-value layer and it protects your logs too.
- Shield and moderate with Prompt Shields on input and Content Safety on output, via the gateway if you can, so it’s one place, not per-agent.
- Add programmable rails only where you need them: topic boundaries and flow constraints NeMo expresses that a classifier can’t. Watch the latency; keep self-check rails off hot paths.
- Make one layer deterministic: FIDES labels (or, until .NET ships, a least-privilege tool-call gate) so a missed injection still can’t reach a sink.
- Don’t stack blindly. A read-only internal agent doesn’t need the full stack; each layer is 50–100ms and a metered call. Match the layers to the agent’s blast radius.
Next: the two Azure products both named “Groundedness” (one blocks ungrounded answers at runtime, one scores them in CI) and why picking the wrong one for RAG wastes a sprint.
