Shelltools

Das Beta-Python-Paket agent-framework-tools bietet Shellausführungs- und Umgebungsbewusstseinstools über den agent_framework.tools Namespace.

Tool Verwenden Sie ihn, wenn
LocalShellTool Befehle gelten als vertrauenswürdig oder werden einzeln freigegeben und sollten in der Hostumgebung des Agentenprozesses ausgeführt werden.
DockerShellTool Modellgenerierte Shellbefehle benötigen OCI-Containerisolation.
ShellEnvironmentProvider Das Modell benötigt die aktive Shellfamilie, das Betriebssystem, das Arbeitsverzeichnis und die installierten CLI-Versionen.
ShellPolicy Sie benötigen vor der Genehmigung oder Ausführung einen Vorabfilter in Form einer Positivliste oder Negativliste.

Warnung

Die Shellausführung kann Dateien ändern, Prozesse starten, Anmeldeinformationen zugreifen und mit externen Systemen kommunizieren. Verwenden Sie die Ausführungsebene mit den geringsten Rechten, die die Aufgabe unterstützt.

Installiere das Paket

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

Lokale Shell und Umgebungserkennung verwenden

LocalShellExecutor unterstützt zustandslose und persistente Modi. ShellEnvironmentProvider untersucht die aktive Umgebung und fügt dem Agentkontext autoritative Shell-Anleitungen hinzu.

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 ist auch für die Vorabfilterung von Befehlen verfügbar. Ein eigenes ausführbares Beispiel DockerShellExecutor ist derzeit nicht veröffentlicht.

Installiere das Paket

pip install agent-framework-tools --pre

Das Paket installiert psutil, um Kindprozessbäume zu beenden, wenn eine Ausführung das Zeitlimit überschreitet.

LocalShellTool verwenden

LocalShellTool führt Befehle direkt auf dem Host aus. Standardmäßig verwendet es eine persistente Shell, ein 30-Sekunden-Timeout, eine Ausgabebegrenzung auf 64 KiB, eine Beschränkung auf das Arbeitsverzeichnis und eine Genehmigung für jeden Befehl.

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

Verwenden Sie mode="stateless", wenn jeder Aufruf in einem neuen Prozess ausgeführt werden soll. Verwenden Sie die Umgebungsvariable AGENT_FRAMEWORK_SHELL oder das shell Konstruktorargument, um die aufgelöste Shell außer Kraft zu setzen.

Important

LocalShellTool ist kein Sandkasten. Die Genehmigung ist die primäre Sicherheitsgrenze. Zum Deaktivieren der Freigabe ist acknowledge_unsafe=True erforderlich.

Einschränken von Befehlen mit ShellPolicy

ShellPolicy wendet vor der Ausführung Zulassungs- und Sperrlisten für reguläre Ausdrücke an. Verweigerungsregeln haben Vorrang.

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}")

Warnung

Eine Befehlsrichtlinie ist ein Usability-Vorfilter, keine Sicherheitsbarriere. Shellsyntax, Aliase, Variablen, Dolmetscher und codierte Nutzlasten können den einfachen Musterabgleich umgehen.

Fügen Sie ShellEnvironmentProvider hinzu.

ShellEnvironmentProvider prüft die Shellfamilie, Version, Betriebssystem, Arbeitsverzeichnis und ausgewählte CLI-Versionen und fügt diese Informationen ein, bevor der Agent ausgeführt wird. Die Standardsondenliste ist git, node, , pythonund docker.

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 verwenden

DockerShellTool erfordert Docker oder Podman auf PATH. Standardmäßig werden Netzwerke deaktiviert, als Nicht-Stammbenutzer ausgeführt, ein schreibgeschütztes Stammdateisystem, Drop-Funktionen, Speicher auf 512 MiB beschränkt und der Container auf 256 Prozesse begrenzt.

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)

Das Standardbild ist mcr.microsoft.com/azurelinux/base/core:3.0. Geben Sie docker_binary="podman" an, um Podman zu verwenden. Ein eigenständiges ausführbares Beispiel DockerShellTool ist derzeit nicht veröffentlicht.

Auswählen einer Ausführungsstufe

Scenario Tool Isolationsgrenze
Vertrauenswürdige Entwicklungsbefehle LocalShellTool Genehmigung im Hostprozess
Nicht vertrauenswürdige Shellbefehle DockerShellTool OCI-Container mit Standardisolationskennzeichnungen
Nicht vertrauenswürdiger generierter Code ohne Shell Hyperlight CodeAct Hyperlight microVM

Go bietet lokale Ausführung von Shell-Befehlen und die Untersuchung der Umgebung über tool/shelltool. Weitere Informationen finden Sie unter Verwenden des lokalen Shelltools.

DockerShellTool Anleitungen sind derzeit nicht für Go verfügbar.

Shell-Tools mit Harness Agent verwenden

Einfache Agents und HarnessAgent verwenden dieselbe zweiteilige Shell-Konfiguration: die Funktion des Executors als Tool registrieren und ShellEnvironmentProvider hinzufügen, wenn das Modell Kontext zur Shell, zum Betriebssystem, zum Arbeitsverzeichnis und zur CLI-Version erhalten soll. HarnessAgent erstellt keinen Shell-Executor und besitzt auch keinen:

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 wird standardmäßig auf den Namen run_shell und requireApproval: true festgelegt. LocalShellExecutor ist standardmäßig auf den persistenten Modus, eine Obergrenze von 64 KiB pro Ausgabestream und kein Timeout eingestellt; im Beispiel wird ausdrücklich das empfohlene LocalShellExecutor.DefaultTimeout mit 30 Sekunden verwendet. ShellEnvironmentProviderOptions prüft standardmäßig git, dotnet, node, python und docker, mit einem Timeout von fünf Sekunden pro Prüfvorgang.

Erstellen Sie einen dauerhaften Executor pro Benutzersitzung, und löschen Sie ihn, wenn die Sitzung endet. Geben Sie sie nicht für mehrere Benutzer oder gleichzeitige Konversationen frei, da sich das Arbeitsverzeichnis, die Umgebung, der Shell-Verlauf, Hintergrundaufträge und die Befehlswarteschlange gemeinsam genutzt werden. ShellPolicy ist nur ein Vorfilter; lassen Sie die Genehmigung aktiviert, verwenden Sie Anmeldeinformationen mit den geringstmöglichen Rechten und bevorzugen Sie DockerShellExecutor, wenn Befehle eine stärkere Isolationsgrenze erfordern.

Shelltools sind im Vorabversionspaket Microsoft.Agents.AI.Tools.Shell verfügbar. HarnessAgent ist verfügbar von Microsoft.Agents.AI.Harness.

Erstellen Sie für einen einfachen Agenten die Shell-Funktion mit client.get_shell_tool(func=shell.as_function()) und fügen Sie ShellEnvironmentProvider separat hinzu. create_harness_agent führt beide Schritte aus, wenn Sie shell_executor übergeben:

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 ist optional und muss as_function() verfügbar machen. Die Factory fügt das Shell-Tool und ShellEnvironmentProvider nur dann hinzu, wenn der Client SupportsShellTool implementiert; andernfalls protokolliert sie eine Warnung und überspringt beide. shell_environment_provider_options ist optional und wird nur mit shell_executor verwendet.

LocalShellTool verwendet standardmäßig den persistenten Modus, ein 30-Sekunden-Timeout, eine kombinierte 64-KiB-Ausgabe, die Neuverankerung des Arbeitsverzeichnisses und approval_mode="always_require". Da die Toolgenehmigung für Harness standardmäßig aktiviert ist, übergeben Sie AgentSession an run. Der Aufrufer ist für den Lebenszyklus des Executors verantwortlich; verwenden Sie async with oder rufen Sie close() auf, und erstellen Sie pro Benutzersitzung ein dauerhaftes Tool. Geben Sie keinen veränderlichen Shell-Zustand zwischen Benutzern oder gleichzeitigen Unterhaltungen frei.

Die Hostshell ist kein Sandkasten. Lassen Sie Genehmigungen aktiviert, verwenden Sie Anmeldeinformationen nach dem Prinzip der geringsten Rechte und verwenden Sie DockerShellTool zur Containerisolation. Das Deaktivieren der Genehmigung erfordert approval_mode="never_require" und acknowledge_unsafe=True; ShellPolicy allein ist keine Sicherheitsgrenze.

create_harness_agent wird in agent-framework-core veröffentlicht. Die Shell-Integration wird durch das Paket in der Vorabversion agent-framework-tools bereitgestellt und gibt beim Aktivieren ein ExperimentalWarning aus.

Ein verpacktes Go Harness ist derzeit nicht verfügbar. Verfassen Sie das lokale Shelltool und den Umgebungsanbieter direkt auf einem einfachen Go-Agent.