agno-opensandbox-toolkit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.3
2
+ Name: agno-opensandbox-toolkit
3
+ Version: 0.1.0
4
+ Summary: OpenSandbox support for the Agno agent framework
5
+ Keywords: agno,opensandbox,sandbox,ai-agent,toolkit
6
+ Author: Valeryi Savich
7
+ Author-email: Valeryi Savich <relrin78@gmail.com>
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Dist: agno==2.5.16
19
+ Requires-Dist: opensandbox==0.1.7
20
+ Requires-Dist: opensandbox-code-interpreter==0.1.2 ; extra == 'code-interpreter'
21
+ Requires-Python: >=3.10, <4
22
+ Provides-Extra: code-interpreter
23
+ Description-Content-Type: text/markdown
24
+
25
+ # agno-opensandbox-toolkit
26
+
27
+ [![CI](https://github.com/Relrin/agno-opensandbox-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/Relrin/agno-opensandbox-toolkit/actions/workflows/ci.yml)
28
+ [![PyPI](https://img.shields.io/pypi/v/agno-opensandbox-toolkit)](https://pypi.org/project/agno-opensandbox-toolkit/)
29
+ [![Python](https://img.shields.io/pypi/pyversions/agno-opensandbox-toolkit)](https://pypi.org/project/agno-opensandbox-toolkit/)
30
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
31
+
32
+ [OpenSandbox](https://github.com/alibaba/OpenSandbox) support for the
33
+ [Agno](https://github.com/agno-agi/agno) agent framework.
34
+
35
+ Give your Agno agents the ability to execute shell commands, manage files, and run code
36
+ in secure, isolated OpenSandbox containers.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ # Base: shell commands + file operations
42
+ uv add agno-opensandbox-toolkit
43
+
44
+ # With code interpreter support (Python, JS, Java, Go, Bash)
45
+ uv add "agno-opensandbox-toolkit[code-interpreter]"
46
+ ```
47
+
48
+ ## Prerequisites
49
+
50
+ You need a running OpenSandbox server. The quickest way:
51
+
52
+ ```bash
53
+ uv pip install opensandbox-server
54
+ opensandbox-server init-config ~/.sandbox.toml --example docker
55
+ opensandbox-server
56
+ ```
57
+
58
+ See the [OpenSandbox docs](https://github.com/alibaba/OpenSandbox/blob/main/server/README.md)
59
+ for full setup instructions.
60
+
61
+ ## Quick Start
62
+
63
+ ```python
64
+ from agno.agent import Agent
65
+ from agno.models.openai import OpenAIChat
66
+ from agno_opensandbox_toolkit import OpenSandboxTools
67
+
68
+ agent = Agent(
69
+ name="Sandbox Agent",
70
+ model=OpenAIChat(id="gpt-4o-mini"),
71
+ tools=[OpenSandboxTools()],
72
+ markdown=True,
73
+ )
74
+
75
+ agent.print_response("Create a file /tmp/hello.py that prints 'Hello from OpenSandbox!', then run it")
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ | Parameter | Env Variable | Default | Description |
81
+ |---|---|---|---|
82
+ | `domain` | `OPENSANDBOX_DOMAIN` | `localhost:8080` | OpenSandbox server address |
83
+ | `api_key` | `OPENSANDBOX_API_KEY` | `""` | API key for authentication |
84
+ | `protocol` | — | `http` | Connection protocol (`http` or `https`) |
85
+ | `image` | — | `ubuntu` | Container image |
86
+ | `timeout_minutes` | — | `10` | Sandbox TTL |
87
+ | `sandbox_env` | — | `{}` | Env vars inside sandbox |
88
+ | `sandbox_resources` | — | `None` | e.g. `{"cpu": "2", "memory": "4Gi"}` |
89
+ | `sandbox_entrypoint` | — | `None` | Custom entrypoint command list |
90
+ | `sandbox_metadata` | — | `None` | Metadata labels dict |
91
+ | `enable_code_interpreter` | — | `False` | Enable `run_code` tool |
92
+ | `persistent` | — | `True` | Reuse sandbox across calls |
93
+ | `sandbox_id` | — | `None` | Reconnect to existing sandbox |
94
+
95
+ ## Available Tools
96
+
97
+ | Tool | Description |
98
+ |---|---|
99
+ | `run_shell_command` | Execute bash commands |
100
+ | `create_file` | Create/update files |
101
+ | `read_file` | Read file content |
102
+ | `list_files` | List directory contents |
103
+ | `delete_file` | Delete files/directories |
104
+ | `change_directory` | Change working directory |
105
+ | `get_sandbox_status` | Get sandbox info |
106
+ | `shutdown_sandbox` | Kill the sandbox |
107
+ | `run_code` | Execute code via interpreter (optional) |
108
+
109
+ ## License
110
+
111
+ The project is published under the BSD 3-Clause license. For details see the [LICENSE](https://github.com/Relrin/agno-opensandbox-toolkit/blob/master/LICENSE) file.
@@ -0,0 +1,87 @@
1
+ # agno-opensandbox-toolkit
2
+
3
+ [![CI](https://github.com/Relrin/agno-opensandbox-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/Relrin/agno-opensandbox-toolkit/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/agno-opensandbox-toolkit)](https://pypi.org/project/agno-opensandbox-toolkit/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/agno-opensandbox-toolkit)](https://pypi.org/project/agno-opensandbox-toolkit/)
6
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
7
+
8
+ [OpenSandbox](https://github.com/alibaba/OpenSandbox) support for the
9
+ [Agno](https://github.com/agno-agi/agno) agent framework.
10
+
11
+ Give your Agno agents the ability to execute shell commands, manage files, and run code
12
+ in secure, isolated OpenSandbox containers.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # Base: shell commands + file operations
18
+ uv add agno-opensandbox-toolkit
19
+
20
+ # With code interpreter support (Python, JS, Java, Go, Bash)
21
+ uv add "agno-opensandbox-toolkit[code-interpreter]"
22
+ ```
23
+
24
+ ## Prerequisites
25
+
26
+ You need a running OpenSandbox server. The quickest way:
27
+
28
+ ```bash
29
+ uv pip install opensandbox-server
30
+ opensandbox-server init-config ~/.sandbox.toml --example docker
31
+ opensandbox-server
32
+ ```
33
+
34
+ See the [OpenSandbox docs](https://github.com/alibaba/OpenSandbox/blob/main/server/README.md)
35
+ for full setup instructions.
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from agno.agent import Agent
41
+ from agno.models.openai import OpenAIChat
42
+ from agno_opensandbox_toolkit import OpenSandboxTools
43
+
44
+ agent = Agent(
45
+ name="Sandbox Agent",
46
+ model=OpenAIChat(id="gpt-4o-mini"),
47
+ tools=[OpenSandboxTools()],
48
+ markdown=True,
49
+ )
50
+
51
+ agent.print_response("Create a file /tmp/hello.py that prints 'Hello from OpenSandbox!', then run it")
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ | Parameter | Env Variable | Default | Description |
57
+ |---|---|---|---|
58
+ | `domain` | `OPENSANDBOX_DOMAIN` | `localhost:8080` | OpenSandbox server address |
59
+ | `api_key` | `OPENSANDBOX_API_KEY` | `""` | API key for authentication |
60
+ | `protocol` | — | `http` | Connection protocol (`http` or `https`) |
61
+ | `image` | — | `ubuntu` | Container image |
62
+ | `timeout_minutes` | — | `10` | Sandbox TTL |
63
+ | `sandbox_env` | — | `{}` | Env vars inside sandbox |
64
+ | `sandbox_resources` | — | `None` | e.g. `{"cpu": "2", "memory": "4Gi"}` |
65
+ | `sandbox_entrypoint` | — | `None` | Custom entrypoint command list |
66
+ | `sandbox_metadata` | — | `None` | Metadata labels dict |
67
+ | `enable_code_interpreter` | — | `False` | Enable `run_code` tool |
68
+ | `persistent` | — | `True` | Reuse sandbox across calls |
69
+ | `sandbox_id` | — | `None` | Reconnect to existing sandbox |
70
+
71
+ ## Available Tools
72
+
73
+ | Tool | Description |
74
+ |---|---|
75
+ | `run_shell_command` | Execute bash commands |
76
+ | `create_file` | Create/update files |
77
+ | `read_file` | Read file content |
78
+ | `list_files` | List directory contents |
79
+ | `delete_file` | Delete files/directories |
80
+ | `change_directory` | Change working directory |
81
+ | `get_sandbox_status` | Get sandbox info |
82
+ | `shutdown_sandbox` | Kill the sandbox |
83
+ | `run_code` | Execute code via interpreter (optional) |
84
+
85
+ ## License
86
+
87
+ The project is published under the BSD 3-Clause license. For details see the [LICENSE](https://github.com/Relrin/agno-opensandbox-toolkit/blob/master/LICENSE) file.
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "agno-opensandbox-toolkit"
3
+ version = "0.1.0"
4
+ description = "OpenSandbox support for the Agno agent framework"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Valeryi Savich", email = "relrin78@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10,<4"
10
+ keywords = ["agno", "opensandbox", "sandbox", "ai-agent", "toolkit"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: BSD License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+ dependencies = [
24
+ "agno==2.5.16",
25
+ "opensandbox==0.1.7",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ code-interpreter = [
30
+ "opensandbox-code-interpreter==0.1.2",
31
+ ]
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.10.12,<0.11.0"]
35
+ build-backend = "uv_build"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "mypy==1.20.0",
40
+ "pytest===9.0.3",
41
+ "pytest-asyncio==1.3.0",
42
+ "pytest-cov==7.1.0",
43
+ "respx==0.23.1",
44
+ "ruff==0.15.10",
45
+ ]
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "auto"
49
+ testpaths = ["tests"]
50
+ markers = [
51
+ "integration: requires a running OpenSandbox server (deselect with '-m not integration')",
52
+ "e2e: end-to-end tests requiring LLM API key + OpenSandbox server",
53
+ ]
54
+
55
+ [tool.ruff]
56
+ target-version = "py310"
57
+ line-length = 120
58
+ src = ["src", "tests"]
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
62
+
63
+ [tool.mypy]
64
+ python_version = "3.10"
65
+ strict = true
66
+ warn_return_any = true
67
+ warn_unused_configs = true
68
+ packages = ["agno_opensandbox_toolkit"]
69
+ mypy_path = "src"
@@ -0,0 +1,3 @@
1
+ from agno_opensandbox_toolkit.tools import OpenSandboxTools
2
+
3
+ __all__ = ["OpenSandboxTools"]
@@ -0,0 +1,38 @@
1
+ from textwrap import dedent
2
+
3
+ DEFAULT_IMAGE = "ubuntu"
4
+ CODE_INTERPRETER_IMAGE = "opensandbox/code-interpreter:v1.0.2"
5
+ CODE_INTERPRETER_ENTRYPOINT = ["/opt/opensandbox/code-interpreter.sh"]
6
+ DEFAULT_WORKING_DIRECTORY = "/root"
7
+
8
+ DEFAULT_INSTRUCTIONS = dedent(
9
+ """\
10
+ You have access to a persistent OpenSandbox sandbox for code execution.
11
+ The sandbox maintains state across interactions. And our goal is to provide working, executed code examples.
12
+
13
+ Available tools:
14
+ - `run_code`: Execute code in the sandbox (requires code-interpreter image)
15
+ - `run_shell_command`: Execute shell commands (bash)
16
+ - `create_file`: Create or update files
17
+ - `read_file`: Read file contents
18
+ - `list_files`: List directory contents
19
+ - `delete_file`: Delete files or directories
20
+ - `change_directory`: Change the working directory
21
+ - `get_sandbox_status`: Get the sandbox status
22
+ - `shutdown_sandbox`: Shut down the sandbox
23
+
24
+ When users ask for code (Python, JavaScript, TypeScript, etc.), you should:
25
+ 1. Write the code
26
+ 2. Execute it using run_code tool
27
+ 3. Show the actual output/results
28
+ 4. Never just provide code without executing it
29
+
30
+ Recommended workflow:
31
+ 1. Before running any code, check if required packages for the target languages are installed
32
+ 2. Install missing packages, e.g. run_shell_command("pip install package1 package2")
33
+ 3. When running scripts, capture both output AND errors
34
+ 4. If a script produces no output, check for errors or add print statements
35
+
36
+ IMPORTANT: Always use single quotes for the content parameter when creating files
37
+ """
38
+ )
@@ -0,0 +1,556 @@
1
+ import json
2
+ import stat
3
+ from datetime import timedelta
4
+ from os import getenv
5
+ from pathlib import PurePosixPath
6
+ from typing import Any
7
+
8
+ from agno.agent import Agent
9
+ from agno.team import Team
10
+ from agno.tools import Toolkit
11
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
12
+
13
+ from agno_opensandbox_toolkit._constants import (
14
+ CODE_INTERPRETER_ENTRYPOINT,
15
+ CODE_INTERPRETER_IMAGE,
16
+ DEFAULT_IMAGE,
17
+ DEFAULT_INSTRUCTIONS,
18
+ DEFAULT_WORKING_DIRECTORY,
19
+ )
20
+
21
+ try:
22
+ from opensandbox import SandboxSync
23
+ from opensandbox.config import ConnectionConfigSync
24
+ from opensandbox.models.execd import Execution, RunCommandOpts
25
+ except ImportError as _err:
26
+ raise ImportError("`opensandbox` not installed. Please install using `pip install opensandbox`") from _err
27
+
28
+ _CODE_INTERPRETER_AVAILABLE = False
29
+ try:
30
+ from code_interpreter import CodeInterpreterSync
31
+ from code_interpreter.models.code import SupportedLanguage
32
+
33
+ _CODE_INTERPRETER_AVAILABLE = True
34
+ except ImportError:
35
+ pass
36
+
37
+
38
+ class OpenSandboxTools(Toolkit):
39
+ """
40
+ OpenSandbox integration toolkit for Agno agents.
41
+
42
+ Provides sandboxed code execution, shell commands, and file operations
43
+ via the OpenSandbox platform. Follows the same patterns as the built-in
44
+ DaytonaTools and E2BTools integrations.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ domain: str | None = None,
50
+ api_key: str | None = None,
51
+ protocol: str = "http",
52
+ image: str | None = None,
53
+ timeout_minutes: int = 10,
54
+ sandbox_env: dict[str, str] | None = None,
55
+ sandbox_resources: dict[str, str] | None = None,
56
+ sandbox_metadata: dict[str, str] | None = None,
57
+ sandbox_entrypoint: list[str] | None = None,
58
+ sandbox_id: str | None = None,
59
+ persistent: bool = True,
60
+ enable_code_interpreter: bool = False,
61
+ instructions: str | None = None,
62
+ add_instructions: bool = False,
63
+ **kwargs: Any,
64
+ ):
65
+ """
66
+ Initialize OpenSandbox toolkit for Agno agents.
67
+
68
+ Args:
69
+ domain: OpenSandbox server domain (default: env OPENSANDBOX_DOMAIN or "localhost:8080").
70
+ api_key: API key (default: env OPENSANDBOX_API_KEY or "").
71
+ protocol: Connection protocol, "http" or "https" (default: "http").
72
+ image: Container image for the sandbox (default: "ubuntu",
73
+ auto-set to code-interpreter image when enable_code_interpreter=True).
74
+ timeout_minutes: Sandbox TTL in minutes.
75
+ sandbox_env: Environment variables to set inside the sandbox.
76
+ sandbox_resources: Resource limits e.g. {"cpu": "2", "memory": "4Gi"}.
77
+ sandbox_metadata: User metadata labels for the sandbox.
78
+ sandbox_entrypoint: Custom entrypoint command list.
79
+ sandbox_id: Existing sandbox ID to reconnect to (skips creation).
80
+ persistent: If True, store sandbox ID in agent.session_state for reuse.
81
+ enable_code_interpreter: If True, adds `run_code` tool (requires
82
+ `opensandbox-code-interpreter` package).
83
+ instructions: Custom instructions string (replaces default).
84
+ add_instructions: If True, prepend instructions to agent system prompt.
85
+ """
86
+ self.domain = domain or getenv("OPENSANDBOX_DOMAIN", "localhost:8080")
87
+ self.api_key = api_key or getenv("OPENSANDBOX_API_KEY", "")
88
+
89
+ self.sandbox_id = sandbox_id
90
+ self.persistent = persistent
91
+ self.timeout_minutes = timeout_minutes
92
+ self.sandbox_env = sandbox_env or {}
93
+ self.sandbox_resources = sandbox_resources
94
+ self.sandbox_metadata = sandbox_metadata or {}
95
+ self.sandbox_entrypoint = sandbox_entrypoint
96
+ self.enable_code_interpreter = enable_code_interpreter
97
+
98
+ if enable_code_interpreter:
99
+ self.image = image or CODE_INTERPRETER_IMAGE
100
+ if self.sandbox_entrypoint is None:
101
+ self.sandbox_entrypoint = CODE_INTERPRETER_ENTRYPOINT
102
+ else:
103
+ self.image = image or DEFAULT_IMAGE
104
+
105
+ if enable_code_interpreter and not _CODE_INTERPRETER_AVAILABLE:
106
+ raise ImportError(
107
+ "`opensandbox-code-interpreter` not installed. "
108
+ "Please install using `pip install opensandbox-code-interpreter`"
109
+ )
110
+
111
+ self._sandbox: SandboxSync | None = None
112
+ self._interpreter: Any | None = None
113
+
114
+ self._connection_config = ConnectionConfigSync(
115
+ domain=self.domain,
116
+ api_key=self.api_key,
117
+ protocol=protocol,
118
+ )
119
+
120
+ _instructions = instructions or DEFAULT_INSTRUCTIONS
121
+
122
+ tools: list[Any] = [
123
+ self.run_shell_command,
124
+ self.create_file,
125
+ self.read_file,
126
+ self.list_files,
127
+ self.delete_file,
128
+ self.change_directory,
129
+ self.get_sandbox_status,
130
+ self.shutdown_sandbox,
131
+ ]
132
+
133
+ if enable_code_interpreter:
134
+ tools.append(self.run_code)
135
+
136
+ super().__init__(
137
+ name="opensandbox_tools",
138
+ tools=tools,
139
+ instructions=_instructions,
140
+ add_instructions=add_instructions,
141
+ **kwargs,
142
+ )
143
+
144
+ def _get_working_directory(self, agent: Agent | Team) -> str:
145
+ """Get the current working directory from agent session state."""
146
+ if agent and hasattr(agent, "session_state"):
147
+ if agent.session_state is None:
148
+ agent.session_state = {}
149
+ return str(agent.session_state.get("working_directory", DEFAULT_WORKING_DIRECTORY))
150
+ return DEFAULT_WORKING_DIRECTORY
151
+
152
+ def _set_working_directory(self, agent: Agent | Team, directory: str) -> None:
153
+ """Set the working directory in agent session state."""
154
+ if agent and hasattr(agent, "session_state"):
155
+ if agent.session_state is None:
156
+ agent.session_state = {}
157
+ agent.session_state["working_directory"] = directory
158
+ log_info(f"Updated working directory to: {directory}")
159
+
160
+ def _get_or_create_sandbox(self, agent: Agent | Team) -> SandboxSync:
161
+ """Get existing sandbox or create new one. Stores ID in agent session_state."""
162
+ try:
163
+ sandbox = None
164
+
165
+ # Reuse cached sandbox instance
166
+ if self._sandbox is not None:
167
+ return self._sandbox
168
+
169
+ # Use explicit sandbox_id from constructor
170
+ if self.sandbox_id:
171
+ try:
172
+ sandbox = SandboxSync.connect(
173
+ self.sandbox_id,
174
+ connection_config=self._connection_config,
175
+ )
176
+ log_debug(f"Connected to explicit sandbox: {self.sandbox_id}")
177
+ except Exception as e:
178
+ log_debug(f"Failed to connect to sandbox {self.sandbox_id}: {e}")
179
+ sandbox = None
180
+
181
+ # Use persistent sandbox from session state
182
+ elif self.persistent and hasattr(agent, "session_state"):
183
+ if agent.session_state is None:
184
+ agent.session_state = {}
185
+
186
+ saved_id = agent.session_state.get("opensandbox_id")
187
+ if saved_id:
188
+ try:
189
+ sandbox = SandboxSync.connect(
190
+ saved_id,
191
+ connection_config=self._connection_config,
192
+ )
193
+ log_debug(f"Reconnected to persistent sandbox: {saved_id}")
194
+ except Exception as e:
195
+ log_debug(f"Failed to reconnect to sandbox {saved_id}: {e}")
196
+ sandbox = None
197
+
198
+ # Create new sandbox
199
+ if sandbox is None:
200
+ sandbox = self._create_new_sandbox(agent)
201
+
202
+ self._sandbox = sandbox
203
+ return sandbox
204
+
205
+ except Exception as e:
206
+ log_warning(f"Error in sandbox management, creating new sandbox: {e}") # type: ignore[no-untyped-call]
207
+ sandbox = self._create_new_sandbox(agent)
208
+ self._sandbox = sandbox
209
+ return sandbox
210
+
211
+ def _create_new_sandbox(self, agent: Agent | Team | None = None) -> SandboxSync:
212
+ """Create a new OpenSandbox instance."""
213
+ try:
214
+ metadata = self.sandbox_metadata.copy()
215
+ metadata.setdefault("created_by", "agno_opensandbox_toolkit")
216
+
217
+ create_kwargs: dict[str, Any] = {
218
+ "connection_config": self._connection_config,
219
+ "timeout": timedelta(minutes=self.timeout_minutes),
220
+ "env": self.sandbox_env,
221
+ "metadata": metadata,
222
+ }
223
+
224
+ if self.sandbox_resources:
225
+ create_kwargs["resource"] = self.sandbox_resources
226
+
227
+ if self.sandbox_entrypoint:
228
+ create_kwargs["entrypoint"] = self.sandbox_entrypoint
229
+
230
+ sandbox = SandboxSync.create(self.image, **create_kwargs)
231
+
232
+ # Store sandbox ID for persistence
233
+ if self.persistent and agent and hasattr(agent, "session_state"):
234
+ if agent.session_state is None:
235
+ agent.session_state = {}
236
+ agent.session_state["opensandbox_id"] = sandbox.id
237
+
238
+ log_info(f"Created new OpenSandbox: {sandbox.id}")
239
+ return sandbox
240
+
241
+ except Exception as e:
242
+ log_error(f"Error creating OpenSandbox: {e}") # type: ignore[no-untyped-call]
243
+ raise
244
+
245
+ def _get_or_create_interpreter(self, sandbox: SandboxSync) -> Any:
246
+ """Lazily create the code interpreter."""
247
+ if self._interpreter is not None:
248
+ return self._interpreter
249
+ self._interpreter = CodeInterpreterSync.create(sandbox=sandbox)
250
+ log_info("Created CodeInterpreter for sandbox")
251
+ return self._interpreter
252
+
253
+ def _format_execution(self, execution: Execution) -> str:
254
+ """Format an Execution result into a readable string."""
255
+ parts: list[str] = []
256
+
257
+ # Combined stdout + result text via SDK property
258
+ text = execution.text
259
+ if text:
260
+ parts.append(text)
261
+
262
+ # Stderr (not included in .text)
263
+ if execution.logs and execution.logs.stderr:
264
+ stderr_lines = [msg.text for msg in execution.logs.stderr]
265
+ stderr_text = "\n".join(stderr_lines)
266
+ if stderr_text.strip():
267
+ parts.append(f"STDERR:\n{stderr_text}")
268
+
269
+ # Error info
270
+ if execution.error:
271
+ parts.append(f"ERROR: {execution.error}")
272
+
273
+ return "\n".join(parts) if parts else "Command executed successfully with no output."
274
+
275
+ def _resolve_path(self, file_path: str, cwd: str) -> str:
276
+ """
277
+ Resolve a file path relative to the current working directory.
278
+
279
+ Uses PurePosixPath since the sandbox is always Linux.
280
+ """
281
+ path = PurePosixPath(file_path)
282
+ if path.is_absolute():
283
+ return str(path)
284
+ return str(PurePosixPath(cwd) / path)
285
+
286
+ def run_code(self, agent: Agent | Team, code: str, language: str = "python") -> str:
287
+ """
288
+ Execute code in the OpenSandbox code interpreter.
289
+
290
+ Args:
291
+ agent: An Agno agent instance or a group of agents
292
+ code: The source code to execute.
293
+ language: Programming language — "python", "javascript", "typescript",
294
+ "java", "go", or "bash" (default: "python").
295
+
296
+ Returns:
297
+ Execution output as a string, or error details.
298
+ """
299
+ try:
300
+ from agno.utils.code_execution import prepare_python_code
301
+
302
+ current_sandbox = self._get_or_create_sandbox(agent)
303
+ interpreter = self._get_or_create_interpreter(current_sandbox)
304
+
305
+ # Map string language to enum
306
+ lang_map = {
307
+ "python": SupportedLanguage.PYTHON,
308
+ "javascript": SupportedLanguage.JAVASCRIPT,
309
+ "typescript": SupportedLanguage.TYPESCRIPT,
310
+ "java": SupportedLanguage.JAVA,
311
+ "go": SupportedLanguage.GO,
312
+ "bash": SupportedLanguage.BASH,
313
+ }
314
+ lang_enum = lang_map.get(language.lower(), SupportedLanguage.PYTHON)
315
+
316
+ if lang_enum == SupportedLanguage.PYTHON:
317
+ code = prepare_python_code(code)
318
+
319
+ result = interpreter.codes.run(code, language=lang_enum)
320
+ return self._format_execution(result)
321
+
322
+ except Exception as e:
323
+ return json.dumps({"status": "error", "message": f"Error executing code: {e!s}"})
324
+
325
+ def run_shell_command(self, agent: Agent | Team, command: str) -> str:
326
+ """
327
+ Execute a shell command in the OpenSandbox environment.
328
+
329
+ Args:
330
+ agent: An Agno agent instance or a group of agents
331
+ command: Shell command to execute (e.g., "ls -la /tmp").
332
+
333
+ Returns:
334
+ Command output as a string.
335
+ """
336
+ try:
337
+ current_sandbox = self._get_or_create_sandbox(agent)
338
+ cwd = self._get_working_directory(agent)
339
+
340
+ if command.strip().startswith("cd "):
341
+ new_dir = command.strip()[3:].strip()
342
+ new_path = PurePosixPath(new_dir)
343
+
344
+ if not new_path.is_absolute():
345
+ new_path = PurePosixPath(cwd) / new_path
346
+
347
+ new_path_str = str(new_path)
348
+
349
+ test_result = current_sandbox.commands.run(
350
+ f"test -d '{new_path_str}' && echo 'exists' || echo 'not found'",
351
+ opts=RunCommandOpts(working_directory="/"),
352
+ )
353
+ stdout_text = test_result.text
354
+ if "exists" in stdout_text:
355
+ self._set_working_directory(agent, new_path_str)
356
+ return f"Changed directory to: {new_path_str}"
357
+ else:
358
+ return f"Error: Directory {new_path_str} not found"
359
+
360
+ execution = current_sandbox.commands.run(
361
+ command,
362
+ opts=RunCommandOpts(working_directory=cwd),
363
+ )
364
+ return self._format_execution(execution)
365
+
366
+ except Exception as e:
367
+ return json.dumps({"status": "error", "message": f"Error executing command: {e!s}"})
368
+
369
+ def create_file(self, agent: Agent | Team, file_path: str, content: str) -> str:
370
+ """
371
+ Create or update a file in the sandbox.
372
+
373
+ Args:
374
+ agent: An Agno agent instance or a group of agents
375
+ file_path: Path to the file (relative to current directory or absolute).
376
+ content: Content to write to the file.
377
+
378
+ Returns:
379
+ Success message or error.
380
+ """
381
+ try:
382
+ current_sandbox = self._get_or_create_sandbox(agent)
383
+ cwd = self._get_working_directory(agent)
384
+ resolved = self._resolve_path(file_path, cwd)
385
+
386
+ # Create parent directories
387
+ parent_dir = str(PurePosixPath(resolved).parent)
388
+ if parent_dir and parent_dir != "/":
389
+ current_sandbox.commands.run(
390
+ f"mkdir -p '{parent_dir}'",
391
+ opts=RunCommandOpts(working_directory="/"),
392
+ )
393
+
394
+ # Write file via SDK
395
+ current_sandbox.files.write_file(resolved, content)
396
+
397
+ return f"File created/updated: {resolved}"
398
+
399
+ except Exception as e:
400
+ return json.dumps({"status": "error", "message": f"Error creating file: {e!s}"})
401
+
402
+ def read_file(self, agent: Agent | Team, file_path: str) -> str:
403
+ """
404
+ Read a file from the sandbox.
405
+
406
+ Args:
407
+ agent: An Agno agent instance or a group of agents
408
+ file_path: Path to the file (relative to current directory or absolute).
409
+
410
+ Returns:
411
+ File content or error message.
412
+ """
413
+ try:
414
+ current_sandbox = self._get_or_create_sandbox(agent)
415
+ cwd = self._get_working_directory(agent)
416
+ resolved = self._resolve_path(file_path, cwd)
417
+
418
+ content = current_sandbox.files.read_file(resolved)
419
+ return content
420
+
421
+ except Exception as e:
422
+ return json.dumps({"status": "error", "message": f"Error reading file: {e!s}"})
423
+
424
+ def list_files(self, agent: Agent | Team, directory: str | None = None) -> str:
425
+ """
426
+ List files in a directory.
427
+
428
+ Args:
429
+ agent: An Agno agent instance or a group of agents
430
+ directory: Directory to list (defaults to current working directory).
431
+
432
+ Returns:
433
+ List of files and directories as formatted string.
434
+ """
435
+ try:
436
+ current_sandbox = self._get_or_create_sandbox(agent)
437
+ cwd = self._get_working_directory(agent)
438
+
439
+ dir_path = cwd if directory is None else self._resolve_path(directory, cwd)
440
+
441
+ execution = current_sandbox.commands.run(
442
+ f"ls -la '{dir_path}'",
443
+ opts=RunCommandOpts(working_directory="/"),
444
+ )
445
+
446
+ output = self._format_execution(execution)
447
+ return f"Contents of {dir_path}:\n{output}"
448
+
449
+ except Exception as e:
450
+ return json.dumps({"status": "error", "message": f"Error listing files: {e!s}"})
451
+
452
+ def delete_file(self, agent: Agent | Team, file_path: str) -> str:
453
+ """
454
+ Delete a file or directory from the sandbox.
455
+
456
+ Args:
457
+ agent: An Agno agent instance or a group of agents
458
+ file_path: Path to the file or directory (relative or absolute).
459
+
460
+ Returns:
461
+ Success message or error.
462
+ """
463
+ try:
464
+ current_sandbox = self._get_or_create_sandbox(agent)
465
+ cwd = self._get_working_directory(agent)
466
+ resolved = self._resolve_path(file_path, cwd)
467
+
468
+ # Determine if target is a file or directory via file info
469
+ try:
470
+ info = current_sandbox.files.get_file_info([resolved])
471
+ if resolved in info:
472
+ entry_info = info[resolved]
473
+ # Check if directory using Unix mode bits
474
+ if stat.S_ISDIR(entry_info.mode):
475
+ current_sandbox.files.delete_directories([resolved])
476
+ else:
477
+ current_sandbox.files.delete_files([resolved])
478
+ else:
479
+ # Path not found in info, try both
480
+ current_sandbox.files.delete_files([resolved])
481
+ except Exception:
482
+ # Fallback to shell rm if SDK methods fail
483
+ current_sandbox.commands.run(
484
+ f"rm -rf '{resolved}'",
485
+ opts=RunCommandOpts(working_directory="/"),
486
+ )
487
+
488
+ return f"Deleted: {resolved}"
489
+
490
+ except Exception as e:
491
+ return json.dumps({"status": "error", "message": f"Error deleting file: {e!s}"})
492
+
493
+ def change_directory(self, agent: Agent | Team, directory: str) -> str:
494
+ """
495
+ Change the current working directory.
496
+
497
+ Args:
498
+ agent: An Agno agent instance or a group of agents
499
+ directory: Directory to change to (relative or absolute).
500
+
501
+ Returns:
502
+ Success message or error.
503
+ """
504
+ try:
505
+ return self.run_shell_command(agent, f"cd {directory}")
506
+ except Exception as e:
507
+ return json.dumps({"status": "error", "message": f"Error changing directory: {e!s}"})
508
+
509
+ def get_sandbox_status(self, agent: Agent | Team) -> str:
510
+ """
511
+ Get the current status of the sandbox.
512
+
513
+ Args:
514
+ agent: An Agno agent instance or a group of agents
515
+
516
+ Returns:
517
+ Sandbox status information.
518
+ """
519
+ try:
520
+ current_sandbox = self._get_or_create_sandbox(agent)
521
+ sandbox_id = getattr(current_sandbox, "id", "Unknown")
522
+ return json.dumps(
523
+ {
524
+ "status": "success",
525
+ "sandbox_id": sandbox_id,
526
+ "image": self.image,
527
+ "working_directory": self._get_working_directory(agent),
528
+ }
529
+ )
530
+ except Exception as e:
531
+ return json.dumps({"status": "error", "message": f"Error getting status: {e!s}"})
532
+
533
+ def shutdown_sandbox(self, agent: Agent | Team) -> str:
534
+ """
535
+ Shut down the sandbox immediately.
536
+
537
+ Args:
538
+ agent: An Agno agent instance or a group of agents
539
+
540
+ Returns:
541
+ Success message or error.
542
+ """
543
+ try:
544
+ current_sandbox = self._get_or_create_sandbox(agent)
545
+ current_sandbox.kill()
546
+ self._sandbox = None
547
+ self._interpreter = None
548
+
549
+ # Clear session state
550
+ if self.persistent and hasattr(agent, "session_state") and agent.session_state:
551
+ agent.session_state.pop("opensandbox_id", None)
552
+ agent.session_state.pop("working_directory", None)
553
+
554
+ return json.dumps({"status": "success", "message": "Sandbox shut down successfully"})
555
+ except Exception as e:
556
+ return json.dumps({"status": "error", "message": f"Error shutting down: {e!s}"})