Policy Engine¶
The policy engine is the core decision-making component of ai-tool-guard. It evaluates every tool call against a set of rules and an optional external backend, producing a DecisionRecord that explains why a call was allowed, denied, or sent for approval.
Overview¶
When a guarded tool is invoked, the engine runs the following pipeline:
- Resolve the tool's
riskLevel(from per-tool config ordefaultRiskLevel). - If a
PolicyBackendis configured, delegate to it first. - Evaluate the built-in
PolicyRulelist in priority order. - Merge results using escalation semantics:
deny>require-approval>allow. - Return a
DecisionRecordcapturing the verdict, matched rules, duration, and reason.
The default verdict when no rule matches is "allow".
Basic Usage¶
Pass an array of PolicyRule objects (or use a preset such as defaultPolicy()) when creating your guard:
import { createToolGuard, defaultPolicy } from "ai-tool-guard";
const guard = createToolGuard({
rules: defaultPolicy(),
onDecision: (record) => {
console.log(`[${record.verdict}] ${record.toolName} — ${record.reason}`);
},
});
Rules can also be written by hand:
import { createToolGuard } from "ai-tool-guard";
import type { PolicyRule } from "ai-tool-guard";
const rules: PolicyRule[] = [
{
id: "deny-delete-tools",
description: "Block all deletion tools unconditionally.",
toolPatterns: ["*delete*", "*remove*", "*drop*"],
verdict: "deny",
priority: 100,
},
{
id: "allow-reads",
description: "Allow all read-only tools.",
toolPatterns: ["read*", "get*", "list*", "search*"],
verdict: "allow",
priority: 10,
},
];
const guard = createToolGuard({ rules });
Configuration Options¶
PolicyRule¶
| Property | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Stable identifier included in DecisionRecord.matchedRules. |
toolPatterns |
string[] |
Yes | Glob patterns matched against the tool name. |
verdict |
DecisionVerdict |
Yes | One of "allow", "deny", or "require-approval". |
riskLevels |
RiskLevel[] |
No | When set, the rule only applies to tools at these risk levels. |
condition |
(ctx: PolicyContext) => boolean \| Promise<boolean> |
No | Predicate for attribute- or context-based matching. Supports async. |
priority |
number |
No | Evaluation order. Higher values are evaluated first. Default 0. |
description |
string |
No | Human-readable description recorded in DecisionRecord.reason. |
Glob Pattern Matching¶
Tool names are matched against each pattern in toolPatterns using a minimal glob matcher. The pattern is anchored at both ends.
| Wildcard | Matches |
|---|---|
* |
Any sequence of characters, including the empty string. |
? |
Exactly one character. |
"*" // matches every tool name
"db.*" // matches "db.query", "db.insert", "db.delete"
"read*" // matches "readFile", "readStream" but not "canRead"
"*File" // matches "readFile", "writeFile", "deleteFile"
"get?sers" // matches "getUsers"; the ? substitutes exactly one character
Dot characters are literal
The dot (.) in a glob pattern matches a literal dot, not any character. Use db.* to match namespaced tool names such as db.query without matching dbquery.
Risk Level Filtering¶
When riskLevels is set on a rule, the rule is skipped for tools that do not match one of the listed levels:
const rules: PolicyRule[] = [
{
id: "approve-medium-risk",
toolPatterns: ["*"],
riskLevels: ["medium"],
verdict: "require-approval",
priority: 0,
},
];
If riskLevels is omitted, the rule applies to tools at any risk level.
Priority and Escalation¶
Rules are sorted by priority in descending order before evaluation. All matching rules are collected, and the most restrictive verdict wins across all matches:
A high-priority allow rule does not suppress a lower-priority deny rule if both match. The engine accumulates every match and selects the strictest outcome.
All matched rules are recorded
DecisionRecord.matchedRules lists every rule that matched, not only the one that determined the final verdict. This gives you a complete audit trail even when escalation occurs across multiple rules.
Advanced Examples¶
Role-Based Access Control¶
Use userAttributes combined with a condition predicate to restrict tools based on the caller's role:
import { createToolGuard } from "ai-tool-guard";
import type { PolicyRule } from "ai-tool-guard";
const rules: PolicyRule[] = [
{
id: "deny-admin-tools-for-non-admins",
description: "Block billing and admin tools for callers without the admin role.",
toolPatterns: ["billing.*", "admin.*"],
verdict: "deny",
priority: 50,
condition: (ctx) => {
const roles = ctx.userAttributes["roles"] as string[] | undefined;
return !roles?.includes("admin");
},
},
{
id: "allow-admin-tools-for-admins",
description: "Admins may use billing and admin tools.",
toolPatterns: ["billing.*", "admin.*"],
verdict: "allow",
priority: 60,
condition: (ctx) => {
const roles = ctx.userAttributes["roles"] as string[] | undefined;
return roles?.includes("admin") ?? false;
},
},
];
const guard = createToolGuard({
rules,
resolveUserAttributes: async () => {
return { roles: await getCurrentUserRoles() };
},
});
Time-Based Restrictions¶
Async conditions let you query external data sources, including time-sensitive business logic:
import type { PolicyRule } from "ai-tool-guard";
const businessHoursOnly: PolicyRule = {
id: "business-hours-only",
description: "Block payment tools outside of UTC 09:00–17:00.",
toolPatterns: ["payment.*", "charge*", "refund*"],
verdict: "deny",
priority: 80,
condition: async (_ctx) => {
const hour = new Date().getUTCHours();
// Return true (condition met → rule fires) when outside business hours.
return hour < 9 || hour >= 17;
},
};
Conversation-Aware Escalation¶
Rules can inspect the conversation context to tighten policy after repeated failures in a session:
import type { PolicyRule } from "ai-tool-guard";
const escalateAfterFailures: PolicyRule = {
id: "escalate-on-repeated-failures",
description: "Require approval for any tool after 3 prior failures in a session.",
toolPatterns: ["*"],
verdict: "require-approval",
priority: 200,
condition: (ctx) => {
return (ctx.conversation?.priorFailures ?? 0) >= 3;
},
};
How It Works¶
The evaluation function evaluatePolicy (in src/policy/engine.ts) runs in this sequence:
-
Risk resolution — The tool's
riskLevelis taken fromToolGuardConfig.riskLevel, falling back toGuardOptions.defaultRiskLevel, then to"low"if neither is set. -
External backend — If
GuardOptions.backendis configured,backend.evaluate(ctx)is called and its result seeds the initialverdict,reason,matchedRules, andattributesfields on the record. -
Built-in rules — The rules array is sorted by
prioritydescending. Each rule is tested in turn: glob match againsttoolName, then risk level filter, then the optional asyncconditionpredicate. Every matching rule is collected. -
Escalation merge — The built-in rules result is compared to the backend result. If the rules verdict is stricter, it replaces the backend verdict. Matched rule IDs from both sources are merged into
DecisionRecord.matchedRules. -
DecisionRecord construction — A complete record is assembled with a unique
id, ISO-8601timestamp, finalverdict, human-readablereason, mergedattributes,evalDurationMs, anddryRunflag.
The default verdict is allow
If no rule matches and no backend is configured, the verdict is "allow". Deploy defaultPolicy() or an explicit catch-all deny rule to avoid unintentional permissiveness in production environments.
Related¶
- Preset Policies — ready-made rule bundles for common scenarios.
- External Backends — delegate decisions to OPA, Cedar, or a custom service.
- API Reference — full type documentation for
PolicyRule,PolicyContext, andDecisionRecord.