Isolated execution environments for autonomous agents
Sandbox tools give agents the ability to write code, run shell commands, and explore a codebase - all inside a controlled environment.
A single call to sandbox_tools() creates six tools (exec, read, write, edit, glob, grep) that share an execution environment.
from polos import Agent, sandbox_tools, SandboxToolsConfig, DockerEnvironmentConfigtools = sandbox_tools(SandboxToolsConfig( env="docker", docker=DockerEnvironmentConfig( image="node:20-slim", workspace_dir="./workspace", ),))coding_agent = Agent( id="coding_agent", provider="anthropic", model="claude-sonnet-4-5", system_prompt="You are a coding assistant. The repo is at /workspace.", tools=tools,)
Commands run directly on your host machine. Since there’s no container isolation, exec security defaults to approval-always and file operations default to requiring approval.
from polos import sandbox_tools, SandboxToolsConfig, LocalEnvironmentConfigtools = sandbox_tools(SandboxToolsConfig( env="local", local=LocalEnvironmentConfig( cwd="./workspace", path_restriction="./workspace", # confine file access ),))
All commands run without approval. Default for Docker and E2B.
allowlist
Commands matching patterns run automatically. Non-matching commands suspend for approval.
approval-always
Every command suspends for user approval. Default for local mode.
Allowlist patterns use glob-style matching. "node *" matches node hello.js but not npm install.When a command is rejected, the agent receives the rejection along with any feedback the user provided, and can adjust its approach.
When path_restriction / pathRestriction is set (local mode), read-only tools (read, glob, grep) run freely within the restricted directory but suspend for approval when accessing paths outside it. Symlink traversal outside the restriction is blocked.
The execution environment (Docker container, E2B sandbox) is created lazily on the first tool call. Call cleanup() to destroy it when you’re done.
import signal, asyncioloop = asyncio.get_event_loop()loop.add_signal_handler(signal.SIGINT, lambda: asyncio.ensure_future(tools.cleanup()))# Or in a try/finallytry: await worker.run()finally: await tools.cleanup()