Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
<Project Path="samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step20_AdditionalAIContext/Agent_Step20_AdditionalAIContext.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step21_ShellTool/Agent_Step21_ShellTool.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/DeclarativeAgents/">
<Project Path="samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
Expand Down Expand Up @@ -411,6 +412,7 @@
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
<Project Path="src/Microsoft.Agents.AI.Shell.Local/Microsoft.Agents.AI.Shell.Local.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
Expand All @@ -429,6 +431,7 @@
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Shell.Local.IntegrationTests/Microsoft.Agents.AI.Shell.Local.IntegrationTests.csproj" />
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" />
<Project Path="tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj" />
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj" />
Expand Down
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>
134 changes: 134 additions & 0 deletions dotnet/samples/GettingStarted/Agents/Agent_Step21_ShellTool/Program.cs
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"],
Copy link
Contributor

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.


// 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();
}
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.
```
1 change: 1 addition & 0 deletions dotnet/samples/GettingStarted/Agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Before you begin, ensure you have the following prerequisites:
|[Deep research with an agent](./Agent_Step18_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
|[Declarative agent](./Agent_Step19_Declarative/)|This sample demonstrates how to declaratively define an agent.|
|[Providing additional AI Context to an agent using multiple AIContextProviders](./Agent_Step20_AdditionalAIContext/)|This sample demonstrates how to inject additional AI context into a ChatClientAgent using multiple custom AIContextProvider components that are attached to the agent.|
|[Using Shell Tool with security controls](./Agent_Step21_ShellTool/)|This sample demonstrates how to use the ShellTool with an AI agent, including security configuration options and human-in-the-loop approval for shell commands.|

## Running the samples from the console

Expand Down
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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; }
}
Loading
Loading