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.
Files changed (121) hide show
  1. {withruntime-0.6.0/withruntime.egg-info → withruntime-0.6.1}/PKG-INFO +5 -1
  2. {withruntime-0.6.0 → withruntime-0.6.1}/pyproject.toml +8 -1
  3. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_async_stream_cleanup.py +47 -0
  4. withruntime-0.6.1/tests/test_deepagents.py +194 -0
  5. withruntime-0.6.1/tests/test_harbor.py +554 -0
  6. withruntime-0.6.1/tests/test_inspect_ai.py +234 -0
  7. withruntime-0.6.1/withruntime/_eval_sandbox.py +356 -0
  8. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_http.py +35 -13
  9. withruntime-0.6.1/withruntime/_version.py +1 -0
  10. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_ws.py +2 -3
  11. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/deepagents.py +15 -7
  12. withruntime-0.6.1/withruntime/harbor.py +309 -0
  13. withruntime-0.6.1/withruntime/inspect_ai.py +462 -0
  14. {withruntime-0.6.0 → withruntime-0.6.1/withruntime.egg-info}/PKG-INFO +5 -1
  15. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/SOURCES.txt +6 -0
  16. withruntime-0.6.1/withruntime.egg-info/entry_points.txt +2 -0
  17. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/requires.txt +6 -0
  18. withruntime-0.6.0/tests/test_deepagents.py +0 -79
  19. withruntime-0.6.0/withruntime/_version.py +0 -1
  20. {withruntime-0.6.0 → withruntime-0.6.1}/LICENSE +0 -0
  21. {withruntime-0.6.0 → withruntime-0.6.1}/README.md +0 -0
  22. {withruntime-0.6.0 → withruntime-0.6.1}/setup.cfg +0 -0
  23. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_audit.py +0 -0
  24. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_capacity_wait.py +0 -0
  25. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_client.py +0 -0
  26. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_daytona.py +0 -0
  27. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_e2b_code_interpreter.py +0 -0
  28. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_e2b_sandbox.py +0 -0
  29. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_files_mode.py +0 -0
  30. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_identity_token.py +0 -0
  31. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_images_v2.py +0 -0
  32. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_images_volumes_interpreter.py +0 -0
  33. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_lifecycle_settings.py +0 -0
  34. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_limits.py +0 -0
  35. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_mounts.py +0 -0
  36. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_network_funding.py +0 -0
  37. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_network_products.py +0 -0
  38. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_observability.py +0 -0
  39. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_openai_agents.py +0 -0
  40. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_previews_network_desktop.py +0 -0
  41. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_proxy.py +0 -0
  42. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_referrals.py +0 -0
  43. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_secrets.py +0 -0
  44. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_snapshot_after_fork.py +0 -0
  45. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_switching.py +0 -0
  46. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_tools.py +0 -0
  47. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_vercel.py +0 -0
  48. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_volume_backups.py +0 -0
  49. {withruntime-0.6.0 → withruntime-0.6.1}/tests/test_watch.py +0 -0
  50. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/__init__.py +0 -0
  51. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_client.py +0 -0
  52. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/__init__.py +0 -0
  53. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/audit.py +0 -0
  54. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/desktop.py +0 -0
  55. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/images.py +0 -0
  56. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/interpreter.py +0 -0
  57. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/limits.py +0 -0
  58. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/mcp.py +0 -0
  59. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/mounts.py +0 -0
  60. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/network.py +0 -0
  61. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/network_products.py +0 -0
  62. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/observability.py +0 -0
  63. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/previews.py +0 -0
  64. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/referrals.py +0 -0
  65. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/secrets.py +0 -0
  66. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/switching.py +0 -0
  67. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/volumes.py +0 -0
  68. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_async_products/watch.py +0 -0
  69. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_clock.py +0 -0
  70. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_connection.py +0 -0
  71. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_errors.py +0 -0
  72. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_proxy.py +0 -0
  73. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_client.py +0 -0
  74. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/__init__.py +0 -0
  75. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/audit.py +0 -0
  76. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/desktop.py +0 -0
  77. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/images.py +0 -0
  78. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/interpreter.py +0 -0
  79. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/limits.py +0 -0
  80. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/mcp.py +0 -0
  81. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/mounts.py +0 -0
  82. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/network.py +0 -0
  83. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/network_products.py +0 -0
  84. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/observability.py +0 -0
  85. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/previews.py +0 -0
  86. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/referrals.py +0 -0
  87. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/secrets.py +0 -0
  88. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/switching.py +0 -0
  89. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/volumes.py +0 -0
  90. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_sync_products/watch.py +0 -0
  91. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/_tunnel.py +0 -0
  92. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/__init__.py +0 -0
  93. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_async_daytona.py +0 -0
  94. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_async_io.py +0 -0
  95. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_core.py +0 -0
  96. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_sync_daytona.py +0 -0
  97. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/daytona/_sync_io.py +0 -0
  98. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/__init__.py +0 -0
  99. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_async_io.py +0 -0
  100. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_async_sandbox.py +0 -0
  101. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_core.py +0 -0
  102. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_sync_io.py +0 -0
  103. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/_sync_sandbox.py +0 -0
  104. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/__init__.py +0 -0
  105. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
  106. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_models.py +0 -0
  107. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
  108. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/openai_agents.py +0 -0
  109. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/tools.py +0 -0
  110. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/__init__.py +0 -0
  111. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_async_io.py +0 -0
  112. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_async_sandbox.py +0 -0
  113. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_core.py +0 -0
  114. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_sync_io.py +0 -0
  115. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/_sync_sandbox.py +0 -0
  116. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/api.py +0 -0
  117. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/sandbox/__init__.py +0 -0
  118. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/vercel/sandbox/sync.py +0 -0
  119. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime/webhooks.py +0 -0
  120. {withruntime-0.6.0 → withruntime-0.6.1}/withruntime.egg-info/dependency_links.txt +0 -0
  121. {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.0
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.0"
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()