Shell 工具

beta agent-framework-tools Python 包通过agent_framework.tools命名空间提供 shell 执行和环境感知工具。

工具 在以下情况下使用
LocalShellTool 命令受信任或单独批准,应在代理进程的主机环境中运行。
DockerShellTool 模型生成的 shell 命令需要 OCI 容器隔离。
ShellEnvironmentProvider 该模型需要活动的 shell 系列、操作系统、工作目录和已安装的 CLI 版本。
ShellPolicy 在审批或执行之前,需要允许列表或拒绝列表预筛选。

Warning

模型生成的 shell 命令不受信任。 在执行之前验证并批准它们。 使用支持该任务的最低权限执行层级。

安装软件包

dotnet add package Microsoft.Agents.AI.Tools.Shell --prerelease

使用本地命令行解释器和环境感知功能

LocalShellExecutor 支持无状态模式和持久性模式。 ShellEnvironmentProvider 探测当前环境,并将权威的 Shell 指导信息添加到代理上下文中。

using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Tools.Shell;
using Microsoft.Extensions.AI;

var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";

// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());

const string Instructions = """
    You are an agent with a single tool: run_shell. Use it to satisfy the
    user's request. Do not describe what you would do — actually run the
    commands. Reply with the final answer derived from real output.
    """;

// --------------------------------------------------------------------
// 1. Stateless mode — each call gets a fresh shell.
// --------------------------------------------------------------------
Console.WriteLine("### Stateless mode\n");
await using (var statelessShell = new LocalShellExecutor(new() { Mode = ShellMode.Stateless, AcknowledgeUnsafe = true }))
{
    var envProvider = new ShellEnvironmentProvider(statelessShell);
    var statelessAgent = aiProjectClient.AsAIAgent(new ChatClientAgentOptions
    {
        ChatOptions = new()
        {
            ModelId = deploymentName,
            Instructions = Instructions,
            Tools = [statelessShell.AsAIFunction(requireApproval: false)],
        },
        AIContextProviders = [envProvider],
    });
// --------------------------------------------------------------------
// 2. Persistent mode — one shell, reused across calls. State carries.
// --------------------------------------------------------------------
Console.WriteLine("\n### Persistent mode\n");
await using (var persistentShell = new LocalShellExecutor(new() { Mode = ShellMode.Persistent, AcknowledgeUnsafe = true }))
{
    var envProvider = new ShellEnvironmentProvider(persistentShell);
    var persistentAgent = aiProjectClient.AsAIAgent(new ChatClientAgentOptions
    {
        ChatOptions = new()
        {
            ModelId = deploymentName,
            Instructions = Instructions,
            Tools = [persistentShell.AsAIFunction(requireApproval: false)],
        },
        AIContextProviders = [envProvider],
    });

    var persistentSession = await persistentAgent.CreateSessionAsync();

    // State carries across calls in persistent mode: cd into temp, then
    // verify the next call sees the new CWD.
    Console.WriteLine(await persistentAgent.RunAsync("Change directory into the system temp folder, then print the current working directory.", persistentSession));
    Console.WriteLine();
    Console.WriteLine(await persistentAgent.RunAsync("In a NEW shell call, print the current working directory again. Tell me whether it still matches the temp folder.", persistentSession));
    Console.WriteLine();

    // Same idea with an exported variable: set in one call, read in the next.
    Console.WriteLine(await persistentAgent.RunAsync("Set the environment variable DEMO_TOKEN to the value 'hello-world'.", persistentSession));
    Console.WriteLine();
    Console.WriteLine(await persistentAgent.RunAsync("Print the current value of DEMO_TOKEN. Tell me exactly what value the shell reports.", persistentSession));
    Console.WriteLine();

    PrintSnapshot(envProvider.CurrentSnapshot!);
}

ShellPolicy 也可用于命令预筛选。 目前尚未发布专门的 DockerShellExecutor 可运行示例。

安装软件包

pip install agent-framework-tools --pre

该软件包会安装 psutil,以便在执行超时时终止子进程树。

使用 LocalShellTool

LocalShellTool 直接在主机上运行命令。 它默认为持久性 shell、30 秒超时、标准输出和标准错误各自独立的 64 KiB 保留限额,以及工作目录重新定位。 默认情况下,通过 as_function() 发起的代理调用需要获得批准。 直接调用 run() 不请求审批。

import asyncio
from typing import Any

from agent_framework import Agent, Message
from agent_framework.openai import OpenAIChatClient
from agent_framework.tools import LocalShellTool
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()
async def main() -> None:
    print("=== OpenAI Agent with LocalShellTool Example ===")
    print("NOTE: Commands will execute on your local machine.\n")

    client = OpenAIChatClient(model="gpt-5.4-nano")

    async with LocalShellTool() as shell:
        agent = Agent(
            client=client,
            instructions="You are a helpful assistant that can run shell commands to help the user.",
            tools=[client.get_shell_tool(func=shell.as_function())],
        )

        query = "Use the shell tool to execute `python --version` and show only the command output."
        print(f"User: {query}")
        result = await run_with_approvals(query, agent)
        if isinstance(result, str):
            print(f"Agent: {result}\n")
            return
        if result.text:
            print(f"Agent: {result.text}\n")
        else:
            printed = False
            for message in result.messages:
                for content in message.contents:
                    if content.type == "function_result" and content.result:
                        print(f"Agent (tool output): {content.result}\n")
                        printed = True
            if not printed:
                print("Agent: (no text output returned)\n")


async def run_with_approvals(query: str, agent: Agent) -> Any:
    """Run the agent and handle shell approvals outside tool execution."""
    current_input: str | list[Any] = query

    while True:
        result = await agent.run(current_input)
        if not result.user_input_requests:
            return result

        next_input: list[Any] = [query]
        rejected = False
        for user_input_needed in result.user_input_requests:
            if user_input_needed.function_call is None:
                continue
            print(
                f"\nShell request: {user_input_needed.function_call.name}"
                f"\nArguments: {user_input_needed.function_call.arguments}"
            )
            user_approval = await asyncio.to_thread(input, "\nApprove shell command? (y/n): ")
            approved = user_approval.strip().lower() == "y"
            next_input.append(Message("assistant", [user_input_needed]))
            next_input.append(Message("user", [user_input_needed.to_function_approval_response(approved)]))
            if not approved:
                rejected = True
                break
        if rejected:
            print("\nShell command rejected. Stopping without additional approval prompts.")
            return "Shell command execution was rejected by user."
        current_input = next_input


if __name__ == "__main__":
    asyncio.run(main())

直接调用 run() 会返回一个 ShellResult,其中包含单独的 stdout、stderr、exit_code、duration_ms、truncated 和 timed_out 字段。 当 shell 通过 OpenAI Responses 或 Foundry 托管运行时,Agent Framework 会在提供程序延续执行的过程中保留标准输出、标准错误、退出结果和超时结果。 非零退出仍然失败,标准错误仍然保持独立,超时不会显示为成功的退出。

设置正的 max_stdout_bytes 和 max_stderr_bytes 值,以针对 LocalShellTool 或 DockerShellTool 更改独立的保留输出限制。 这些设置限制保留的输出,而不是命令可以生成多少输出。 截断通知和 UTF-8 替换可能会使返回的文本略大于配置的原始字节限额。

即使配置了本地 shell 执行程序,OpenAI 提供程序托管的 shell 脚本项仍保持信息性。 只有格式正确的显式 local_shell_call 调用,或标记有 environment.type="local" 的 shell 调用,才会进入本地函数和审批流程。 单独配置 LocalShellTool 不会导致提供程序托管的 shell 调用在主机上执行。

当每个调用应在新的进程中运行时使用 mode="stateless" 。 使用 AGENT_FRAMEWORK_SHELL 环境变量或 shell 构造函数参数来覆盖已解析的 shell。

持久命令已序列化。 在输出溢出或使会话无法恢复的超时之后,排队的命令将在新的 shell 中启动,并丢失终止的会话中的 shell-local 变量、函数和工作目录更改。 正常中断的 POSIX 超时可以保留现有 shell 及其状态。 在 Windows 上,即使被中断的命令发出了完成标记,超时也始终会关闭当前的持久 Shell。

调用 close() 会拒绝在关闭之前或关闭期间排队的命令,而不会重启 shell。 稍后的调用可以打开一个新会话。

重要

LocalShellTool 不是沙盒。 人工审批增加了审查门,但它不会隔离外壳。 要禁用对代理调用的审批,需要 acknowledge_unsafe=True。

使用 限制命令

ShellPolicy 在执行前将正则表达式允许和拒绝列表应用于命令文本。 拒绝规则优先。 它不会检查 shell 最终执行或限制文件访问的内容。

import asyncio

from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from agent_framework.tools import LocalShellTool, ShellPolicy
from dotenv import load_dotenv
load_dotenv()
async def main() -> None:
    client = OpenAIChatClient(model="gpt-5.4-nano")

    shell = LocalShellTool(
        mode="stateless",
        # Unsafe for production as shown: these filters do not replace human approval or isolation.
        approval_mode="never_require",
        acknowledge_unsafe=True,
        policy=ShellPolicy(
            allowlist=[
                r"^ls(\s|$)",
                r"^pwd$",
                r"^cat\s[^|;&]+$",
                r"^git\s+(status|log|diff)(\s|$)",
                r"^python\s+--version$",
            ],
        ),
        timeout=10,
    )

    agent = Agent(
        client=client,
        instructions=("Use only these shell commands: ls, pwd, cat, git status/log/diff, python --version."),
        tools=[client.get_shell_tool(func=shell.as_function())],
    )

Warning

该示例禁用了人工审批,仅适用于不包含机密信息或有价值数据的隔离、一次性环境。 命令替换(例如 $(...) 和反引号)可通过简单的允许列表模式。 仅与命令开头匹配的模式还可以允许额外的操作。 对生产使用单独强制执行的隔离和受限权限。 人工审核可以增加一道检查,但并不能隔离 shell。

首选字符串模式。 Python使用regex引擎编译字符串,并为每个匹配项应用一秒的预算。 拒绝列表超时拒绝命令,允许列表超时不授予权限。 预编译的 regex.Pattern 使用相同的界限。 预编译的标准库 re.Pattern 会保留其标志,但无法中断,因此请避免这种形式的昂贵或模糊表达式。

添加 ShellEnvironmentProvider

ShellEnvironmentProvider 探测 shell 系列、版本、操作系统、工作目录和所选 CLI 版本,然后在代理运行之前注入该信息。 默认探测列表为git、node和pythondocker。

import asyncio

from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from agent_framework.tools import (
    LocalShellTool,
    ShellEnvironmentProvider,
    ShellEnvironmentProviderOptions,
)
from dotenv import load_dotenv
load_dotenv()
def _print_snapshot(label: str, provider: ShellEnvironmentProvider) -> None:
    snapshot = provider.current_snapshot
    if snapshot is None:
        print(f"[{label}] no snapshot captured")
        return
    print(f"\n[{label}] snapshot:")
    print(f"  family            = {snapshot.family.value}")
    print(f"  os                = {snapshot.os_description}")
    print(f"  shell_version     = {snapshot.shell_version}")
    print(f"  working_directory = {snapshot.working_directory}")
    for tool, version in snapshot.tool_versions.items():
        print(f"  {tool:<17} = {version}")


async def _ask(agent: Agent, query: str) -> None:
    print(f"\nUser: {query}")
    result = await agent.run(query)
    if result.text:
        print(f"Agent: {result.text}")


async def main() -> None:
    client = OpenAIChatClient(model="gpt-5.4-nano")
    options = ShellEnvironmentProviderOptions(
        probe_tools=("git", "python", "uv", "node"),
    )

    print("=== stateless mode ===")
    async with LocalShellTool(
        mode="stateless",
        approval_mode="never_require",
        acknowledge_unsafe=True,
    ) as shell:
        provider = ShellEnvironmentProvider(shell, options)
        agent = Agent(
            client=client,
            instructions="Use the shell tool to answer the user's question.",
            tools=[client.get_shell_tool(func=shell.as_function())],
            context_providers=[provider],
        )
        await _ask(agent, "Show me the current working directory.")
        await _ask(agent, "Now `cd ..` then show the working directory again.")
        await _ask(agent, "Show the working directory once more — did `cd` persist?")
        _print_snapshot("stateless", provider)

    print("\n=== persistent mode ===")
    async with LocalShellTool(
        mode="persistent",
        confine_workdir=False,
        approval_mode="never_require",
        acknowledge_unsafe=True,
    ) as shell:
        provider = ShellEnvironmentProvider(shell, options)
        agent = Agent(
            client=client,
            instructions="Use the shell tool to answer the user's question.",
            tools=[client.get_shell_tool(func=shell.as_function())],
            context_providers=[provider],
        )
        await _ask(agent, "Show me the current working directory.")
        await _ask(agent, "Now `cd ..` then show the working directory again.")
        await _ask(agent, "Show the working directory once more — did `cd` persist?")
        _print_snapshot("persistent", provider)

使用 DockerShellTool

DockerShellTool 需要在 PATH 上安装 Docker 或 Podman。 默认值禁用网络,以非根用户身份运行,使用只读根文件系统,删除功能,将内存限制为 512 MiB,并将容器上限为 256 个进程。

from agent_framework.tools import DockerShellTool

async with DockerShellTool(
    image="mcr.microsoft.com/azurelinux/base/core:3.0",
    approval_mode="never_require",
) as shell:
    result = await shell.run("uname -a && id")
    print(result.stdout)

默认图像为 mcr.microsoft.com/azurelinux/base/core:3.0. 传递docker_binary="podman"以使用 Podman。 目前尚未发布专门的 DockerShellTool 可运行示例。

仅在不存在专用构造函数选项时使用 unsafe_extra_run_args 。 这些令牌将逐字追加到 Docker 或 Podman 命令,可以削弱或绕过配置的隔离和资源限制。 使其由开发人员控制,且绝不要根据用户或模型的输出填充它们。 如果有相应的 DockerShellTool 构造函数选项可用,请改用它。

选择执行层级

Scenario 工具 隔离边界
受信任的开发命令 LocalShellTool 无;默认情况下启用人工审批
不可信的 shell 命令 DockerShellTool 具有默认隔离标志的 OCI 容器
无 shell 的不受信任生成代码 Hyperlight CodeAct Hyperlight microVM

Go 通过 tool/shelltool 提供本地 Shell 执行和环境探测功能。 请参阅 使用本地 shell 工具。

DockerShellTool 指南当前不适用于 Go。

将命令行工具与工具套件代理结合使用

纯代理和 HarnessAgent 使用相同的两部分 shell 配置:将执行器的函数注册为工具,并在模型需要接收 shell、操作系统、工作目录和 CLI 版本上下文时添加 ShellEnvironmentProvider。 HarnessAgent 不创建或拥有 shell 执行器:

using System.IO;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Tools.Shell;
using Microsoft.Extensions.AI;

await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
{
    WorkingDirectory = Directory.GetCurrentDirectory(),
    Timeout = LocalShellExecutor.DefaultTimeout,
});

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    AIContextProviders = [new ShellEnvironmentProvider(shell)],
    ChatOptions = new ChatOptions
    {
        Tools = [shell.AsAIFunction(requireApproval: true)],
    },
});

AsAIFunction 默认为名称 run_shell 和 requireApproval: true。 LocalShellExecutor 默认为永久性模式,每个输出流 64-KiB 上限,且无超时;该示例显式使用建议的 30 秒 LocalShellExecutor.DefaultTimeout。 ShellEnvironmentProviderOptions 默认会探测 git、dotnet、node、python 和 docker,每次探测的超时时间为 5 秒。

为每个用户会话创建一个永久性执行程序,并在会话结束时释放它。 不要在用户或并发对话之间共享,因为工作目录、环境、shell 历史记录、后台作业和命令队列是共享的。 ShellPolicy 只是预筛选;保持审批启用状态,使用最低特权凭据,并在命令需要更强大的隔离边界时优先 DockerShellExecutor 使用。

预发行版 Microsoft.Agents.AI.Tools.Shell 包中提供了 Shell 工具。 可从 Microsoft.Agents.AI.Harness 获取 HarnessAgent。

对于普通代理,请使用 client.get_shell_tool(func=shell.as_function()) 创建 shell 函数,并单独添加 ShellEnvironmentProvider。 create_harness_agent 在你传入 shell_executor 时会同时执行这两个步骤:

from agent_framework import create_harness_agent
from agent_framework.tools import LocalShellTool, ShellEnvironmentProviderOptions

async with LocalShellTool() as shell:
    agent = create_harness_agent(
        client=client,
        shell_executor=shell,
        shell_environment_provider_options=ShellEnvironmentProviderOptions(
            probe_tools=("git", "python"),
        ),
    )

    session = agent.create_session()
    response = await agent.run("Inspect the current repository.", session=session)

shell_executor需要选择启用,并且必须公开as_function()。 仅当客户端实现SupportsShellTool时,工厂才会添加命令行工具和ShellEnvironmentProvider;否则,工厂会记录警告并跳过这两项。 shell_environment_provider_options 是可选的,仅用于 shell_executor.

LocalShellTool 默认为永久性模式、30 秒超时、标准输出和标准错误各自单独的 64 KiB 限制、工作目录重新定位以及 approval_mode="always_require"。 由于工具套件的工具审批功能默认启用,请将AgentSession传递给run。 调用方拥有执行程序生命周期;使用 async with 或调用 close(),并为每个用户会话创建一个持久性工具。 不要在用户或并发对话之间共享可变 shell 状态。

主机命令行环境不是沙盒。 保持审批处于启用状态,使用最小权限凭据,并使用 DockerShellTool 实现容器隔离。 禁用审批需要approval_mode="never_require"和acknowledge_unsafe=True;ShellPolicy本身并不构成安全边界。

create_harness_agent 于 agent-framework-core 发布。 Shell 集成功能由预发布的 agent-framework-tools 包提供,并会在启用时触发 ExperimentalWarning。

默认情况下,Go harness 不会添加 shell 工具。 在将配置传递给 agent.Config.ContextProviders 之前,将本地 shell 工具添加到 harness.AgentConfig,并将其环境提供程序添加到 agent.Config.Tools。 帮助程序保留这些调用方提供的组件并添加测试框架默认值。

有关设置和审批要求,请参阅使用本地 shell 工具。