Table of Contents

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:

  1. 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.
  2. 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:

  1. Explicit deny by name
  2. Deny by wildcard pattern
  3. Deny by category
  4. Risk-level gate (tools above the threshold are denied)
  5. Approval check (explicit or category-based)
  6. Explicit allow by name
  7. Allow by wildcard pattern
  8. Allow by category
  9. DefaultAction (fallback, defaults to Allow)

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 with process_ requires user approval.
  • SetMaxRiskLevel(High): Tools rated Critical are 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");