Ship Least-Privilege AI Agents in TypeScript: The TokenFence Policy Engine Hits Node.js
The Policy Engine Crosses the Language Barrier
Two days ago, we shipped the AgentGuard Policy engine in TokenFence's Python SDK. The response was immediate: "When is this coming to Node.js?"
Today. Right now. npm install tokenfence@0.2.0.
If you're building AI agents in TypeScript — whether with the Vercel AI SDK, LangChain.js, or raw OpenAI/Anthropic clients — you now have the same least-privilege enforcement that Python developers get. Zero dependencies. Full TypeScript types. 53 tests passing.
Why AI Agents Need Policies, Not Just Prompts
The pattern is becoming painfully clear in 2026:
- Developer builds an AI agent with tool access
- Agent gets system prompt: "You can read the database but never delete anything"
- Agent encounters an edge case the prompt didn't cover
- Data is lost, emails are sent, files are deleted
Prompts are suggestions. Policies are enforcement. The Meta AI agent incident, the Grigorev database wipe — both would have been prevented by a deny list that the agent literally cannot bypass.
How It Works in TypeScript
The API is designed to be instantly familiar to TypeScript developers:
import { Policy } from "tokenfence";
// Define what your database agent can do
const policy = new Policy({
name: "database-agent",
allow: ["db_read_*", "db_list_*", "db_count_*"],
deny: ["db_delete_*", "db_drop_*", "db_truncate_*"],
requireApproval: ["db_write_*", "db_update_*"],
default: "deny", // anything not listed = blocked
});
// Before every tool call, check the policy
const result = policy.check("db_read_users");
console.log(result.allowed); // true
const dangerous = policy.check("db_drop_table");
console.log(dangerous.denied); // true
console.log(dangerous.reason); // "denied by pattern: db_drop_*"
Enforce Mode: Throw on Denied
For production agents, use enforce() instead of check(). It throws a ToolDenied exception when a tool call is blocked — your agent literally cannot proceed with the dangerous action:
import { Policy, ToolDenied } from "tokenfence";
const policy = new Policy({
allow: ["read_*", "search_*"],
deny: ["delete_*", "drop_*"],
});
try {
policy.enforce("delete_user_data");
} catch (e) {
if (e instanceof ToolDenied) {
console.log(e.tool); // "delete_user_data"
console.log(e.reason); // "denied by pattern: delete_*"
console.log(e.matchedRule); // "delete_*"
}
}
Approval Gates: Human-in-the-Loop Where It Matters
Some operations aren't dangerous enough to block outright, but you want a human to confirm. The requireApproval pattern with onApproval callbacks gives you exactly this:
const policy = new Policy({
allow: ["email_read_*", "email_draft_*"],
deny: ["email_delete_*"],
requireApproval: ["email_send_*"],
onApproval: (result) => {
// Your approval logic — check a queue, ask a human, etc.
return userApproved(result.tool);
},
});
// Reads go through instantly
policy.check("email_read_inbox"); // allowed
// Sends get routed through approval
policy.check("email_send_newsletter"); // depends on callback
Full Audit Trail
Every policy decision is recorded with timestamps. This is critical for compliance, debugging, and understanding what your agents actually did:
const policy = new Policy({
allow: ["*"],
deny: ["rm_*"],
audit: true, // enabled by default
});
policy.check("read_file", { user: "agent-1", session: "abc" });
policy.check("rm_important_data", { user: "agent-1", session: "abc" });
for (const entry of policy.auditLog) {
console.log(`${entry.tool} → ${entry.decision} (${entry.reason})`);
// read_file → allow (allowed by pattern: *)
// rm_important_data → deny (denied by pattern: rm_*)
}
Policy as Code: Serialize and Version Control
Policies can be serialized to JSON and loaded from config files. This means you can version-control your security policies alongside your code:
// Export policy to JSON
const dict = policy.toDict();
fs.writeFileSync("policy.json", JSON.stringify(dict, null, 2));
// Load policy from JSON
const loaded = JSON.parse(fs.readFileSync("policy.json", "utf-8"));
const restoredPolicy = Policy.fromDict(loaded);
Python + Node.js Parity
The TypeScript Policy engine has full feature parity with the Python version:
| Feature | Python SDK | Node.js SDK |
|---|---|---|
| Allow/Deny/RequireApproval | ✅ v0.3.0 | ✅ v0.2.0 |
| Glob wildcards (* and ?) | ✅ | ✅ |
| Deny-by-default | ✅ | ✅ |
| Approval callbacks | ✅ | ✅ |
| Full audit trail | ✅ | ✅ |
| enforce() with exceptions | ✅ | ✅ |
| Serialization (toDict/fromDict) | ✅ | ✅ |
| Zero dependencies | ✅ | ✅ |
| Full type annotations | ✅ | ✅ |
| Test coverage | 96 tests | 53 tests |
Integrating with Your Agent Framework
The Policy engine works with any TypeScript agent framework. Here's a pattern for the Vercel AI SDK:
import { Policy } from "tokenfence";
import { generateText, tool } from "ai";
const policy = new Policy({
allow: ["searchWeb", "readFile"],
deny: ["deleteFile", "executeCode"],
requireApproval: ["sendEmail"],
});
// Wrap your tool execution
async function safeToolCall(toolName: string, args: any) {
const result = policy.enforce(toolName); // throws if denied
if (result.needsApproval) {
const approved = await requestHumanApproval(toolName, args);
if (!approved) throw new Error("Tool call not approved");
}
return executeOriginalTool(toolName, args);
}
Get Started
npm install tokenfence
The full API reference is in the docs. The source is at github.com/u4ma-kev/tokenfence-node.
TokenFence v0.2.0: cost circuit breaker + least-privilege policy engine. Because your AI agents should have budgets and boundaries.
Ready to protect your AI budget?
Two lines of code. Per-workflow budgets. Automatic model downgrade. Hard kill switch.