aury-agent 0.0.4__py3-none-any.whl
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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Sandbox module for isolated code execution environments.
|
|
2
|
+
|
|
3
|
+
Provides abstracted sandbox interfaces supporting:
|
|
4
|
+
- Local Docker-based sandboxes for CLI/development
|
|
5
|
+
- Remote API-based sandboxes for SaaS deployment
|
|
6
|
+
"""
|
|
7
|
+
from .types import ExecutionResult, SandboxConfig, Sandbox, SandboxProvider
|
|
8
|
+
from .local import LocalSandbox, LocalSandboxProvider
|
|
9
|
+
from .remote import RemoteSandbox, RemoteSandboxProvider
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
# Types
|
|
13
|
+
"ExecutionResult",
|
|
14
|
+
"SandboxConfig",
|
|
15
|
+
"Sandbox",
|
|
16
|
+
"SandboxProvider",
|
|
17
|
+
# Local
|
|
18
|
+
"LocalSandbox",
|
|
19
|
+
"LocalSandboxProvider",
|
|
20
|
+
# Remote
|
|
21
|
+
"RemoteSandbox",
|
|
22
|
+
"RemoteSandboxProvider",
|
|
23
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Local Docker-based sandbox implementation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import tarfile
|
|
7
|
+
import time
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from .types import ExecutionResult, SandboxConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LocalSandbox:
|
|
16
|
+
"""Local Docker-based sandbox implementation.
|
|
17
|
+
|
|
18
|
+
Uses Docker containers for isolation. Requires Docker to be installed
|
|
19
|
+
and accessible via the Docker socket.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, container: Any, config: SandboxConfig) -> None:
|
|
23
|
+
self._container = container
|
|
24
|
+
self._config = config
|
|
25
|
+
self._status: Literal["creating", "running", "stopped", "failed"] = "running"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def id(self) -> str:
|
|
29
|
+
return self._container.id
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def status(self) -> Literal["creating", "running", "stopped", "failed"]:
|
|
33
|
+
return self._status
|
|
34
|
+
|
|
35
|
+
async def execute(
|
|
36
|
+
self,
|
|
37
|
+
command: str | list[str],
|
|
38
|
+
*,
|
|
39
|
+
timeout: int | None = None,
|
|
40
|
+
stdin: str | None = None,
|
|
41
|
+
env: dict[str, str] | None = None,
|
|
42
|
+
workdir: str | None = None,
|
|
43
|
+
) -> ExecutionResult:
|
|
44
|
+
"""Execute command in Docker container."""
|
|
45
|
+
if isinstance(command, list):
|
|
46
|
+
cmd = command
|
|
47
|
+
else:
|
|
48
|
+
cmd = ["/bin/sh", "-c", command]
|
|
49
|
+
|
|
50
|
+
work_dir = workdir or self._config.workdir
|
|
51
|
+
effective_timeout = timeout or self._config.timeout
|
|
52
|
+
start_time = time.time()
|
|
53
|
+
|
|
54
|
+
def _exec() -> tuple[int, bytes]:
|
|
55
|
+
exec_result = self._container.exec_run(
|
|
56
|
+
cmd=cmd,
|
|
57
|
+
environment=env,
|
|
58
|
+
workdir=work_dir,
|
|
59
|
+
stdin=stdin is not None,
|
|
60
|
+
demux=True,
|
|
61
|
+
)
|
|
62
|
+
return exec_result.exit_code, exec_result.output
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
loop = asyncio.get_event_loop()
|
|
66
|
+
exit_code, output = await asyncio.wait_for(
|
|
67
|
+
loop.run_in_executor(None, _exec),
|
|
68
|
+
timeout=effective_timeout,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
stdout = ""
|
|
72
|
+
stderr = ""
|
|
73
|
+
if output:
|
|
74
|
+
if isinstance(output, tuple):
|
|
75
|
+
stdout = (output[0] or b"").decode("utf-8", errors="replace")
|
|
76
|
+
stderr = (output[1] or b"").decode("utf-8", errors="replace")
|
|
77
|
+
else:
|
|
78
|
+
stdout = output.decode("utf-8", errors="replace")
|
|
79
|
+
|
|
80
|
+
return ExecutionResult(
|
|
81
|
+
exit_code=exit_code or 0,
|
|
82
|
+
stdout=stdout,
|
|
83
|
+
stderr=stderr,
|
|
84
|
+
duration_ms=int((time.time() - start_time) * 1000),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
except asyncio.TimeoutError:
|
|
88
|
+
return ExecutionResult(
|
|
89
|
+
exit_code=-1,
|
|
90
|
+
stdout="",
|
|
91
|
+
stderr=f"Command timed out after {effective_timeout}s",
|
|
92
|
+
duration_ms=effective_timeout * 1000,
|
|
93
|
+
timed_out=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def write_file(self, path: str, content: str | bytes) -> None:
|
|
97
|
+
"""Write file to container using tar archive."""
|
|
98
|
+
if isinstance(content, str):
|
|
99
|
+
content = content.encode("utf-8")
|
|
100
|
+
|
|
101
|
+
def _write() -> None:
|
|
102
|
+
tar_stream = BytesIO()
|
|
103
|
+
with tarfile.open(fileobj=tar_stream, mode="w") as tar:
|
|
104
|
+
file_info = tarfile.TarInfo(name=os.path.basename(path))
|
|
105
|
+
file_info.size = len(content)
|
|
106
|
+
tar.addfile(file_info, BytesIO(content))
|
|
107
|
+
tar_stream.seek(0)
|
|
108
|
+
self._container.put_archive(os.path.dirname(path) or "/", tar_stream)
|
|
109
|
+
|
|
110
|
+
loop = asyncio.get_event_loop()
|
|
111
|
+
await loop.run_in_executor(None, _write)
|
|
112
|
+
|
|
113
|
+
async def read_file(self, path: str) -> bytes:
|
|
114
|
+
"""Read file from container using tar archive."""
|
|
115
|
+
def _read() -> bytes:
|
|
116
|
+
bits, _ = self._container.get_archive(path)
|
|
117
|
+
tar_stream = BytesIO()
|
|
118
|
+
for chunk in bits:
|
|
119
|
+
tar_stream.write(chunk)
|
|
120
|
+
tar_stream.seek(0)
|
|
121
|
+
|
|
122
|
+
with tarfile.open(fileobj=tar_stream, mode="r") as tar:
|
|
123
|
+
member = tar.getmembers()[0]
|
|
124
|
+
f = tar.extractfile(member)
|
|
125
|
+
if f:
|
|
126
|
+
return f.read()
|
|
127
|
+
return b""
|
|
128
|
+
|
|
129
|
+
loop = asyncio.get_event_loop()
|
|
130
|
+
return await loop.run_in_executor(None, _read)
|
|
131
|
+
|
|
132
|
+
async def upload(self, local_path: Path, remote_path: str) -> None:
|
|
133
|
+
"""Upload file/directory to container."""
|
|
134
|
+
def _upload() -> None:
|
|
135
|
+
tar_stream = BytesIO()
|
|
136
|
+
with tarfile.open(fileobj=tar_stream, mode="w") as tar:
|
|
137
|
+
tar.add(str(local_path), arcname=os.path.basename(remote_path))
|
|
138
|
+
tar_stream.seek(0)
|
|
139
|
+
dest_dir = os.path.dirname(remote_path) or "/"
|
|
140
|
+
self._container.put_archive(dest_dir, tar_stream)
|
|
141
|
+
|
|
142
|
+
loop = asyncio.get_event_loop()
|
|
143
|
+
await loop.run_in_executor(None, _upload)
|
|
144
|
+
|
|
145
|
+
async def download(self, remote_path: str, local_path: Path) -> None:
|
|
146
|
+
"""Download file/directory from container."""
|
|
147
|
+
def _download() -> None:
|
|
148
|
+
bits, _ = self._container.get_archive(remote_path)
|
|
149
|
+
tar_stream = BytesIO()
|
|
150
|
+
for chunk in bits:
|
|
151
|
+
tar_stream.write(chunk)
|
|
152
|
+
tar_stream.seek(0)
|
|
153
|
+
with tarfile.open(fileobj=tar_stream, mode="r") as tar:
|
|
154
|
+
tar.extractall(str(local_path.parent))
|
|
155
|
+
|
|
156
|
+
loop = asyncio.get_event_loop()
|
|
157
|
+
await loop.run_in_executor(None, _download)
|
|
158
|
+
|
|
159
|
+
async def stop(self) -> None:
|
|
160
|
+
"""Stop container."""
|
|
161
|
+
def _stop() -> None:
|
|
162
|
+
self._container.stop()
|
|
163
|
+
|
|
164
|
+
loop = asyncio.get_event_loop()
|
|
165
|
+
await loop.run_in_executor(None, _stop)
|
|
166
|
+
self._status = "stopped"
|
|
167
|
+
|
|
168
|
+
async def destroy(self) -> None:
|
|
169
|
+
"""Remove container."""
|
|
170
|
+
def _remove() -> None:
|
|
171
|
+
self._container.remove(force=True)
|
|
172
|
+
|
|
173
|
+
loop = asyncio.get_event_loop()
|
|
174
|
+
await loop.run_in_executor(None, _remove)
|
|
175
|
+
self._status = "stopped"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class LocalSandboxProvider:
|
|
179
|
+
"""Local Docker-based sandbox provider.
|
|
180
|
+
|
|
181
|
+
Creates sandboxes using local Docker installation.
|
|
182
|
+
Suitable for CLI usage and development.
|
|
183
|
+
|
|
184
|
+
Requires: docker package (`pip install docker`)
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
docker_socket: str = "unix:///var/run/docker.sock",
|
|
190
|
+
default_config: SandboxConfig | None = None,
|
|
191
|
+
) -> None:
|
|
192
|
+
self.docker_socket = docker_socket
|
|
193
|
+
self.default_config = default_config or SandboxConfig()
|
|
194
|
+
self._client: Any = None
|
|
195
|
+
|
|
196
|
+
def _get_client(self) -> Any:
|
|
197
|
+
"""Lazy-load Docker client."""
|
|
198
|
+
if self._client is None:
|
|
199
|
+
try:
|
|
200
|
+
import docker
|
|
201
|
+
self._client = docker.DockerClient(base_url=self.docker_socket)
|
|
202
|
+
except ImportError:
|
|
203
|
+
raise ImportError(
|
|
204
|
+
"docker package required for LocalSandboxProvider. "
|
|
205
|
+
"Install with: pip install docker"
|
|
206
|
+
)
|
|
207
|
+
return self._client
|
|
208
|
+
|
|
209
|
+
async def create(self, config: SandboxConfig | None = None) -> LocalSandbox:
|
|
210
|
+
"""Create a new Docker-based sandbox."""
|
|
211
|
+
cfg = config or self.default_config
|
|
212
|
+
client = self._get_client()
|
|
213
|
+
|
|
214
|
+
def _create() -> Any:
|
|
215
|
+
volumes = {}
|
|
216
|
+
for host, container in cfg.volumes.items():
|
|
217
|
+
volumes[host] = {"bind": container, "mode": "rw"}
|
|
218
|
+
|
|
219
|
+
container = client.containers.run(
|
|
220
|
+
image=cfg.image,
|
|
221
|
+
detach=True,
|
|
222
|
+
mem_limit=cfg.memory_limit,
|
|
223
|
+
cpu_period=100000,
|
|
224
|
+
cpu_quota=int(cfg.cpu_limit * 100000),
|
|
225
|
+
network_disabled=not cfg.network,
|
|
226
|
+
volumes=volumes or None,
|
|
227
|
+
environment=cfg.env or None,
|
|
228
|
+
working_dir=cfg.workdir,
|
|
229
|
+
command="sleep infinity",
|
|
230
|
+
)
|
|
231
|
+
return container
|
|
232
|
+
|
|
233
|
+
loop = asyncio.get_event_loop()
|
|
234
|
+
container = await loop.run_in_executor(None, _create)
|
|
235
|
+
|
|
236
|
+
return LocalSandbox(container, cfg)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ["LocalSandbox", "LocalSandboxProvider"]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Remote API-based sandbox implementation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from .types import ExecutionResult, SandboxConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RemoteSandbox:
|
|
12
|
+
"""Remote sandbox accessed via HTTP API.
|
|
13
|
+
|
|
14
|
+
Connects to a sandbox service for SaaS deployments.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
client: Any, # httpx.AsyncClient
|
|
20
|
+
api_url: str,
|
|
21
|
+
sandbox_id: str,
|
|
22
|
+
api_key: str,
|
|
23
|
+
config: SandboxConfig,
|
|
24
|
+
) -> None:
|
|
25
|
+
self._client = client
|
|
26
|
+
self._api_url = api_url
|
|
27
|
+
self._sandbox_id = sandbox_id
|
|
28
|
+
self._api_key = api_key
|
|
29
|
+
self._config = config
|
|
30
|
+
self._status: Literal["creating", "running", "stopped", "failed"] = "running"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def id(self) -> str:
|
|
34
|
+
return self._sandbox_id
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def status(self) -> Literal["creating", "running", "stopped", "failed"]:
|
|
38
|
+
return self._status
|
|
39
|
+
|
|
40
|
+
def _headers(self) -> dict[str, str]:
|
|
41
|
+
return {"Authorization": f"Bearer {self._api_key}"}
|
|
42
|
+
|
|
43
|
+
async def execute(
|
|
44
|
+
self,
|
|
45
|
+
command: str | list[str],
|
|
46
|
+
*,
|
|
47
|
+
timeout: int | None = None,
|
|
48
|
+
stdin: str | None = None,
|
|
49
|
+
env: dict[str, str] | None = None,
|
|
50
|
+
workdir: str | None = None,
|
|
51
|
+
) -> ExecutionResult:
|
|
52
|
+
"""Execute command via API."""
|
|
53
|
+
response = await self._client.post(
|
|
54
|
+
f"{self._api_url}/sandboxes/{self._sandbox_id}/exec",
|
|
55
|
+
headers=self._headers(),
|
|
56
|
+
json={
|
|
57
|
+
"command": command if isinstance(command, str) else " ".join(command),
|
|
58
|
+
"timeout": timeout or self._config.timeout,
|
|
59
|
+
"stdin": stdin,
|
|
60
|
+
"env": env,
|
|
61
|
+
"workdir": workdir or self._config.workdir,
|
|
62
|
+
},
|
|
63
|
+
timeout=timeout or self._config.timeout + 10,
|
|
64
|
+
)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
data = response.json()
|
|
67
|
+
|
|
68
|
+
return ExecutionResult(
|
|
69
|
+
exit_code=data.get("exit_code", 0),
|
|
70
|
+
stdout=data.get("stdout", ""),
|
|
71
|
+
stderr=data.get("stderr", ""),
|
|
72
|
+
duration_ms=data.get("duration_ms", 0),
|
|
73
|
+
timed_out=data.get("timed_out", False),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def write_file(self, path: str, content: str | bytes) -> None:
|
|
77
|
+
"""Write file via API."""
|
|
78
|
+
if isinstance(content, str):
|
|
79
|
+
content = content.encode("utf-8")
|
|
80
|
+
|
|
81
|
+
response = await self._client.post(
|
|
82
|
+
f"{self._api_url}/sandboxes/{self._sandbox_id}/files",
|
|
83
|
+
headers=self._headers(),
|
|
84
|
+
json={
|
|
85
|
+
"path": path,
|
|
86
|
+
"content": base64.b64encode(content).decode("ascii"),
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
|
|
91
|
+
async def read_file(self, path: str) -> bytes:
|
|
92
|
+
"""Read file via API."""
|
|
93
|
+
response = await self._client.get(
|
|
94
|
+
f"{self._api_url}/sandboxes/{self._sandbox_id}/files",
|
|
95
|
+
headers=self._headers(),
|
|
96
|
+
params={"path": path},
|
|
97
|
+
)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
data = response.json()
|
|
100
|
+
return base64.b64decode(data.get("content", ""))
|
|
101
|
+
|
|
102
|
+
async def upload(self, local_path: Path, remote_path: str) -> None:
|
|
103
|
+
"""Upload file via API."""
|
|
104
|
+
content = local_path.read_bytes()
|
|
105
|
+
await self.write_file(remote_path, content)
|
|
106
|
+
|
|
107
|
+
async def download(self, remote_path: str, local_path: Path) -> None:
|
|
108
|
+
"""Download file via API."""
|
|
109
|
+
content = await self.read_file(remote_path)
|
|
110
|
+
local_path.write_bytes(content)
|
|
111
|
+
|
|
112
|
+
async def stop(self) -> None:
|
|
113
|
+
"""Stop sandbox via API."""
|
|
114
|
+
response = await self._client.post(
|
|
115
|
+
f"{self._api_url}/sandboxes/{self._sandbox_id}/stop",
|
|
116
|
+
headers=self._headers(),
|
|
117
|
+
)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
self._status = "stopped"
|
|
120
|
+
|
|
121
|
+
async def destroy(self) -> None:
|
|
122
|
+
"""Destroy sandbox via API."""
|
|
123
|
+
response = await self._client.delete(
|
|
124
|
+
f"{self._api_url}/sandboxes/{self._sandbox_id}",
|
|
125
|
+
headers=self._headers(),
|
|
126
|
+
)
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
self._status = "stopped"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class RemoteSandboxProvider:
|
|
132
|
+
"""Remote sandbox provider via HTTP API.
|
|
133
|
+
|
|
134
|
+
Creates sandboxes by calling a remote sandbox service.
|
|
135
|
+
Suitable for SaaS deployments with multi-tenancy.
|
|
136
|
+
|
|
137
|
+
Requires: httpx package (`pip install httpx`)
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
api_url: str,
|
|
143
|
+
api_key: str,
|
|
144
|
+
default_config: SandboxConfig | None = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
self.api_url = api_url.rstrip("/")
|
|
147
|
+
self.api_key = api_key
|
|
148
|
+
self.default_config = default_config or SandboxConfig()
|
|
149
|
+
self._client: Any = None
|
|
150
|
+
|
|
151
|
+
async def _get_client(self) -> Any:
|
|
152
|
+
"""Lazy-load httpx client."""
|
|
153
|
+
if self._client is None:
|
|
154
|
+
try:
|
|
155
|
+
import httpx
|
|
156
|
+
self._client = httpx.AsyncClient()
|
|
157
|
+
except ImportError:
|
|
158
|
+
raise ImportError(
|
|
159
|
+
"httpx package required for RemoteSandboxProvider. "
|
|
160
|
+
"Install with: pip install httpx"
|
|
161
|
+
)
|
|
162
|
+
return self._client
|
|
163
|
+
|
|
164
|
+
async def create(self, config: SandboxConfig | None = None) -> RemoteSandbox:
|
|
165
|
+
"""Create a new remote sandbox."""
|
|
166
|
+
cfg = config or self.default_config
|
|
167
|
+
client = await self._get_client()
|
|
168
|
+
|
|
169
|
+
response = await client.post(
|
|
170
|
+
f"{self.api_url}/sandboxes",
|
|
171
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
172
|
+
json={
|
|
173
|
+
"image": cfg.image,
|
|
174
|
+
"timeout": cfg.timeout,
|
|
175
|
+
"memory_limit": cfg.memory_limit,
|
|
176
|
+
"cpu_limit": cfg.cpu_limit,
|
|
177
|
+
"network": cfg.network,
|
|
178
|
+
"env": cfg.env,
|
|
179
|
+
"workdir": cfg.workdir,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
response.raise_for_status()
|
|
183
|
+
data = response.json()
|
|
184
|
+
|
|
185
|
+
return RemoteSandbox(
|
|
186
|
+
client=client,
|
|
187
|
+
api_url=self.api_url,
|
|
188
|
+
sandbox_id=data["id"],
|
|
189
|
+
api_key=self.api_key,
|
|
190
|
+
config=cfg,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def close(self) -> None:
|
|
194
|
+
"""Close the HTTP client."""
|
|
195
|
+
if self._client:
|
|
196
|
+
await self._client.aclose()
|
|
197
|
+
self._client = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = ["RemoteSandbox", "RemoteSandboxProvider"]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Sandbox types and protocols."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ExecutionResult:
|
|
11
|
+
"""Result of command execution in sandbox."""
|
|
12
|
+
exit_code: int
|
|
13
|
+
stdout: str
|
|
14
|
+
stderr: str
|
|
15
|
+
duration_ms: int
|
|
16
|
+
timed_out: bool = False
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def success(self) -> bool:
|
|
20
|
+
return self.exit_code == 0 and not self.timed_out
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def output(self) -> str:
|
|
24
|
+
"""Combined output for display."""
|
|
25
|
+
if self.success:
|
|
26
|
+
return self.stdout
|
|
27
|
+
return f"{self.stdout}\n{self.stderr}".strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SandboxConfig:
|
|
32
|
+
"""Configuration for sandbox creation."""
|
|
33
|
+
image: str = "python:3.11-slim"
|
|
34
|
+
timeout: int = 300 # seconds
|
|
35
|
+
memory_limit: str = "512m"
|
|
36
|
+
cpu_limit: float = 1.0
|
|
37
|
+
network: bool = False
|
|
38
|
+
volumes: dict[str, str] = field(default_factory=dict)
|
|
39
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
40
|
+
workdir: str = "/workspace"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@runtime_checkable
|
|
44
|
+
class Sandbox(Protocol):
|
|
45
|
+
"""Protocol for sandbox instances.
|
|
46
|
+
|
|
47
|
+
A sandbox provides an isolated environment for executing
|
|
48
|
+
commands and managing files safely.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def id(self) -> str:
|
|
53
|
+
"""Unique sandbox identifier."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def status(self) -> Literal["creating", "running", "stopped", "failed"]:
|
|
58
|
+
"""Current sandbox status."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
async def execute(
|
|
62
|
+
self,
|
|
63
|
+
command: str | list[str],
|
|
64
|
+
*,
|
|
65
|
+
timeout: int | None = None,
|
|
66
|
+
stdin: str | None = None,
|
|
67
|
+
env: dict[str, str] | None = None,
|
|
68
|
+
workdir: str | None = None,
|
|
69
|
+
) -> ExecutionResult:
|
|
70
|
+
"""Execute a command in the sandbox."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
async def write_file(self, path: str, content: str | bytes) -> None:
|
|
74
|
+
"""Write a file to the sandbox."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
async def read_file(self, path: str) -> bytes:
|
|
78
|
+
"""Read a file from the sandbox."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
async def upload(self, local_path: Path, remote_path: str) -> None:
|
|
82
|
+
"""Upload a file or directory to the sandbox."""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
async def download(self, remote_path: str, local_path: Path) -> None:
|
|
86
|
+
"""Download a file or directory from the sandbox."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def stop(self) -> None:
|
|
90
|
+
"""Stop the sandbox (can be restarted)."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
async def destroy(self) -> None:
|
|
94
|
+
"""Destroy the sandbox permanently."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@runtime_checkable
|
|
99
|
+
class SandboxProvider(Protocol):
|
|
100
|
+
"""Protocol for sandbox providers.
|
|
101
|
+
|
|
102
|
+
Creates and manages sandbox instances.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
async def create(self, config: SandboxConfig | None = None) -> Sandbox:
|
|
106
|
+
"""Create a new sandbox instance."""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"ExecutionResult",
|
|
112
|
+
"SandboxConfig",
|
|
113
|
+
"Sandbox",
|
|
114
|
+
"SandboxProvider",
|
|
115
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Skill module for agent capabilities.
|
|
2
|
+
|
|
3
|
+
Skills are reusable capability bundles that can be loaded and injected
|
|
4
|
+
into agent context. Each skill contains instructions, scripts, and references.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
Use SkillProvider (from providers module) to inject skills into agent context.
|
|
8
|
+
"""
|
|
9
|
+
from .types import Skill
|
|
10
|
+
from .loader import SkillLoader
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Skill",
|
|
15
|
+
"SkillLoader",
|
|
16
|
+
]
|