Files
agent-framework/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellExecutor.cs
T
westeyandGitHub bda40ba0e1 .NET: Add shell support to the HarnessAgent (#6005)
* Add shell support to the HarnessAgent

* Address PR comments

* Address PR comments
2026-05-21 17:25:33 +00:00

86 lines
4.0 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Tools.Shell;
/// <summary>
/// Pluggable backend that runs shell commands on behalf of a tool.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="LocalShellExecutor"/> runs commands directly on the host (no
/// isolation; approval-in-the-loop is the security boundary).
/// <see cref="DockerShellExecutor"/> runs them inside a container with resource
/// limits, network isolation, and a non-root user.
/// </para>
/// <para>
/// This is an abstract class rather than an interface so the surface can be
/// extended in future versions (e.g., adding new lifecycle hooks) without
/// breaking existing third-party implementations. Mirrors the Python
/// <c>ShellExecutor</c> Protocol in
/// <c>agent_framework_tools.shell._executor_base</c>.
/// </para>
/// <para>
/// Lifetime: <see cref="InitializeAsync"/> is invoked at most once per
/// instance (idempotent); <see cref="DisposeAsync"/> tears the executor down
/// at the end of its life. There is no public Shutdown step — disposal is the
/// teardown.
/// </para>
/// <para>
/// <b>Concurrency and session ownership.</b> A single executor instance is
/// intended to serve a single conversation / agent session — i.e., a single
/// user. Stateless mode is safe to share across concurrent callers (each
/// <c>RunAsync</c> spawns a fresh process or container, so there is no
/// shared mutable state). Persistent mode is <em>not</em> shareable: a
/// single long-lived shell process backs every call, it carries mutable
/// state (working directory, exported variables, history, in-flight
/// background jobs) that is visible to every subsequent command, and
/// concurrent commands would interleave on its stdin/stdout. The framework
/// does not isolate one caller's state from another's. Build one executor
/// per session, treat it as owned by that session for its lifetime, and
/// dispose it when the session ends. If you register an executor with a DI
/// container, use a per-request / per-conversation scope, not a singleton.
/// </para>
/// </remarks>
public abstract class ShellExecutor : IAsyncDisposable
{
/// <summary>
/// Eagerly initialize the backend. Idempotent; subsequent calls are
/// no-ops once the executor is started. For stateless executors this is
/// typically a no-op (the default implementation returns
/// <see cref="Task.CompletedTask"/>).
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
public virtual Task InitializeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
/// <summary>
/// Run a single command and return its result. Implementations are
/// expected to apply the configured per-command timeout and surface it
/// via <see cref="ShellResult.TimedOut"/> + <c>ExitCode = 124</c>.
/// </summary>
/// <param name="command">The shell command to execute.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public abstract Task<ShellResult> RunAsync(string command, CancellationToken cancellationToken = default);
/// <summary>
/// Build an <see cref="AIFunction"/> bound to this executor, suitable for
/// registering with an agent as a callable tool.
/// </summary>
/// <param name="name">Function name visible to the model.</param>
/// <param name="description">Function description for the model.</param>
/// <param name="requireApproval">
/// When <see langword="true"/> (the default), wraps the function in
/// <see cref="ApprovalRequiredAIFunction"/> so every invocation requires
/// explicit user approval before executing.
/// </param>
/// <returns>An <see cref="AIFunction"/> wrapping <see cref="RunAsync"/>.</returns>
public abstract AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true);
/// <inheritdoc />
public abstract ValueTask DisposeAsync();
}