db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
172 lines
6.2 KiB
Markdown
172 lines
6.2 KiB
Markdown
# 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.
|