mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* Add Microsoft.Agents.AI.Hyperlight package for CodeAct integration Introduces a new Microsoft.Agents.AI.Hyperlight package that enables CodeAct-style sandboxed code execution via Hyperlight (hyperlight-sandbox .NET SDK, PR #46) for .NET agents, following the docs/features/code_act/dotnet-implementation.md design and the Python agent_framework_hyperlight reference. Highlights: - HyperlightCodeActProvider (AIContextProvider): injects an execute_code tool and CodeAct guidance per invocation; single-instance-per-agent via a fixed StateKeys value; supports multiple provider-owned tools (exposed inside the sandbox via call_tool), file mounts, and an outbound domain allow-list; snapshot/restore per run. - HyperlightExecuteCodeFunction: standalone AIFunction for manual/static wiring when the sandbox configuration is fixed. - Approval model via CodeActApprovalMode (AlwaysRequire / NeverRequire) with propagation from ApprovalRequiredAIFunction-wrapped tools. - Unit tests (instruction builder, tool bridge, approval computation, provider CRUD, ProvideAIContextAsync snapshot isolation and approval wrapping). - Env-gated integration test (HYPERLIGHT_PYTHON_GUEST_PATH). - Three samples under samples/02-agents/AgentWithCodeAct (interpreter, tool-enabled, manual wiring). Build is not yet runnable: requires .NET SDK 10.0.200 and the not-yet-published HyperlightSandbox.Api 0.1.0-preview NuGet package. Package is marked IsPackable=false until the dependency is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #5329 review feedback for Hyperlight CodeAct provider - A. Build-breakers: drop unused usings, override test TargetFrameworks off net472, drop redundant Microsoft.Extensions.AI.Abstractions PackageRef. - B. API: keep CRUD but rebuild sandbox when config fingerprint changes; add HyperlightCodeActProviderOptions.CreateForWasm/CreateForJavaScript factory methods (Backend/ModulePath now read-only); rename WorkspaceRoot to HostInputDirectory; convert AllowedDomain & FileMount from record to sealed class; drop ToolBridge.Unwrap (ApprovalRequiredAIFunction is invocable as-is). - C. ToolBridge: collapse SerializeResult switch; add comment explaining AOT-driven choice to keep JsonNode.Parse over typed Deserialize. - D. InstructionBuilder: drop language-specific 'Python code' phrasing; strip host filesystem paths from execute_code description. - E. Style polish: ternary expression-body for ComputeApprovalRequired, .Where(x is not null), .ToList() over .ToArray() in IReadOnlyList returns. - F. Samples: add guest-module / KVM-WHP build instructions to Step01; note future Excel-upload sample in Step02. Also adds SandboxExecutorTests covering the new RunSnapshot.ComputeFingerprint used for sandbox-rebuild detection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align Hyperlight package id and JS warm-up with merged upstream SDK The .NET SDK in hyperlight-dev/hyperlight-sandbox PR #46 has merged. The published package id is Hyperlight.HyperlightSandbox.Api (the bare HyperlightSandbox.Api remains the assembly/namespace) and the reference CodeExecutionTool uses 'void 0;' as the JavaScript warm-up no-op. Update the package reference, project comment, README, and SandboxExecutor warm-up accordingly. No functional change beyond that — all other public APIs we depend on (SandboxBuilder.With*, Sandbox.Run/RegisterToolAsync/AllowDomain/Snapshot/ Restore, ExecutionResult, SandboxBackend) match the merged shape. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump Hyperlight package to 0.4.0 and fix build/test issues Hyperlight.HyperlightSandbox.Api 0.4.0 is now published on nuget.org. Bump the version reference and address the analyzer/runtime issues that surfaced once restore could complete: - Add HyperlightJsonContext source-generated JsonSerializerContext for the execute_code result + tool error envelopes; route arbitrary AIFunction results through AIJsonUtilities.DefaultOptions to keep IsAotCompatible=true. - Replace explicit ObjectDisposedException throws with ObjectDisposedException.ThrowIf (CA1513). - Use HyperlightSandbox.Api.SandboxBackend in cref docs to disambiguate. - Update tests to match AIContext.Tools being IEnumerable<AITool>, drop ConfigureAwait(false) in xUnit test methods (xUnit1030), use collection expressions for AllowedDomain methods. - Add 'using OpenAI.Chat;' to all three samples so AsAIAgent resolves. - Verified: dotnet build of all four hyperlight projects + samples succeeds on net8/9/10; dotnet test for the unit tests passes 32/32 on net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI check failures: file encoding (UTF-8 BOM + LF) and broken markdown link - Convert all new .cs/.csproj files to UTF-8 with BOM and LF line endings to satisfy the dotnet/.editorconfig charset/end_of_line settings enforced by check-format. - Drop unused System.Collections.Generic using in HyperlightCodeActProviderTests. - Add missing using Microsoft.Extensions.AI in CodeActApprovalMode.cs and shorten ApprovalRequiredAIFunction cref (IDE0001). - Fix broken README link to docs/decisions/0024-codeact-integration.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: AIFunction inheritance, packaging, GetService approval check - HyperlightExecuteCodeFunction now inherits AIFunction directly. The AsAIFunction() indirection is gone; instances are accepted anywhere an AIFunction is. Approval requirement is surfaced via GetService<ApprovalRequiredAIFunction>() which lazily exposes a wrapping ApprovalRequiredAIFunction proxy when the effective ApprovalMode/tool stack requires it. - ComputeApprovalRequired now uses GetService<ApprovalRequiredAIFunction>() so approval-required tools nested anywhere in the AITool decorator stack are detected (not just the top-most class). - csproj: drop IsPackable=false (ready to release with the published Hyperlight.HyperlightSandbox.Api 0.4.0 dependency); add PackageReadmeFile and pack README.md at the package root, matching the pattern used by Aspire.Hosting.AgentFramework.DevUI / Microsoft.Agents.AI.DurableTask. - Update Step03 sample and README wording to reflect direct AIFunction usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
325 lines
11 KiB
C#
325 lines
11 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.AI.Hyperlight.Internal;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI.Hyperlight;
|
|
|
|
/// <summary>
|
|
/// An <see cref="AIContextProvider"/> that enables CodeAct execution through a
|
|
/// Hyperlight-backed sandbox.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The provider injects an <c>execute_code</c> tool into the model-facing tool
|
|
/// surface and contributes a short CodeAct guidance block through
|
|
/// <see cref="AIContext.Instructions"/>. Guest code executed via
|
|
/// <c>execute_code</c> runs in an isolated Hyperlight sandbox with
|
|
/// snapshot/restore for clean state per invocation.
|
|
/// </para>
|
|
/// <para>
|
|
/// If no CodeAct-managed tools are configured the provider behaves as a code
|
|
/// interpreter. If one or more tools are configured they are exposed to guest
|
|
/// code via <c>call_tool(...)</c> but not to the model directly.
|
|
/// </para>
|
|
/// <para>
|
|
/// Only a single <see cref="HyperlightCodeActProvider"/> may be attached to a
|
|
/// given agent. <see cref="StateKeys"/> returns a fixed value so
|
|
/// <c>ChatClientAgent</c>'s state-key uniqueness validation rejects duplicate
|
|
/// registrations.
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Security considerations:</strong> guest code runs with only the
|
|
/// capabilities explicitly configured on this provider (file mounts, allowed
|
|
/// outbound domains). Callers should configure the smallest capability set
|
|
/// sufficient for the task and consider using
|
|
/// <see cref="CodeActApprovalMode.AlwaysRequire"/> when guest code can reach
|
|
/// sensitive resources.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class HyperlightCodeActProvider : AIContextProvider, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Fixed state key used to enforce a single provider-per-agent.
|
|
/// </summary>
|
|
internal const string FixedStateKey = "HyperlightCodeActProvider";
|
|
|
|
private static readonly IReadOnlyList<string> s_stateKeys = [FixedStateKey];
|
|
|
|
private readonly object _gate = new();
|
|
private readonly HyperlightCodeActProviderOptions _options;
|
|
private readonly SandboxExecutor _executor;
|
|
|
|
private readonly Dictionary<string, AIFunction> _tools = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, FileMount> _fileMounts = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, AllowedDomain> _allowedDomains = new(StringComparer.Ordinal);
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="HyperlightCodeActProvider"/> class.
|
|
/// </summary>
|
|
/// <param name="options">
|
|
/// Optional configuration options for the provider. When <see langword="null"/> the provider
|
|
/// uses the defaults of <see cref="HyperlightCodeActProviderOptions"/> (the
|
|
/// <see cref="HyperlightSandbox.Api.SandboxBackend.JavaScript"/> backend with no tools, mounts, or allow-list entries).
|
|
/// Use <see cref="HyperlightCodeActProviderOptions.CreateForWasm(string)"/> to target a Wasm
|
|
/// guest module instead.
|
|
/// </param>
|
|
public HyperlightCodeActProvider(HyperlightCodeActProviderOptions? options = null)
|
|
{
|
|
this._options = options ?? new HyperlightCodeActProviderOptions();
|
|
this._executor = new SandboxExecutor(this._options);
|
|
|
|
if (this._options.Tools is not null)
|
|
{
|
|
foreach (var tool in this._options.Tools.Where(t => t is not null))
|
|
{
|
|
this._tools[tool.Name] = tool;
|
|
}
|
|
}
|
|
|
|
if (this._options.FileMounts is not null)
|
|
{
|
|
foreach (var mount in this._options.FileMounts.Where(m => m is not null))
|
|
{
|
|
this._fileMounts[mount.MountPath] = mount;
|
|
}
|
|
}
|
|
|
|
if (this._options.AllowedDomains is not null)
|
|
{
|
|
foreach (var domain in this._options.AllowedDomains.Where(d => d is not null))
|
|
{
|
|
this._allowedDomains[domain.Target] = domain;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override IReadOnlyList<string> StateKeys => s_stateKeys;
|
|
|
|
// -------------------------------------------------------------------
|
|
// Tool registry
|
|
// -------------------------------------------------------------------
|
|
|
|
/// <summary>Adds tools to the provider-owned CodeAct tool registry. Tools with a duplicate name replace the existing registration.</summary>
|
|
/// <param name="tools">The tools to add.</param>
|
|
public void AddTools(params AIFunction[] tools)
|
|
{
|
|
_ = Throw.IfNull(tools);
|
|
lock (this._gate)
|
|
{
|
|
this.ThrowIfDisposed();
|
|
foreach (var tool in tools.Where(t => t is not null))
|
|
{
|
|
this._tools[tool.Name] = tool;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the current CodeAct-managed tools.</summary>
|
|
public IReadOnlyList<AIFunction> GetTools()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
return this._tools.Values.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>Removes tools by name from the CodeAct tool registry.</summary>
|
|
/// <param name="names">The names of the tools to remove.</param>
|
|
public void RemoveTools(params string[] names)
|
|
{
|
|
_ = Throw.IfNull(names);
|
|
lock (this._gate)
|
|
{
|
|
foreach (var name in names.Where(n => n is not null))
|
|
{
|
|
_ = this._tools.Remove(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Removes all CodeAct-managed tools.</summary>
|
|
public void ClearTools()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
this._tools.Clear();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// File mounts
|
|
// -------------------------------------------------------------------
|
|
|
|
/// <summary>Adds file mount configurations. Mounts with a duplicate mount path replace the existing entry.</summary>
|
|
/// <param name="mounts">The mount configurations to add.</param>
|
|
public void AddFileMounts(params FileMount[] mounts)
|
|
{
|
|
_ = Throw.IfNull(mounts);
|
|
lock (this._gate)
|
|
{
|
|
foreach (var mount in mounts.Where(m => m is not null))
|
|
{
|
|
this._fileMounts[mount.MountPath] = mount;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the current file mount configurations.</summary>
|
|
public IReadOnlyList<FileMount> GetFileMounts()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
return this._fileMounts.Values.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>Removes file mounts by sandbox mount path.</summary>
|
|
/// <param name="mountPaths">The mount paths to remove.</param>
|
|
public void RemoveFileMounts(params string[] mountPaths)
|
|
{
|
|
_ = Throw.IfNull(mountPaths);
|
|
lock (this._gate)
|
|
{
|
|
foreach (var path in mountPaths.Where(p => p is not null))
|
|
{
|
|
_ = this._fileMounts.Remove(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Removes all file mount configurations.</summary>
|
|
public void ClearFileMounts()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
this._fileMounts.Clear();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Network allow-list
|
|
// -------------------------------------------------------------------
|
|
|
|
/// <summary>Adds outbound network allow-list entries. Entries with a duplicate target replace the existing entry.</summary>
|
|
/// <param name="domains">The allow-list entries to add.</param>
|
|
public void AddAllowedDomains(params AllowedDomain[] domains)
|
|
{
|
|
_ = Throw.IfNull(domains);
|
|
lock (this._gate)
|
|
{
|
|
foreach (var domain in domains.Where(d => d is not null))
|
|
{
|
|
this._allowedDomains[domain.Target] = domain;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the current outbound allow-list entries.</summary>
|
|
public IReadOnlyList<AllowedDomain> GetAllowedDomains()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
return this._allowedDomains.Values.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>Removes allow-list entries by target.</summary>
|
|
/// <param name="targets">The targets to remove.</param>
|
|
public void RemoveAllowedDomains(params string[] targets)
|
|
{
|
|
_ = Throw.IfNull(targets);
|
|
lock (this._gate)
|
|
{
|
|
foreach (var target in targets.Where(t => t is not null))
|
|
{
|
|
_ = this._allowedDomains.Remove(target);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Removes all outbound allow-list entries.</summary>
|
|
public void ClearAllowedDomains()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
this._allowedDomains.Clear();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// AIContextProvider implementation
|
|
// -------------------------------------------------------------------
|
|
|
|
/// <inheritdoc />
|
|
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
|
{
|
|
_ = Throw.IfNull(context);
|
|
|
|
SandboxExecutor.RunSnapshot snapshot;
|
|
lock (this._gate)
|
|
{
|
|
this.ThrowIfDisposed();
|
|
snapshot = new SandboxExecutor.RunSnapshot(
|
|
this._tools.Values.ToList(),
|
|
this._fileMounts.Values.ToList(),
|
|
this._allowedDomains.Values.ToList(),
|
|
this._options.HostInputDirectory);
|
|
}
|
|
|
|
var approvalRequired = ComputeApprovalRequired(this._options.ApprovalMode, snapshot.Tools);
|
|
|
|
var description = InstructionBuilder.BuildExecuteCodeDescription(
|
|
snapshot.Tools,
|
|
snapshot.FileMounts,
|
|
snapshot.AllowedDomains,
|
|
hasHostInputDirectory: !string.IsNullOrEmpty(snapshot.HostInputDirectory));
|
|
|
|
AIFunction executeCode = new ExecuteCodeFunction(this._executor, snapshot, description);
|
|
if (approvalRequired)
|
|
{
|
|
executeCode = new ApprovalRequiredAIFunction(executeCode);
|
|
}
|
|
|
|
var instructions = InstructionBuilder.BuildContextInstructions(toolsVisibleToModel: false);
|
|
|
|
var result = new AIContext
|
|
{
|
|
Instructions = instructions,
|
|
Tools = [executeCode],
|
|
};
|
|
|
|
return new ValueTask<AIContext>(result);
|
|
}
|
|
|
|
internal static bool ComputeApprovalRequired(CodeActApprovalMode mode, IReadOnlyList<AIFunction> tools) =>
|
|
mode == CodeActApprovalMode.AlwaysRequire
|
|
|| tools.Any(t => t.GetService<ApprovalRequiredAIFunction>() is not null);
|
|
|
|
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this._disposed, this);
|
|
|
|
/// <summary>Releases the underlying sandbox and associated native resources.</summary>
|
|
public void Dispose()
|
|
{
|
|
lock (this._gate)
|
|
{
|
|
if (this._disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this._disposed = true;
|
|
}
|
|
|
|
this._executor.Dispose();
|
|
}
|
|
}
|