withruntime 0.6.0__tar.gz → 0.6.1__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.
- {withruntime-0.6.0/withruntime.egg-info → withruntime-0.6.1}/PKG-INFO +5 -1
- {withruntime-0.6.0 → withruntime-0.6.1}/pyproject.toml +8 -1
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_async_stream_cleanup.py +47 -0
- withruntime-0.6.1/tests/test_deepagents.py +194 -0
- withruntime-0.6.1/tests/test_harbor.py +554 -0
- withruntime-0.6.1/tests/test_inspect_ai.py +234 -0
- withruntime-0.6.1/withruntime/_eval_sandbox.py +356 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_http.py +35 -13
- withruntime-0.6.1/withruntime/_version.py +1 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_ws.py +2 -3
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/deepagents.py +15 -7
- withruntime-0.6.1/withruntime/harbor.py +309 -0
- withruntime-0.6.1/withruntime/inspect_ai.py +462 -0
- {withruntime-0.6.0 → withruntime-0.6.1/withruntime.egg-info}/PKG-INFO +5 -1
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/SOURCES.txt +6 -0
- withruntime-0.6.1/withruntime.egg-info/entry_points.txt +2 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/requires.txt +6 -0
- withruntime-0.6.0/tests/test_deepagents.py +0 -79
- withruntime-0.6.0/withruntime/_version.py +0 -1
- {withruntime-0.6.0 → withruntime-0.6.1}/LICENSE +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/README.md +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/setup.cfg +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_audit.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_capacity_wait.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_client.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_daytona.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_e2b_code_interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_e2b_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_files_mode.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_identity_token.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_images_v2.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_images_volumes_interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_lifecycle_settings.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_limits.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_mounts.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_network_funding.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_network_products.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_observability.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_openai_agents.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_previews_network_desktop.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_proxy.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_referrals.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_secrets.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_snapshot_after_fork.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_switching.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_tools.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_vercel.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_volume_backups.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_watch.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_client.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/audit.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/desktop.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/images.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/limits.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/mcp.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/mounts.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/network.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/network_products.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/observability.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/previews.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/referrals.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/secrets.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/switching.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/volumes.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/watch.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_clock.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_connection.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_errors.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_proxy.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_client.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/audit.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/desktop.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/images.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/limits.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/mcp.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/mounts.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/network.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/network_products.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/observability.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/previews.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/referrals.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/secrets.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/switching.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/volumes.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/watch.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_tunnel.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_async_daytona.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_async_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_core.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_sync_daytona.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_sync_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_async_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_async_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_core.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_sync_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_sync_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_models.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/openai_agents.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/tools.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_async_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_async_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_core.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_sync_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_sync_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/api.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/sandbox/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/sandbox/sync.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/webhooks.py +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/dependency_links.txt +0 -0
- {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withruntime
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -10,6 +10,10 @@ Provides-Extra: openai-agents
|
|
|
10
10
|
Requires-Dist: openai-agents>=0.22.3; extra == "openai-agents"
|
|
11
11
|
Provides-Extra: deepagents
|
|
12
12
|
Requires-Dist: deepagents>=0.7; extra == "deepagents"
|
|
13
|
+
Provides-Extra: harbor
|
|
14
|
+
Requires-Dist: harbor>=0.23.0; extra == "harbor"
|
|
15
|
+
Provides-Extra: inspect-ai
|
|
16
|
+
Requires-Dist: inspect-ai>=0.3.268; extra == "inspect-ai"
|
|
13
17
|
Dynamic: license-file
|
|
14
18
|
|
|
15
19
|
# Runtime Cloud Python SDK
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "withruntime"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -16,6 +16,13 @@ dependencies = []
|
|
|
16
16
|
openai-agents = ["openai-agents>=0.22.3"]
|
|
17
17
|
# withruntime.deepagents: a LangChain Deep Agents sandbox backend.
|
|
18
18
|
deepagents = ["deepagents>=0.7"]
|
|
19
|
+
# withruntime.harbor: an environment for Harbor (Terminal-Bench): -e withruntime.harbor:RuntimeEnvironment.
|
|
20
|
+
harbor = ["harbor>=0.23.0"]
|
|
21
|
+
# withruntime.inspect_ai: sandbox="runtime" for Inspect AI, registered by the entry point below.
|
|
22
|
+
inspect-ai = ["inspect-ai>=0.3.268"]
|
|
23
|
+
|
|
24
|
+
[project.entry-points.inspect_ai]
|
|
25
|
+
withruntime = "withruntime.inspect_ai"
|
|
19
26
|
|
|
20
27
|
[tool.setuptools.packages.find]
|
|
21
28
|
where = ["."]
|
|
@@ -63,6 +63,38 @@ class AsyncStreamCleanupTest(unittest.IsolatedAsyncioTestCase):
|
|
|
63
63
|
await asyncio.wait_for(asyncio.gather(*(event.wait() for event in self.peer_eofs)), 0.5)
|
|
64
64
|
self.assertEqual(len(self.transport._http._idle), 0)
|
|
65
65
|
|
|
66
|
+
async def test_cancel_in_the_turn_a_read_completes_is_not_swallowed(self):
|
|
67
|
+
"""Python 3.11's asyncio.wait_for returned the read's data and dropped the cancel
|
|
68
|
+
(CPython gh-86296); the stream then read on until its timeout, which is how
|
|
69
|
+
test_cancelled_pending_read_closes_peer failed there. Both paths of within()."""
|
|
70
|
+
import unittest.mock
|
|
71
|
+
from withruntime._http import within
|
|
72
|
+
|
|
73
|
+
async def race():
|
|
74
|
+
ready = asyncio.get_running_loop().create_future()
|
|
75
|
+
|
|
76
|
+
async def read():
|
|
77
|
+
return await ready
|
|
78
|
+
task = asyncio.create_task(within(read(), 5))
|
|
79
|
+
await asyncio.sleep(0)
|
|
80
|
+
ready.set_result(b'data')
|
|
81
|
+
task.cancel()
|
|
82
|
+
with self.assertRaises(asyncio.CancelledError):
|
|
83
|
+
await task
|
|
84
|
+
|
|
85
|
+
async def results():
|
|
86
|
+
self.assertEqual(await within(asyncio.sleep(0, b'ok'), 5), b'ok')
|
|
87
|
+
with self.assertRaises(asyncio.TimeoutError):
|
|
88
|
+
await within(asyncio.sleep(5), 0.01)
|
|
89
|
+
|
|
90
|
+
await race()
|
|
91
|
+
await results()
|
|
92
|
+
# Python 3.10 has no asyncio.timeout: the fallback waits on a task instead.
|
|
93
|
+
with unittest.mock.patch.dict(asyncio.__dict__):
|
|
94
|
+
asyncio.__dict__.pop('timeout', None)
|
|
95
|
+
await race()
|
|
96
|
+
await results()
|
|
97
|
+
|
|
66
98
|
async def test_buffered_stream_close_then_transport_close_does_not_repool_later(self):
|
|
67
99
|
stream = self.transport.events('GET', '/events')
|
|
68
100
|
self.assertEqual(await anext(stream), {'event': 1})
|
|
@@ -220,3 +252,18 @@ class SyncStreamCleanupTest(unittest.TestCase):
|
|
|
220
252
|
response.close()
|
|
221
253
|
self.assertTrue(self.eof.wait(0.5), 'closed pool retained a late response')
|
|
222
254
|
self.assertEqual(self.transport._http._idle.qsize(), 0)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class NoWaitForTest(unittest.TestCase):
|
|
258
|
+
def test_no_module_uses_asyncio_wait_for(self):
|
|
259
|
+
# asyncio.wait_for loses a cancel before Python 3.12 (gh-86296); the
|
|
260
|
+
# client waits through _http.within instead, everywhere.
|
|
261
|
+
import re
|
|
262
|
+
from pathlib import Path
|
|
263
|
+
package = Path(__file__).resolve().parent.parent / "withruntime"
|
|
264
|
+
found = [f"{path.relative_to(package)}:{number}"
|
|
265
|
+
for path in sorted(package.rglob("*.py"))
|
|
266
|
+
for number, line in enumerate(path.read_text().splitlines(), 1)
|
|
267
|
+
if re.search(r"\basyncio\.wait_for\(", line)]
|
|
268
|
+
self.assertEqual(found, [])
|
|
269
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""withruntime.deepagents against stand-in sandboxes. Skipped without Deep Agents
|
|
2
|
+
(pip install "withruntime[deepagents]"). A real sandbox and create_deep_agent
|
|
3
|
+
were exercised on 23 September 2026; see packages/cloud-guide/docs/frameworks.md.
|
|
4
|
+
LocalSandbox runs Deep Agents' own commands on this machine, so every protocol
|
|
5
|
+
method is checked end to end; sdks/python/integrations/langchain-withruntime
|
|
6
|
+
runs LangChain's standard sandbox suite the same way."""
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import deepagents # noqa: F401
|
|
16
|
+
HAVE_DEEPAGENTS = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAVE_DEEPAGENTS = False
|
|
19
|
+
|
|
20
|
+
from withruntime import CommandResult
|
|
21
|
+
from withruntime._errors import NotFoundError
|
|
22
|
+
from withruntime._errors import RuntimeError as RuntimeCloudError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Files:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.data, self.removed = {}, []
|
|
28
|
+
|
|
29
|
+
def read(self, path):
|
|
30
|
+
if path not in self.data:
|
|
31
|
+
raise NotFoundError("missing", code="file_not_found", status=404)
|
|
32
|
+
return self.data[path]
|
|
33
|
+
|
|
34
|
+
def write(self, path, data):
|
|
35
|
+
self.data[path] = data
|
|
36
|
+
|
|
37
|
+
def remove(self, path, recursive=False):
|
|
38
|
+
self.removed.append(path)
|
|
39
|
+
self.data.pop(path, None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class StubSandbox:
|
|
43
|
+
id = "sbx-1"
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self.files, self.commands, self.outside = Files(), [], {}
|
|
47
|
+
|
|
48
|
+
def exec(self, command, **kwargs):
|
|
49
|
+
self.commands.append((command, kwargs))
|
|
50
|
+
if isinstance(command, list) and command[:2] == ["sh", "-c"]:
|
|
51
|
+
source, target = command[4], command[5]
|
|
52
|
+
if source.startswith("/workspace/"):
|
|
53
|
+
self.outside[target] = self.files.data[source]
|
|
54
|
+
elif source in self.outside:
|
|
55
|
+
self.files.data[target] = self.outside[source]
|
|
56
|
+
else:
|
|
57
|
+
return CommandResult(exit_code=1, stdout="", stderr=f"cat: {source}: No such file or directory\n")
|
|
58
|
+
return CommandResult(exit_code=0, stdout="", stderr="")
|
|
59
|
+
return CommandResult(exit_code=2, stdout="out\n", stderr="err\n", timed_out=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LocalFiles:
|
|
63
|
+
"""The Files API over a temporary directory that stands for /workspace."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, root):
|
|
66
|
+
self.root = root
|
|
67
|
+
|
|
68
|
+
def host(self, path):
|
|
69
|
+
assert path.startswith("/workspace/"), path
|
|
70
|
+
return self.root / path[len("/workspace/"):]
|
|
71
|
+
|
|
72
|
+
def read(self, path):
|
|
73
|
+
target = self.host(path)
|
|
74
|
+
if target.is_dir():
|
|
75
|
+
raise RuntimeCloudError(f"{path} is a directory", code="is_directory", status=400)
|
|
76
|
+
if not target.exists():
|
|
77
|
+
raise NotFoundError("missing", code="file_not_found", status=404)
|
|
78
|
+
return target.read_bytes()
|
|
79
|
+
|
|
80
|
+
def write(self, path, data):
|
|
81
|
+
target = self.host(path)
|
|
82
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
target.write_bytes(data)
|
|
84
|
+
|
|
85
|
+
def remove(self, path, recursive=False):
|
|
86
|
+
self.host(path).unlink()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LocalSandbox:
|
|
90
|
+
"""exec runs on this machine: a string under bash -c, a list without a shell,
|
|
91
|
+
with a /workspace argument mapped to the stand-in directory."""
|
|
92
|
+
id = "sbx-local"
|
|
93
|
+
|
|
94
|
+
def __init__(self, root):
|
|
95
|
+
self.files = LocalFiles(root / "workspace")
|
|
96
|
+
|
|
97
|
+
def exec(self, command, timeout_ms=None, **kwargs):
|
|
98
|
+
if isinstance(command, str):
|
|
99
|
+
argv = ["bash", "-c", command]
|
|
100
|
+
else:
|
|
101
|
+
argv = [str(self.files.host(a)) if a.startswith("/workspace/") else a for a in command]
|
|
102
|
+
done = subprocess.run(argv, capture_output=True, timeout=None if timeout_ms is None else timeout_ms / 1000)
|
|
103
|
+
return CommandResult(exit_code=done.returncode, stdout=done.stdout.decode(errors="replace"),
|
|
104
|
+
stderr=done.stderr.decode(errors="replace"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@unittest.skipUnless(HAVE_DEEPAGENTS, "Deep Agents is not installed")
|
|
108
|
+
class DeepAgentsTest(unittest.TestCase):
|
|
109
|
+
def test_execute_joins_output_and_reports_timeouts(self):
|
|
110
|
+
from withruntime.deepagents import RuntimeSandbox
|
|
111
|
+
sandbox = StubSandbox()
|
|
112
|
+
response = RuntimeSandbox(sandbox, timeout_seconds=9).execute("make")
|
|
113
|
+
self.assertEqual(response.exit_code, 2)
|
|
114
|
+
self.assertTrue(response.output.startswith("out\nerr\n"))
|
|
115
|
+
self.assertIn("ran past 9 seconds", response.output)
|
|
116
|
+
self.assertEqual(sandbox.commands[-1][1], {"timeout_ms": 9000})
|
|
117
|
+
|
|
118
|
+
def test_files_inside_and_outside_the_workspace(self):
|
|
119
|
+
from withruntime.deepagents import RuntimeSandbox
|
|
120
|
+
sandbox = StubSandbox()
|
|
121
|
+
backend = RuntimeSandbox(sandbox)
|
|
122
|
+
uploaded = backend.upload_files([("/workspace/a.txt", b"a"), ("/etc/app.conf", b"b"), ("rel.txt", b"c")])
|
|
123
|
+
self.assertEqual([u.error for u in uploaded], [None, None, "invalid_path"])
|
|
124
|
+
self.assertEqual(sandbox.outside["/etc/app.conf"], b"b")
|
|
125
|
+
downloaded = backend.download_files(["/workspace/a.txt", "/etc/app.conf", "/workspace/none", "/etc/none"])
|
|
126
|
+
self.assertEqual([d.content for d in downloaded[:2]], [b"a", b"b"])
|
|
127
|
+
self.assertEqual([d.error for d in downloaded[2:]], ["file_not_found", "file_not_found"])
|
|
128
|
+
self.assertTrue(all(path.startswith("/workspace/.deepagents-staging/") for path in sandbox.files.removed))
|
|
129
|
+
self.assertEqual(backend.id, "sbx-1")
|
|
130
|
+
|
|
131
|
+
def test_long_output_is_returned_whole_by_default(self):
|
|
132
|
+
from withruntime.deepagents import RuntimeSandbox
|
|
133
|
+
|
|
134
|
+
class Loud(StubSandbox):
|
|
135
|
+
def exec(self, command, **kwargs):
|
|
136
|
+
return CommandResult(exit_code=0, stdout="x" * 250_000, stderr="")
|
|
137
|
+
|
|
138
|
+
response = RuntimeSandbox(Loud()).execute("cat big")
|
|
139
|
+
self.assertEqual((len(response.output), response.truncated), (250_000, False))
|
|
140
|
+
capped = RuntimeSandbox(Loud(), max_output_chars=10).execute("cat big")
|
|
141
|
+
self.assertEqual((capped.output, capped.truncated), ("x" * 10, True))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@unittest.skipUnless(HAVE_DEEPAGENTS, "Deep Agents is not installed")
|
|
145
|
+
@unittest.skipUnless(sys.platform.startswith("linux") and shutil.which("bash") and shutil.which("python3"),
|
|
146
|
+
"LocalSandbox runs Deep Agents' GNU shell and python3 commands on this machine")
|
|
147
|
+
class DeepAgentsEndToEndTest(unittest.TestCase):
|
|
148
|
+
def setUp(self):
|
|
149
|
+
from withruntime.deepagents import RuntimeSandbox
|
|
150
|
+
self.dir = Path(tempfile.mkdtemp(prefix="runtime-deepagents-test-"))
|
|
151
|
+
self.root = str(self.dir / "outside")
|
|
152
|
+
self.backend = RuntimeSandbox(LocalSandbox(self.dir))
|
|
153
|
+
|
|
154
|
+
def tearDown(self):
|
|
155
|
+
shutil.rmtree(self.dir, ignore_errors=True)
|
|
156
|
+
|
|
157
|
+
def test_every_protocol_method(self):
|
|
158
|
+
backend, root = self.backend, self.root
|
|
159
|
+
self.assertIsNone(backend.write(f"{root}/src/app.py", "print('hello')\n").error)
|
|
160
|
+
edited = backend.edit(f"{root}/src/app.py", "hello", "world")
|
|
161
|
+
self.assertEqual((edited.error, edited.occurrences), (None, 1))
|
|
162
|
+
read = backend.read(f"{root}/src/app.py")
|
|
163
|
+
self.assertIn("print('world')", read.file_data["content"])
|
|
164
|
+
ran = backend.execute(f"python3 {root}/src/app.py")
|
|
165
|
+
self.assertEqual((ran.output, ran.exit_code, ran.truncated), ("world\n", 0, False))
|
|
166
|
+
self.assertEqual([e["path"] for e in backend.ls(f"{root}/src").entries], [f"{root}/src/app.py"])
|
|
167
|
+
self.assertEqual([m["path"] for m in backend.glob("**/*.py", path=root).matches], [f"{root}/src/app.py"])
|
|
168
|
+
grep = backend.grep("world", path=root)
|
|
169
|
+
self.assertEqual([(m["path"], m["line"]) for m in grep.matches], [(f"{root}/src/app.py", 1)])
|
|
170
|
+
self.assertIsNone(backend.delete(f"{root}/src/app.py").error)
|
|
171
|
+
self.assertIsNotNone(backend.read(f"{root}/src/app.py").error)
|
|
172
|
+
# Nothing staged outside the workspace was left behind.
|
|
173
|
+
self.assertEqual(list((self.dir / "workspace" / ".deepagents-staging").iterdir()), [])
|
|
174
|
+
|
|
175
|
+
def test_a_file_past_the_old_output_cap_reads_back(self):
|
|
176
|
+
backend, path = self.backend, f"{self.root}/big.txt"
|
|
177
|
+
text = "".join(f"line {i:06d} {'x' * 90}\n" for i in range(1500)) # about 150 KB
|
|
178
|
+
self.assertIsNone(backend.write(path, text).error)
|
|
179
|
+
read = backend.read(path, limit=2000)
|
|
180
|
+
self.assertIsNone(read.error)
|
|
181
|
+
self.assertIn("line 001499", read.file_data["content"])
|
|
182
|
+
|
|
183
|
+
def test_async_methods(self):
|
|
184
|
+
import asyncio
|
|
185
|
+
|
|
186
|
+
async def run():
|
|
187
|
+
self.assertIsNone((await self.backend.awrite(f"{self.root}/a.txt", "one")).error)
|
|
188
|
+
return await self.backend.aread(f"{self.root}/a.txt")
|
|
189
|
+
|
|
190
|
+
self.assertIn("one", asyncio.run(run()).file_data["content"])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
unittest.main()
|