Skip to main content

Your AI Agent Is a Privileged User. Architect It Like One.

• By Yuriy Mykhalchuk • 13 min read
Featured image for Your AI Agent Is a Privileged User. Architect It Like One.
Image credit: Original illustration

Your AI Agent Is a Privileged User. Architect It Like One.

For a long time, adding AI to a SaaS product mostly meant adding another kind of input and output. A user sends a question, the application builds a prompt, the model returns text, and maybe the result gets stored somewhere or shown to the user.

That architecture already carries real risks — prompt injection, data leakage, hallucinations, accidental disclosure of tenant data. But the security model changes far more dramatically the moment the AI can actually do things. Give a model access to tools, APIs, databases, queues, email, billing systems, or admin functions, and it stops being a text-generation component. It becomes an actor inside your system.

And I think the most useful mental model here is a fairly boring one:

Treat your AI agent as a privileged user.

Not as a magical colleague, and not as some intelligent exception to your security model — just another principal that can request operations, whose authority should be constrained, observable, and revocable.

Recent events make that distinction feel a lot less theoretical.

When the model gets hands

In September 2026, Spain’s data protection authority, AEPD, disclosed what it described as the first breach notification it had received involving an attack executed through an AI agent.

According to the report submitted to the regulator, the agent identified vulnerabilities, gained access to a system, modified personal data, and accessed billing records with minimal human intervention. The investigation is still ongoing, so it’d be premature to draw firm conclusions about the root cause or the technologies involved — Reuters also noted that neither the underlying language model nor its provider were believed to have been compromised.

This doesn’t prove autonomous agents are suddenly escaping everywhere. It demonstrates something more mundane, and honestly more useful for software architects: AI can compress an attack chain that previously required a lot more human interaction. Once an agent can discover, decide, and execute on its own, the speed and scale of an existing security problem can change substantially.

Around the same time, JPMorgan was taking a surprisingly pragmatic approach to the flip side of the same problem — the bank introduced monthly Claude spending limits of up to $2,000 for selected users, and has been rolling out an isolated AWS environment called Devspace, meant to separate AI-assisted development from sensitive internal systems and employee credentials.

Those two stories look pretty different on the surface — one’s about security, the other’s mostly about cost. Architecturally, though, they’re closely related. Both are really about bounding autonomy: what can the agent access, what can it change, how far can one action propagate, how many times can it retry, how much money can it spend, and what actually stops it if things go wrong.


A prompt is not an authorization system

Imagine an AI agent with a tool called:

DeleteCustomerDocument(documentId)

There are two very different ways to make this “safe.” The first is to tell the model:

Never delete a document unless the user is authorized
and explicitly asks you to do so.

That instruction might be useful context, but it isn’t a security boundary. The model operates on probabilistic interpretation, and its context may contain user input, retrieved documents, emails, web pages, tool results, or other content you don’t fully control. An authorization decision shouldn’t depend on whether the model correctly remembers a sentence buried in its system prompt.

The safer architecture looks more like this:

User / external input
        ↓
   LLM / planner
        ↓
   Proposed action
        ↓
   Policy gate
        ↓
   Tool executor
        ↓
   Business system

The model can say “I want to delete document f397...,” but the application independently decides whether that action is actually allowed. This is now explicitly reflected in OWASP’s guidance for AI agents too: use least privilege, scope permissions per tool, require independent validation for sensitive actions, separate decision-making from execution, and never rely solely on model output for authorization.

It’s a lot less exciting than prompt engineering. That’s exactly why I like it.


The agent can propose. The application must authorize.

A simplified .NET implementation might look something like this:

public sealed record ProposedAgentAction(
    string Tool,
    Guid ResourceId,
    IReadOnlyDictionary<string, object?> Arguments);

public async Task<ActionAuthorizationResult> AuthorizeAsync(
    AgentExecutionContext context,
    ProposedAgentAction action,
    CancellationToken cancellationToken)
{
    await tenantAccess.EnsureResourceBelongsToTenantAsync(
        context.TenantId,
        action.ResourceId,
        cancellationToken);

    await permissions.EnsureAllowedAsync(
        context.ActorId,
        action.Tool,
        action.ResourceId,
        cancellationToken);

    await budget.EnsureAvailableAsync(
        context.TenantId,
        action.Tool,
        cancellationToken);

    return ActionAuthorizationResult.Allowed();
}

The exact implementation isn’t the important part — the separation is. The LLM produces an intent; deterministic application code decides whether that intent gets to become a side effect. If deleting data requires approval, the model can’t invent that approval. If a tenant doesn’t own a resource, the model can’t reason its way around that. And if the budget’s exhausted, no matter how persuasive the prompt is, it doesn’t create more money.


Tenant context is not prompt context

This becomes especially important in multi-tenant SaaS.

Say the model is currently working for Tenant A. You could include this in the prompt:

The current tenant is Acme Ltd.
Only work with Acme Ltd. data.

Useful context, again — but a terrible isolation boundary. The real tenant identifier should come from trusted application state (authenticated identity, a validated execution context, a resource lookup), not from a conversation with the model.

This is one of the patterns I already use in AlturaQuantera, an AI-assisted compliance SaaS I’ve been building. For example, when the application retrieves a generated document asset, the query is scoped not just by the asset ID, but independently by TenantId:

return await dbContext.DocumentAssets
    .FirstOrDefaultAsync(
        a => a.Id == documentAssetId &&
             a.TenantId == tenantId &&
             a.SourceType == DocumentSourceType.GeneratedPolicy &&
             !a.IsDeleted,
        cancellationToken);

The interesting part isn’t Entity Framework — it’s that knowing a resource ID doesn’t imply authority to access it. The resource only gets resolved inside an already-established tenant boundary. The same idea has to hold once execution leaves the original HTTP request.


Background jobs need an identity too

AI-heavy work often moves to queues and background workers, which creates a subtle problem: the original HTTP request had an authenticated user, claims, and tenant context — a Hangfire worker running thirty seconds later doesn’t.

That doesn’t mean the job should just run in some mysterious global context. In AlturaQuantera, an assessment background job first resolves the persisted assessment and pulls its tenant from there:

Assessment? assessment = await dbContext.Assessments
    .AsNoTracking()
    .Include(a => a.Framework)
    .FirstOrDefaultAsync(
        a => a.Id == assessmentId,
        cancellationToken);

var command = new RunAiGovAssessmentCommand(
    assessmentId,
    assessment!.TenantId);

await commandHandler.Handle(command, cancellationToken);

Policy generation follows the same pattern — the job reloads the policy revision, pulls its TenantId, and passes it explicitly into the application command. This matters a lot more once agents start chaining operations together. A queue shouldn’t magically erase security context, and neither should an MCP call, a webhook, a scheduled task, or a secondary agent. Somewhere in that chain, there has to be a trusted answer to: on whose authority is this operation being performed?


Not every AI result should become system state

There’s another distinction I keep finding useful: generation is not execution.

An AI model generating a proposed policy is one operation. Publishing that policy into the organization’s document set is a different one entirely. An agent recommending a customer record should change is one thing; actually changing it is another. An agent drafting an email is one operation — sending it to 4,000 customers is very much another.

AlturaQuantera happens to have a practical example of this. Policy generation and policy publication are kept as separate workflows, and publishing records the document lineage, linking the generated policy revision to a published document revision:

dbContext.DocumentRevisions.Add(newRevision);

asset.SetCurrentRevision(newRevision.Id);

policyRevision.SetPublishedDocumentRevision(
    newRevision.Id);

policyAsset.LinkDocumentAsset(asset.Id);

It also checks existing publication state so it doesn’t accidentally create duplicate published revisions. There’s no autonomous agent deciding to publish policies in AlturaQuantera today — but the architectural boundary is already valuable on its own. If an agent got added later, “the model produced content” still wouldn’t mean “the system has accepted this as published business state.” That gap is exactly where approval, validation, and policy enforcement belong.


Side effects should be boring

Which leads to a broader principle: the more interesting the AI becomes, the more boring its execution layer should be.

Explicit APIs. Explicit permissions. Typed commands. Idempotent operations. Queues, retries with limits, transactions, audit records, approval states, circuit breakers — the usual tools of software engineering suddenly double as AI safety mechanisms.

Take idempotency. If an agent retries PublishPolicy(123) three times because it’s unsure whether the first call succeeded, you really don’t want three published copies. AlturaQuantera’s publication flow checks whether a policy revision already has a published document revision and just returns the existing one when appropriate, rather than blindly producing another side effect.

That pattern wasn’t invented for AI. But agents make it a lot more important — probabilistic planners sitting on top of deterministic systems make retries, duplicate requests, and partially completed workflows something you should expect, not treat as bizarre edge cases.


Authority is not the only thing that needs a limit

Security engineers naturally ask what an agent is allowed to do. Increasingly, SaaS builders need to ask a second question too: how much is this agent allowed to spend while doing it?

That’s why I find the JPMorgan example interesting. A monthly Claude spending limit and a sandboxed execution environment look like separate FinOps and security decisions on paper. In an agentic architecture, they’re really two versions of the same control — a boundary around an expensive capability.

An autonomous workflow can fail financially without ever crossing a traditional authorization boundary at all. It might repeatedly call the same model, retry an unsuccessful plan, fan out into dozens of sub-tasks, read increasingly large documents, invoke expensive tools recursively, or just quietly process far more workload than anyone expected.

OWASP explicitly describes this as a denial-of-wallet risk, and recommends limits on tokens, cost, retries, and tool-chain depth — along with per-tenant request and spend limits, monitoring, and circuit breakers for unusual cost or tool-call spikes.

In a SaaS product, I’d think about these controls as layers:

Product quota
    ↓
Request / operation limit
    ↓
Token budget
    ↓
Monetary budget
    ↓
Retry / chain-depth limit
    ↓
Emergency disable

You probably don’t need every layer on day one. But “unlimited until the invoice arrives” isn’t a particularly robust architecture.


What I currently enforce — and what I don’t

This is another place where terminology matters.

AlturaQuantera already has real product-level runtime quota enforcement. Before some expensive operations, the application checks the tenant’s subscription and current usage:

await usageLimiter.CheckLimitAsync(
    tenantId,
    UsageAction.AssessmentRun,
    1,
    cancellationToken);

The limiter can reject the operation when there’s no valid subscription, or when the tenant would exceed its configured quota:

if (current + amount > max)
{
    throw new LimitExceededException(...);
}

The operation is checked before execution, and successful usage is accounted for afterward, so a failed assessment doesn’t just silently consume the quota. Separately, AI usage logging records token counts and can calculate an estimated dollar cost:

decimal? estimatedCostUsd = costOptions.Enabled
    ? AiCostCalculator.Calculate(
        operationType,
        inputTokens,
        outputTokens,
        costOptions)
    : null;

That gives observability — but it’s not the same thing as a financial guardrail. Today, AlturaQuantera doesn’t reject an AI request because a tenant crossed some token count or dollar-denominated AI budget. Cost is measured, product quotas are enforced, but hard token and monetary ceilings aren’t there yet. If I were giving an agent broader autonomous tool access, that’s one of the first boundaries I’d add.

The distinction is worth keeping in mind generally: measuring a risk is not the same as constraining it.


Logging everything is not observability

There’s also a fairly dangerous reading of “audit your AI” floating around — the idea that you should store the entire prompt, retrieved context, model response, and every tool payload, so any incident can theoretically be reconstructed later.

That can quietly create a second data-leak problem. Your logs may now contain personal data, confidential customer documents, credentials, internal instructions, and whatever else happened to land in the model’s context.

The AI usage entity in AlturaQuantera deliberately takes the opposite approach:

// Do NOT store prompts, completions,
// document text, or tenant document content.
//
// ErrorCode must be a short message class/code,
// never a full exception message containing prompt content.

Instead, it keeps operational metadata — tenant, operation type, model, token usage, estimated cost, resource identifiers, correlation IDs. There’s no universal answer here, and some systems will legitimately need richer audit evidence. But the principle is worth holding onto: audit the action without automatically duplicating all the sensitive data that caused it.


Human approval should bind to the action, not the conversation

For sufficiently sensitive operations, deterministic authorization on its own might not be enough — you may genuinely want a human checkpoint. But even there, a button that just says “Approve agent?” doesn’t really mean much.

Approval should bind to a concrete operation. Something like:

Actor: agent:invoice-assistant
Tenant: 84b1...
Action: issue_refund
Resource: payment:91827
Amount: €1,240
Expires: 14:35 UTC

If the agent changes the amount or the target after approval, it should need a fresh approval — not reuse the old one. This lines up with what OWASP recommends for high-impact agent actions too: bind authorization to the actor, tool, resource, and normalized parameters, keep it short-lived, protect against replay, and use stronger authentication for the most critical operations.

Once again — the model may propose. It shouldn’t get to manufacture its own authorization.


The architecture I would want

If I were designing a tool-using agent inside a multi-tenant SaaS today, I’d want every executable action to pass through something like this:

                         ┌───────────────┐
                         │   Identity    │
                         ├───────────────┤
                         │ Tenant scope  │
                         ├───────────────┤
User → LLM → Proposal →  │ Capabilities  │ → Executor → Business system
                         ├───────────────┤
                         │   Approval    │
                         ├───────────────┤
                         │    Budget     │
                         └───────────────┘
                                │
                                ↓
                       Audit / telemetry

Not every action needs every check — reading a public help article isn’t the same as deleting customer data, generating a draft isn’t the same as publishing it, and running a €0.002 classification isn’t the same as launching a recursive research workflow. The trick isn’t eliminating autonomy. It’s making autonomy explicit and bounded.


AI security is becoming ordinary software engineering again

There’s a tendency to treat agent safety as a completely new discipline that needs completely new answers. Some of it genuinely is new — prompt injection is strange, probabilistic planning is strange, and giving software a natural-language objective instead of a deterministic sequence of instructions creates failure modes we’re still learning to understand.

But a surprising amount of production agent safety looks pretty familiar once you get into it: least-privilege identities, scoped APIs, tenant isolation, explicit state transitions, idempotency, queues and controlled workers, human approval for high-impact operations, usage quotas, cost budgets, structured audit logs, circuit breakers and kill switches.

In other words, the safest agent architecture is probably a lot less futuristic than the agent itself.

The moment your AI gains tools, credentials, and the ability to change state, stop treating it as a smarter text box. Treat it as a privileged user. Then architect it like one.