DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

agentguard: Network Egress Allowlist for AI Agent Tools

When the agent called the wrong API

A teammate built an agent that pulled customer data from an internal API, enriched it with a third-party service, and wrote a summary. It had been running in staging for a few weeks with no issues. During a code review before the production push, someone ran a quick network trace and noticed the agent was also calling an endpoint that had been deprecated three months ago. The deprecated endpoint still responded with a 200. So there was no error in the logs. The data it returned was stale. The summaries it fed into the enrichment step were wrong in ways that were easy to miss at a glance.

Nobody added that call intentionally. It came in through a dependency update that switched which internal HTTP helper was used. The old helper had the deprecated endpoint hardcoded as a fallback for one specific error code. The new code path hit that error code more often, so the fallback fired, and the agent started pulling from an endpoint that should have been dead months ago.

This is the class of bug agentguard is designed to catch: your agent reaching somewhere it should not, because nobody ever wrote down which domains it was allowed to call. If you declare the allowlist explicitly in code, violations surface the moment the call is attempted, before the request goes out, before the wrong response comes back, before downstream logic processes stale data. The allowlist is also documentation. Anyone reading the agent code can see which external services it is supposed to talk to, without tracing through all the HTTP calls manually.

The fix in that case would have been immediate. The call to the deprecated endpoint would have thrown an EgressViolation on the first run after the dependency update. The test would have failed in staging before anyone merged anything.

Shape of the fix

import { AgentGuard } from "@mukundakatta/agentguard";

const guard = new AgentGuard({
  allow: ["api.openai.com", "api.anthropic.com", "api.github.com"]
});

// This passes:
const response = await guard.fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  body: JSON.stringify(request)
});

// This throws EgressViolation before the request goes out:
await guard.fetch("https://api.deprecated-internal.example.com/data");
Enter fullscreen mode Exit fullscreen mode

The guard.fetch method is a drop-in replacement for the global fetch. Same signature. The only difference is the allowlist check that runs before any network I/O. If the hostname is not in the list, the promise rejects with an EgressViolation error that includes the attempted URL and the current allowlist. No request was sent. The error is catchable and inspectable, so you can handle it specifically in tests or log it with full context in production.

You can also wrap the global fetch directly if you want to protect all HTTP calls in scope, not just the ones you explicitly route through guard.fetch. There is a guard.wrapGlobal() method that replaces globalThis.fetch for the current scope. This is useful when you want to add the guard to an existing codebase without touching every individual fetch call.

What it does NOT do

agentguard is not a firewall. It does not intercept system-level TCP connections or operate at the OS network layer. It operates at the JavaScript fetch call level. If a dependency bypasses fetch and uses raw Node http or https modules directly, agentguard will not catch that. There is no way to intercept those calls without monkey-patching Node core, which agentguard does not do.

It also does not inspect request payloads or response bodies. The check is domain-only. If you need to validate what data the agent is sending to an allowed domain, or what it is receiving back, that is a different concern. agentguard is for the common case: making sure the agent does not call a domain that was never supposed to be in scope.

Inside the lib

The allowlist is checked against the hostname of the parsed URL. Subdomains require explicit listing unless you use a wildcard prefix. api.openai.com does not cover files.openai.com. This is intentional. Broad wildcard rules defeat the purpose of an allowlist. If you find yourself listing many subdomains of the same domain, use *.openai.com to cover all subdomains under that base domain. But start explicit and widen only when needed.

Port is treated as part of the host by default. If you allow localhost, calls to localhost:3000 and localhost:8080 are both covered, because the hostname portion of both URLs is localhost. This is the most common local development pattern, so it is the default. If you need strict port matching, there is a strictPorts constructor option that treats localhost:3000 and localhost:8080 as separate entries.

The EgressViolation error is a proper subclass of Error with url, hostname, and allowlist fields. This makes it straightforward to catch specifically in tests and assert on the attempted URL, which is more useful than catching Error and asserting on a message string that could change.

One situation that comes up frequently: agents that call different APIs depending on environment. Staging hits a mock server. Production hits the real API. agentguard supports passing the allowlist as a function that returns an array, not just a static array. The function is called at fetch time, not at construction time. This lets you pull the allowlist from environment config at the moment the call is made, which keeps the guard useful across environments without requiring separate guard instances per environment.

When useful

  • You want an explicit, code-level declaration of which external services your agent is allowed to contact
  • You are debugging an agent that is making unexpected outbound calls and you want to surface violations immediately rather than tracing logs
  • You have compliance or security requirements around data egress and need a clear, auditable allowlist in the codebase
  • You are reviewing agent code in a PR and want the allowed domains to be visible in the code itself, not buried in config files or inferred from network logs

When not useful

  • You need syscall-level or network-level egress control that catches non-fetch HTTP calls
  • Your agent legitimately calls many different dynamic domains based on user input (a browsing agent, for example)
  • You want request or response payload inspection in addition to domain filtering
  • You are in a serverless environment where wrapping or replacing global fetch is not reliable

Install

npm install @mukundakatta/agentguard
# or
yarn add @mukundakatta/agentguard
Enter fullscreen mode Exit fullscreen mode

Requires Node 18+. Zero runtime dependencies.

Siblings

Library What it does Registry
agentguard-rs Rust port of the same egress allowlist concept crates.io
@mukundakatta/agentvet Validates tool call signatures before execution npm
@mukundakatta/agentsnap Snapshot tests for agent tool call sequences npm
tool-secret-scrubber Strip secrets from tool call logs crates.io / PyPI
@mukundakatta/agentcast Repair-validate-retry for LLM structured output npm

What is next

The most requested addition is an audit log: a structured record of every fetch call the agent made, whether it was allowed or blocked, the timestamp, and the full URL. Right now you only hear about violations when they happen. A persistent audit log would let you review what was allowed after the fact, which is useful for compliance reviews and debugging agent runs in production.

A middleware mode is also on the list, where the guard sits in an Express-style middleware chain rather than wrapping fetch directly. That would make it easier to drop into existing agent server setups without touching individual fetch calls throughout the codebase.

Top comments (0)