-
Notifications
You must be signed in to change notification settings - Fork 1.1k
.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
base: main
Are you sure you want to change the base?
Changes from all 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.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # Security Warning | ||
|
|
||
| **This sample executes real shell commands on your system.** Before running: | ||
|
|
||
| 1. **Review the code** to understand what commands may be executed | ||
| 2. **Run in an isolated environment** (container, VM, or sandbox) when possible | ||
| 3. **Configure strict security options** to limit what the agent can do | ||
| 4. **Always use human-in-the-loop approval** for shell command execution | ||
| 5. **Never run with elevated privileges** (root/administrator) | ||
|
|
||
| The ShellTool includes security controls, but defense-in-depth is essential when executing arbitrary commands. | ||
|
|
||
| --- | ||
|
|
||
| ## What this sample demonstrates | ||
|
|
||
| This sample demonstrates how to use the ShellTool with an AI agent to execute shell commands with security controls and human-in-the-loop approval. | ||
|
|
||
| Key features: | ||
|
|
||
| - Configuring ShellTool security options (allowlist, denylist, path restrictions) | ||
| - Blocking dangerous patterns, command chaining, and privilege escalation | ||
| - Using ApprovalRequiredAIFunction for human-in-the-loop command approval | ||
| - Cross-platform support (Windows and Unix/Linux) | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| Set the following environment variables on Windows: | ||
|
|
||
| ```powershell | ||
| # Required: Your OpenAI API key | ||
| $env:OPENAI_API_KEY="sk-..." | ||
|
|
||
| # Optional: Model to use (defaults to gpt-4o-mini) | ||
| $env:OPENAI_MODEL="gpt-4o-mini" | ||
|
|
||
| # Optional: Working directory for shell commands (defaults to temp folder) | ||
| $env:SHELL_WORKING_DIR="C:\path\to\working\directory" | ||
| ``` | ||
|
|
||
| Or on Unix/Linux: | ||
|
|
||
| ```bash | ||
| export OPENAI_API_KEY="sk-..." | ||
| export OPENAI_MODEL="gpt-4o-mini" | ||
| export SHELL_WORKING_DIR="/path/to/working/directory" | ||
| ``` | ||
|
|
||
| ## Running in Docker (Recommended for Safety) | ||
|
|
||
| For safer testing, run the sample in a Docker container: | ||
|
|
||
| ```bash | ||
| # Build the container | ||
| docker build -t shell-tool-sample . | ||
|
|
||
| # Run interactively | ||
| docker run -it --rm -e OPENAI_API_KEY="sk-..." shell-tool-sample | ||
| ``` | ||
|
|
||
| ## Security Configuration | ||
|
|
||
| The sample demonstrates several security options: | ||
|
|
||
| | Option | Default | Description | | ||
| |--------|---------|-------------| | ||
| | `AllowedCommands` | null | Regex patterns for allowed commands | | ||
| | `DeniedCommands` | null | Regex patterns for blocked commands | | ||
| | `AllowedPaths` | null | Paths commands can access | | ||
| | `BlockedPaths` | null | Paths commands cannot access | | ||
| | `BlockDangerousPatterns` | true | Block fork bombs, rm -rf /, etc. | | ||
| | `BlockCommandChaining` | true | Block ; \| && \|\| $() operators | | ||
| | `BlockPrivilegeEscalation` | true | Block sudo, su, runas, etc. | | ||
| | `TimeoutInMilliseconds` | 60000 | Command execution timeout | | ||
| | `MaxOutputLength` | 51200 | Maximum output size in bytes | | ||
|
|
||
| ## Example Interaction | ||
|
|
||
| ``` | ||
| You: Create a folder called test and list its contents | ||
|
|
||
| [APPROVAL REQUIRED] The agent wants to execute: shell | ||
| Commands: ["mkdir test"] | ||
| Approve? (Y/N): Y | ||
|
|
||
| [APPROVAL REQUIRED] The agent wants to execute: shell | ||
| Commands: ["ls test"] | ||
| Approve? (Y/N): Y | ||
|
|
||
| Agent: I created the "test" folder and listed its contents. The folder is currently empty. | ||
| ``` |
| 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; } | ||
| } |
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.