Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions notebooks/sandbox_testing.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Sandbox Abstraction - Testing Notebook\n",
"\n",
"Interactive notebook for testing the Sandbox abstraction.\n",
"\n",
"## Setup\n",
"```bash\n",
"git clone https://github.com/mkmeral/sdk-python.git\n",
"cd sdk-python && git checkout feat/sandbox-abstraction\n",
"pip install -e '.[dev]'\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import asyncio\n",
"from strands.sandbox import LocalSandbox, DockerSandbox, ExecutionResult, Sandbox, ShellBasedSandbox\n",
"print('Imports OK')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. LocalSandbox Basics"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"async def test_local_basics():\n",
" sandbox = LocalSandbox(working_dir='/tmp/sandbox-test')\n",
" \n",
" # Execute a command\n",
" print('=== Execute ===')\n",
" async for chunk in sandbox.execute('echo Hello && ls -la /tmp/sandbox-test'):\n",
" if isinstance(chunk, str):\n",
" print(chunk, end='')\n",
" else:\n",
" print(f'exit_code={chunk.exit_code}')\n",
" \n",
" # File operations\n",
" print('\\n=== File Ops ===')\n",
" await sandbox.write_file('hello.py', 'print(42)')\n",
" content = await sandbox.read_file('hello.py')\n",
" print(f'Content: {content}')\n",
" print(f'Files: {await sandbox.list_files(\".\")}')\n",
" \n",
" # Execute code\n",
" print('\\n=== Execute Code ===')\n",
" async for chunk in sandbox.execute_code('import sys; print(sys.version)'):\n",
" if isinstance(chunk, str):\n",
" print(chunk, end='')\n",
" else:\n",
" print(f'exit_code={chunk.exit_code}')\n",
"\n",
"await test_local_basics()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Agent + Sandbox Integration"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from strands import Agent\n",
"\n",
"agent1 = Agent()\n",
"print(f'Default: {type(agent1.sandbox).__name__}, dir={agent1.sandbox.working_dir}')\n",
"\n",
"agent2 = Agent(sandbox=LocalSandbox(working_dir='/tmp/custom'))\n",
"print(f'Custom: {type(agent2.sandbox).__name__}, dir={agent2.sandbox.working_dir}')\n",
"\n",
"shared = LocalSandbox(working_dir='/tmp/shared')\n",
"a = Agent(sandbox=shared)\n",
"b = Agent(sandbox=shared)\n",
"print(f'Shared same instance: {a.sandbox is b.sandbox}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Concurrent Execution"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"async def test_concurrent():\n",
" sandbox = LocalSandbox(working_dir='/tmp/concurrent-test')\n",
" \n",
" async def run(name, delay):\n",
" r = await sandbox._execute_to_result(f'sleep {delay} && echo {name}')\n",
" return f'{name}: {r.stdout.strip()}'\n",
" \n",
" start = time.time()\n",
" results = await asyncio.gather(\n",
" run('task1', 0.5), run('task2', 0.3), run('task3', 0.1),\n",
" run('task4', 0.2), run('task5', 0.4),\n",
" )\n",
" elapsed = time.time() - start\n",
" \n",
" for r in results:\n",
" print(r)\n",
" print(f'\\nTotal: {elapsed:.2f}s (should be ~0.5s, not 1.5s)')\n",
"\n",
"await test_concurrent()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Custom Sandbox (ShellBasedSandbox)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class LoggingSandbox(ShellBasedSandbox):\n",
" def __init__(self, working_dir='/tmp/logging-sandbox'):\n",
" super().__init__()\n",
" self.working_dir = working_dir\n",
" self.log = []\n",
" self._local = LocalSandbox(working_dir=working_dir)\n",
" \n",
" async def start(self):\n",
" await self._local.start()\n",
" self._started = True\n",
" \n",
" async def stop(self):\n",
" await self._local.stop()\n",
" self._started = False\n",
" \n",
" async def execute(self, command, timeout=None):\n",
" self.log.append(command)\n",
" print(f'LOG: {command[:60]}')\n",
" async for chunk in self._local.execute(command, timeout=timeout):\n",
" yield chunk\n",
"\n",
"async def test_custom():\n",
" s = LoggingSandbox()\n",
" await s.write_file('test.txt', 'hello')\n",
" print(f'Content: {await s.read_file(\"test.txt\")}')\n",
" print(f'Files: {await s.list_files(\".\")}')\n",
" r = await s._execute_code_to_result('print(2+2)')\n",
" print(f'Code: {r.stdout.strip()}')\n",
" print(f'Commands logged: {len(s.log)}')\n",
"\n",
"await test_custom()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Context Manager and Lifecycle"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"async def test_lifecycle():\n",
" s = LocalSandbox(working_dir='/tmp/lifecycle-test')\n",
" print(f'Before: started={s._started}')\n",
" \n",
" r = await s._execute_to_result('echo auto')\n",
" print(f'After auto-start: started={s._started}, out={r.stdout.strip()}')\n",
" \n",
" await s.stop()\n",
" print(f'After stop: started={s._started}')\n",
" \n",
" async with LocalSandbox(working_dir='/tmp/ctx') as ctx:\n",
" print(f'In ctx: started={ctx._started}')\n",
" r = await ctx._execute_to_result('echo context')\n",
" print(f'Out: {r.stdout.strip()}')\n",
" print(f'After ctx: started={ctx._started}')\n",
"\n",
"await test_lifecycle()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. DockerSandbox (requires Docker)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import shutil\n",
"if shutil.which('docker'):\n",
" async def test_docker():\n",
" async with DockerSandbox(image='python:3.12-slim') as s:\n",
" print(f'Container: {s._container_id}')\n",
" r = await s._execute_to_result('python --version')\n",
" print(f'Python: {r.stdout.strip()}')\n",
" await s.write_file('test.py', 'print(42)')\n",
" print(f'File: {await s.read_file(\"test.py\")}')\n",
" async for c in s.execute_code('import os; print(os.getpid())'):\n",
" if isinstance(c, str): print(c, end='')\n",
" else: print(f'exit={c.exit_code}')\n",
" await test_docker()\n",
"else:\n",
" print('Docker not available')"
]
}
],
"metadata": {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.12.0"}
},
"nbformat": 4,
"nbformat_minor": 4
}
11 changes: 10 additions & 1 deletion src/strands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""A framework for building, deploying, and managing AI agents."""

from . import agent, models, telemetry, types
from . import agent, models, sandbox, telemetry, types
from .agent.agent import Agent
from .agent.base import AgentBase
from .event_loop._retry import ModelRetryStrategy
from .plugins import Plugin
from .sandbox.base import ExecutionResult, Sandbox, ShellBasedSandbox
from .sandbox.docker import DockerSandbox
from .sandbox.local import LocalSandbox
from .tools.decorator import tool
from .types.tools import ToolContext
from .vended_plugins.skills import AgentSkills, Skill
Expand All @@ -14,9 +17,15 @@
"AgentBase",
"AgentSkills",
"agent",
"DockerSandbox",
"ExecutionResult",
"LocalSandbox",
"models",
"ModelRetryStrategy",
"Plugin",
"sandbox",
"Sandbox",
"ShellBasedSandbox",
"Skill",
"tool",
"ToolContext",
Expand Down
9 changes: 9 additions & 0 deletions src/strands/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from .._async import run_async
from ..event_loop._retry import ModelRetryStrategy
from ..event_loop.event_loop import INITIAL_DELAY, MAX_ATTEMPTS, MAX_DELAY, event_loop_cycle
from ..sandbox.base import Sandbox
from ..sandbox.local import LocalSandbox
from ..tools._tool_helpers import generate_missing_tool_result_content

if TYPE_CHECKING:
Expand Down Expand Up @@ -135,6 +137,7 @@ def __init__(
tool_executor: ToolExecutor | None = None,
retry_strategy: ModelRetryStrategy | _DefaultRetryStrategySentinel | None = _DEFAULT_RETRY_STRATEGY,
concurrent_invocation_mode: ConcurrentInvocationMode = ConcurrentInvocationMode.THROW,
sandbox: Sandbox | None = None,
):
"""Initialize the Agent with the specified configuration.

Expand Down Expand Up @@ -201,6 +204,9 @@ def __init__(
Set to "unsafe_reentrant" to skip lock acquisition entirely, allowing concurrent invocations.
Warning: "unsafe_reentrant" makes no guarantees about resulting behavior and is provided
only for advanced use cases where the caller understands the risks.
sandbox: Execution environment for agent tools. Tools access the sandbox via
tool_context.agent.sandbox to execute commands, code, and filesystem operations.
Defaults to LocalSandbox (local host execution) when not specified.

Raises:
ValueError: If agent id contains path separators.
Expand Down Expand Up @@ -273,6 +279,9 @@ def __init__(

self.tool_caller = _ToolCaller(self)

# Initialize sandbox for tool execution environment
self.sandbox: Sandbox = sandbox if sandbox is not None else LocalSandbox()

self.hooks = HookRegistry()

self._plugin_registry = _PluginRegistry(self)
Expand Down
25 changes: 25 additions & 0 deletions src/strands/sandbox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Sandbox abstraction for agent code execution environments.

This module provides the Sandbox interface that decouples tool logic from where code runs.
Tools that need to execute code or access a filesystem receive a Sandbox instead of managing
their own execution, enabling portability across local, Docker, and cloud environments.

Class hierarchy::

Sandbox (ABC, all 5 abstract + lifecycle)
└── ShellBasedSandbox (ABC, only execute() abstract — shell-based file ops + execute_code)
├── LocalSandbox — runs on the host via asyncio subprocesses (default)
└── DockerSandbox — runs inside a Docker container
"""

from .base import ExecutionResult, Sandbox, ShellBasedSandbox
from .docker import DockerSandbox
from .local import LocalSandbox

__all__ = [
"DockerSandbox",
"ExecutionResult",
"LocalSandbox",
"Sandbox",
"ShellBasedSandbox",
]
Loading
Loading