// 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;
///
/// An that enables CodeAct execution through a
/// Hyperlight-backed sandbox.
///
///
///
/// The provider injects an execute_code tool into the model-facing tool
/// surface and contributes a short CodeAct guidance block through
/// . Guest code executed via
/// execute_code runs in an isolated Hyperlight sandbox with
/// snapshot/restore for clean state per invocation.
///
///
/// 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 call_tool(...) but not to the model directly.
///
///
/// Only a single may be attached to a
/// given agent. returns a fixed value so
/// ChatClientAgent's state-key uniqueness validation rejects duplicate
/// registrations.
///
///
/// Security considerations: 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
/// when guest code can reach
/// sensitive resources.
///
///
public sealed class HyperlightCodeActProvider : AIContextProvider, IDisposable
{
///
/// Fixed state key used to enforce a single provider-per-agent.
///
internal const string FixedStateKey = "HyperlightCodeActProvider";
private static readonly IReadOnlyList s_stateKeys = [FixedStateKey];
private readonly object _gate = new();
private readonly HyperlightCodeActProviderOptions _options;
private readonly SandboxExecutor _executor;
private readonly Dictionary _tools = new(StringComparer.Ordinal);
private readonly Dictionary _fileMounts = new(StringComparer.Ordinal);
private readonly Dictionary _allowedDomains = new(StringComparer.Ordinal);
private bool _disposed;
///
/// Initializes a new instance of the class.
///
///
/// Optional configuration options for the provider. When the provider
/// uses the defaults of (the
/// backend with no tools, mounts, or allow-list entries).
/// Use to target a Wasm
/// guest module instead.
///
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;
}
}
}
///
public override IReadOnlyList StateKeys => s_stateKeys;
// -------------------------------------------------------------------
// Tool registry
// -------------------------------------------------------------------
/// Adds tools to the provider-owned CodeAct tool registry. Tools with a duplicate name replace the existing registration.
/// The tools to add.
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;
}
}
}
/// Returns the current CodeAct-managed tools.
public IReadOnlyList GetTools()
{
lock (this._gate)
{
return this._tools.Values.ToList();
}
}
/// Removes tools by name from the CodeAct tool registry.
/// The names of the tools to remove.
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);
}
}
}
/// Removes all CodeAct-managed tools.
public void ClearTools()
{
lock (this._gate)
{
this._tools.Clear();
}
}
// -------------------------------------------------------------------
// File mounts
// -------------------------------------------------------------------
/// Adds file mount configurations. Mounts with a duplicate mount path replace the existing entry.
/// The mount configurations to add.
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;
}
}
}
/// Returns the current file mount configurations.
public IReadOnlyList GetFileMounts()
{
lock (this._gate)
{
return this._fileMounts.Values.ToList();
}
}
/// Removes file mounts by sandbox mount path.
/// The mount paths to remove.
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);
}
}
}
/// Removes all file mount configurations.
public void ClearFileMounts()
{
lock (this._gate)
{
this._fileMounts.Clear();
}
}
// -------------------------------------------------------------------
// Network allow-list
// -------------------------------------------------------------------
/// Adds outbound network allow-list entries. Entries with a duplicate target replace the existing entry.
/// The allow-list entries to add.
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;
}
}
}
/// Returns the current outbound allow-list entries.
public IReadOnlyList GetAllowedDomains()
{
lock (this._gate)
{
return this._allowedDomains.Values.ToList();
}
}
/// Removes allow-list entries by target.
/// The targets to remove.
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);
}
}
}
/// Removes all outbound allow-list entries.
public void ClearAllowedDomains()
{
lock (this._gate)
{
this._allowedDomains.Clear();
}
}
// -------------------------------------------------------------------
// AIContextProvider implementation
// -------------------------------------------------------------------
///
protected override ValueTask 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(result);
}
internal static bool ComputeApprovalRequired(CodeActApprovalMode mode, IReadOnlyList tools) =>
mode == CodeActApprovalMode.AlwaysRequire
|| tools.Any(t => t.GetService() is not null);
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this._disposed, this);
/// Releases the underlying sandbox and associated native resources.
public void Dispose()
{
lock (this._gate)
{
if (this._disposed)
{
return;
}
this._disposed = true;
}
this._executor.Dispose();
}
}