Prompt guardrails are dead. The data layer won.

Prompt guardrails are dead. The data layer won.

When an agent chains tool calls and decides to delete /prod, the only check that matters is at the resource.

Why can’t I just tell the agent not to delete production?

Because a system prompt is a suggestion, not a fence. “Never delete production” lives in the same context window an attacker — or an over-eager ticket — can overwrite. Instructions and data share a channel, and the model can’t reliably tell which is which. You’re writing “do not touch” on a door that anyone downstream can un-write. Guardrails at the framework level are worth having. They are the first thing that fails.

So put the approval in the app, right?

That works until the agent stops asking. The whole point of an agentic workflow is that it plans, calls a tool, reads the result, and calls the next tool — without returning to a human between steps. “Approval before action” assumes there’s one action and one moment to intercept it. An agent doing SharePoint cleanup might make forty Graph calls in nine seconds. Gate every one and you’ve built a worse Clippy: a bot that asks “are you sure?” until the human clicks yes on reflex. Gate none and you’ve built an unsupervised intern with root.

What does “governance in the data layer” actually mean?

Move the check to where the damage happens, not where the intent is expressed. The agent can ask to delete a site all it likes; the question is whether the resource manager, database, or API gateway will honor it. Concretely: Entra Conditional Access decides whether the identity even connects, Azure API Management inspects and throttles the call, Azure Policy refuses to delete a resource tagged env=prod, and row-level security means the query returns nothing it shouldn’t. None of these read the prompt. None of them care what the model “meant.” They enforce on the object.

Walk me through where each layer catches a bad delete.

The scenario: an agent reads a Slack ticket that says “clean up old files,” calls Graph to list sites, and decides the marketing archive is old. It holds a service principal token with Sites.FullControl.All. It fires:

Program.csC#
await graphClient.Sites["marketing-archive-site-id"]
    .DeleteAsync();  // valid token, valid scope, catastrophic intent

Layer one — framework guardrails — never sees a policy violation; the model reasoned its way here. Layer two — the API scope — passes, because someone granted Sites.FullControl.All and a token with that scope is exactly what deletes sites. Layer three is the only place left. If a retention lock or an Azure Policy deny sits on that resource, the delete returns 403 and your Sunday stays quiet.

Which layer actually stops it, then?

The data layer — but only if you narrowed the identity first. The real fix is that the agent should never have held Sites.FullControl.All. Scope it to Sites.Selected, grant it two sites, and the marketing archive isn’t in its universe. Defense in depth means all three layers, but they aren’t equal: identity scope shrinks the blast radius, and data-layer policy catches what leaks through.

run.shbash — zsh
# Azure Policy denies delete on prod-tagged resources — token or no token
az policy assignment create \
  --name "deny-delete-prod" \
  --policy "catalog-deny-delete-tagged" \
  --params '{"tagName":{"value":"env"},"tagValue":{"value":"prod"}}'

How do I find out what my agents can already do?

You almost certainly have a service principal with more Graph permissions than anyone remembers granting. Enumerate the app role assignments before you go to bed tonight:

audit.ps1PowerShell
Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.Read.All"
Get-MgServicePrincipal -All | ForEach-Object {
  $sp = $_
  Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
    Select-Object @{n='Agent';e={$sp.DisplayName}}, AppRoleId, ResourceDisplayName
} | Where-Object ResourceDisplayName -eq "Microsoft Graph"

Cross-reference the AppRoleId values against the Graph permissions reference. Anything holding .All write scopes for an autonomous agent is a finding, not a feature. Do the same for managed identities with az role assignment list --all --assignee <principal-id>.

Does this fix everything?

No, and pretending otherwise is how you get breached politely. Data-layer policy stops unauthorized actions. It does nothing about an agent exfiltrating data it’s genuinely allowed to read — a summarizer with legitimate access to the finance channel that helpfully pastes numbers into an external tool is inside every rule you wrote. And no policy engine stops the agent that social-engineers a human into clicking approve. Authorization is the floor. The behavior of things you authorized is the next problem, and it’s harder.

Write “don’t delete prod” in the prompt if it makes you feel better. Then go take away the token that could.