Shell 工具

agent-framework-tools 測試版 Python 套件透過命名空間提供 shell 執行與環境感知工具agent_framework.tools

Tool 在下列情況下使用
LocalShellTool 指令是受信任的或個別核准的,且應在代理程序的宿主環境中執行。
DockerShellTool 模型產生的 Shell 指令需要以 OCI 容器進行隔離。
ShellEnvironmentProvider 此模型需要目前使用中的 shell 類型、作業系統、工作目錄,以及已安裝的 CLI 版本。
ShellPolicy 你需要在批准或執行前先做允許清單或拒絕清單的預篩選。

Warning

殼層執行可以修改檔案、啟動程序、存取憑證,並與外部系統通訊。 使用支援該任務的最低權限執行層級。

安裝套件

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 輸出截斷、限制在工作目錄內,以及每個指令都需核准。

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())

當每次呼叫都應在新的程序中執行時,請使用 mode="stateless"。 使用 AGENT_FRAMEWORK_SHELL 環境變數或 shell 建構函數參數來覆寫已解析的 shell。

這很重要

LocalShellTool 不是沙箱。 核准是主要的安全邊界。 停用核准需要 acknowledge_unsafe=True

使用 ShellPolicy 限制指令

ShellPolicy 執行前套用正則表達式的允許與拒絕清單。 拒絕規則具有優先權。

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",
        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=(
            "You can run a narrow set of read-only shell commands (ls, pwd, cat, "
            "git status/log/diff, python --version). Anything else will be rejected."
        ),
        tools=[client.get_shell_tool(func=shell.as_function())],
    )

    query = "Summarise the current directory and print the Python version."
    print(f"User: {query}")
    result = await agent.run(query)
    print(f"Agent: {result.text}")

Warning

指令政策是可用性的預過濾器,而非安全邊界。 殼層語法、別名、變數、直譯器及編碼酬載都可能繞過簡單的模式比對。

加上 ShellEnvironmentProvider

ShellEnvironmentProvider 會先偵測 Shell 類型、版本、作業系統、工作目錄及選定的 CLI 版本,然後在代理程式執行前將這些資訊注入。 預設的探測清單為 gitnodepythondocker和 。

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 範例。

選擇執行層級

情境 Tool 隔離界限
受信任的開發指令 LocalShellTool 主機處理程序中的核准
不可信任的 Shell 指令 DockerShellTool 帶有預設隔離標誌的 OCI 容器
不含 shell 的不受信任產生程式碼 超光代碼法案 Hyperlight microVM

Go 提供透過 tool/shelltool 進行本機 Shell 執行與環境偵測。 請參見「使用本機 shell 工具」

DockerShellTool 目前沒有針對 Go 的指引。

使用 Harness Agent 搭配 Shell 工具

純代理和 HarnessAgent 都使用相同的兩部分 shell 設定:將執行器的函式註冊為工具,並在模型應接收 shell、作業系統、工作目錄和 CLI 版本脈絡時加入 ShellEnvironmentProviderHarnessAgent 不會建立或擁有 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_shellrequireApproval: trueLocalShellExecutor 預設為持久模式,每條輸出串流限制為 64 KiB,且無逾時;範例明確使用建議的30秒 LocalShellExecutor.DefaultTimeoutShellEnvironmentProviderOptions預設會探測gitdotnetnodepythondocker,每次探測的逾時時間為五秒。

每個使用者會話建立一個持久執行器,並在會話結束時將其處置。 不要在使用者間或同時對話中分享,因為工作目錄、環境、shell 歷史、背景工作和指令佇列都是共享的。 ShellPolicy 僅為預過濾器;保持啟用批准,使用權限最低的憑證,且當指令需要更強的隔離邊界時,優先 DockerShellExecutor 使用。

Shell 工具可從預發布 Microsoft.Agents.AI.Tools.Shell 套件中取得。 HarnessAgent 可從 Microsoft.Agents.AI.Harness取得。

對於一般代理程式,請使用 client.get_shell_tool(func=shell.as_function()) 建立 shell 函式,並另外加入 ShellEnvironmentProvidercreate_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 時,才會加入 shell 工具和 ShellEnvironmentProvider;否則會記錄警告並略過兩者。 shell_environment_provider_options 是可選的,僅在 shell_executor時使用 。

LocalShellTool 預設為持久模式、30 秒逾時時間、64 KiB 的合併輸出、工作目錄重新定位,以及 approval_mode="always_require"。 由於預設已啟用 Harness 工具批准功能,請將 AgentSession 傳遞給 run。 呼叫者擁有執行者生命週期;使用 async with 或呼叫 close(),並在每個使用者會話中建立一個持久化工具。 不要在使用者間或同時對話中共享可變的 shell 狀態。

主機殼不是沙盒。 保持啟用核准、使用最低權限的憑證,並使用 DockerShellTool 進行容器隔離。 停用核准需要 approval_mode="never_require"acknowledge_unsafe=True;單獨使用 ShellPolicy 並不構成安全邊界。

create_harness_agentagent-framework-core 釋出。 Shell 整合功能由預先發行的 agent-framework-tools 套件提供,啟用時會產生一個 ExperimentalWarning

目前沒有可用的打包版 Go Harness。 直接在純 Go 代理程式上組合本機 Shell 工具與環境提供者。