Skip to main content

Authorization overview

AI.Sentinel has two complementary pillars:

PillarPurposeDecides...
DetectionClassify what happened — prompt injection, PII leak, hallucinationSeverity (None / Low / Medium / High / Critical)
AuthorizationDecide what's allowed to happen before it doesAllow / Deny on a per-tool-call basis

Detection answers "is this content dangerous?". Authorization answers "is this caller allowed to invoke this tool?". They run on different signals, at different lifecycle points, and address different threats.

IToolCallGuard

IToolCallGuard is the authorization runtime. Every tool call across all four AI.Sentinel surfaces (in-process middleware, Claude Code hook, Copilot hook, MCP proxy) routes through it. The decision model is binary — Allow or Deny — based on a configured IAuthorizationPolicy matched against the tool name.

[AuthorizationPolicy("admin-only")]
public sealed class AdminOnlyPolicy : IAuthorizationPolicy
{
public bool IsAuthorized(ISecurityContext ctx) => ctx.Roles.Contains("admin");
}

services.AddSingleton<IAuthorizationPolicy, AdminOnlyPolicy>();
services.AddAISentinel(opts =>
{
opts.RequireToolPolicy("Bash", "admin-only");
opts.RequireToolPolicy("delete_*", "admin-only");
opts.DefaultToolPolicy = ToolPolicyDefault.Allow; // default
});

builder.Services.AddChatClient(pipeline =>
pipeline.UseAISentinel()
.UseToolCallAuthorization() // wires IToolCallGuard into the pipeline
.UseFunctionInvocation()
.Use(new OpenAIChatClient(...)));

Concepts

TypePurpose
IAuthorizationPolicyImplementation that evaluates "is this caller authorized?". Stateless, attribute-decorated with [AuthorizationPolicy("name")].
[AuthorizationPolicy("name")]Attribute that names the policy. Used by RequireToolPolicy(pattern, "name") to wire it up.
ISecurityContextThe caller identity. Carries roles, tenant ID, user ID, claims. Default implementation returns Anonymous.
ToolPolicyDefaultWhat happens when a tool call doesn't match any binding — Allow (default) or Deny.
IToolCallGuardThe runtime that pulls the right policies, evaluates them, and emits a decision.

Decision flow

1. Tool call invoked → "Bash" with arguments {...}
2. Guard looks up bindings: opts.RequireToolPolicy("Bash", "admin-only") matches
3. Guard resolves the "admin-only" policy via [AuthorizationPolicy] attribute scan
4. Guard resolves ISecurityContext from DI (or surface-specific provider)
5. Guard calls policy.IsAuthorized(ctx)
6. Allow → tool executes
Deny → tool execution short-circuited, surface-specific deny semantics applied

If multiple bindings match a single tool call, all matching policies must authorize (logical AND). If no bindings match, DefaultToolPolicy decides.

Wildcards in bindings

RequireToolPolicy accepts glob-style wildcards in the tool-name pattern:

opts.RequireToolPolicy("delete_*", "admin-only"); // delete_user, delete_database, ...
opts.RequireToolPolicy("get_*", "read-only"); // get_user, get_invoice, ...
opts.RequireToolPolicy("Bash", "admin-only"); // exact match
opts.RequireToolPolicy("*", "any-authenticated"); // catch-all (overlaps with DefaultToolPolicy)

Wildcards don't anchor — delete_* matches delete_user and delete_database but not user_delete.

Default behavior

If no policies are registered, every tool call is allowed — drop-in upgrade for existing AI.Sentinel users. No surprises, no breaking change.

If policies are registered but DefaultToolPolicy = Deny, calls without a matching binding are denied. This is the strict-deny pattern: explicitly allow what you want, deny everything else.

Per-surface deny semantics

Each surface signals deny in its native way:

SurfaceCaller resolutionDeny semantics
In-process middlewareIServiceProvider.GetService<ISecurityContext>() → Anonymousthrows ToolCallAuthorizationException
Claude Code hookHookConfig.CallerContextProvider → AnonymousHookOutput(Block, reason) (exit code 2)
Copilot hookCopilotHookConfig.CallerContextProvider → AnonymousHookOutput(Block, reason) (exit code 2)
MCP proxyDI provider → SENTINEL_MCP_CALLER_ID/_ROLES env → AnonymousMcpProtocolException(InvalidRequest, reason)

The same policy implementation works on all four surfaces — write the policy once, AI.Sentinel handles surface-specific signaling.

Why authorization vs. detection

Detection runs on content. It asks "does this prompt or response look like a threat?" — pattern matching, semantic similarity, hallucination heuristics. False positives are tolerable because the action tier (Quarantine / Alert / Log / PassThrough) is configurable per severity.

Authorization runs on caller identity. It asks "is this principal allowed to invoke this tool?" — a hard yes/no. It runs whether or not the call's content looks suspicious. A junior dev's Bash invocation is denied even when the bash command is benign — because they're not in the admin role.

In layered defense:

ThreatPillar that catches it
Prompt injection inside an echo "hello" callDetection (SEC-01)
PII inside a tool resultDetection (SEC-23)
Junior dev invoking delete_database()Authorization
External user reaching restricted MCP tool via the proxyAuthorization
Compromised LLM convincing the model to call a privileged toolDetection (catches the prompt-injection signal) AND Authorization (catches the unauthorized call attempt)

Phase A limitations

Per the Configuration → Named pipelines doc, tool-call authorization is global, not per-named-pipeline. Calling opts.RequireToolPolicy(...) on a named pipeline is silently ignored — only the default pipeline's bindings are consulted by IToolCallGuard.

For multi-tenant authorization where different named pipelines need different policies, pre-Phase B you wire surface-specific ISecurityContext resolution and use a single shared binding set on the default pipeline. Per-name auth bindings are on the backlog.

Where it lives

ComponentType
Policy registrationservices.AddSingleton<IAuthorizationPolicy, MyPolicy>() (DI)
Policy naming[AuthorizationPolicy("name")] attribute
Tool-name bindingopts.RequireToolPolicy(pattern, name) on SentinelOptions
Default policyopts.DefaultToolPolicy = ToolPolicyDefault.Allow / Deny
Caller identityISecurityContext — register your implementation in DI
RuntimeIToolCallGuard (default DefaultToolCallGuard) — resolved by the framework
Pipeline wiring.UseToolCallAuthorization() on the chat client builder, before .UseFunctionInvocation()

Async policies

IAuthorizationPolicy.IsAuthorized is synchronous in v1. For policies that need async resolution (tenant lookup, OPA call, external IdP), the work-around is to cache async results in ISecurityContext at request boundary so the policy itself reads from a pre-populated cache.

A Task<bool> IsAuthorizedAsync(ISecurityContext ctx) overload is on the backlog. Coordinated with the planned ZeroAlloc.Mediator.Authorization design before changing the interface.

Next: Policies — writing IAuthorizationPolicy implementations + common patterns