DEV Community

Cover image for Your AI Agent Has Too Much Power
Thien Ban BUI
Thien Ban BUI

Posted on AI-assisted

Your AI Agent Has Too Much Power

If you've connected an LLM to tool calling, APIs, or database queries recently, you know the feeling: agents are terrifyingly unpredictable.

Giving an LLM the ability to decide its own next step means shifting from passive text generation to active system execution. In demos, it looks like magic: the agent reasons, picks a tool, fetches data, and loops until the task is solved.

In real life?

  • An indirect prompt injection slipped inside an ingested PDF instructs your agent to exfiltrate private DB records via an outbound HTTP webhook.
  • A looping reasoning process burns 80,000 reasoning tokens in 12 turns on a dead-end premise, costing 50x more than expected.
  • Sensitive customer PII (credit cards, emails, internal keys) is handed directly to third-party model providers inside execution context.

The traditional answer to this is the classic LLMOps sledgehammer: spin up Docker containers for Langfuse, Helicone, OpenLit, a vector database, ClickHouse, Redis, and an S3 bucket just to trace calls and apply basic guardrails.

For most developers and lean teams, the monitoring infrastructure ends up costing more in maintenance and compute than the LLM calls themselves.

There is a better way: handling security, guardrails, and cost controls in-process, directly at the application runtime level.


1. The Anatomy of an Agent Failure

prompt injection to AI Agent

When building autonomous workflows or multi-step agents, security vulnerabilities aren't theoretical — they fall into three clear failure modes:

[User / External Input]
         │
         ▼
 ┌───────────────┐
 │   Prompt /    │  <-- 1. Prompt Injection / Malicious Context
 │ Ingested Data │
 └───────┬───────┘
         ▼
 ┌───────────────┐
 │  LLM Engine   │  <-- 2. Token Loop & Budget Drainage (Hallucination)
 └───────┬───────┘
         ▼
 ┌───────────────┐
 │ Tool Execution│  <-- 3. Unauthorized Side-Effects / PII Exfiltration
 └───────────────┘

Enter fullscreen mode Exit fullscreen mode
  1. Unchecked Tool Execution & Side-Effects: Once an agent has tools, an injection is no longer just a jailbreak: it becomes Remote Code Execution (RCE) or arbitrary API manipulation.
  2. Context Contamination & Data Leaks: If your RAG or tool outputs include raw PII, your LLM will gladly send it over the wire to external APIs or echo it into log sinks.
  3. Runaway Loops & Cost Explosion: Without per-turn token limits and hard budget circuit-breakers, recursive tool loops can easily drain your monthly quota in minutes.

2. The Solution: Zero-Infra Control Plane with avantGate

a super hero crew

Meet AG-Men (avantGate's in-process engine): a tiny, sub-millisecond guardian that sits directly between your model's thoughts and your backend APIs.
Avant Gate runs directly in your TypeScript process.

The Journey of an Intercepted Run

Imagine a user submits an invoice query packed with an indirect prompt injection and a customer's raw credit card. Here is how the AG-Men defend your runtime step by step:

  • The Cleaner (Local PII Sanitizer): Before a single byte leaves server memory, The Cleaner scrubs emails and credit cards on the fly. The external LLM never catches a glimpse of the private data.
  • The Breaker (Budget Circuit-Breaker): Turn three hits an infinite recursive loop triggered by the injection. The Breaker detects the runaway token surge, trips the fuse, and cuts the execution instantly.
  • The Law (Deterministic Guardrails): The hallucinating agent tries to force an unvetted database write. The Law blocks it cold against a strict Zod contract — zero unauthorized side-effects.
  • The Shapeshifter (Resilient Fallback): The primary model throws a sudden 529 Overloaded. The Shapeshifter catches the fall mid-air and swaps to a backup provider, landing the mission safely without a glitch.

3. Hands-on: Securing an Agent Workflow

Here is how you wrap your agent runtime with avantGate in TypeScript:

Step 1: Initialize the Control Plane

import { AvantGate } from 'avantgate';

const gate = new AvantGate({
  budget: {
    maxCostPerMinute: 0.50, // Circuit breaker: halts calls if threshold is reached
    maxTokensPerTurn: 4000
  },
  pii: {
    redact: ['email', 'credit-card'],
    maskChar: '*'
  },
  fallback: [
    'anthropic/claude-3-5-sonnet',
    'openai/gpt-4o-mini'
  ]
});

Enter fullscreen mode Exit fullscreen mode

Step 2: Safe Tool Execution & Workflow Interception

In an agentic workflow, you must never let the model invoke tools with unchecked parameters. You can intercept and validate both the step input and the agent's intent:

import { z } from 'zod';

// Define strict contract for the tool
const SearchDatabaseSchema = z.object({
  query: z.string().max(200),
  limit: z.number().int().min(1).max(20),
});

async function runSecuredAgentStep(userPrompt: string) {
  // 1. AvantGate sanitizes input (PII redaction) & checks budget in-process
  const safeExecution = await gate.run(async (context) => {

    const response = await context.completion({
      model: 'anthropic/claude-3-5-sonnet',
      messages: [{ role: 'user', content: userPrompt }],
      tools: [
        {
          name: 'search_database',
          description: 'Search internal records',
          parameters: SearchDatabaseSchema,
        },
      ],
    });

    // 2. Validate agent tool calls before execution
    if (response.toolCalls) {
      for (const call of response.toolCalls) {
        if (call.name === 'search_database') {
          const validatedArgs = SearchDatabaseSchema.parse(call.args);
          return await executeInternalSearch(validatedArgs);
        }
      }
    }

    return response.content;
  });

  return safeExecution;
}

Enter fullscreen mode Exit fullscreen mode

If an injection attempts to inject DROP TABLE or trigger a recursive chain that blows past your token limits, avantGate halts the execution context before it wrecks your infrastructure.


4. Why In-Process Beats External LLMOps for 90% of Use Cases

Concern External LLMOps Stack In-Process Control Plane (avantGate)
Infra overhead Docker, Postgres, ClickHouse, Redis Zero (Pure TypeScript module)
Network Latency Additional network hops per call Sub-millisecond (runs in memory)
Data Privacy Prompts/logs stored in external databases Zero egress (data never leaves your runtime)
Maintenance Migrations, backups, version upgrades Update avantGate

Conclusion & Next Steps

Agents are the future of software engineering, but building agents without deterministic boundaries is an invitation for disaster. You don't need a 5-container infrastructure stack to build secure, cost-contained AI tools.

Check out the project and docs on GitHub:
👉 Avant Gate

  • How are you currently handling runaway agent loops and tool validation in your stack? Let's discuss in the comments below! 🚀

Top comments (10)

Collapse
 
carbonlayer profile image
CarbonLayer •

The in-process approach is appealing, especially for teams that don’t need a separate observability stack just to enforce basic limits. But I’d be careful not to let “the guard runs locally” blur into “the data stays local”: if the model call still goes to an external provider, the protection depends on exactly what the sanitizer removes—and whether enough context remains for a useful answer.

I’d also separate valid input from authorized action. A Zod schema can rule out malformed arguments, but a perfectly valid query or write can still be outside the user’s permissions or the agent’s intended scope. That’s where least-privilege tools and checks against the actual caller’s authorization still matter.

For runaway loops, per-turn token and per-minute cost limits are a good start, but I’d want to know whether the budget is enforced across concurrent runs and fallback calls too. Otherwise the individual guardrails can all look sensible while the total spend still escapes the limit. How does avantGate account for those cases—and what does it do when PII redaction removes context the model needs to answer correctly?

Collapse
 
thienban profile image
Thien Ban BUI •

Thanks for the sharp critique—you’ve hit on three of the most common failure modes in production agent architectures: the illusion of data locality, the gap between schema validation and authorization, and distributed cost leaks.

Here is a quick summary of how avantGate approaches each of these challenges today:

1. "Local Guard" vs. "Local Data" & Context Starvation

Running an in-process guardrail doesn't mean data stays local if prompts are dispatched to OpenAI or Anthropic. When PII redaction strips essential fields, the model can easily suffer from context starvation (inability to distinguish entities or relationships).

  • Dual-Channel Out-of-Band (toClientData): In client-facing apps, the LLM rarely needs raw PII. Our tools project a lean structural summary to the model ({ status: "active", recordsFound: 2 }), while the raw PII payload bypasses the LLM entirely and streams directly to the authenticated client UI over secure HTTP/SSE.
  • Deterministic Surrogate Vaulting: When the model must reason over sensitive entities (e.g., "Compare account A with account B"), avantGate replaces PII with session-scoped surrogate tokens ([IBAN_REF_#1], [PERSON_#2]). The model reasons accurately about equivalence without ever seeing raw data, and values can be re-hydrated downstream only when invoking authorized transactional tools.
  • True Zero-Egress: For air-gapped or strictly regulated domains, avantGate supports on-premise local runtimes (Ollama, vLLM, Mistral), where data locality is absolute.

2. Valid Input vs. Authorized Action (BOLA / IDOR Defense)

Zod enforces syntactic correctness, not caller authority. An agent can emit a perfectly valid { invoiceId: "inv_9988" } that belongs to another tenant or exceeds the user's role.

  • Pre-Execution Tenant Assertions: Tools implement fail-closed contextual checks (assertTenant, assertOwnership) before any business logic or DTO projection executes. If caller.tenantId !== record.tenantId, a DataAccessDeniedError is thrown immediately.
  • Dynamic Tool Allowlisting (Least Privilege): avantGate's AccessControlToolStrategy dynamically filters the tool catalog against the authenticated caller's roles and domain. A non-admin user's LLM isn't just blocked from executing refund_payment—the tool definition is completely omitted from the model's tool dictionary.
  • Blast Radius Governance: Destructive operations automatically trigger Human-in-the-Loop (requireApproval: true) checkpoints.

3. Distributed Spends, Concurrency & Fallback Storms

Per-turn and per-minute rate limits protect individual loops, but fall short when 20 concurrent tasks spawn or when fallback models cascade.

  • Atomic Pre-flight Reservation: PreflightBudgetGuard estimates the maximum token cost before the network call is dispatched and claims it against a shared tenant quota (SQLite with WAL locks). Concurrent runs trip the circuit breaker at the gate before burning spend.
  • Cumulative Failover Tracking: When using MultiModelFailover (e.g., GPT-4o ➔ Claude 3.5 Sonnet ➔ Mistral), subsequent retry attempts inherit and increment the parent session’s totalCostUsd and totalTokens. If a session cap is $0.50 and the primary burns $0.40 before erroring, the fallback won't launch if it risks breaching the threshold.
  • Loop Shield: 3 identical argument hash signatures across consecutive calls trip an immediate fail-safe loop alarm.

We'd love your thoughts:

If you have specific patterns or edge cases you've encountered around distributed budget locking or fine-grained caller authorization, we’d be eager to hear your suggestions for improving this further!

Collapse
 
carbonlayer profile image
CarbonLayer •

The strongest part of this design, to me, is that the controls sit at the point of execution: schemas shape inputs, caller-bound checks authorize actions, and the budget guard decides whether work can start.

One edge case I’d pressure-test is an ambiguous timeout: a provider may have accepted a request even though your app saw a failure, and then a retry or fallback begins. The reservation needs a stable identity across those attempts and a clear settle, release, or reconciliation path—otherwise you risk either overspending or stranding budget. Same principle for authorization: filtering tools from the model’s catalog is useful least privilege, but the check at the actual read or write remains the authority, especially for delayed retries where access may have changed. How do you handle that reservation lifecycle today?

Thread Thread
 
thienban profile image
Thien Ban BUI •

Great pressure-test!
Rather than importing a heavyweight banking settlement engine (Hold ➔ Settle ➔ Clearing ➔ 2PC) designed for multi-day credit card holds, AvantGate solves the Overspending vs. Stranded Budget tradeoff using a pragmatic 3-pillar lifecycle:
Here is how we handle both edge cases without over-engineering a full banking settlement engine:

1. Authorization: Execution-Time Authority

Tool catalog filtering is just prompt shaping, not a security boundary.
When a delayed retry happens (e.g. via useTask.retry() hours later or after human approval), authorization is re-evaluated at the actual point of execution inside the tool handler with the caller’s current session. If their role or tenant access was revoked in the meantime, it fails closed with an immediate error before any read or write.

2. Budget Lifecycle on Ambiguous Timeouts

To avoid the Overspending vs. Stranded Budget trap, we use a 3-step pragmatic flow:

  1. Stable Causal Identity (parentRunId ?? runId): The client and gateway preserve the original run ID across retries so all layers track the same operation.
  2. Preflight Solvency vs. Post Settlement: Our preflight guard is a fast solvency check (not a permanent escrow lock), preventing stranded budget on network drops. True cost settlement only happens post-execution based on actual returned tokens (usage.total_tokens).
  3. Gateway Deduplication: Because most providers (Mistral, Anthropic) lack native Idempotency-Key support unlike OpenAI, our gateway caches completed runs by their causal ID. A client retry after a socket drop is served directly from the gateway cache without re-billing the model, with a strict retry ceiling (maxManualRetries: 3) capping worst-case exposure.
Thread Thread
 
carbonlayer profile image
CarbonLayer •

The “preflight solvency, settle on actual usage” distinction is useful—it avoids turning a temporary network failure into stranded budget. The hardest seam seems to be when the provider completes the run but the gateway times out before it records the result: a retry could miss the cache and trigger a second execution. How do you handle that window, especially for providers without native idempotency keys?

Thread Thread
 
thienban profile image
Thien Ban BUI • • Edited

Spot on—that’s the exact distributed systems seam. Unlike Stripe, most LLM endpoints (Anthropic, Gemini, Bedrock, Groq) don't have native HTTP idempotency caching for synchronous inference.

We tackle that phantom window across three layers:

  1. Decouple Blast Radius (Inference vs. Tools)

    A duplicate text completion is just a minor FinOps loss ($0.02). The real danger is duplicating mutative side-effects (charge_card, db_write). AvantGate places an atomic claim lock at the tool execution boundary (withServerIdempotency + storage adapter). Even if the model reruns and outputs the exact same tool call, the tool itself short-circuits.

  2. Stream-First Socket Timeouts

    Most ambiguous timeouts happen when a monolithic 30s HTTP timeout drops mid-generation. We stream by default and switch to a Time-To-First-Token (TTFT) and socket idle read timeout. As long as tokens keep flowing, the connection stays alive.

  3. Pessimistic FinOps Accounting (No Silent Retries)

    On an unconfirmed timeout, the preflight reservation is held in UNCONFIRMED_TIMEOUT—never blindly refunded. A manual retry is only allowed if the tenant can absorb the worst case. No automatic retry loops.

(For long-running tasks >30s, we route to asynchronous batch endpoints with deterministic custom_id: runId, making retries pure, non-mutative status polling.)

Thread Thread
 
carbonlayer profile image
CarbonLayer •

Really appreciate you laying out all three layers. The distinction between a duplicate completion and a duplicated tool side effect is the key one: the former is a bounded cost; the latter can change customer or system state. Putting idempotency at the execution boundary, rather than trusting the model or retry loop, feels like the right place to contain that risk.

The claim-lock path raises the hardest edge case for me: suppose the tool succeeds, then the worker dies before the result is written back. On retry, how does the adapter distinguish “side effect completed, response lost” from “claim abandoned and safe to run”? Do you persist and replay the tool result against the same key, or does recovery depend on the storage adapter?

I also like the UNCONFIRMED_TIMEOUT treatment—holding the reservation is less friendly in the moment, but safer than refunding or retrying when the provider outcome is unknown. And using deterministic custom_id for long jobs makes retries status checks, not second executions. Where has the most operational complexity landed so far: lock recovery, or keeping tool adapters consistent?

Thread Thread
 
thienban profile image
Thien Ban BUI •

Hello!
No, to be completely candid, our open source library avantGate doesn't currently handle distributed database transaction locking, worker crash recovery, or idempotency keys for side effects. Those are heavy infrastructure concerns typically handled by orchestration engines like Temporal or durable queue layers rather than an LLM security and PII control plane.

Saga Replay & Storage: However, avantGate can optionally integrate with a database backend to support saga-style tool execution replays, state tracking, and error handling for critical agent actions.

Collapse
 
brianainews profile image
Brian · AI News •

The three failure modes are a useful framing. I especially like making budget and tool contracts first class runtime controls instead of bolting observability on after the fact.

Collapse
 
thienban profile image
Thien Ban BUI •

Thanks! That's exactly why I built it this way.

Observability only tells you the server was breached or your budget was drained after the fact. By enforcing deterministic tool contracts (Zod) and budget circuit-breakers directly in-process, we stop the damage before the side effect ever touches downstream systems or external APIs.

How are you currently managing boundaries around your tool calls?