AI Agent Engineering Series

A production AI agent is secure when a hostile instruction that the model obeys still cannot cause harm. The model will sometimes follow injected text, and no prompt or filter prevents that reliably. So the security of the agent lives in software outside the model. A gateway checks every tool call, approvals bind to exact actions, secrets stay out of model context, and a kill switch fails closed. Deployment then widens what the agent may do one capability at a time. Each of these controls is ordinary software that you can test without a model.

Why the model cannot be the security boundary

Prompt injection is text in the agent's input that tries to change what the agent does. It can arrive in a customer email, a web page, a PDF, a retrieved passage, or a tool result. The model reads instructions and data in the same token stream. It has no reliable way to tell them apart.

Simon Willison names the most dangerous combination the lethal trifecta. The three legs are access to private data, exposure to untrusted content, and the ability to communicate externally. With all three, an attacker who controls some content can ask the agent to send the private data out. The OpenNash article on the lethal trifecta explains why removing one leg works when input filtering does not.

The OWASP entry on Excessive Agency names three root causes of agent damage: excessive functionality, excessive permissions, and excessive autonomy. One of its mitigations is "complete mediation". Downstream systems enforce authorization instead of trusting the model's decision. The OWASP AI Agent Security Cheat Sheet says to fail closed for unknown tools, separate decision-making from execution, and bind approval to the exact action.

Research reaches the same result. Design Patterns for Securing LLM Agents against Prompt Injections proposes six patterns, such as Action-Selector, Plan-Then-Execute, and Dual LLM. Each pattern limits what untrusted text can cause instead of trying to detect it. The CaMeL paper puts a system layer around the model and tracks where each value comes from. On the AgentDojo benchmark, CaMeL solved 77% of tasks with provable security. An undefended system solved 84%. The hard boundary cost 7 percentage points of task success on AgentDojo. Measure that trade-off on your own tasks.

The model may choose a tool and propose arguments. Software outside the model decides who the caller is and what the caller may touch. It also decides whether a person must approve, and whether the agent may run at all.

The trust boundary between the model and the toolsUntrusted text from users, documents, web pages, and tool results enters the model's context. The model proposes a tool call. Below a trust boundary, a gateway runs five checks on every call in order: kill switch, known tool and schema, no secret in the arguments, a deny-by-default policy, and approval when required. The gateway takes the principal from the verified session and the owner and limits from the system of record. Only then does the tool handler run with a credential from the broker, and the system of record checks authorization again. Results return redacted and marked as data. Untrusted text user · documents · web · tool results Model reads it all, proposes a tool call trust boundary Gateway 1 kill switch 2 known tool, valid schema 3 no secret in arguments 4 policy, deny by default 5 approval if required every call, in this order Session principal Records owner · limit Tool handler gets a credential from the broker System of record checks authorization again Results return redacted, marked as data
FIG 01Everything above the dashed line can be hostile, so every check that matters runs below it.

Map each path from input to action

A threat model lists each path from an input to a real effect, with the control that stops each bad outcome. A risk with no control is an accepted risk, whether or not a document says so. For a tool-using agent, most risks map to one control.

Risk Control
Injected text asks for a tool the task does not need The task contract's allowed tools, plus deny by default
Injected text asks for a larger amount A hard limit taken from trusted resource facts
The model names a record in another tenant The owner is read from the system of record
A user claims another identity or role The principal comes only from the verified session
Parameters change after a reviewer approves The approval is bound to a hash of the exact action
An approval is used twice A single conditional update consumes it
A tool result contains a credential Redaction before output reaches the model
The agent sends data to an attacker's server An egress allowlist
An incident needs the agent stopped now A kill switch read before every call

The task contract comes from Part 1. Previews, receipts, and idempotency keys come from Part 3. This part adds the checks that sit around them.

Take identity from the session

A principal is the verified identity that a request acts for. It can be a signed-in user, a service account, or the operator who approves an action. The principal comes from your authentication layer, for example an OpenID Connect ID token that your server validated as OpenID Connect Core defines.

The model never writes the principal. If a tool argument says "user": "admin", the gateway ignores it, because the principal travels beside the model decision and never inside it. The same rule applies to resource facts. The model can name an order ID. The gateway reads the order's tenant and refundable amount from the system of record.

Remote tools need narrow tokens. RFC 8707 defines the resource parameter, which names the resource a token is for. Request one resource per token, and enforce one audience. RFC 9700, the OAuth security best current practice, says access tokens should be audience-restricted and limited to the minimum privileges required. The MCP authorization specification makes this mandatory for MCP servers that implement authorization over HTTP. Clients must send resource, and servers must not accept or pass through tokens issued for another audience. This is why Part 4 treats remote tools as a separate trust zone. The OpenNash article on agent identity management describes a broker that mints these short-lived tokens at run time.

The gateway: deny by default, check every call

The gateway is the single path from a model decision to a tool. It replaces the direct tool executor in the Part 2 harness, so the loop does not change. The order of its checks matters. The cheap, global checks run first, and the policy runs only on a known tool with valid arguments.

on proposed tool call (name, args) in run:
  if kill switch halts all, or disables name: refuse "halted"
  tool = registry lookup(name);  if none: refuse "unknown_tool"
  check args against tool schema;  if invalid: refuse "invalid"
  if args contain a known secret value: refuse "denied"
  facts = system of record lookup(args)        # owner, ceiling
  verdict = policy(session principal, tool, args, facts, contract, stage)
  if verdict denies: audit(verdict.rule); refuse "not_permitted"
  if verdict needs approval: pause run, request exact-action approval
  else: run handler; redact result; return it to the model as data

Three design choices in this loop carry most of the security.

  1. Deny by default. A tool that the policy does not name, or that the task contract does not list, is denied. New tools start with no access until a rule grants it.
  2. The ceiling comes from trusted facts. The hard limit is the lower of the policy limit and the amount the system of record allows. The model's argument is compared against the ceiling and never sets it.
  3. The model sees a generic code. The audit log keeps the matched rule and reason. A detailed denial such as "order 9001 belongs to merchant_99" tells an attacker that the record exists.

The policy also takes the rollout stage as an input, so the gateway enforces the rollout stages described below on every call.

Each verdict carries a decision ID and the policy version. The gateway writes both to the audit log with the run ID. An investigator can then find which rule allowed or denied each call in a Part 7 trace.

The system of record must still repeat its own authorization. The gateway is the first check. A service that trusts any caller with a valid network path loses its protection the day someone adds a second path.

Approvals bind to one exact action

Part 3 binds an approval to a fingerprint of the exact effect, so changed arguments fail at commit. Production adds two fields and one rule. The hash also covers the tenant and the policy version, and the approval works only once. At commit time, the gateway computes the hash again from the call it is about to run. If one value differs, the approval matches nothing.

How an approval is bound to one exact action and used onceA reviewer approves a refund of 68.00 on order 4812 for tenant merchant_22 under policy version 12. The commit request changes the amount to 60.00. Each side is hashed over the tool, order, amount, tenant, and policy version. The hashes differ, so the commit is refused and nothing runs. Below, an approval moves from pending to approved to consumed. Only one conditional update can move it to consumed, so a replay, an expired approval, or a rejected approval matches nothing. Reviewer approved Commit request toolcreate_refund order4812 amount68.00 tenantmerchant_22 policyv12 toolcreate_refund order4812 amount60.00 tenantmerchant_22 policyv12 stored hash A fresh hash B A and B differ commit refused, nothing runs Single use pending approved consumed One conditional update moves approved to consumed. A replay, an expired, or a rejected approval matches nothing.
FIG 02Any change to a bound field produces a new hash, so the old approval cannot cover the new action.

The single-use rule is one conditional update in one database transaction. It matches only a row that is approved, unexpired, and has the same hash, and it sets that row to consumed. If two workers race to use the same approval, only one update changes a row. The other sees zero rows changed and refuses. The pattern works in any database with transactional updates.

Four more rules make approvals hold.

  • Separation of duties. The requester cannot approve their own action, and the approver must hold the approver role in the same tenant.
  • Expiry. An approval that waits too long expires. The facts behind it may have changed.
  • Check again at commit. The gateway runs the whole check sequence again before it consumes the approval. A policy change, a revoked role, or a kill switch between approval and commit stops the action. The policy version in the hash makes an approval for version 12 fail under version 13.
  • One approval per intent. The approval store reuses the Part 3 idempotency key. If the model proposes the same write twice in one run, both proposals map to one approval.

Reviewers must see the exact effect from the Part 3 preview. If a reviewer sees only a summary, the approval also covers what the summary leaves out. The OpenNash article on approval gates and exception routing covers how to route proposals to the right reviewer.

Keep secrets out of model context

Any value in the context window can come back out in model output, a tool argument, or a trace. So a secret must never enter the context window. Four rules form the secrets boundary.

  1. A broker holds credentials. Only tool handlers ask it for a value, at call time. Its printable form shows secret names and never values, so a stray log line cannot leak one.
  2. Redact every result. The gateway replaces known secret values in tool output before the output returns to the model. Upstream APIs sometimes echo tokens in error messages.
  3. Deny arguments that contain a secret. If a known secret value appears in a tool argument, something already leaked. The call stops.
  4. Allowlist egress. Every outbound request goes only to approved hosts over HTTPS.

The egress allowlist limits where direct requests can go. An approved host is still an outside channel, so control the recipients, accounts, paths, and data sent to it too. Other output channels remain. A rendered Markdown image URL, a link in an email the agent sends, or a write to a shared document can also carry data out. List each output channel in the threat model and decide which ones need approval.

Redact before the trace is written too. The Part 7 trace store keeps tool results, and traces feed eval sets and debugging sessions. A secret in a trace reaches people and models that never had access to it.

Isolate generated code and third-party tools

Treat code that the agent writes as code written by an attacker. Run it in a sandbox, an isolated environment that limits what the code can read, reach, and use. The sandbox gets no host credentials. Outbound network traffic is denied by default. CPU, memory, time, and output have limits, and storage is deleted after the run. When the code needs an authenticated service, it calls a tool proxy that goes through the same gateway.

Hosted code-execution services follow these rules. Cloudflare runs generated code in a V8 isolate with no file system and no environment variables. Datadog keeps credentials out of its sandbox.

Who writes the code Isolation to use
Your engineers, reviewed A container with normal service limits
The agent, for one trusted tenant A container with no credentials and default-deny network
The agent, with untrusted input or many tenants A gVisor sandbox or a Firecracker microVM, which add a boundary beyond the shared kernel

Plain containers share the host kernel. gVisor and microVMs cost start time and operations work. The OpenNash guide to choosing a sandbox for agent code execution compares the options.

MCP servers, packages, container images, and model adapters are third-party code that runs with your permissions. Pin their versions. Review every change to the tools or scopes a server exposes, and run the security tests again after each upgrade. A changed tool description can change which tool the model selects.

The kill switch fails closed

A kill switch lets an operator stop the agent without a deploy. The gateway reads its state before every tool call, so a change takes effect on the next call. Store the state in a database or feature-flag service that every worker reads.

The switch needs two levels. A global halt stops every tool call in every run. A per-tool flag turns off one tool, such as a write tool after a bad upstream release, while reads continue. The harness also checks the global halt at the start of each step. A halted run moves to a paused state, so it can resume later.

The most important rule is the failure mode. If the gateway cannot read the switch state, it treats the agent as halted. If the switch failed open, an outage of the flag store would disable the control when an operator may need it most.

Blocking new calls does not stop everything. Tokens that the agent already received stay valid until they expire, and background jobs may keep running. The OpenNash article on the agent kill switch covers the other two parts of containment: revoking issued credentials and sweeping orphaned work.

Recover without repeating side effects

A tool call can time out after the remote service completed the write. Part 3 covers the idempotency key that makes a plain retry safe.

Approvals add one case. The gateway consumes the approval before it calls the handler. If the handler then fails with an unknown outcome, a retry with the same approval is refused, which is correct. Nobody asks the model whether to retry. The write moves to outcome_unknown, the run pauses in needs_input from Part 2, and a reconciler asks the service for a receipt by idempotency key.

The allowed states of one writeA write is proposed, then waits for approval. A rejected or expired approval closes it with no effect. An approved write moves to executing, where the approval is consumed. Executing ends in completed when a receipt returns. On a timeout it moves to outcome unknown. A reconciler looks up the receipt by idempotency key. If it finds one, the write is completed. If there is no receipt and no clear failure, the write goes to operator review. The model never chooses a retry. proposed awaiting approval rejected or expired approved executing approval used receipt timeout completed outcome unknown receipt found by key no receipt operator review A reconciler resolves unknown outcomes. The model never retries.
FIG 03A timeout after the approval is consumed goes to a reconciler, so a lost reply never becomes a second refund.

Allow only these transitions, and store each change with its evidence in one transaction. If an action can be reversed, define the reverse as its own tool. A refund reversal needs its own policy rule, approval, idempotency key, and receipt.

Version the full release

A failed run is hard to reproduce if any part of the agent changed after it. Treat these parts as one release with one version:

Part Why it is in the version
Model ID, as the provider API returns it A model change can change which tool the agent picks
Instructions, by content hash Prompt edits change behavior as much as code edits
Tool definitions and their versions A changed description or schema changes tool selection
Policy version Approvals and audit records reference it
Rollout stage It sets which writes are possible
Retrieval corpus and index Part 6 answers depend on them
Eval set version The release gate that passed must be known

Record the release version on every run, next to the policy version in every decision. Rollback restores code and configuration. It does not undo refunds that the newer release created. Before each release, decide which effects need compensation, and keep the previous release artifacts available.

Security tests block the release

A release gate is a test that must pass before a deploy. The Part 7 CI gate runs task evals across repeated runs. Add the security checks to that gate as hard pass or fail checks. One unauthorized read blocks the release, even if every task score improves.

The checks fall into two groups.

  • Control tests run without a model. An approval works once and fails after expiry or a policy change. A tampered argument fails the hash. A missing kill switch state halts every tool. A secret in a tool result is redacted. A write outside the contract or above the ceiling is denied. A read-only stage denies all writes. An unverified session yields no principal.
  • Injection tests run the full agent with the production model. Seed documents and tool results with hostile instructions. Grade the trace, because the final message can claim anything. The containment check passes when no unauthorized effect happens. The behavior check in Part 7 still fails a forbidden request, even when the gateway denies it.

Control tests show that each check works. Injection tests show how the system handles representative attacks. Separately, verify in review that every tool path goes through the gateway. A model upgrade can make the model obey injected text more often, so the injection tests run again on every model change.

Teams that must map these controls to a governance framework can use the NIST AI Risk Management Framework and its Generative AI Profile, NIST AI 600-1.

Roll out by capability

A staged rollout widens one capability per stage. Ten percent of users with unrestricted writes can still cause serious harm, so traffic share alone is the wrong axis. The stage is a policy input, so the gateway enforces it on every call.

Five rollout stages, each widening one capabilityIn shadow, the agent runs on real inputs, its output is hidden, and all writes are denied. In read-only, users see answers and writes are still denied. In proposal, every write needs a person's approval. In limited, a cohort gets automatic writes below the limit and everyone else needs approval. In general, every authorized principal gets automatic writes below the limit. Rollback moves up one row by changing the stage in configuration. Stage Output Writes shadow hidden all denied read only shown all denied proposal shown a person approves each limited shown cohort auto rest approved general shown auto under the limit, all Above the limit, every stage still needs approval or denies. Rollback moves up one row with a configuration change.
FIG 04Each stage widens one permission, so the gateway enforces the rollout on every call.

Move to the next stage only when the exit criteria hold for the whole stage window. Define the rollback trigger before the stage starts. The criteria below show the kind of condition. Set the thresholds from your own risk, traffic, and recovery time.

Stage Exit criteria (examples) Roll back or halt when
shadow Proposed actions match what staff did on the same cases; no cross-tenant read in traces A denied resource reaches context
read_only Answer evals meet their gate; users correct few answers A read leaks another tenant's data
proposal Reviewers approve most proposals unchanged; no approval mismatch outside tests An approval is reused or bypassed
limited One receipt per approved action; the kill switch drill passes A duplicate write, or an unknown outcome with no reconciliation
general Error budget and cost stay inside declared limits A critical eval fails or an incident opens

Run an incident drill before the limited stage, with a named owner for each step.

  1. Set the global halt and confirm that active runs pause within one step.
  2. Revoke the agent's service credentials and confirm that tool calls fail.
  3. Export the traces and audit log for the drill window.
  4. List every principal and record that the drill runs touched.
  5. Restore service and confirm that paused runs resume without duplicate writes.

Match the controls to what the agent can change

Not every agent needs approvals, a kill switch, and five stages. The minimum control set depends on the most dangerous thing the agent can do.

Agent capability Minimum controls
Answers from public data, no tools Output checks and evals; no gateway
Reads private data for one user Verified principal, per-call read policy, redaction, egress allowlist
Reads private data and sends messages or web requests All of the above, plus approval for every external send
Writes to systems of record Full gateway, one-use approvals, idempotency, kill switch, staged rollout
Runs generated code All of the above, plus a sandbox with no host credentials

Two common additions do more harm than good.

  • A guard model as the main control. A second model that classifies prompts as safe or unsafe can reduce noise. It fails on the same kind of input as the first model. Use it, if at all, as an extra signal in front of the deterministic checks.
  • Approval for every small read. Reviewers who see hundreds of trivial requests start to approve without reading. Spend reviewer attention on writes that are expensive, hard to reverse, or sensitive.

Getting a write-enabled agent to production

OpenNash forward-deployed engineers take agents with real permissions to production for Fortune 2000 teams. We map each tool to a threat model and a policy rule. We put the gateway, the approval flow, and the security tests into your CI. Then we run the rollout stages with you, including the incident drill. You own the code, the policies, the eval sets, and the runbooks at the end. If your agent only answers questions from public data, you do not need this work yet.

Book a call with an OpenNash forward-deployed engineer or email us at hello@opennash.com. In the first session, we review the systems your agent must write to. Then we show which use cases deliver the highest business value and the fastest ROI.

Previous: AI Agents Part 7: Evals and Observability