Runtime Security for AI Agents: Why Static Guardrails Are Not Enough
Why Static Guardrails Fail
A system prompt set at deploy time doesn't know that the agent just read a poisoned document three calls ago. Runtime enforcement does. Every AI agent deployment starts with static guardrails: a system prompt, model settings, maybe network firewall rules. These feel comprehensive at deploy time. They leave wide gaps in practice.
System prompts can be overridden. Prompt injection — whether from user input or content the agent reads — instructs the model to ignore the system prompt. The prompt is just text in the context window; it has no cryptographic authority over what the model does next.
Network controls are blind to content. A firewall can block evil.com but sees only HTTP headers, not the LLM's reasoning. A call to api.openai.com/v1/chat/completions looks identical whether it's a legitimate product query or a prompt-injected exfiltration attempt. Firewalls also have zero visibility into local models — Ollama, vLLM, llama.cpp — because those never make an outbound network call.
Static configuration can't see session state. Whether a tool call is safe depends on what happened earlier in the session: did the agent read an untrusted document? Has it accessed private customer data? Static configuration was set before the session began; it cannot answer those questions.
Policy changes require a redeploy. If you discover at 2am that a model is being misused, a static configuration means waiting for a deployment cycle — minutes to hours — before the block takes effect across running agents.
What Runtime Security Actually Means
Runtime security for AI agents means enforcement at the moment of each call — not at deploy time, not at network egress, but at the instant the agent invokes an LLM or a tool. A runtime security layer has access to four things a static system cannot have: the full prompt as assembled including all retrieved context and injected content, the complete session history, the tool name and arguments, and a live-refreshed policy bundle updatable without redeployment.
This architecture must be in-process. A proxy-based system routes agent traffic through a sidecar or gateway, adding network latency to every call and creating a dependency agents route around when it becomes inconvenient. An in-process runtime runs in the same memory space as the agent with zero-copy access to all call context, and covers every call regardless of which model or network path it uses.
Local-First Policy Enforcement
The key design question is where the enforcement decision happens. A naive implementation calls the policy server on every LLM call — adding a network round-trip to every inference request, adding latency and an availability dependency. A better design is local-first: the policy bundle is cached in-process and enforcement decisions happen locally, with no network call except for the one check that genuinely requires server state.
InferenceFort's enforcement model:
- Model access, content rules, egress, tool policy — evaluated locally from the cached bundle in under 1ms. The bundle is refreshed every 5 minutes via ETag polling; between refreshes, the cached copy is authoritative.
- Budget enforcement — the one check requiring a server round-trip, because daily spend is shared team state no single process can know. The round-trip only fires for calls matching a policy with a
max_daily_usdcap. - Audit recording — fire-and-forget async to the backend. Never blocks the call path. The backend re-evaluates as a backstop, but the SDK's local decision is what gates the call.
import inferencefort from langchain_anthropic import ChatAnthropic # engine.init() runs at import time: # 1. Fetches GET /v1/policy → caches bundle in process memory # 2. Starts daemon thread that refreshes every ttl_seconds (default 300) # 3. Patches BaseChatModel, LiteLLM, BaseTool, MCP ClientSession inferencefort.set_context( user="priya@corp.com", agent_id="research-assistant", thread_id="sess_7c4e" ) llm = ChatAnthropic(model="claude-sonnet-4-5") # Each invoke() call: # enforce() → local policy check (~0.2ms, no network) # → if budget cap: POST /v1/budget (~15ms round-trip) # → call LLM (the actual inference) # → record() → async POST /v1/ingest (fire-and-forget) response = llm.invoke("What are the key findings from the 2025 report?")
Policy changes propagate within one TTL cycle — at most 5 minutes — with no deployment required. If you discover at 2am that a model is being misused, you block it in the dashboard and it's blocked across all running agent processes within 5 minutes, without touching deployment infrastructure.
Dynamic Tool Control
In a static system, blocking a tool means removing it from the agent's tool list and redeploying. In a runtime system, you update the policy bundle and the change propagates to every running agent within the TTL window. Tool policies have three states at the call layer:
- Allowed — the tool call proceeds normally and an audit event is recorded.
- Blocked — the tool call raises
PolicyViolationbefore the tool executes. Use for permanently prohibited operations (e.g.,delete_user_accountin a read-only agent) or tools you've discovered are being abused. - Approval required — the call pauses and waits for a human reviewer via dashboard or webhook. Use for high-risk operations that are legitimate but should not happen autonomously: wire transfers, external communications, infrastructure changes.
Policies use glob patterns: delete_* blocks all delete-prefixed tools, *_external requires approval for anything ending in _external. One rule covers a category; your policy stays concise as the tool catalog grows.
Token Monitoring and Budget Enforcement
Token costs are a security signal, not just a billing issue. In 2025, a well-documented incident pattern emerged: a coding agent's retry logic on a transient API error triggered a runaway loop that consumed $40,000 of inference budget overnight before anyone noticed. With a daily budget cap set at $500/day per agent, that loop would have been blocked after the first few hundred dollars — at 2am, automatically, with no on-call page required.
Anomalously high token consumption is also the first observable symptom of a prompt injection attack that's triggered a loop, or a model-jacking attack using your inference budget for attacker-controlled tasks. Runtime budget enforcement has two components:
Per-call cost estimation (pre-call gate)
Before the LLM call executes, the SDK estimates cost using the tenant's pricing configuration (tokens in context × input price + estimated output tokens × output price). If the estimate would push the session or team over a daily cap, the call is blocked with a PolicyViolation before it reaches the model — no tokens consumed, no cost incurred.
Server-authoritative daily budget
Daily spend is shared across all processes serving a team — no single process has authoritative knowledge of the current total. When a policy includes a max_daily_usd cap, the SDK calls POST /v1/budget for a server-authoritative verdict. This is the only enforcement check requiring a network call, and it only fires for calls matching a budget-capped policy.
Human Approval Gates
The right model for high-risk agent actions is not "always allow" or "always block" — it's "pause and ask a human." Runtime approval gates implement this at the tool-call layer: when a tool call matches an approval policy, the call is held and a notification goes to a designated reviewer via dashboard or webhook (Slack, PagerDuty). The reviewer approves or denies; the agent resumes or raises a PolicyViolation.
Gate any action that is:
- Irreversible — sent emails can't be recalled, deleted records may not be recoverable, deployed code runs in production
- High blast radius — bulk operations, multi-user actions, production system changes
- External-facing — any communication leaving your organization's perimeter
- High-value — financial transactions, contract signing, account modifications
from inferencefort import PolicyViolation # Policy: send_email has tool_action = "approval_required" # When the agent calls send_email, instead of executing: # 1. SDK emits audit event with verdict="approval_required" # 2. Dashboard shows pending approval notification # 3. If reviewer denies: PolicyViolation raised here # 4. If reviewer approves: tool call proceeds try: tool_result = send_email.run({ "to": "client@enterprise.com", "subject": "Contract renewal proposal", "body": draft_body }) except PolicyViolation as e: # Reviewer denied, or timeout waiting for approval agent_response = "Email draft prepared but requires approval before sending."
Audit Trail: Every Call a Structured Record
A runtime security layer is only as valuable as the audit trail it produces. Every LLM call and every tool call should generate a structured event with enough information to reconstruct what happened, who authorized it, what data was involved, and what the outcome was. The fields that matter:
| Field | Type | Why it matters |
|---|---|---|
| trace_id | UUID | Links all spans in a multi-step agent session |
| user / agent_id | string | Attribution — who was responsible for this call |
| model | string | Which model was called — policy compliance evidence |
| provider + resolved_endpoint | string | Egress compliance — where the call actually went |
| verdict | enum | allow / block / redact / approval_required |
| rule_id | string | Which policy rule triggered a block/redact |
| input_tokens / output_tokens | int | Cost accounting + anomaly detection |
| cost_usd | float | Per-call and cumulative cost tracking |
| local_decision | bool | Distinguishes SDK enforcement from server backstop |
| latency_ms | int | Performance baseline; spikes warrant investigation |
Runtime DLP: Redaction Before the LLM Sees It
Data loss prevention at the runtime layer means intercepting sensitive data before it leaves the process — including before it's sent to a cloud LLM. The enforcement order matters: content rules run first on the original text (so they can detect sensitive patterns and record what was found), then redaction runs before the call executes. The LLM only ever sees the sanitized version.
For HIPAA-regulated workloads, the PHI detector (backed by Microsoft Presidio) identifies 18 HIPAA Safe Harbor identifier categories — names, dates, geographic subdivisions, phone numbers, SSNs, medical record numbers, device identifiers, and more — and replaces them with generic placeholders before the prompt reaches any cloud model. A patient record entering the agent as "John Smith, DOB 1978-03-15, SSN 123-45-6789" exits the redaction layer as "[PERSON], DOB [DATE], SSN [SSN]". The LLM can still reason about the record structure; it never sees the actual PHI.
This matters for two reasons: compliance (PHI cannot be transmitted to a processor without a signed BAA) and security (if the LLM's responses are intercepted or logged by the provider, the actual PHI was never in the payload).
Runtime Architecture
Implementation Checklist
- SDK installed and
INFERENCEFORT_KEYset — confirm a test call produces an audit event in the dashboard before going further - Policy bundle fetching at startup — verify
GET /v1/policyreturns 200 with a non-empty bundle; a missing bundle means enforcement runs in fail-open mode - All LLM call sites governed — grep for direct
httpxorrequestscalls to AI provider APIs and verify none bypass the SDK - Content rules active for your data categories — at minimum: SSN, credit card, medical record number, API key patterns
- PHI redaction enabled if handling health data — test with a fake SSN and DOB that the redacted version (not the original) reaches the LLM
- Per-agent and per-team daily budget caps set in the dashboard — verify a cap of $1 triggers a block on the next test call
- Tool policies configured: blocked tools listed explicitly, approval-gated tools identified with glob patterns
- MCP server allow-list populated and tested — confirm a test call to an unlisted server is blocked before the first tool call
- Trifecta guard enabled with your specific exfil/private-data/untrusted tool classification patterns
- Audit events flowing to the dashboard and to your SIEM with alerts on block events and trifecta triggers
- Approval workflow configured (dashboard or Slack/PagerDuty webhook) for approval-gated tool calls — test the full flow end-to-end
- Fail-mode set intentionally:
fail_open(default) for availability-critical agents;fail_closedwhen a policy server outage should halt the agent rather than let calls through
Secure your AI agents today.
InferenceFort enforces policy on every LLM call and tool call — in-process, before it runs.
Get early access →