From 2c17c4dcb0958b99ab591229142ef82613db75dd Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 11 Sep 2026 09:57:58 +0000
Subject: [PATCH] Implement same approval process for LocalCodeAct as is used
by Hyperlight
---
.../LocalCodeActApprovalMode.cs | 25 ++
.../LocalCodeActProvider.cs | 24 +-
.../LocalCodeActProviderOptions.cs | 13 ++
.../LocalExecuteCodeFunction.cs | 22 ++
.../README.md | 27 +++
.../ApprovalPropagationTests.cs | 218 ++++++++++++++++++
6 files changed, 328 insertions(+), 1 deletion(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActApprovalMode.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/ApprovalPropagationTests.cs
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActApprovalMode.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActApprovalMode.cs
new file mode 100644
index 00000000000..5e10b88a64c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActApprovalMode.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.LocalCodeAct;
+
+///
+/// Controls the approval behavior for the execute_code tool exposed by
+/// and .
+///
+public enum LocalCodeActApprovalMode
+{
+ ///
+ /// execute_code always requires user approval before invocation.
+ ///
+ AlwaysRequire,
+
+ ///
+ /// Approval is derived from the provider-owned CodeAct tool registry.
+ /// If any configured tool is an
+ /// ,
+ /// execute_code also requires approval. Otherwise it does not.
+ ///
+ NeverRequire,
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProvider.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProvider.cs
index 9d36c5e5706..33f9205f735 100644
--- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProvider.cs
@@ -36,6 +36,7 @@ public sealed class LocalCodeActProvider : AIContextProvider, IDisposable
private static readonly IReadOnlyList s_stateKeys = [FixedStateKey];
private readonly CodeExecutor _executor;
+ private readonly LocalCodeActApprovalMode _approvalMode;
private readonly ConcurrentDictionary _tools = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary _fileMounts = new(StringComparer.Ordinal);
@@ -66,6 +67,8 @@ public LocalCodeActProvider(string pythonExecutablePath, LocalCodeActProviderOpt
options.BlockedBuiltins?.ToList());
}
+ this._approvalMode = options.ApprovalMode;
+
this._executor = new CodeExecutor(
pythonExecutablePath,
runnerScript,
@@ -190,8 +193,15 @@ protected override ValueTask ProvideAIContextAsync(InvokingContext co
this._fileMounts.Values.ToList());
FeatureUsageMarker.MarkUsed();
+ var approvalRequired = ComputeApprovalRequired(this._approvalMode, snapshot.Tools);
+
var description = InstructionBuilder.BuildExecuteCodeDescription(snapshot.Tools, snapshot.FileMounts);
- var executeCode = new ExecuteCodeFunction(this._executor, snapshot, description);
+
+ AIFunction executeCode = new ExecuteCodeFunction(this._executor, snapshot, description);
+ if (approvalRequired)
+ {
+ executeCode = new ApprovalRequiredAIFunction(executeCode);
+ }
var instructions = InstructionBuilder.BuildContextInstructions();
@@ -202,6 +212,18 @@ protected override ValueTask ProvideAIContextAsync(InvokingContext co
});
}
+ ///
+ /// Computes whether execute_code must require approval for the supplied tool set.
+ ///
+ ///
+ /// Approval is bundled: because generated code can reach any registered tool through
+ /// call_tool(...) after execution has begun, a single approval-required tool escalates
+ /// the approval requirement to the whole execute_code invocation.
+ ///
+ internal static bool ComputeApprovalRequired(LocalCodeActApprovalMode mode, IReadOnlyList tools) =>
+ mode == LocalCodeActApprovalMode.AlwaysRequire
+ || tools.Any(t => t.GetService() is not null);
+
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this._disposed, this);
///
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProviderOptions.cs
index 208b9fe24f5..51a161a20ea 100644
--- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProviderOptions.cs
@@ -18,6 +18,19 @@ public sealed class LocalCodeActProviderOptions
///
public IEnumerable? Tools { get; set; }
+ ///
+ /// Gets or sets the approval mode for execute_code.
+ /// Defaults to .
+ ///
+ ///
+ /// Under , approval still propagates from the
+ /// tools in : if any of them is an ,
+ /// execute_code requires approval as well. This is required because generated code can
+ /// invoke any registered tool via call_tool(...) once execution has started, at which
+ /// point per-tool approval can no longer be enforced.
+ ///
+ public LocalCodeActApprovalMode ApprovalMode { get; set; } = LocalCodeActApprovalMode.NeverRequire;
+
///
/// Gets or sets the initial set of file mounts exposed to generated code.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalExecuteCodeFunction.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalExecuteCodeFunction.cs
index 96736ab4b16..a8b0fd34e22 100644
--- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalExecuteCodeFunction.cs
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalExecuteCodeFunction.cs
@@ -20,6 +20,11 @@ namespace Microsoft.Agents.AI.LocalCodeAct;
/// Use this when you want to expose code execution directly as a model-facing function without
/// the indirection. Tools and file mounts are captured at
/// construction time and immutable for the lifetime of the function.
+/// When the configuration requires approval (per
+/// or because a configured tool is itself
+/// an ), the instance surfaces an
+/// via ,
+/// which is how the rest of the framework discovers approval requirements.
///
public sealed class LocalExecuteCodeFunction : AIFunction
{
@@ -28,6 +33,8 @@ public sealed class LocalExecuteCodeFunction : AIFunction
private readonly CodeExecutor _executor;
private readonly CodeExecutor.RunSnapshot _snapshot;
private readonly AIFunction _inner;
+ private readonly bool _approvalRequired;
+ private ApprovalRequiredAIFunction? _approvalProxy;
/// Initializes a new instance of the class.
/// Path to the Python interpreter used for execution and validation.
@@ -73,6 +80,8 @@ public LocalExecuteCodeFunction(string pythonExecutablePath, LocalCodeActProvide
Name = ExecuteCodeName,
Description = InstructionBuilder.BuildExecuteCodeDescription(tools, fileMounts),
});
+
+ this._approvalRequired = LocalCodeActProvider.ComputeApprovalRequired(options.ApprovalMode, tools);
}
///
@@ -84,6 +93,19 @@ public LocalExecuteCodeFunction(string pythonExecutablePath, LocalCodeActProvide
///
public override JsonElement JsonSchema => this._inner.JsonSchema;
+ ///
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ if (serviceKey is null
+ && this._approvalRequired
+ && serviceType == typeof(ApprovalRequiredAIFunction))
+ {
+ return this._approvalProxy ??= new ApprovalRequiredAIFunction(this);
+ }
+
+ return base.GetService(serviceType, serviceKey);
+ }
+
///
protected override ValueTask