Ferramentas de concha

O pacote Python beta agent-framework-tools fornece ferramentas de execução em shell e consciência ambiental através do agent_framework.tools namespace.

Tool Use-o quando
LocalShellTool Os comandos são confiáveis ou aprovados individualmente e devem ser executados no ambiente anfitrião do processo agente.
DockerShellTool Os comandos de shell gerados por modelos requerem isolamento em contentores OCI.
ShellEnvironmentProvider O modelo necessita da família de shell ativa, do sistema operativo, do diretório de trabalho e das versões de CLI instaladas.
ShellPolicy Pretende um pré-filtro de lista de permissões ou de lista de bloqueio antes da aprovação ou da execução.

Warning

A execução shell pode modificar ficheiros, iniciar processos, aceder a credenciais e comunicar com sistemas externos. Use o nível de execução menos privilegiado que suporte a tarefa.

Instale o pacote

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

Use a concha local e a consciência ambiental

LocalShellExecutor suporta modos sem estado e persistentes. ShellEnvironmentProvider sonda o ambiente ativo e adiciona orientação autoritativa do shell ao contexto do agente.

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 também está disponível para pré-filtragem por comandos. De momento, não está publicado um exemplo executável DockerShellExecutor dedicado.

Instale o pacote

pip install agent-framework-tools --pre

O pacote instala psutil para terminar árvores de processos subordinados quando uma execução atinge o tempo limite.

Utilize LocalShellTool

LocalShellTool executa comandos diretamente no sistema anfitrião. A predefinição é um shell persistente, um tempo limite de 30 segundos, truncagem da saída em 64 KiB e reposicionamento do diretório de trabalho. As chamadas de agente através de as_function() requerem aprovação por defeito. As chamadas diretas para run() não pedem aprovação.

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

Chamadas diretas para run() devolvem a ShellResult com campos stderr, exit_code, duration_ms, truncated, timed_out e stdout separados. Quando o shell é executado na OpenAI Responses ou no alojamento Foundry, o Agent Framework preserva a saída padrão, o erro padrão, os resultados de saída do processo e os resultados por tempo limite ao longo da continuação pelo fornecedor. Códigos de saída diferentes de zero continuam a ser falhas, a saída de erro padrão mantém-se separada e um timeout não aparece como uma saída bem-sucedida.

Os itens de transcrição shell alojados pelo fornecedor OpenAI permanecem informativos, mesmo quando um executor shell local está configurado. Apenas uma chamada explícita local_shell_call, bem formada, ou uma chamada de shell marcada com environment.type="local", entra na função local e no fluxo de aprovação. Configurar LocalShellTool sozinho não faz com que chamadas shell alojadas pelo fornecedor sejam executadas no host.

Use mode="stateless" quando cada chamada deve ser executada num processo novo. Use a AGENT_FRAMEWORK_SHELL variável de ambiente ou o shell argumento construtor para sobrescrever o shell resolvido.

Importante

LocalShellTool não é um ambiente de teste. A aprovação humana acrescenta uma etapa de revisão, mas não isola a shell. Desativar a aprovação para chamadas de agente requer acknowledge_unsafe=True.

Restringir comandos com ShellPolicy

ShellPolicy aplica listas de permissões e bloqueios baseadas em expressões regulares ao texto do comando antes de ser executado. As regras de negação têm prioridade. Não inspeciona o que o shell executa em última instância nem restringe o acesso a ficheiros.

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

A amostra incapacita a aprovação humana e só é adequada para um ambiente isolado e descartável, sem segredos ou dados valiosos. Substituições de comandos, como $(...) e backticks, podem transmitir padrões simples de lista de permissões. Padrões que apenas correspondem ao início de um comando também podem permitir operações extra. Use isolamento aplicado separadamente e permissões restritas para produção. A revisão humana pode acrescentar uma verificação, mas não isola a shell.

Prefira padrões de cadeias de caracteres. O Python compila strings com o regex motor e aplica um orçamento de um segundo a cada partida. Um timeout da lista de bloqueio nega o comando, e um timeout da lista de permissões não concede permissão. Um pré-compilado regex.Pattern usa o mesmo limite. Uma biblioteca re.Pattern padrão pré-compilada preserva os seus flags, mas não pode ser interrompida, por isso evite expressões dispendiosas ou ambíguas nessa forma.

Adicionar ShellEnvironmentProvider

ShellEnvironmentProvider sonda a família de shell, versão, sistema operativo, diretório de trabalho e versões selecionadas da CLI, depois injeta essa informação antes da execução do agente. A lista padrão de sondas é git, node, python, e 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)

Utilize DockerShellTool

DockerShellTool requer Docker ou Podman em PATH. Os valores definidos desativam a rede, executam como utilizador não-root, usam um sistema de ficheiros raiz apenas de leitura, reduzem capacidades, limitam a memória a 512 MiB e limitam o contentor a 256 processos.

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)

A imagem padrão é mcr.microsoft.com/azurelinux/base/core:3.0. Passe docker_binary="podman" para usar o Podman. De momento, não está publicado um exemplo executável DockerShellTool dedicado.

extra_run_args Use apenas para opções Docker que não enfraqueçam o isolamento configurado ou os limites de recursos. A validação reconhece opções longas, opções curtas, valores associados e opções curtas agrupadas. Rejeita sobreposições como -u--user / ,--memory-m / , / --volume-v , --network, e .--pids-limit Utilize, em vez dessa, a opção DockerShellTool do construtor correspondente.

Escolha um nível de execução

Scenario Tool Limite de isolamento
Comandos de desenvolvimento confiáveis LocalShellTool Nenhum; a aprovação humana está ativada por defeito
Comandos de linha de comandos não fiáveis DockerShellTool Container OCI com opções de isolamento predefinidas
Código gerado não confiável sem shell Hyperlight CodeAct MicroVM hiperleve

O Go fornece execução local da shell e sondagem do ambiente através de tool/shelltool. Consulte Utilizar a ferramenta de shell local.

DockerShellTool As orientações não estão atualmente disponíveis para Go.

Utilize ferramentas de shell com o Harness Agent

Agentes simples e HarnessAgent usam a mesma configuração de shell em duas partes: registar a função do executor como uma ferramenta e adicionar ShellEnvironmentProvider quando o modelo deve receber shell, sistema operativo, diretório de trabalho e contexto da versão CLI. HarnessAgent não cria nem possui um executor de 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 tem por predefinição o nome run_shell e requireApproval: true. LocalShellExecutor define por defeito o modo persistente, um limite de 64 KiB por fluxo de saída e sem limite de tempo; o exemplo utiliza explicitamente o LocalShellExecutor.DefaultTimeout recomendado de 30 segundos. ShellEnvironmentProviderOptionspor defeito, sonda git, dotnet, nodepython, , e docker, com um tempo de espera de cinco segundos por sonda.

Crie um executor persistente por sessão de utilizador e elimine-o quando a sessão terminar. Não o partilhes entre utilizadores nem por conversas em simultâneo, porque o diretório de trabalho, o ambiente, o histórico da shell, os processos em segundo plano e a fila de comandos são partilhados. ShellPolicy é apenas um pré-filtro; Mantenha a aprovação ativada, use credenciais com privilégios mínimos e prefira DockerShellExecutor quando os comandos exigem uma fronteira de isolamento mais forte.

As ferramentas shell estão disponíveis no pacote de pré-lançamento Microsoft.Agents.AI.Tools.Shell . HarnessAgent está disponível em Microsoft.Agents.AI.Harness.

Para um agente simples, crie a função shell com client.get_shell_tool(func=shell.as_function()) e adicione ShellEnvironmentProvider separadamente. create_harness_agent executa ambos os passos ao passar 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 é opcional e tem de expor as_function(). A fábrica adiciona a ferramenta shell e ShellEnvironmentProvider só quando o cliente implementa SupportsShellTool; caso contrário, regista um aviso e salta ambos. shell_environment_provider_options é opcional e é usado apenas com shell_executor.

LocalShellTool tem, por predefinição, modo persistente, um tempo limite de 30 segundos, 64 KiB de saída combinada, reancoragem do diretório de trabalho e approval_mode="always_require". Como a aprovação da ferramenta harness está ativada por defeito, passe um AgentSession para run. O chamador é responsável pelo ciclo de vida do executor; use async with ou chame close() e crie uma ferramenta persistente para cada sessão de utilizador. Não partilhe estados mutáveis do shell entre utilizadores ou conversas simultâneas.

A concha do host não é um sandbox. Mantenha a aprovação ativada, use credenciais com privilégios mínimos e use DockerShellTool para isolamento de contentores. Desativar a aprovação requer approval_mode="never_require" e acknowledge_unsafe=True; ShellPolicy sozinho não é um limite de segurança.

create_harness_agent é lançado em agent-framework-core. A integração com a linha de comandos é fornecida pelo pacote de pré-lançamento agent-framework-tools e gera um ExperimentalWarning quando esta funcionalidade é ativada.

Uma versão empacotada do Go Harness não se encontra atualmente disponível. Componha a ferramenta de shell local e o fornecedor de ambiente diretamente num agente Go simples.