Skip to main content
Glama
krishnashahane

agent-trust-firewall

Agent Trust Firewall

A firewall for AI agents. It sits between an agent and the API (or other agent) it is calling, proves who is actually asking, scores how dangerous the request is, and blocks the bad ones before they execute.

Live: https://agentwall-pbl.vercel.app

The problem

AI agents now book, pay and run code on their own. When one is compromised — a leaked key, a poisoned prompt — its requests look exactly like a healthy agent's: same credentials, same endpoints, no alarm. An API key proves someone once had a secret; it does not prove that this request came from the agent you think it did, that the agent is allowed to do this, or that the request is not a replay of yesterday's.

Related MCP server: BlastRadius MCP

What it does

Every intercepted call must clear four checks before it runs:

  1. Sealed in transit — the agent encrypts the call with a fresh key each time (X25519 → HKDF-SHA256 → XChaCha20-Poly1305). Fields it marks sensitive are encrypted to the destination and pass through the firewall unreadable.

  2. Identity, proved not claimed — a Schnorr zero-knowledge proof of key possession. The key is never transmitted, and the proof is bound to one single-use challenge and one specific request, so it cannot be replayed or moved onto a different call.

  3. Authority, minimally disclosed — a Merkle inclusion proof for the one capability this call needs. The firewall never learns the rest of the agent's grant set.

  4. Risk, scored and explained — seven signals (identity, context, behaviour, rate, payload, reputation, live threat intel) produce a 0–100 score in which every point traces back to a named reason in plain language.

The outcome is ALLOW, FLAG (hold for a human) or BLOCK, and every decision is written to a SHA-256 hash-chained audit log.

Hard rules are not tunable. Unknown agent, revoked key, unverified proof, replayed challenge and ungranted capability block at every configuration. A confident prompt-injection or credential-leak detection forces at least a FLAG whatever the weighted score says.

Quick start

Requires Node.js 22.6 or newer (the verify, mcp and seed scripts run TypeScript directly with --experimental-strip-types). Production runs on Node 24.

npm install
npm run dev        # http://localhost:3000

It runs with no configuration at all: without DATABASE_URL it uses an in-process store and seeds a demo fleet of six agents with half an hour of real traffic on boot. /api/health tells you which store is live.

Using it

The console

Page

What it is for

/

What ATF is, with a live decision running in the hero

/console

Live traffic: every decision, its score, and why

/console/attack-range

Run an adversary (forged identity, replay, revoked key, capability escalation, rate flood, off-baseline behaviour, prompt injection, credential exfiltration) through the real decision path and watch the verdict

/console/agents

The enrolled fleet, with live external threat intel per origin address

/console/tuning

Move signal weights and thresholds and preview what that would have done to recent traffic. Sandboxed to your browser — it never changes anyone else's scoring, and cannot disable a hard rule

/console/audit

The hash-chained audit log and a live tamper check

/glossary

Every term in plain language

Calling it as an agent

An agent integrates with the SDK in src/lib/atf/client.ts. The flow, all over plain HTTP:

import { generateKeyPair } from "./src/lib/atf/identity/schnorr.ts";
import { generateEncryptionKeyPair, open } from "./src/lib/atf/crypto/e2ee.ts";
import { signCall, sealCall } from "./src/lib/atf/client.ts";

const BASE = "http://localhost:3000";
const post = (path: string, body: unknown) =>
  fetch(BASE + path, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  }).then((r) => r.json());

// 1. Enrol once. Only public keys are sent.
const signing = generateKeyPair();
const encryption = generateEncryptionKeyPair();
const capabilities = ["calendar:createEvent", "search:query"];
await post("/api/agents/register", {
  agentId: "my-agent",
  displayName: "My agent",
  signingPublicKey: signing.publicKey,
  encryptionPublicKey: encryption.publicKey,
  capabilities,
});

// 2. Per call: get a single-use challenge, prove, seal, submit.
const { publicKey: gatewayKey } = await fetch(BASE + "/api/gateway/pubkey").then((r) => r.json());
const { nonce } = await post("/api/agents/challenge", { agentId: "my-agent" });
const call = signCall(
  { agentId: "my-agent", signingSecretKey: signing.secretKey, capabilities },
  {
    tool: "calendar.createEvent",
    destination: "calendar.example.com",
    capability: "calendar:createEvent",
    args: { title: "Design review" },
    nonce,
  },
);
const { sealed } = await post("/api/gateway/intercept", {
  envelope: sealCall(gatewayKey, call),
  replyTo: encryption.publicKey, // the verdict comes back sealed to you
});
const verdict = open(encryption.secretKey, sealed); // { verdict: "ALLOW", score, explanation, ... }

Capabilities are namespace:action and must match the tool exactly (calendar:createEvent authorises calendar.createEvent) or by wildcard (calendar:*). Holding calendar:write does not authorise calendar.createEvent; that is a capability-escalation and is blocked.

Submitting { "call": … } instead of { "envelope": … } is accepted for integrators who terminate encryption elsewhere; the response reports transport: "plaintext" rather than implying protection that was not applied. A BLOCK on the plaintext path returns HTTP 403.

API

Route

Method

Purpose

/api/health

GET

Liveness, storage backend, audit chain validity

/api/agents

GET

Enrolled fleet with intel

/api/agents/register

POST

Enrol an agent (public keys + capabilities)

/api/agents/challenge

POST

Issue a single-use, agent-bound challenge

/api/gateway/pubkey

GET

The gateway's X25519 public key

/api/gateway/intercept

POST

Submit a call for a decision

/api/traffic

GET

Recent decisions and a summary

/api/audit, /api/audit/verify

GET

Audit log; chain verification

/api/config

GET / PUT / DELETE

Read, set or clear your risk-config sandbox

/api/config/preview

POST

Re-score recent traffic against a proposed config

/api/simulate

GET / POST

List or run attack-range scenarios

/api/sealed-demo

POST

Show a destination-sealed field passing through unread

/api/osint

POST

Threat intel for a public IPv4 address

Every route applies, in order, an origin check (cross-site browser requests are refused), a per-client rate limit, and Zod schema validation of the body.

The MCP server

npm run mcp

A real Model Context Protocol server over stdio (JSON-RPC 2.0, one message per line). Every tools/call is routed through the same decide() path as the HTTP gateway; a blocked call returns a structured refusal with isError: true and the upstream tool is never invoked. To use it from an MCP client, point the client at:

{
  "command": "node",
  "args": ["--experimental-strip-types", "src/lib/atf/mcp/server.ts"],
  "cwd": "/path/to/agent-trust-firewall"
}

The server fronts three demo tools (search.query, payments.charge, storage.read) and runs as a demo agent holding a subset of capabilities, so you can see one call allowed and another blocked. It keeps its own in-process store.

Architecture

Three separable layers. The risk engine knows nothing about HTTP, and nothing in src/lib/atf/ imports Next.js.

agent ──sealed envelope──▶ /api/gateway/intercept  (or MCP tools/call)
                                   │
                                   ▼
              src/lib/atf/interceptor/decide.ts
   open envelope → verify identity → spend nonce → prove capability
               → build context → score → apply policy → audit
                                   │
                                   ▼
                ALLOW / FLAG / BLOCK  + hash-chained audit record

Every step is fail-closed: an error anywhere produces a BLOCK with a stated reason, never a silent pass.

Path

Role

src/lib/atf/identity/

Schnorr NIZK, Merkle capability proofs, single-use nonces, agent registry

src/lib/atf/crypto/

Sealed envelopes and destination-sealed fields

src/lib/atf/risk/

One file per signal, plus a pure weighted engine

src/lib/atf/interceptor/

The decision path, hard rules and escalations

src/lib/atf/osint/

GreyNoise, Shodan InternetDB, AbuseIPDB, OTX — cached, SSRF-guarded

src/lib/atf/audit/chain.ts

SHA-256 hash chain; verifyChain() names the record that broke

src/lib/atf/store/

Storage port with in-memory and Postgres implementations

src/lib/atf/mcp/server.ts

The MCP stdio server

src/lib/atf/client.ts

Agent-side SDK

src/lib/guard.ts

Origin check, rate limit, body validation, opaque errors

src/proxy.ts

Security headers and a per-request CSP nonce

src/app/

Next.js App Router pages and API routes

claude.md has the long-form architecture and security model.

Configuration

Nothing is required. Every variable is optional and server-side only — never prefix any of them with NEXT_PUBLIC_. See .env.example.

Variable

Effect when set

DATABASE_URL

Durable Postgres (Neon) storage instead of the in-process store

ATF_DEMO_SEED

Derives the demo fleet's keys deterministically. Required with a shared database, so every instance signs with the keys that are enrolled

ATF_GATEWAY_SECRET_KEY

Pins the gateway's X25519 keypair (64 hex chars). Set it on any multi-instance deployment — otherwise each instance has its own key and an envelope sealed to one cannot be opened by another

ABUSEIPDB_API_KEY

Adds AbuseIPDB to threat intel

OTX_API_KEY

Adds AlienVault OTX to threat intel

Generate the two secrets with openssl rand -hex 32.

Deployment (Vercel)

vercel link                      # once
vercel integration add neon      # provisions Postgres, injects DATABASE_URL
vercel env add ATF_DEMO_SEED production
vercel env add ATF_GATEWAY_SECRET_KEY production
vercel env pull .env.local
npm run seed                     # once per fresh database: ~500 statements, ~45s
vercel --prod

Seeding is a deployment step rather than something the app does on demand. Every seeded call is a real challenge, a real Schnorr proof and a real scoring pass; running that inside a request would put tens of seconds of database round-trips on a cold start. npm run seed is safe to re-run — it does nothing if the store already holds traffic. With Postgres configured, the app registers the demo fleet on boot but never seeds traffic itself.

vercel.json pins the region to bom1. Every page renders dynamically so the CSP can carry a per-request nonce.

Verifying the claims

npm run lint
npm run typecheck
npm run test           # 172 tests: crypto, signals, engine, interceptor, audit, MCP, guards, scenarios, OSINT
npm run verify         # 25 security invariants, printed one per line
npm run build
npm audit --omit=dev   # 0 vulnerabilities in shipped dependencies

npm run verify checks the load-bearing properties directly, outside the test runner: an unenrolled agent is blocked, a replayed proof is blocked, a proof cannot be moved onto a different request, a hard rule cannot be tuned away at any configuration, the firewall cannot open a destination-sealed field, an altered or deleted audit record is detected, and no argument value or secret key is ever retained.

A full npm audit (including dev tooling) currently reports advisories in braces, reached only through eslint-config-next at lint time. No patched release exists yet; none of it ships in the application.

Security

  • No secret leaves the server. The gateway's X25519 secret lives only in gateway.ts; agent secret keys never enter the system, and the protocol has no field that could carry one.

  • The audit log stores digests, not payloads. npm run verify asserts this with a canary value.

  • Opaque errors. Unexpected failures return a reference id, and every envelope failure returns one identical message, so a prober gets no decryption oracle.

  • Strict CSP. Nonce-based script-src with no unsafe-inline and no external origins, plus HSTS, frame-ancestors 'none', nosniff, no-referrer, COOP/CORP.

  • SSRF-guarded intel. Lookups accept public IPv4 only — private, loopback, link-local (including cloud metadata), CGNAT and reserved ranges are refused.

  • Bounded work. Argument traversal, Merkle path length, request bodies (512 KB), the in-memory store and rate-limit buckets are all capped.

See security.md for the posture and how to report a vulnerability, docs/security.md for the threat model, and docs/decisions/ for why Schnorr rather than zkSNARKs, and why hard rules sit outside the score.

Troubleshooting

Symptom

Cause and fix

/api/health says in-process (resets on cold start)

DATABASE_URL is not set. Expected locally; set it for durable history

Console is empty on a Postgres deployment

The database has not been seeded. Run npm run seed with DATABASE_URL set

Attack-range calls intermittently fail with identity-unproven

ATF_DEMO_SEED is unset with a shared database, so instances fight over the fleet's keys. Set it and redeploy

An agent gets Envelope could not be opened. intermittently

ATF_GATEWAY_SECRET_KEY is unset on a multi-instance deployment. Pin it and redeploy. /api/health reports gatewayKeyPinned

A legitimate call is blocked with capability-escalation

The proved capability does not match the tool. Grant namespace:action matching the tool name, or namespace:*

replayed-challenge

Each challenge is single-use and expires. Request a fresh one per call

403 Cross-site requests are not accepted here.

A browser on another origin called a state-changing route. Call from the same origin or from a server (no Origin header)

429 Too many requests

Per-client, per-route rate limit. Wait a minute

Intel rows say synthetic only

Providers were unreachable or timed out (4s). That is reported, never shown as a clean result

npm run verify / mcp fails with an unknown flag

Node is older than 22.6. Upgrade Node

Honest scope

The demo fleet is synthetic, but its keys, proofs, verdicts and origin addresses are real — the addresses are live cloud hosts and the intelligence shown about them is fetched at request time. Rate limiting is per instance. Payload inspection is heuristic, which is why it escalates to FLAG rather than blocking. And a firewall must read a request to score it, so ATF is a trusted endpoint for the outer envelope; only destination-sealed fields are opaque to it.

License

MIT — see LICENSE.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Deterministic policy enforcement for AI agent tool calls. It evaluates every tool call against user-defined rules before execution, with no LLM in the authorization path.
    4
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables agents to score, inspect, and gate tool calls before they execute, resolving shell commands the way a shell would in order to block destructive actions and detect multi-step attack sequences. It also redacts credentials and PII in either direction, requires signed single-use approval tokens for high-impact actions, and keeps a tamper-evident audit trail.
    6
    11 npm
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to securely reach enterprise tools and other agents through a governed gateway with pinned tool definitions, delegated authorization, content inspection, human approval, and signed audit trails.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to safely invoke enterprise tools such as Supabase PostgreSQL and the Notion API through a zero-trust gateway that performs server-side policy and risk evaluation, cryptographically binds approval tickets to exact parameters, and requires out-of-band human approval for destructive operations. It also scrubs credentials from logs and responses while providing audit trails, rate limiting, and a web console for reviewing pending authorizations.
    Apache 2.0