From 783d204da4a25e0c3ef5633f623d205c7fc6dad9 Mon Sep 17 00:00:00 2001
From: jw_ond <67523717+jw-ond@users.noreply.github.com>
Date: Sat, 1 Aug 2026 00:05:48 +0800
Subject: [PATCH 1/3] Add external governance checkpoint sample
---
.../Filtering/ExternalGovernanceCheckpoint.cs | 223 ++++++++++++++++++
1 file changed, 223 insertions(+)
create mode 100644 dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
diff --git a/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs b/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
new file mode 100644
index 000000000000..ff1d56ca52cf
--- /dev/null
+++ b/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Security.Cryptography;
+using System.Text.Json;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace Filtering;
+
+///
+/// Shows how to place an external governance checkpoint in front of automatic function invocation.
+///
+public class ExternalGovernanceCheckpoint(ITestOutputHelper output) : BaseTest(output)
+{
+ [Theory]
+ [InlineData("allow", "Executed wire transfer", "executed")]
+ [InlineData("require_approval", "Paused for approval", "paused")]
+ public async Task ExternalCheckpointCanAllowOrPauseFunctionInvocationAsync(string requestedVerdict, string expectedResult, string expectedStatus)
+ {
+ var builder = Kernel.CreateBuilder();
+ builder.Services.AddSingleton(new ExampleCheckpointClient(requestedVerdict));
+ builder.Services.AddSingleton();
+
+ var kernel = builder.Build();
+ var function = KernelFunctionFactory.CreateFromMethod(
+ (decimal amount, string recipient) => "Executed wire transfer",
+ "WireTransfer");
+
+ kernel.ImportPluginFromFunctions("Payments", [function]);
+
+ var context = CreateAutoFunctionInvocationContext(
+ kernel,
+ function,
+ new KernelArguments
+ {
+ ["amount"] = 1250m,
+ ["recipient"] = "Fabrikam"
+ });
+
+ var filter = kernel.Services.GetRequiredService();
+ await filter.OnAutoFunctionInvocationAsync(context, async invocationContext =>
+ {
+ invocationContext.Result = await invocationContext.Function.InvokeAsync(kernel, invocationContext.Arguments);
+ });
+
+ Console.WriteLine(context.Result);
+ Assert.Equal(expectedResult, context.Result.GetValue());
+ Assert.Equal(expectedStatus, context.Result.Metadata?["governance_status"]);
+
+ // Output for allow:
+ // Executed wire transfer
+ //
+ // Output for require_approval:
+ // Paused for approval
+ }
+
+ [Fact]
+ public async Task ExternalCheckpointCanDenyFunctionInvocationAsync()
+ {
+ var builder = Kernel.CreateBuilder();
+ builder.Services.AddSingleton(new ExampleCheckpointClient("deny"));
+ builder.Services.AddSingleton();
+
+ var kernel = builder.Build();
+ var function = KernelFunctionFactory.CreateFromMethod(() => "Deleted customer record", "DeleteCustomerRecord");
+
+ kernel.ImportPluginFromFunctions("CustomerAdmin", [function]);
+
+ var context = CreateAutoFunctionInvocationContext(kernel, function, new KernelArguments());
+ var filter = kernel.Services.GetRequiredService();
+
+ var exception = await Assert.ThrowsAsync(() =>
+ filter.OnAutoFunctionInvocationAsync(context, _ => throw new InvalidOperationException("The function should not execute.")));
+
+ Assert.Contains("denied", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static AutoFunctionInvocationContext CreateAutoFunctionInvocationContext(
+ Kernel kernel,
+ KernelFunction function,
+ KernelArguments arguments)
+ {
+ var chatHistory = new ChatHistory("Transfer $1,250 to Fabrikam.");
+ var functionCall = new FunctionCallContent(
+ functionName: function.Name,
+ pluginName: function.PluginName,
+ id: "call_123",
+ arguments: arguments);
+
+ var chatMessageContent = new ChatMessageContent(AuthorRole.Assistant, [functionCall]);
+ chatHistory.Add(chatMessageContent);
+
+ return new AutoFunctionInvocationContext(
+ kernel,
+ function,
+ new FunctionResult(function),
+ chatHistory,
+ chatMessageContent)
+ {
+ Arguments = arguments,
+ RequestSequenceIndex = 0,
+ FunctionSequenceIndex = 0,
+ ToolCallId = functionCall.Id
+ };
+ }
+
+ private sealed class ExternalGovernanceFilter(IExternalCheckpointClient checkpointClient) : IAutoFunctionInvocationFilter
+ {
+ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next)
+ {
+ ActionEnvelope envelope = ActionEnvelope.FromContext(context);
+ string checkpointReference = ActionEnvelopeDigest.ComputeReference(envelope);
+
+ CheckpointVerdict verdict = await checkpointClient.EvaluateAsync(envelope, checkpointReference, context.CancellationToken);
+
+ switch (verdict.Decision)
+ {
+ case "allow":
+ await next(context);
+ context.Result = WithGovernanceMetadata(context.Result, checkpointReference, "executed");
+ return;
+
+ case "require_approval":
+ context.Result = WithGovernanceMetadata(
+ context.Result,
+ checkpointReference,
+ "paused",
+ "Paused for approval");
+ context.Terminate = true;
+ return;
+
+ case "deny":
+ throw new UnauthorizedAccessException(
+ $"Function call '{envelope.PluginName}.{envelope.FunctionName}' was denied by checkpoint {checkpointReference}.");
+
+ default:
+ throw new InvalidOperationException($"Unknown checkpoint verdict '{verdict.Decision}'.");
+ }
+ }
+
+ private static FunctionResult WithGovernanceMetadata(
+ FunctionResult result,
+ string checkpointReference,
+ string status,
+ string? value = null)
+ {
+ Dictionary metadata = result.Metadata is not null ? new(result.Metadata) : [];
+ metadata["governance_checkpoint"] = checkpointReference;
+ metadata["governance_status"] = status;
+
+ return new FunctionResult(result, value)
+ {
+ Metadata = metadata
+ };
+ }
+ }
+
+ private sealed record ActionEnvelope(
+ string PluginName,
+ string FunctionName,
+ IReadOnlyDictionary Arguments,
+ int RequestSequenceIndex,
+ int FunctionSequenceIndex,
+ string? ToolCallId)
+ {
+ public static ActionEnvelope FromContext(AutoFunctionInvocationContext context)
+ {
+ SortedDictionary arguments = new(StringComparer.Ordinal);
+
+ if (context.Arguments is not null)
+ {
+ foreach (var argument in context.Arguments.OrderBy(static item => item.Key, StringComparer.Ordinal))
+ {
+ arguments[argument.Key] = argument.Value;
+ }
+ }
+
+ return new(
+ context.Function.PluginName,
+ context.Function.Name,
+ arguments,
+ context.RequestSequenceIndex,
+ context.FunctionSequenceIndex,
+ context.ToolCallId);
+ }
+ }
+
+ private static class ActionEnvelopeDigest
+ {
+ private static readonly JsonSerializerOptions s_serializerOptions = new(JsonSerializerDefaults.Web);
+
+ public static string ComputeReference(ActionEnvelope envelope)
+ {
+ byte[] envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(envelope, s_serializerOptions);
+ byte[] digest = SHA256.HashData(envelopeBytes);
+
+ return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
+ }
+ }
+
+ private interface IExternalCheckpointClient
+ {
+ Task EvaluateAsync(ActionEnvelope envelope, string checkpointReference, CancellationToken cancellationToken);
+ }
+
+ private sealed class ExampleCheckpointClient(string decision) : IExternalCheckpointClient
+ {
+ public Task EvaluateAsync(
+ ActionEnvelope envelope,
+ string checkpointReference,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Console.WriteLine($"Checkpoint {checkpointReference}: {decision} {envelope.PluginName}.{envelope.FunctionName}");
+
+ return Task.FromResult(new CheckpointVerdict(decision));
+ }
+ }
+
+ private sealed record CheckpointVerdict(string Decision);
+}
From 40792f0d9be91902189fb694cc2970057c180a82 Mon Sep 17 00:00:00 2001
From: jw_ond <67523717+jw-ond@users.noreply.github.com>
Date: Sat, 1 Aug 2026 00:06:31 +0800
Subject: [PATCH 2/3] List external governance checkpoint sample
---
dotnet/samples/Concepts/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md
index 77fe10e7a8ab..103e919f68d4 100644
--- a/dotnet/samples/Concepts/README.md
+++ b/dotnet/samples/Concepts/README.md
@@ -109,6 +109,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
- [AutoFunctionInvocationFiltering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs)
- [FunctionInvocationFiltering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/FunctionInvocationFiltering.cs)
+- [ExternalGovernanceCheckpoint](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs)
- [MaxTokensWithFilters](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/MaxTokensWithFilters.cs)
- [PIIDetection](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/PIIDetection.cs)
- [PromptRenderFiltering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/PromptRenderFiltering.cs)
From 1fbf61ef6bbacc63080e4f122efc6d72fbb6c4a4 Mon Sep 17 00:00:00 2001
From: jw_ond <67523717+jw-ond@users.noreply.github.com>
Date: Sun, 2 Aug 2026 14:55:28 +0800
Subject: [PATCH 3/3] samples: stabilize governance checkpoint reference
---
.../Filtering/ExternalGovernanceCheckpoint.cs | 24 ++++++++++++++-----
1 file changed, 18 insertions(+), 6 deletions(-)
diff --git a/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs b/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
index ff1d56ca52cf..594bcfe971c4 100644
--- a/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
+++ b/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
@@ -28,10 +28,11 @@ public async Task ExternalCheckpointCanAllowOrPauseFunctionInvocationAsync(strin
"WireTransfer");
kernel.ImportPluginFromFunctions("Payments", [function]);
+ KernelFunction importedFunction = kernel.Plugins.GetFunction("Payments", "WireTransfer");
var context = CreateAutoFunctionInvocationContext(
kernel,
- function,
+ importedFunction,
new KernelArguments
{
["amount"] = 1250m,
@@ -66,8 +67,9 @@ public async Task ExternalCheckpointCanDenyFunctionInvocationAsync()
var function = KernelFunctionFactory.CreateFromMethod(() => "Deleted customer record", "DeleteCustomerRecord");
kernel.ImportPluginFromFunctions("CustomerAdmin", [function]);
+ KernelFunction importedFunction = kernel.Plugins.GetFunction("CustomerAdmin", "DeleteCustomerRecord");
- var context = CreateAutoFunctionInvocationContext(kernel, function, new KernelArguments());
+ var context = CreateAutoFunctionInvocationContext(kernel, importedFunction, new KernelArguments());
var filter = kernel.Services.GetRequiredService();
var exception = await Assert.ThrowsAsync(() =>
@@ -132,7 +134,7 @@ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext co
case "deny":
throw new UnauthorizedAccessException(
- $"Function call '{envelope.PluginName}.{envelope.FunctionName}' was denied by checkpoint {checkpointReference}.");
+ $"Function call '{envelope.PluginName ?? ""}.{envelope.FunctionName}' was denied by checkpoint {checkpointReference}.");
default:
throw new InvalidOperationException($"Unknown checkpoint verdict '{verdict.Decision}'.");
@@ -157,7 +159,7 @@ private static FunctionResult WithGovernanceMetadata(
}
private sealed record ActionEnvelope(
- string PluginName,
+ string? PluginName,
string FunctionName,
IReadOnlyDictionary Arguments,
int RequestSequenceIndex,
@@ -192,11 +194,21 @@ private static class ActionEnvelopeDigest
public static string ComputeReference(ActionEnvelope envelope)
{
- byte[] envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(envelope, s_serializerOptions);
+ StableActionEnvelope stableEnvelope = new(
+ envelope.PluginName,
+ envelope.FunctionName,
+ envelope.Arguments);
+
+ byte[] envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(stableEnvelope, s_serializerOptions);
byte[] digest = SHA256.HashData(envelopeBytes);
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
}
+
+ private sealed record StableActionEnvelope(
+ string? PluginName,
+ string FunctionName,
+ IReadOnlyDictionary Arguments);
}
private interface IExternalCheckpointClient
@@ -213,7 +225,7 @@ public Task EvaluateAsync(
{
cancellationToken.ThrowIfCancellationRequested();
- Console.WriteLine($"Checkpoint {checkpointReference}: {decision} {envelope.PluginName}.{envelope.FunctionName}");
+ Console.WriteLine($"Checkpoint {checkpointReference}: {decision} {envelope.PluginName ?? ""}.{envelope.FunctionName}");
return Task.FromResult(new CheckpointVerdict(decision));
}