Secure Agent Tool Access with Permission Policies
When an agent has access to tools, you need to control which tools it can invoke and under what conditions. ToolPermissionPolicy provides a declarative, composable rule system that governs tool access at runtime. It supports allow/deny lists, category rules, wildcard patterns, risk-level gates, and an approval workflow for human-in-the-loop scenarios.
For background on what tools are and how they work, see the AI Agent Tools glossary entry.
Why This Matters
Two production problems that permission policies solve:
- Preventing unintended side effects. An agent equipped with file system and HTTP tools could delete files or send data to external endpoints. A permission policy lets you allow read operations while blocking writes and network mutations, ensuring the agent cannot exceed its intended scope.
- Compliance and audit requirements. Enterprise deployments require documented control over what AI agents can do. Permission policies provide a single, inspectable configuration that maps directly to security requirements, making compliance reviews straightforward.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| VRAM | 6+ GB (for a tool-calling model) |
| Model | Must support tool calling |
Step 1: Create the Project
dotnet new console -n ToolPolicies
cd ToolPolicies
dotnet add package LM-Kit.NET
Step 2: Understand the Permission Model
ToolPermissionPolicy evaluates every tool invocation and returns one of three results:
| Result | Meaning |
|---|---|
Allowed |
The tool may execute immediately |
Denied |
The tool is blocked. The denial reason is sent back to the model so it can adapt |
ApprovalRequired |
The tool needs explicit user approval before execution |
Rules are evaluated in a strict order where deny always wins:
- Explicit deny by name
- Deny by wildcard pattern
- Deny by category
- Risk-level gate (tools above the threshold are denied)
- Approval check (explicit or category-based)
- Explicit allow by name
- Allow by wildcard pattern
- Allow by category
DefaultAction(fallback, defaults toAllow)
For an overview of built-in tool categories, risk levels, and metadata, see the Equip an Agent with Built-In Tools guide.
Step 3: Build a Safe Chat Policy
This policy restricts the agent to pure computation tools with no I/O or network access.
using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
loadingProgress: p => { Console.Write($"\rLoading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// Define a restrictive policy: only safe categories, no I/O or network
var safePolicy = new ToolPermissionPolicy()
.AllowCategory("data", "text", "numeric", "utility", "security")
.DenyCategory("io", "net")
.SetMaxRiskLevel(ToolRiskLevel.Low);
var agent = Agent.CreateBuilder(model)
.WithPersona("Calculator Assistant")
.WithInstruction("You answer math and data questions using your tools.")
.WithTools(tools =>
{
tools.Register(BuiltInTools.CalcArithmetic);
tools.Register(BuiltInTools.DateTimeNow);
tools.Register(BuiltInTools.JsonParse);
})
.WithPermissionPolicy(safePolicy)
.Build();
var executor = new AgentExecutor();
// The agent can use CalcArithmetic (numeric category, low risk) but
// any io or net tool would be denied if registered.
var result = executor.Execute(agent, "What is 1547 * 23 + 891?");
Console.WriteLine(result.Content);
Step 4: Build a Developer Assistant Policy with Granular Control
This policy uses wildcard patterns to allow file reads but deny file deletes, and requires approval for process execution.
// Fine-grained policy using wildcard patterns
var devPolicy = new ToolPermissionPolicy()
{
DefaultAction = ToolPermissionAction.Deny // whitelist mode
};
devPolicy
.AllowCategory("data", "text", "numeric", "utility", "security")
.Allow("filesystem_read", "filesystem_list", "filesystem_search")
.Deny("filesystem_delete", "filesystem_write")
.Allow("http_get", "http_head")
.Deny("http_post", "http_put", "http_delete")
.RequireApproval("process_*")
.SetMaxRiskLevel(ToolRiskLevel.High);
Key decisions in this policy:
DefaultAction = Deny: Only explicitly allowed tools can run (whitelist mode).- Wildcard
"process_*": Any tool whose name starts withprocess_requires user approval. SetMaxRiskLevel(High): Tools ratedCriticalare blocked unless explicitly allowed by name.
Step 5: Handle Approval Requests
When a tool evaluation returns ApprovalRequired, the ToolApprovalRequired event fires on the AgentExecutor. If no handler subscribes (or no handler sets Approved = true), the tool is denied.
var executor = new AgentExecutor();
executor.ToolApprovalRequired += (sender, args) =>
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[APPROVAL] Tool: {args.ToolCall.Name}");
Console.WriteLine($" Arguments: {args.ToolCall.ArgumentsJson}");
if (args.RiskLevel.HasValue)
Console.WriteLine($" Risk level: {args.RiskLevel.Value}");
if (args.SideEffect.HasValue)
Console.WriteLine($" Side effect: {args.SideEffect.Value}");
Console.ResetColor();
Console.Write("Allow this tool call? (y/n): ");
string input = Console.ReadLine()?.Trim().ToLower() ?? "n";
args.Approved = input == "y";
if (!args.Approved)
{
args.DenialReason = "User denied the tool invocation.";
}
};
When denied, the model receives the denial reason as a ToolCallResultType.Denied result and can adjust its approach (e.g., explaining what it would have done, or trying an alternative tool).
Step 6: Monitor Tool Invocations
Use BeforeToolInvocation and AfterToolInvocation events to log and audit every tool call.
executor.BeforeToolInvocation += (sender, e) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"[BEFORE] {e.ToolCall.Name} | Permission: {e.PermissionResult}");
Console.ResetColor();
// Optionally cancel the invocation
// e.Cancel = true;
};
executor.AfterToolInvocation += (sender, e) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write($"[AFTER] {e.ToolCallResult.ToolName} | Result: {e.ToolCallResult.Type}");
if (e.ToolCallResult.Type == ToolCallResultType.Denied)
Console.Write($" | Reason: {e.ToolCallResult.ResultJson}");
Console.WriteLine();
Console.ResetColor();
};
Step 7: Evaluate Permissions Programmatically
You can evaluate permissions outside the agent loop, useful for building UIs that show which tools are available.
var policy = new ToolPermissionPolicy()
.AllowCategory("numeric", "text")
.DenyCategory("io", "net")
.SetMaxRiskLevel(ToolRiskLevel.Medium);
// Evaluate individual tools
ToolPermissionResult calcResult = policy.Evaluate(BuiltInTools.CalcArithmetic);
Console.WriteLine($"CalcArithmetic: {calcResult}"); // Allowed
// Evaluate via the ToolRegistry
var registry = new ToolRegistry();
registry.Register(BuiltInTools.CalcArithmetic);
registry.Register(BuiltInTools.DateTimeNow);
registry.PermissionPolicy = policy;
ToolPermissionResult result = registry.EvaluatePermission("calc_arithmetic");
Console.WriteLine($"calc_arithmetic via registry: {result}");
Policy Recipes
Read-Only Agent
Allows all tools that only read state, blocks any tool that writes.
var readOnlyPolicy = new ToolPermissionPolicy()
{
DefaultAction = ToolPermissionAction.Deny
};
readOnlyPolicy
.AllowCategory("data", "text", "numeric", "utility", "security")
.Allow("filesystem_read", "filesystem_list", "filesystem_search")
.Allow("http_get", "http_head")
.Deny("filesystem_write", "filesystem_delete")
.Deny("http_post", "http_put", "http_delete");
Full Access with Approval Gate
Allows everything but requires approval for high-risk operations.
var gatedPolicy = new ToolPermissionPolicy()
.RequireApprovalForCategory("io", "net")
.SetMaxRiskLevel(ToolRiskLevel.Critical);
Strict Whitelist
Only the named tools are allowed; everything else is blocked.
var strictPolicy = new ToolPermissionPolicy()
{
DefaultAction = ToolPermissionAction.Deny
};
strictPolicy.Allow("calc_arithmetic", "datetime_now", "json_parse");
What to Read Next
- Equip an Agent with Built-In Tools: register tools and understand the catalog
- Intercept and Control Tool Invocations: advanced interception patterns
- Monitor Agent Execution with Tracing: observability for tool calls
- AI Agent Guardrails: broader guardrail concepts
- Add Middleware Filters to Agents and Conversations: combine permission policies with middleware filters for defense-in-depth