-
Notifications
You must be signed in to change notification settings - Fork 2k
.NET: Added ShellTool and LocalShellExecutor #3369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
57938f4
1b91910
5b024e9
09f8022
03404bf
4cc16b7
8c9a721
fab1035
bf535bb
c662f4d
2419961
d4d96fc
bff6cb7
2ddf8a9
889ba25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
|
|
||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="OpenAI" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI.OpenAI" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" /> | ||
| <ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Shell.Local\Microsoft.Agents.AI.Shell.Local.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample demonstrates how to use the ShellTool with an AI agent. | ||
| // It shows security configuration options and human-in-the-loop approval for shell commands. | ||
| // | ||
| // SECURITY NOTE: The ShellTool executes real shell commands on your system. | ||
| // Always configure appropriate security restrictions before use. | ||
| // The safest approach is to run shell commands in isolated environments (containers, VMs, sandboxes) | ||
| // with restricted permissions and network access. | ||
|
|
||
| using Microsoft.Agents.AI; | ||
| using Microsoft.Extensions.AI; | ||
| using OpenAI; | ||
| using ChatMessage = Microsoft.Extensions.AI.ChatMessage; | ||
|
|
||
| var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") | ||
| ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); | ||
| var modelName = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini"; | ||
|
|
||
| // Get working directory (from environment variable or use temp folder) | ||
| var workingDirectory = Environment.GetEnvironmentVariable("SHELL_WORKING_DIR") | ||
| ?? Path.Combine(Path.GetTempPath(), "shell-tool-sample"); | ||
| Directory.CreateDirectory(workingDirectory); | ||
|
|
||
| Console.WriteLine($"Working directory: {workingDirectory}"); | ||
| Console.WriteLine(); | ||
|
|
||
| // Create the shell tool with security options. | ||
| // This configuration restricts what commands can be executed. | ||
| var shellTool = new ShellTool( | ||
| executor: new LocalShellExecutor(), | ||
| options: new ShellToolOptions | ||
| { | ||
| // Set the working directory for command execution | ||
| WorkingDirectory = workingDirectory, | ||
|
|
||
| // Restrict file system access to specific paths | ||
| AllowedPaths = [workingDirectory], | ||
|
|
||
| // Block access to sensitive paths (takes priority over AllowedPaths) | ||
| // BlockedPaths = ["/etc", "/var"], | ||
|
|
||
| // Only allow specific commands (regex patterns supported) | ||
| AllowedCommands = ["^ls", "^dir", "^echo", "^cat", "^type", "^mkdir", "^pwd", "^cd"], | ||
|
|
||
| // Block dangerous patterns (enabled by default) | ||
| BlockDangerousPatterns = true, | ||
|
|
||
| // Block command chaining operators like ; | && || (enabled by default) | ||
| BlockCommandChaining = true, | ||
|
|
||
| // Block privilege escalation commands like sudo, su (enabled by default) | ||
| BlockPrivilegeEscalation = true, | ||
|
|
||
| // Set execution timeout (default: 60 seconds) | ||
| TimeoutInMilliseconds = 30000, | ||
|
|
||
| // Set maximum output size (default: 50KB) | ||
| MaxOutputLength = 10240 | ||
| }); | ||
|
|
||
| // Convert the shell tool to an AIFunction for use with agents. | ||
| // Wrap with ApprovalRequiredAIFunction to require user approval before execution. | ||
| var shellFunction = new ApprovalRequiredAIFunction(shellTool.AsAIFunction()); | ||
|
|
||
| // Detect platform for shell command guidance | ||
| var operatingSystem = OperatingSystem.IsWindows() ? "Windows" : "Unix/Linux"; | ||
|
|
||
| // Create the chat client and agent with the shell tool. | ||
| AIAgent agent = new OpenAIClient(apiKey) | ||
| .GetChatClient(modelName) | ||
| .AsIChatClient() | ||
| .AsAIAgent( | ||
| instructions: $""" | ||
| You are a helpful assistant with access to a shell tool. | ||
| You can execute shell commands to help the user with file system tasks. | ||
| The operating system is {operatingSystem}. | ||
| """, | ||
| tools: [shellFunction]); | ||
|
|
||
| Console.WriteLine("Agent with Shell Tool"); | ||
| Console.WriteLine("====================="); | ||
| Console.WriteLine("This agent can execute shell commands with security restrictions."); | ||
| Console.WriteLine("Commands require user approval before execution."); | ||
| Console.WriteLine(); | ||
|
|
||
| // Interactive conversation loop | ||
| AgentThread thread = await agent.GetNewThreadAsync(); | ||
|
|
||
| while (true) | ||
| { | ||
| Console.Write("You: "); | ||
| var userInput = Console.ReadLine(); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| var response = await agent.RunAsync(userInput, thread); | ||
| var userInputRequests = response.UserInputRequests.ToList(); | ||
|
|
||
| // Handle approval requests for shell commands | ||
| while (userInputRequests.Count > 0) | ||
| { | ||
| var userInputResponses = userInputRequests | ||
| .OfType<FunctionApprovalRequestContent>() | ||
| .Select(functionApprovalRequest => | ||
| { | ||
| Console.WriteLine(); | ||
| Console.WriteLine($"[APPROVAL REQUIRED] The agent wants to execute: {functionApprovalRequest.FunctionCall.Name}"); | ||
|
|
||
| // Display the commands that will be executed | ||
| var arguments = functionApprovalRequest.FunctionCall.Arguments; | ||
| if (arguments is not null && arguments.TryGetValue("commands", out var commands) && commands is not null) | ||
| { | ||
| Console.WriteLine($"Commands: {commands}"); | ||
| } | ||
|
|
||
| Console.Write("Approve? (Y/N): "); | ||
| var approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false; | ||
|
|
||
| return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]); | ||
| }) | ||
| .ToList(); | ||
|
|
||
| response = await agent.RunAsync(userInputResponses, thread); | ||
| userInputRequests = response.UserInputRequests.ToList(); | ||
| } | ||
|
|
||
| Console.WriteLine(); | ||
| Console.WriteLine($"Agent: {response}"); | ||
| Console.WriteLine(); | ||
| } | ||
|
dmytrostruk marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Text.Json.Serialization; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Shared.Diagnostics; | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// Represents a shell command execution request. | ||
| /// </summary> | ||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | ||
| public sealed class ShellCallContent : AIContent | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we unsealed FCC/FRC and made this extend that, I think FunctionInvokingChatClient as-is could be able to handle these shell calls. Alternatively, I think these shell contents could be completely written in terms of FCC/FRC but it may be too loose i.e. some properties would need to travel in AdditionalProperties.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Custom AIContent needs to be added into the AgentsAbstractionJsonUtilities' options so that it correctly participates in polymorphic serialization. |
||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="ShellCallContent"/> class. | ||
| /// </summary> | ||
| /// <param name="callId">The unique identifier for this shell call.</param> | ||
| /// <param name="commands">The commands to execute.</param> | ||
| [JsonConstructor] | ||
| public ShellCallContent(string callId, IReadOnlyList<string> commands) | ||
| { | ||
| this.CallId = Throw.IfNull(callId); | ||
| this.Commands = Throw.IfNull(commands); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the unique identifier for this shell call. | ||
| /// </summary> | ||
| public string CallId { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the commands to execute. | ||
| /// </summary> | ||
| public IReadOnlyList<string> Commands { get; } | ||
|
|
||
| /// <summary>Gets a string representing this instance to display in the debugger.</summary> | ||
| [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||
| private string DebuggerDisplay => | ||
| $"ShellCall = {this.CallId}, Commands = {this.Commands.Count}"; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// Represents the output of a single shell command execution. | ||
| /// </summary> | ||
| public sealed class ShellCommandOutput | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the command that was executed. | ||
| /// </summary> | ||
| public string? Command { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the standard output from the command. | ||
| /// </summary> | ||
| public string? StandardOutput { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the standard error from the command. | ||
| /// </summary> | ||
| public string? StandardError { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the exit code. Null if the command timed out or failed to start. | ||
| /// </summary> | ||
| public int? ExitCode { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the command execution timed out. | ||
| /// </summary> | ||
| public bool IsTimedOut { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the output was truncated due to MaxOutputLength. | ||
| /// </summary> | ||
| public bool IsTruncated { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets an error message if the command failed to start. | ||
| /// </summary> | ||
| public string? Error { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// Abstract base class for shell command execution. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Implementations of this class handle the actual execution of shell commands. | ||
| /// The base class is designed to be extensible for different execution contexts | ||
| /// (local, SSH, container, etc.). | ||
| /// </para> | ||
| /// <para> | ||
| /// Executors return raw <see cref="ShellExecutorOutput"/> objects, which are | ||
| /// converted to <see cref="ShellCommandOutput"/> by <see cref="ShellTool"/>. | ||
| /// </para> | ||
| /// </remarks> | ||
| public abstract class ShellExecutor | ||
| { | ||
| /// <summary> | ||
| /// Executes the specified shell commands. | ||
| /// </summary> | ||
| /// <param name="commands">The commands to execute.</param> | ||
| /// <param name="options">The options controlling execution behavior.</param> | ||
| /// <param name="cancellationToken">The cancellation token.</param> | ||
| /// <returns>Raw output data for each command.</returns> | ||
| public abstract Task<IReadOnlyList<ShellExecutorOutput>> ExecuteAsync( | ||
| IReadOnlyList<string> commands, | ||
| ShellToolOptions options, | ||
| CancellationToken cancellationToken = default); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// Raw output from shell executor. | ||
| /// </summary> | ||
| public sealed class ShellExecutorOutput | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also consider ShellExecutorResponse to match AgentRunResponse, and ChatResponse. |
||
| { | ||
| /// <summary> | ||
| /// Gets or sets the command that was executed. | ||
| /// </summary> | ||
| public string? Command { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the standard output from the command. | ||
| /// </summary> | ||
| public string? StandardOutput { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the standard error from the command. | ||
| /// </summary> | ||
| public string? StandardError { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the exit code. Null if the command timed out or failed to start. | ||
| /// </summary> | ||
| public int? ExitCode { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the command execution timed out. | ||
| /// </summary> | ||
| public bool IsTimedOut { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the output was truncated due to MaxOutputLength. | ||
| /// </summary> | ||
| public bool IsTruncated { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets an error message if the command failed to start. | ||
| /// </summary> | ||
| public string? Error { get; set; } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we vary these by operating system as well? Some of these don't work in the windows command prompt shell.
Also, commands vary by shell type, so on windows powershell supports ls and pwd, but command prompt does not.