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>
244 lines
8.4 KiB
C#
244 lines
8.4 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using HyperlightSandbox.Api;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.Hyperlight.Internal;
|
|
|
|
/// <summary>
|
|
/// Captures a per-run snapshot of the provider state and owns the
|
|
/// lifecycle of the underlying <see cref="Sandbox"/>. A single
|
|
/// <see cref="SandboxExecutor"/> is shared across runs and serializes
|
|
/// execution via snapshot/restore.
|
|
/// </summary>
|
|
internal sealed class SandboxExecutor : IDisposable
|
|
{
|
|
private readonly HyperlightCodeActProviderOptions _options;
|
|
private readonly SemaphoreSlim _executionLock = new(1, 1);
|
|
|
|
private Sandbox? _sandbox;
|
|
private SandboxSnapshot? _warmSnapshot;
|
|
private string? _lastConfigFingerprint;
|
|
private bool _disposed;
|
|
|
|
public SandboxExecutor(HyperlightCodeActProviderOptions options)
|
|
{
|
|
this._options = options;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Immutable snapshot of provider state at the start of a run.
|
|
/// Used to build a run-scoped <c>execute_code</c> function that is
|
|
/// independent of subsequent CRUD mutations.
|
|
/// </summary>
|
|
internal sealed class RunSnapshot
|
|
{
|
|
public RunSnapshot(
|
|
IReadOnlyList<AIFunction> tools,
|
|
IReadOnlyList<FileMount> fileMounts,
|
|
IReadOnlyList<AllowedDomain> allowedDomains,
|
|
string? hostInputDirectory)
|
|
{
|
|
this.Tools = tools;
|
|
this.FileMounts = fileMounts;
|
|
this.AllowedDomains = allowedDomains;
|
|
this.HostInputDirectory = hostInputDirectory;
|
|
this.ConfigFingerprint = ComputeFingerprint(tools, fileMounts, allowedDomains, hostInputDirectory);
|
|
}
|
|
|
|
public IReadOnlyList<AIFunction> Tools { get; }
|
|
|
|
public IReadOnlyList<FileMount> FileMounts { get; }
|
|
|
|
public IReadOnlyList<AllowedDomain> AllowedDomains { get; }
|
|
|
|
public string? HostInputDirectory { get; }
|
|
|
|
/// <summary>
|
|
/// Stable fingerprint of the configuration that materially affects how
|
|
/// the sandbox must be built. Used by <see cref="SandboxExecutor"/> to
|
|
/// decide whether a previously-built sandbox can be reused or must be
|
|
/// rebuilt because tools / mounts / allow-list entries have changed.
|
|
/// </summary>
|
|
public string ConfigFingerprint { get; }
|
|
|
|
internal static string ComputeFingerprint(
|
|
IReadOnlyList<AIFunction> tools,
|
|
IReadOnlyList<FileMount> fileMounts,
|
|
IReadOnlyList<AllowedDomain> allowedDomains,
|
|
string? hostInputDirectory)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append("tools=");
|
|
foreach (var name in tools.Select(t => t.Name).OrderBy(n => n, StringComparer.Ordinal))
|
|
{
|
|
sb.Append(name).Append('|');
|
|
}
|
|
|
|
sb.Append(";mounts=");
|
|
foreach (var m in fileMounts
|
|
.Select(m => m.MountPath + "->" + m.HostPath)
|
|
.OrderBy(s => s, StringComparer.Ordinal))
|
|
{
|
|
sb.Append(m).Append('|');
|
|
}
|
|
|
|
sb.Append(";allow=");
|
|
foreach (var d in allowedDomains
|
|
.Select(d => d.Target + "/" + (d.Methods is null ? "*" : string.Join(",", d.Methods)))
|
|
.OrderBy(s => s, StringComparer.Ordinal))
|
|
{
|
|
sb.Append(d).Append('|');
|
|
}
|
|
|
|
sb.Append(";input=").Append(hostInputDirectory ?? string.Empty);
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes <paramref name="code"/> inside the sandbox using the
|
|
/// captured <paramref name="snapshot"/>. Builds (or rebuilds) the
|
|
/// sandbox lazily when the snapshot's configuration fingerprint
|
|
/// differs from the previously-used one.
|
|
/// </summary>
|
|
public async Task<string> ExecuteAsync(RunSnapshot snapshot, string code, CancellationToken cancellationToken)
|
|
{
|
|
await this._executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
this.EnsureInitialized(snapshot);
|
|
|
|
if (this._warmSnapshot is not null)
|
|
{
|
|
this._sandbox!.Restore(this._warmSnapshot);
|
|
}
|
|
|
|
ExecutionResult result;
|
|
try
|
|
{
|
|
result = this._sandbox!.Run(code);
|
|
}
|
|
#pragma warning disable CA1031 // Surface sandbox execution failures as structured JSON rather than propagating.
|
|
catch (Exception ex)
|
|
#pragma warning restore CA1031
|
|
{
|
|
return BuildErrorResult(ex.Message);
|
|
}
|
|
|
|
return BuildResult(result);
|
|
}
|
|
finally
|
|
{
|
|
this._executionLock.Release();
|
|
}
|
|
}
|
|
|
|
private void EnsureInitialized(RunSnapshot snapshot)
|
|
{
|
|
if (this._sandbox is not null && string.Equals(this._lastConfigFingerprint, snapshot.ConfigFingerprint, StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Configuration changed (or first run) — dispose the previous sandbox
|
|
// so the new one picks up the new tool/mount/allow-list set.
|
|
this._warmSnapshot?.Dispose();
|
|
this._sandbox?.Dispose();
|
|
this._warmSnapshot = null;
|
|
this._sandbox = null;
|
|
|
|
this.BuildAndWarmUp(snapshot);
|
|
}
|
|
|
|
private void BuildAndWarmUp(RunSnapshot snapshot)
|
|
{
|
|
var builder = new SandboxBuilder()
|
|
.WithBackend(this._options.Backend);
|
|
|
|
if (!string.IsNullOrEmpty(this._options.ModulePath))
|
|
{
|
|
builder = builder.WithModulePath(this._options.ModulePath!);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(this._options.HeapSize))
|
|
{
|
|
builder = builder.WithHeapSize(this._options.HeapSize!);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(this._options.StackSize))
|
|
{
|
|
builder = builder.WithStackSize(this._options.StackSize!);
|
|
}
|
|
|
|
var hostInput = snapshot.HostInputDirectory;
|
|
if (!string.IsNullOrEmpty(hostInput))
|
|
{
|
|
builder = builder.WithInputDir(hostInput!);
|
|
}
|
|
|
|
// The Hyperlight .NET SDK currently exposes only a single input + output + temp-output
|
|
// surface; per-mount configuration (`FileMount`) is captured in the execute_code
|
|
// description so the model is aware of the layout, and will be wired to a richer
|
|
// mount API once the SDK exposes one.
|
|
if (snapshot.FileMounts.Count > 0 || !string.IsNullOrEmpty(hostInput))
|
|
{
|
|
builder = builder.WithTempOutput();
|
|
}
|
|
|
|
var sandbox = builder.Build();
|
|
|
|
// Tools must be registered before the first Run() call.
|
|
ToolBridge.RegisterAll(sandbox, snapshot.Tools);
|
|
|
|
foreach (var allowedDomain in snapshot.AllowedDomains)
|
|
{
|
|
sandbox.AllowDomain(allowedDomain.Target, allowedDomain.Methods);
|
|
}
|
|
|
|
// Warm-up run to trigger lazy initialization, then capture a clean snapshot
|
|
// that is restored before every subsequent user invocation.
|
|
// Backend-specific no-op used to trigger lazy guest runtime initialization
|
|
// before the warm snapshot is captured. Matches the values used by the
|
|
// upstream HyperlightSandbox.Extensions.AI CodeExecutionTool reference.
|
|
_ = sandbox.Run(this._options.Backend == SandboxBackend.JavaScript ? "void 0;" : "None");
|
|
this._warmSnapshot = sandbox.Snapshot();
|
|
this._sandbox = sandbox;
|
|
this._lastConfigFingerprint = snapshot.ConfigFingerprint;
|
|
}
|
|
|
|
private static string BuildResult(ExecutionResult result) =>
|
|
JsonSerializer.Serialize(
|
|
new HyperlightExecutionResult(
|
|
result.Stdout ?? string.Empty,
|
|
result.Stderr ?? string.Empty,
|
|
result.ExitCode,
|
|
result.ExitCode == 0),
|
|
HyperlightJsonContext.Default.HyperlightExecutionResult);
|
|
|
|
private static string BuildErrorResult(string message) =>
|
|
JsonSerializer.Serialize(
|
|
new HyperlightExecutionResult(string.Empty, message, -1, false),
|
|
HyperlightJsonContext.Default.HyperlightExecutionResult);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (this._disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this._disposed = true;
|
|
this._warmSnapshot?.Dispose();
|
|
this._sandbox?.Dispose();
|
|
this._executionLock.Dispose();
|
|
}
|
|
}
|