microsoft / microsoft/semantic-kernel

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

Open
#13,661 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
28.6k
Forks
4.8k
Avg merge
14h 13m
Merged PRs (30d)
18

Description

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:

  • #1409 -- "Guardrails for C#" (2023, closed with TypeChat redirect, no guardrail interface shipped)
  • #5436 -- "Filters use cases to be supported before making feature non-experimental" (explicitly lists "Function Call Approval" as a core requirement)
  • #10951 -- "Agent Invocation Filter" (2025, requesting filters that wrap agent invocations "allowing for scenarios like guardrails etc." for both .NET and Python)
  • #13556 -- "Governance Policy Filter for Semantic Kernel" (2026, proposed a governance layer, closed and redirected to microsoft/agent-governance-toolkit)
  • #12294 -- Handoff orchestration triggering OpenAI jailbreak guardrails (demonstrates the practical need for pre-invocation policy checks)
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 -- #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
  • Follows the existing filter pipeline pattern (IAutoFunctionInvocationFilter, IFunctionInvocationFilter, IPromptRenderFilter)
  • Does not modify the filter contract -- composes on top of it
  • Supports the "Function Call Approval" scenario explicitly called out in #5436
  • Works for both .NET and Python with identical semantics
  • Enables the agent-level guardrails requested in #10951
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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the existing IAutoFunctionInvocationFilter pipeline and its .NET and Python entry points, then compare how filters are registered and how invocation context and results are represented. Done means a provider contract and filter integration are defined consistently for both languages, including allow, deny, cancellation, and registration behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, python
Domain
ai, authorization, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.