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