Skip to content

New Feature: IGuardrailProvider interface for policy-based function invocation control #13661

@uchibeke

Description

@uchibeke

Feature Request

Title: New Feature: IGuardrailProvider interface for policy-based function invocation control

Is your feature request related to a problem?

Enterprise deployments of Semantic Kernel agents need a standardized way to enforce authorization policies on function invocations -- deciding whether a tool call should proceed based on caller identity, resource scope, risk level, or organizational policy. Today this is achievable through IAutoFunctionInvocationFilter, but each team builds ad-hoc policy logic inside their filter implementations with no shared contract for pluggable policy providers.

This gap has been raised repeatedly:

Describe the solution you'd like

A thin IGuardrailProvider interface that plugs into the existing filter pipeline. It separates the policy decision (should this call proceed?) from the filter mechanics (intercepting the pipeline).

C# (.NET)

namespace Microsoft.SemanticKernel;

/// <summary>
/// Provides policy-based authorization decisions for function invocations.
/// Implementations are registered with the Kernel and consulted by a
/// built-in AutoFunctionInvocationFilter before each tool call.
/// </summary>
public interface IGuardrailProvider
{
    /// <summary>
    /// Evaluates whether a function invocation should proceed.
    /// </summary>
    Task<GuardrailDecision> EvaluateAsync(
        GuardrailContext context,
        CancellationToken cancellationToken = default);
}

public sealed class GuardrailContext
{
    public KernelFunction Function { get; init; }
    public KernelArguments Arguments { get; init; }
    public AutoFunctionInvocationContext InvocationContext { get; init; }

    /// <summary>Optional caller identity for multi-tenant scenarios.</summary>
    public ClaimsPrincipal? Principal { get; init; }
}

public sealed class GuardrailDecision
{
    public bool IsAllowed { get; init; }
    public string? Reason { get; init; }

    public static GuardrailDecision Allow() => new() { IsAllowed = true };
    public static GuardrailDecision Deny(string reason) =>
        new() { IsAllowed = false, Reason = reason };
}

Python

from abc import ABC, abstractmethod
from dataclasses import dataclass
from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import (
    AutoFunctionInvocationContext,
)

@dataclass
class GuardrailContext:
    function_name: str
    plugin_name: str
    arguments: dict
    invocation_context: AutoFunctionInvocationContext
    principal: dict | None = None  # Caller identity claims

@dataclass
class GuardrailDecision:
    is_allowed: bool
    reason: str | None = None

    @staticmethod
    def allow() -> "GuardrailDecision":
        return GuardrailDecision(is_allowed=True)

    @staticmethod
    def deny(reason: str) -> "GuardrailDecision":
        return GuardrailDecision(is_allowed=False, reason=reason)

class GuardrailProvider(ABC):
    @abstractmethod
    async def evaluate(self, context: GuardrailContext) -> GuardrailDecision:
        """Return whether this function invocation should proceed."""
        ...

Integration with existing filters

The provider does not replace filters -- it plugs into them. A built-in GuardrailAutoFunctionInvocationFilter consults registered providers:

public class GuardrailAutoFunctionInvocationFilter : IAutoFunctionInvocationFilter
{
    private readonly IEnumerable<IGuardrailProvider> _providers;

    public async Task OnAutoFunctionInvocationAsync(
        AutoFunctionInvocationContext context,
        Func<AutoFunctionInvocationContext, Task> next)
    {
        var guardrailContext = new GuardrailContext
        {
            Function = context.Function,
            Arguments = context.Arguments,
            InvocationContext = context
        };

        foreach (var provider in _providers)
        {
            var decision = await provider.EvaluateAsync(guardrailContext);
            if (!decision.IsAllowed)
            {
                context.Result = new FunctionResult(context.Function,
                    $"Blocked by guardrail: {decision.Reason}");
                return; // skip next -- do not invoke function
            }
        }

        await next(context);
    }
}

Registration

var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(...)
    .Build();

kernel.AutoFunctionInvocationFilters.Add(
    new GuardrailAutoFunctionInvocationFilter(
        new MyOrgPolicyProvider(),
        new AzureContentSafetyProvider()
    ));

Describe alternatives you've considered

  1. Raw IAutoFunctionInvocationFilter only -- Works today, but every team re-invents the allow/deny pattern. No shared contract means no ecosystem of pluggable providers.
  2. External governance toolkit -- Python: Proposal: Governance Policy Filter for Semantic Kernel #13556 was redirected to microsoft/agent-governance-toolkit, but a lightweight in-kernel interface would enable third-party providers without requiring a separate toolkit dependency.

Why this fits Semantic Kernel's architecture

Additional context

A reference implementation of this provider pattern exists in APort Agent Guardrails, which enforces passport-based tool authorization policies for AI agents. The interface proposed here is deliberately provider-agnostic so that any policy backend (Azure Content Safety, OPA, custom rules, etc.) can plug in.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions