ActionFence
AI Action Firewall for MCP servers and APIs.
One line of code. Signed receipts. Simulation mode.
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.jsonoutside any tool-accessible directories. If your MCP server has aread_filetool, 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
-
Install the
pgdriver:npm install pg -
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_jsonis redacted/truncated (privacy). payloadRedactormust return a sanitized copy — do not mutate the input.maxPayloadBytesdefaults to 65 536 (64 KB). Payloads exceeding this are replaced with a truncation marker that includes the original hash.
Payload Requirements
paramsmust be JSON-serializable.- Unsupported values such as
BigInt,Symbols, and circular references will fail before receipt creation. undefinedvalues are omitted by canonicalization, andNaNorInfinitybecomenull.- 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 thepostgresstorage 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:
options.secretACTIONFENCE_SECRET.actionfence/key
Operator CLI support is now built in:
actionfence receipts listto inspect stored receipts with filtersactionfence receipts verifyto validate the full hash chain and signaturesactionfence receipts export --format json|csvto 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
ActionFenceToolErrorby default; convert them to a framework-nativeToolMessagein middleware or a custom tool node if you want model-visible error output - No path-policy DSL
requires_human_approvalpauses execution only whenonApprovalRequiredis 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 returnreceipt: null - Money is major-unit only; mixed-currency accounting is out of scope for one policy
- SQLite storage is single-instance only (use the
postgresadapter 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
No comments yet
Be the first to share your take.