All articlesSecurity

Letting an agent act as you - safely

Autonomy without guardrails is a liability. The three decisions that make agent actions shippable: forwarded identity, human-in-the-loop on writes, and an immutable audit trail - at scale.

By Moses OtienoAug 5, 202612 min read

Letting an agent act as you - safely

An agent with tools is a program that takes actions in your systems based on the output of a language model. Read that sentence again and the reason most "AI agents" never make it past a demo becomes obvious: nobody wants a probabilistic process holding a master key to production. Autonomy without guardrails isn't a feature, it's a liability.

The good news is that this is a solved problem — not with a clever prompt, but with three boring, load-bearing engineering decisions. The agents worth putting in production all follow the same model: the agent acts as the signed-in user, it pauses for approval before it changes anything, and it writes an immutable audit record of what it did. Here's how each one works, and how to make it hold at scale.

The threat model: the confused deputy

The classic failure mode of a tool-using agent is the confused deputy: a component with broad privileges is tricked by input into using them on someone's behalf who shouldn't have them. An agent is the perfect confused deputy — it runs with whatever credentials you gave it, and its "input" is natural language that a user (or a document, or another agent) can shape.

If your agent authenticates to downstream services with a single service account that can read everyone's data, then a prompt is all that stands between "summarise my documents" and "summarise everyone's documents." No prompt is a security boundary. So we don't build that agent.

1. Forwarded identity — the agent is never more privileged than you

The foundational move: an agent acts as the person using it. The caller's identity token is forwarded through the agent into every downstream call, and the data services do their normal per-user authorization. The agent gets no ambient authority of its own — it can only ever do what the signed-in user could already do by hand.

goagent/tools/forward.go
 // Every tool call rebuilds its outgoing context from the CALLER's token, // never the agent's service-account token. Downstream services authorize // against the real user and return PermissionDenied if they shouldn't see it. func (t *searchDocuments) Call(ctx context.Context, args Args) (Result, error) { userTok := callerToken(ctx) // the signed-in user's JWT, forwarded in if userTok == "" { return Result{}, status.Error(codes.Unauthenticated, "no caller identity") } reqCtx := metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+userTok) // The docs service runs its OWN per-user visibility check. If this user // can't see the doc, the agent can't either — enforced there, not here. resp, err := docsService.Search(reqCtx, &pb.SearchRequest{Query: args.Query}) if status.Code(err) == codes.PermissionDenied { return Result{Note: "no results you have access to"}, nil } return toResult(resp), err } 

The subtle, important part: exposing a tool to the model does not grant data. The allow-list of tools is about focus, not privilege. Security lives downstream, in the services that enforce record-level access against the forwarded identity — so even a fully-capable agent can't return a byte the caller couldn't already fetch themselves.

Usersigns in → JWTAgentno authority of its ownforwards the user's tokenData serviceper-user authorizationPermissionDenied if not allowedas youstill you
The user's identity flows unbroken through the agent to the data service, which enforces access. The agent is a conduit, never a privilege-escalation point.

2. Human-in-the-loop that actually scales

Reading is automatic; acting isn't. Any operation that changes data — or any long-running, data-touching job — pauses and shows the user a plan before it runs: what it will touch, what it will change, roughly how long. Nothing mutates until a human says go.

The trap is prompt fatigue: approve every trivial step and people start rubber-stamping, which is worse than no gate at all. Two things keep it scalable. First, gate on writes and spend, not reads — most of what an agent does needs no approval. Second, tie the approval to the person and re-check it at run time: the plan a user approved is bound to their identity and re-authorized against the forwarded token when it actually executes, so an approval can't be replayed by something else or drift out of scope between "yes" and "go." A2A's INPUT_REQUIRED task state (see A2A in production) is what carries this over the wire without holding a connection open.

3. Everything is audited — the record auditors actually ask for

Every mutating action lands in an append-only, integrity-checked audit trail: who (the real user, not the service account), what tool, what arguments, what changed, and when. Not debug logs you can grep and lose — a first-class, tamper-evident record. When a client's compliance team asks "what has this agent done, and on whose authority," the answer is a query, not an investigation.

If an agent can take an action and you can't later prove who authorized it and what it did, you don't have an agent you can put in production — you have an incident waiting for a date.

Making it hold at scale

The three principles above are the shape; scale is in the plumbing that carries them across many users, services and clients:

  • Per-user OAuth. Access to each external system is granted, scoped and revoked per person — no shared super-account an agent borrows. When someone leaves, their agent access leaves with them.
  • Zero-trust transport. mTLS between services, OIDC identity, encryption in transit and at rest. Every hop re-verifies; nothing is trusted because it's "inside."
  • Guardrails that don't depend on the model. Budget ceilings, output validation and the approval gate are properties of the architecture, so they hold no matter which model is in the seat — the same reason we can stay model-agnostic without loosening security.
  • Controls mapped to SOC 2 from day one, so "is this auditable?" has a paper trail, not a promise.

Put together, these turn autonomy from a liability into something you can actually sign off on. The agent acts as you, asks before it changes anything, and leaves a record — which is precisely what it takes to let software take real actions in systems that matter. That's the whole game: not the most autonomous agent, but the most trustworthy one. Secure and at scale isn't a tagline; it's the set of decisions that let an agent out of the demo and into production.