ActionFence

AI Action Firewall for MCP servers and APIs.

One line of code. Signed receipts. Simulation mode.

CI License: MIT Node.js

Why ActionFence?

AI agents can now book flights, send emails, and delete records in your system — often without you knowing until the invoice arrives. ActionFence lets you define exactly what they're allowed to do, and proves every decision with a signed receipt.

It sits in front of your MCP tools or HTTP routes and decides whether an incoming agent action is allowed before your real handler runs.

It gives you:

  • Policy enforcement from guard-policy.json
  • Identity tiers: anonymous, token, verified
  • Built-in verified JWT support via JWKS
  • Capability scope checks from JWT capabilities
  • Per-action spend caps plus session/day spend limits
  • Request and transaction rate limiting
  • Signed, hash-chained receipts in SQLite
  • LangGraph / LangChain tool wrappers
  • Simulation mode for dry-run previews

Install

npm install actionfence

Quick Start

MCP Server

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { withGuard } from 'actionfence';

const server = new McpServer({ name: 'my-server', version: '1.0.0' });

withGuard(server, {
  policy: './guard-policy.json',
  identityReaderOptions: {
    jwksUri: 'https://issuer.example/.well-known/jwks.json',
    issuer: 'https://issuer.example',
    audience: 'bookflight-mcp',
  },
});

server.registerTool('search_flights', {}, async () => {
  return { content: [{ type: 'text', text: 'results...' }] };
});

Express / Fastify

import express from 'express';
import { guard } from 'actionfence';

const app = express();
app.use(express.json());

app.use(
  guard({
    policy: './guard-policy.json',
    identityReaderOptions: {
      jwksUri: 'https://issuer.example/.well-known/jwks.json',
      issuer: 'https://issuer.example',
      audience: 'bookflight-api',
    },
    actionResolver: (toolName) => {
      if (/^GET \/bookings\/[^/]+$/.test(toolName)) {
        return 'GET /bookings/:id';
      }
      return toolName;
    },
    spendExtractor: (params) => {
      const body = (params as { body?: { amount?: number } })?.body;
      return body?.amount ?? null;
    },
  }),
);

LangGraph / LangChain

import { tool } from '@langchain/core/tools';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { withLangChainTools } from 'actionfence';
import * as z from 'zod';

const searchOrders = tool(async ({ query }: { query: string }) => {
  return `Found orders for ${query}`;
}, {
  name: 'search_orders',
  description: 'Search orders by query',
  schema: z.object({
    query: z.string(),
  }),
});

const guarded = withLangChainTools([searchOrders], {
  policy: './guard-policy.json',
  contextResolver: ({ config }) => {
    const token = (config as { configurable?: { token?: string } } | undefined)?.configurable
      ?.token;
    return token ? { headers: { authorization: `Bearer ${token}` } } : undefined;
  },
});

const toolNode = new ToolNode(guarded.tools);

ActionFence evaluates the tool before LangGraph or LangChain executes it. If a tool is blocked, the wrapper throws ActionFenceToolError so you can convert it into a ToolMessage in LangChain middleware or a custom LangGraph tool node.

CLI

npx actionfence init
npx actionfence generate "node server.js" --output guard-policy.json --pin-schemas
npx actionfence validate guard-policy.json
npx actionfence validate guard-policy.json "node server.js"
npx actionfence pin-schemas guard-policy.json "node server.js" --diff
npx actionfence simulate guard-policy.json --action book_flight --identity verified --spend 250

Using an AI Coding Assistant?

Copy this prompt into Claude, Cursor, Copilot, or any LLM and let it handle the setup:

Install and integrate the "actionfence" npm package into my current project. Read the full integration guide at https://raw.githubusercontent.com/saifeldeen911/actionfence/main/llms-full.txt then: install the package, create a guard-policy.json for my use case, and wire up the middleware.

Trust Model

ActionFence runs on your server as middleware. The agent communicates with your server via MCP protocol or HTTP — it never has direct access to the policy file or the enforcement engine.

This is server-side enforcement, not a client-side honor system.

  • The agent cannot read guard-policy.json — it's a file on your server
  • Tool calls routed through ActionFence are evaluated before they reach your real handlers
  • The agent cannot tamper with receipts — they're signed with your secret key

ActionFence only enforces actions that pass through the protected MCP or HTTP boundary. It does not replace branch protection, deploy approvals, scoped cloud tokens, scoped GitHub App permissions, or other infrastructure-level controls.

Tip: Keep guard-policy.json outside any tool-accessible directories. If your MCP server has a read_file tool, make sure it can't access the directory where your policy file lives.

Storage

ActionFence stores signed receipts in SQLite by default (zero-config). For serverless or horizontally-scaled deployments, use PostgreSQL:

PostgreSQL Setup

  1. Install the pg driver:

    npm install pg
    
  2. Configure storage in your guard setup:

    withGuard(server, {
      policy: './guard-policy.json',
      storage: {
        adapter: 'postgres',
        connectionString: process.env.DATABASE_URL,
      },
    });
    

ActionFence auto-creates the actionfence_receipts table on first use.

By default, receipt persistence failures are logged and the guard preserves backward-compatible allow mode, returning receipt: null if execution is still allowed by policy. To fail closed when receipt storage is unavailable, opt in:

withGuard(server, {
  policy: './guard-policy.json',
  receiptFailureMode: 'block',
});

Policy File

guard-policy.json defines what agents can do in your system.

{
  "$schema": "https://raw.githubusercontent.com/saifeldeen911/actionfence/main/schemas/guard-policy.schema.json",
  "service": "BookFlight.com",
  "version": "1.0",
  "default_rule": "deny",
  "actions": {
    "search_flights": {
      "allowed": true,
      "identity": "any"
    },
    "book_flight": {
      "allowed": true,
      "identity": "verified",
      "max_spend": 500,
      "currency": "USD",
      "requires_human_approval": true
    },
    "bulk_booking": {
      "allowed": false
    }
  },
  "rate_limits": {
    "requests_per_minute": 30,
    "transactions_per_day": 5
  },
  "spend_limits": {
    "session_max": 1000,
    "daily_max": 2500,
    "window": {
      "max_amount": 500,
      "duration_minutes": 60
    },
    "currency": "USD"
  },
  "circuit_breaker": {
    "global_max_spend": 10000,
    "action": "block_all",
    "currency": "USD"
  },
  "regulations": ["EU_AI_Act_Art50"]
}

Policy Reference

Field Type Required Notes
service string Yes Service name
version string Yes Policy version
default_rule "allow" | "deny" No Defaults to "deny"
actions object Yes Action rules keyed by action name
rate_limits object No Request and transaction limits
spend_limits object No Session, daily, and rolling-window spend limits
circuit_breaker object No Global maximum spend kill-switch
schema_enforcement object No Tool schema drift detection and enforcement
regulations string[] No Parsed as policy metadata but not enforced

Action Rule Fields

Field Type Default Notes
allowed boolean - Required
identity "any" | "token" | "verified" "any" Minimum identity tier
max_spend number - Per-invocation cap in major units
currency string - ISO 4217 currency code
requires_human_approval boolean false When true, pauses evaluation only if onApprovalRequired is configured; otherwise the decision records the requirement and proceeds.
schema_hash string - Pinned SHA-256 hash of the tool's input schema. Set via actionfence pin-schemas.
schema_snapshot object - Pinned copy of the tool input schema used by actionfence pin-schemas --diff to show human-readable field drift.

Identity Tiers

Tier Meaning
anonymous No credentials presented
token Bearer token present but not signature-verified
verified JWT passed JWKS verification

Verified identity is built in when you configure identityReaderOptions.jwksUri and JWKS lookup succeeds from cache or the remote endpoint. When JWKS verification is configured, verification failures, wrong issuers or audiences, unknown kids, and JWKS retrieval errors return anonymous identity. The unverified token tier is used only when no JWKS verifier is configured.

Scope Enforcement

If a decoded or verified token includes a capabilities claim, ActionFence treats it as an exact allowlist of policy action names. A request that passes policy checks but is not listed in capabilities is blocked.

Tool Schema Drift Detection

ActionFence can detect when an MCP server's tool schemas change after you've pinned them. This catches silent breaking changes that could cause agent failures or enable payload injection.

Generating a Starter Policy

actionfence generate "node server.js" --output guard-policy.json --pin-schemas
actionfence validate guard-policy.json "node server.js"

generate connects to the MCP server, discovers tools, and writes a reviewable starter policy. The generated policy defaults to default_rule: "deny" and every discovered action is written with allowed: false; it is a starting point for review, not a completed security policy. Pass --pin-schemas to include current tool schema hashes during generation.

Pinning Schemas

actionfence pin-schemas guard-policy.json "node server.js"

This command connects to the MCP server, fetches all tools, computes SHA-256 hashes of each tool's inputSchema, and writes the hashes into the policy file under each action's schema_hash field. Policies pinned with current versions also store a schema_snapshot so later drift reviews can show field-level changes.

Reviewing Drift Without Rewriting

actionfence pin-schemas guard-policy.json "node server.js" --diff

--diff is read-only. It compares the live MCP schema against the pinned policy snapshot, prints added fields, removed fields, type changes, enum changes, and required-field changes, and returns exit code 2 when drift is found so CI can fail cleanly.

Validating for Drift

actionfence validate guard-policy.json "node server.js"

Compares live tool schemas against pinned hashes and reports any mismatches.

Runtime Enforcement

Configure schema_enforcement in your policy to control drift behavior at runtime:

{
  "schema_enforcement": {
    "on_mismatch": "warn"
  }
}
on_mismatch Behavior
"warn" Logs a warning on drift but allows the action (default if not configured)
"block" Blocks the action if the schema has drifted from the pinned hash

When schema_enforcement is omitted, pinned schema hashes are still checked with warning behavior. Set on_mismatch to "block" to deny drifted tools at runtime.

Payload Processing

ActionFence canonicalizes tool params before hashing receipts. That keeps the receipt chain deterministic, but it also means tool payloads must stay JSON-friendly.

Payload Redaction

By default, full tool params are stored in receipts. To strip sensitive fields (passwords, API keys, PII) before persistence, use payloadRedactor:

withGuard(server, {
  policy: './guard-policy.json',
  payloadRedactor: (params) => {
    const safe = { ...(params as Record<string, unknown>) };
    delete safe.password;
    delete safe.apiKey;
    return safe;
  },
  maxPayloadBytes: 32_768, // optional: truncate stored payloads above 32 KB
});
  • The receipt hash is computed from the original params (integrity is preserved).
  • Only the stored payload_json is redacted/truncated (privacy).
  • payloadRedactor must return a sanitized copy — do not mutate the input.
  • maxPayloadBytes defaults to 65 536 (64 KB). Payloads exceeding this are replaced with a truncation marker that includes the original hash.

Payload Requirements

  • params must be JSON-serializable.
  • Unsupported values such as BigInt, Symbols, and circular references will fail before receipt creation.
  • undefined values are omitted by canonicalization, and NaN or Infinity become null.
  • Tool authors should validate their params are JSON-friendly before calling ActionFence-protected endpoints.

Simulation Mode

Simulation mode runs the full policy pipeline without executing the handler or storing a receipt.

MCP

withGuard(server, {
  policy: './guard-policy.json',
  simulate: true,
});

Express

app.use(
  guard({
    policy: './guard-policy.json',
    simulate: true,
  }),
);

CLI output

actionfence simulate guard-policy.json --action book_flight --identity verified --spend 250

SIMULATION - actionfence

  Action:          book_flight
  Tool:            book_flight
  Identity:        verified
  Status:          PASS
  Spend:           250.00
  Session total:   250.00
  Daily total:     250.00
  Human approval:  required
  Rate limit:      29/30 remaining

Action Receipts

In normal operation, every enforced decision stores a signed receipt in SQLite.

Stored payloads may be redacted or truncated before persistence. The receipt binds the original request hash and the stored payload-view hash so chain verification can still detect tampering.

receipt_id:     a1b2c3d4-...
timestamp:      2026-05-07T14:02:11Z
agent_id:       agt_7x9f2k...
action:         book_flight
status:         PASSED
payload_hash:   0x8f3a...
prev_hash:      0x7e2d...
receipt_sig:    0x4f9b...

Receipts are:

  • Hash-chained
  • HMAC-SHA256 signed
  • Append-only
  • Verifiable with ReceiptStore.verifyChain()

Note: Receipts are stored in a local SQLite file (.actionfence/receipts.db) by default. This works perfectly for single-instance deployments. If you run multiple server instances, use the postgres storage adapter to maintain a single global receipt chain.

Receipt persistence defaults to allow mode for backward compatibility. Set receiptFailureMode: 'block' to make ActionFence return ACTIONFENCE_RECEIPT_PERSISTENCE_FAILED with HTTP 503/MCP error output and prevent handler execution when receipt storage is unavailable. Simulation mode never stores receipts.

Signing key resolution order:

  1. options.secret
  2. ACTIONFENCE_SECRET
  3. .actionfence/key

Operator CLI support is now built in:

  • actionfence receipts list to inspect stored receipts with filters
  • actionfence receipts verify to validate the full hash chain and signatures
  • actionfence receipts export --format json|csv to extract evidence for review or reporting

If you verify receipts from a non-default store location, pass the original signing secret with --secret or the signing key file with --key.

API Reference

withGuard(server, options)

const guardInstance = withGuard(server, {
  policy: './guard-policy.json',
  simulate: false,
  silent: false,
  secret: process.env.ACTIONFENCE_SECRET,
  identityReaderOptions: {
    jwksUri: 'https://issuer.example/.well-known/jwks.json',
    issuer: 'https://issuer.example',
    audience: 'bookflight-mcp',
  },
  actionResolver: (toolName, params) => toolName,
  spendExtractor: (params) => null,
  onDecision: (decision) => {},
  watchPolicy: true,
});

const status = guardInstance.getAgentStatus('agt_7x9f2k');
console.log(`Allowed actions:`, status.allowedActions);

guardInstance.dispose();

guard(options)

const middleware = guard({
  policy: './guard-policy.json',
  identityReaderOptions: {
    jwksUri: 'https://issuer.example/.well-known/jwks.json',
  },
});

withLangChainTools(tools, options)

const guarded = withLangChainTools(tools, {
  policy: './guard-policy.json',
});

const modelWithTools = model.bindTools(guarded.tools);

Returns { tools, engine, dispose() } and shares one GuardEngine across the wrapped tools.

withLangChainTool(tool, options)

const guarded = withLangChainTool(searchOrders, {
  policy: './guard-policy.json',
});

await guarded.tool.invoke({ query: 'pending' });

Returns { tool, engine, dispose() } for single-tool integrations.

GuardOptions

Option Type Default Notes
policy string | GuardPolicy - Required
simulate boolean false Dry-run mode
silent boolean false Suppress console output
secret string - HMAC secret override
identityReaderOptions IdentityReaderOptions - Built-in JWKS verification config
identityReader IdentityReaderLike - Full custom identity resolution override
actionResolver (toolName, params) => string - Map tool names to policy actions
spendExtractor (params) => number | null - Extract spend in major units
transactionResolver (toolName, params, decision) => boolean - Override transaction classification
onDecision (decision) => void - Metrics, logging, hooks
onApprovalRequired (decision) => Promise<boolean> - Webhook callback to pause and await human approval
approvalTimeoutMs number 30000 Timeout for approval in milliseconds
watchPolicy boolean false Hot-reload file-backed policies
storage StorageConfig - Storage backend (SQLite/PostgreSQL)
receiptFailureMode 'allow' | 'block' 'allow' Use 'block' to fail closed when receipt persistence fails
payloadRedactor (params: unknown) => unknown - Strip sensitive fields before receipt storage
maxPayloadBytes number 65536 Max stored payload_json size; larger payloads are truncated

IdentityReaderOptions

Field Type Notes
jwksUri string Remote JWKS endpoint
issuer string | string[] Optional issuer check
audience string | string[] Optional audience check

Current Limitations

  • Capability checks are exact string matches only
  • No APoP / LAS-WG adapters yet
  • LangChain / LangGraph blocked tool calls throw ActionFenceToolError by default; convert them to a framework-native ToolMessage in middleware or a custom tool node if you want model-visible error output
  • No path-policy DSL
  • requires_human_approval pauses execution only when onApprovalRequired is configured; there is no built-in approval UI or queue
  • Receipt persistence fail-closed behavior is opt-in via receiptFailureMode: 'block'; default allow mode can still return receipt: null
  • Money is major-unit only; mixed-currency accounting is out of scope for one policy
  • SQLite storage is single-instance only (use the postgres adapter for multi-instance deployments)

CLI Reference

actionfence init

actionfence init
actionfence init --service MyAPI
actionfence init --output ./policies/guard-policy.json

actionfence generate <server-command>

actionfence generate "node server.js"
actionfence generate "node server.js" --default-rule deny --output guard-policy.json --service MyAPI
actionfence generate "node server.js" --pin-schemas

Discovers tools from an MCP server and writes a reviewable guard-policy.json. The command refuses to overwrite an existing output file, writes discovered actions as blocked (allowed: false), and only adds schema_hash values when --pin-schemas is passed.

actionfence validate <path>

actionfence validate guard-policy.json
actionfence validate guard-policy.json "node server.js"  # check schema drift

actionfence pin-schemas <path> <server-command>

actionfence pin-schemas guard-policy.json "node server.js"
actionfence pin-schemas guard-policy.json "node server.js" --diff

Connects to the MCP server, hashes each tool's input schema, and pins the hashes in the policy file. Pass --diff for a read-only drift review that prints field-level changes and returns exit code 2 when drift is detected.

actionfence simulate <path>

actionfence simulate guard-policy.json --action search_flights
actionfence simulate guard-policy.json --action book_flight --identity verified --spend 250

actionfence receipts <list|verify|export>

actionfence receipts list --db .actionfence/receipts.db --status BLOCKED
actionfence receipts verify --db .actionfence/receipts.db --key .actionfence/key
actionfence receipts export --db .actionfence/receipts.db --format csv --output receipts.csv
actionfence receipts list --postgres-url "$DATABASE_URL" --agent agt_123

Inspects stored receipts without adding a separate dashboard. list supports --agent, --action, --status, --since, --until, and --limit. verify checks the full persisted hash chain and receipt signatures. export writes filtered receipts as JSON or CSV, either to stdout or to --output.

For verify, use the same signing material that created the receipts:

  • --secret <base64url> for explicit HMAC material
  • --key <path> for the generated key file
  • or ACTIONFENCE_SECRET / AGENTGUARD_SECRET

Examples

Development

git clone https://github.com/saifeldeen911/actionfence.git
cd actionfence
npm install
npm run typecheck
npm run lint
npm test
npm run build

License

MIT (c) Saifeldeen