DEV Community

Cover image for Your AI Agent Has Too Much Power

Your AI Agent Has Too Much Power

Thien Ban BUI on September 23, 2026

If you've connected an LLM to tool calling, APIs, or database queries recently, you know the feeling: agents are terrifyingly unpredictable. Givin...
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.)

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?