# agent-framework-tools Alpha built-in tools for the Microsoft Agent Framework. A home for first-party Python tools that plug into any chat client's shell / function surface. The first tool is `LocalShellTool`. ## Installation ```bash pip install agent-framework-tools --pre ``` ## `LocalShellTool` quick start ```python import asyncio from agent_framework import Agent from agent_framework.openai import OpenAIChatClient from agent_framework_tools.shell import LocalShellTool async def main() -> None: 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.", tools=[client.get_shell_tool(func=shell.as_function())], ) result = await agent.run("Print the current working directory.") print(result.text) asyncio.run(main()) ``` ### Modes - **Persistent** (default): a single long-lived shell session. `cd`, `export`, and shell functions persist across tool invocations. - **Stateless** (`mode="stateless"`): each command runs in a fresh subprocess. ### Safety > **`LocalShellTool` is not a sandbox.** It runs commands directly on the > host with the agent process's privileges. The actual security boundary > is **approval-in-the-loop**. For untrusted input use a sandboxed > executor — see [`agent-framework-hyperlight`](#relationship-to-agent-framework-hyperlight). Defenses (in priority order): - **Approval-in-the-loop** — every command surfaces as a `user_input_request`; nothing runs without consent. Disabling this requires `acknowledge_unsafe=True`. - **Process-tree termination on timeout** via `psutil`, so child processes (`make`, watchers, network tools) cannot survive the timeout. - **Output truncation** to 64 KiB (head + tail with marker). - **Audit hook** (`on_command=…`) for SIEM / append-only logs. - **Optional command-pattern filter** via `ShellPolicy(denylist=[...], allowlist=[...])`. **Empty by default.** This is a UX pre-filter, not a security boundary — operators are expected to supply patterns that match their workload (and they can be defeated by trivial obfuscation such as `\rm -rf /`, `${RM:=rm} -rf /`, `python -c "…"`, encoded payloads, or PowerShell-native equivalents). Real isolation comes from approval gating and the sandbox tier (`DockerShellTool`). See `tests/test_security.py` for the documented residual risk surface. Override with `ShellPolicy`: ```python from agent_framework_tools.shell import LocalShellTool, ShellPolicy shell = LocalShellTool( policy=ShellPolicy(allowlist=[r"^ls\b", r"^cat\b", r"^git status$"]), approval_mode="never_require", acknowledge_unsafe=True, # required to bypass approval ) ``` ### Cross-OS - **Windows**: `pwsh -NoProfile -Command -` (falls back to `powershell.exe`). - **Linux / macOS**: `/bin/bash --noprofile --norc` (falls back to `/bin/sh`). - Override via the `shell=` constructor argument or the `AGENT_FRAMEWORK_SHELL` environment variable. ## `ShellEnvironmentProvider` — context provider A model talking to a PowerShell session will sometimes default to bash syntax (`export FOO=bar`, `ls -la`, `> /dev/null`) and vice versa. `ShellEnvironmentProvider` is an `AIContextProvider` that probes the live shell once per session — family, version, OS, working directory, and a configurable list of CLI tools (`git`, `node`, `python`, `docker` by default) — and injects a system-prompt block describing the shell idiom to use and the available CLIs. ```python from agent_framework_tools.shell import ( LocalShellTool, ShellEnvironmentProvider, ShellEnvironmentProviderOptions, ) shell = LocalShellTool() provider = ShellEnvironmentProvider( shell, ShellEnvironmentProviderOptions(probe_tools=("git", "uv", "node")), ) agent = Agent( client=client, tools=[client.get_shell_tool(func=shell.as_function())], context_providers=[provider], ) ``` Probe failures from expected error types (timeouts, policy rejections, spawn failures) are recorded as `None` fields in the snapshot rather than raised; a missing CLI never fails the agent. A failed first probe does not poison the cache — the next call retries. ## `DockerShellTool` — sandboxed tier When commands originate from untrusted input (e.g. the model is acting on prompt-injected document content), prefer `DockerShellTool`. With the default isolation flags and a trusted container runtime, the container is the intended security boundary and approval gating becomes optional. ```python import asyncio from agent_framework_tools.shell import DockerShellTool async def main() -> None: async with DockerShellTool( image="mcr.microsoft.com/azurelinux/base/core:3.0", approval_mode="never_require", # container is the boundary ) as shell: result = await shell.run("uname -a && id") print(result.stdout) asyncio.run(main()) ``` Defaults applied to every container: - `--network none` — no host or external network. - `--user 65534:65534` — runs as `nobody:nogroup`. - `--read-only` root filesystem; only mounted host paths are writable. - `--cap-drop ALL` and `--security-opt no-new-privileges`. - `--memory 512m`, `--pids-limit 256`, ephemeral `tmpfs /tmp`. To expose a host directory, pass `host_workdir="/path"` (mounted read-only by default; `mount_readonly=False` to allow writes). Swap the container runtime with `docker_binary="podman"`. ## Sandbox tiers at a glance | Use case | Tool | Sandbox | |---|---|---| | Run *code* (untrusted) | `HyperlightCodeActProvider.execute_code` (`agent-framework-hyperlight`) | Hyperlight WASM microVM | | Run *shell* (untrusted) | `DockerShellTool` | OCI container (network-off, non-root, capabilities dropped) | | Run *shell* (trusted dev) | `LocalShellTool` | Approval-in-the-loop | ## Relationship to `agent-framework-hyperlight` `agent-framework-hyperlight` is a **code** sandbox (a single WASM guest loaded into a microVM, called via a hostcall ABI — there is no kernel, userland, or shell binary inside). It is the right tier for executing generated *code*. For sandboxing *shell* commands, the realistic tier is OCI, which `DockerShellTool` provides.