Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.AI;

namespace Microsoft.Agents.AI.LocalCodeAct;

/// <summary>
/// Controls the approval behavior for the <c>execute_code</c> tool exposed by
/// <see cref="LocalCodeActProvider"/> and <see cref="LocalExecuteCodeFunction"/>.
/// </summary>
public enum LocalCodeActApprovalMode
{
/// <summary>
/// <c>execute_code</c> always requires user approval before invocation.
/// </summary>
AlwaysRequire,

/// <summary>
/// Approval is derived from the provider-owned CodeAct tool registry.
/// If any configured tool is an
/// <see cref="ApprovalRequiredAIFunction"/>,
/// <c>execute_code</c> also requires approval. Otherwise it does not.
/// </summary>
NeverRequire,
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public sealed class LocalCodeActProvider : AIContextProvider, IDisposable
private static readonly IReadOnlyList<string> s_stateKeys = [FixedStateKey];

private readonly CodeExecutor _executor;
private readonly LocalCodeActApprovalMode _approvalMode;

private readonly ConcurrentDictionary<string, AIFunction> _tools = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileMount> _fileMounts = new(StringComparer.Ordinal);
Expand Down Expand Up @@ -66,6 +67,8 @@ public LocalCodeActProvider(string pythonExecutablePath, LocalCodeActProviderOpt
options.BlockedBuiltins?.ToList());
}

this._approvalMode = options.ApprovalMode;

this._executor = new CodeExecutor(
pythonExecutablePath,
runnerScript,
Expand Down Expand Up @@ -190,8 +193,15 @@ protected override ValueTask<AIContext> 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();

Expand All @@ -202,6 +212,18 @@ protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext co
});
}

/// <summary>
/// Computes whether <c>execute_code</c> must require approval for the supplied tool set.
/// </summary>
/// <remarks>
/// Approval is bundled: because generated code can reach any registered tool through
/// <c>call_tool(...)</c> after execution has begun, a single approval-required tool escalates
/// the approval requirement to the whole <c>execute_code</c> invocation.
/// </remarks>
internal static bool ComputeApprovalRequired(LocalCodeActApprovalMode mode, IReadOnlyList<AIFunction> tools) =>
mode == LocalCodeActApprovalMode.AlwaysRequire
|| tools.Any(t => t.GetService<ApprovalRequiredAIFunction>() is not null);

private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this._disposed, this);

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ public sealed class LocalCodeActProviderOptions
/// </summary>
public IEnumerable<AIFunction>? Tools { get; set; }

/// <summary>
/// Gets or sets the approval mode for <c>execute_code</c>.
/// Defaults to <see cref="LocalCodeActApprovalMode.NeverRequire"/>.
/// </summary>
/// <remarks>
/// Under <see cref="LocalCodeActApprovalMode.NeverRequire"/>, approval still propagates from the
/// tools in <see cref="Tools"/>: if any of them is an <see cref="ApprovalRequiredAIFunction"/>,
/// <c>execute_code</c> requires approval as well. This is required because generated code can
/// invoke any registered tool via <c>call_tool(...)</c> once execution has started, at which
/// point per-tool approval can no longer be enforced.
/// </remarks>
public LocalCodeActApprovalMode ApprovalMode { get; set; } = LocalCodeActApprovalMode.NeverRequire;

/// <summary>
/// Gets or sets the initial set of file mounts exposed to generated code.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="LocalCodeActProvider"/> indirection. Tools and file mounts are captured at
/// construction time and immutable for the lifetime of the function.
/// When the configuration requires approval (per
/// <see cref="LocalCodeActProviderOptions.ApprovalMode"/> or because a configured tool is itself
/// an <see cref="ApprovalRequiredAIFunction"/>), the instance surfaces an
/// <see cref="ApprovalRequiredAIFunction"/> via <see cref="AITool.GetService(Type, object?)"/>,
/// which is how the rest of the framework discovers approval requirements.
/// </remarks>
public sealed class LocalExecuteCodeFunction : AIFunction
{
Expand All @@ -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;

/// <summary>Initializes a new instance of the <see cref="LocalExecuteCodeFunction"/> class.</summary>
/// <param name="pythonExecutablePath">Path to the Python interpreter used for execution and validation.</param>
Expand Down Expand Up @@ -73,6 +80,8 @@ public LocalExecuteCodeFunction(string pythonExecutablePath, LocalCodeActProvide
Name = ExecuteCodeName,
Description = InstructionBuilder.BuildExecuteCodeDescription(tools, fileMounts),
});

this._approvalRequired = LocalCodeActProvider.ComputeApprovalRequired(options.ApprovalMode, tools);
}

/// <inheritdoc/>
Expand All @@ -84,6 +93,19 @@ public LocalExecuteCodeFunction(string pythonExecutablePath, LocalCodeActProvide
/// <inheritdoc/>
public override JsonElement JsonSchema => this._inner.JsonSchema;

/// <inheritdoc/>
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);
}

/// <inheritdoc/>
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) =>
this._inner.InvokeAsync(arguments, cancellationToken);
Expand Down
27 changes: 27 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,33 @@ total = await call_tool("add", a=2, b=3)
print(total)
```

## Tool Approval

Generated code reaches registered tools through `call_tool(...)`, which invokes
them directly. A per-tool approval interaction cannot be surfaced at that point,
so approval is **bundled** onto `execute_code` instead:

* If any registered tool is an `ApprovalRequiredAIFunction`, `execute_code`
itself requires approval before the code runs.
* `LocalCodeActApprovalMode.AlwaysRequire` makes `execute_code` require approval
regardless of the registered tools.

```csharp
var deploy = new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(RunDeployment, name: "deploy"));

using var provider = new LocalCodeActProvider("/usr/bin/python3", new LocalCodeActProviderOptions
{
Tools = new[] { deploy },
// ApprovalMode = LocalCodeActApprovalMode.AlwaysRequire, // optional, opt-in
});
```

For `LocalCodeActProvider`, approval is recomputed on every run, so tools added
via `AddTools` after construction are taken into account. `LocalExecuteCodeFunction`
captures its tools at construction time and exposes the approval requirement
through `GetService<ApprovalRequiredAIFunction>()`.

## Code Validation

By default, the package validates Python code against allow-lists before
Expand Down
Loading
Loading