withruntime 0.6.0__tar.gz → 0.7.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.
- {withruntime-0.6.0/withruntime.egg-info → withruntime-0.7.0}/PKG-INFO +6 -2
- {withruntime-0.6.0 → withruntime-0.7.0}/README.md +1 -1
- {withruntime-0.6.0 → withruntime-0.7.0}/pyproject.toml +8 -1
- withruntime-0.7.0/tests/test_account_errors.py +46 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_async_stream_cleanup.py +47 -0
- withruntime-0.7.0/tests/test_billing.py +72 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_capacity_wait.py +9 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_client.py +3 -1
- withruntime-0.7.0/tests/test_deepagents.py +194 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_e2b_sandbox.py +19 -4
- withruntime-0.7.0/tests/test_eval_sandbox.py +43 -0
- withruntime-0.7.0/tests/test_exec_stream.py +131 -0
- withruntime-0.7.0/tests/test_files_download.py +137 -0
- withruntime-0.7.0/tests/test_harbor.py +554 -0
- withruntime-0.7.0/tests/test_inspect_ai.py +234 -0
- withruntime-0.7.0/tests/test_late_answer.py +69 -0
- withruntime-0.7.0/tests/test_secrets.py +193 -0
- withruntime-0.7.0/tests/test_sso.py +29 -0
- withruntime-0.7.0/tests/test_wake_awake.py +34 -0
- withruntime-0.7.0/tests/test_wallet.py +55 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/__init__.py +4 -3
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_client.py +335 -57
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/__init__.py +6 -1
- withruntime-0.7.0/withruntime/_async_products/billing.py +31 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/images.py +2 -2
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/network_products.py +46 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/secrets.py +23 -3
- withruntime-0.7.0/withruntime/_async_products/sso.py +20 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_clock.py +10 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_errors.py +8 -1
- withruntime-0.7.0/withruntime/_eval_sandbox.py +371 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_http.py +64 -18
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_proxy.py +2 -4
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_client.py +335 -57
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/__init__.py +6 -1
- withruntime-0.7.0/withruntime/_sync_products/billing.py +32 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/images.py +2 -2
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/network_products.py +46 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/secrets.py +23 -3
- withruntime-0.7.0/withruntime/_sync_products/sso.py +21 -0
- withruntime-0.7.0/withruntime/_version.py +1 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_ws.py +2 -3
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/daytona/_async_daytona.py +3 -6
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/daytona/_sync_daytona.py +3 -6
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/deepagents.py +15 -7
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/_async_io.py +14 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/_async_sandbox.py +24 -9
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/_core.py +4 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/_sync_io.py +5 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/_sync_sandbox.py +24 -9
- withruntime-0.7.0/withruntime/harbor.py +309 -0
- withruntime-0.7.0/withruntime/inspect_ai.py +462 -0
- withruntime-0.7.0/withruntime/wallet.py +62 -0
- {withruntime-0.6.0 → withruntime-0.7.0/withruntime.egg-info}/PKG-INFO +6 -2
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime.egg-info/SOURCES.txt +20 -0
- withruntime-0.7.0/withruntime.egg-info/entry_points.txt +2 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime.egg-info/requires.txt +6 -0
- withruntime-0.6.0/tests/test_deepagents.py +0 -79
- withruntime-0.6.0/tests/test_secrets.py +0 -91
- withruntime-0.6.0/withruntime/_version.py +0 -1
- {withruntime-0.6.0 → withruntime-0.7.0}/LICENSE +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/setup.cfg +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_audit.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_daytona.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_e2b_code_interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_files_mode.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_identity_token.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_images_v2.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_images_volumes_interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_lifecycle_settings.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_limits.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_mounts.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_network_funding.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_network_products.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_observability.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_openai_agents.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_previews_network_desktop.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_proxy.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_referrals.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_snapshot_after_fork.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_switching.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_tools.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_vercel.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_volume_backups.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/tests/test_watch.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/audit.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/desktop.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/limits.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/mcp.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/mounts.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/network.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/observability.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/previews.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/referrals.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/switching.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/volumes.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_async_products/watch.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_connection.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/audit.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/desktop.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/interpreter.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/limits.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/mcp.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/mounts.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/network.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/observability.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/previews.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/referrals.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/switching.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/volumes.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_sync_products/watch.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/_tunnel.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/daytona/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/daytona/_async_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/daytona/_core.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/daytona/_sync_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/code_interpreter/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/code_interpreter/_models.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/openai_agents.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/tools.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/_async_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/_async_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/_core.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/_sync_io.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/_sync_sandbox.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/api.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/sandbox/__init__.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/vercel/sandbox/sync.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime/webhooks.py +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/withruntime.egg-info/dependency_links.txt +0 -0
- {withruntime-0.6.0 → withruntime-0.7.0}/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.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -39,7 +43,7 @@ with Sandbox.create() as sbx:
|
|
|
39
43
|
|
|
40
44
|
`Sandbox.create()` needs no arguments and returns once the sandbox is running;
|
|
41
45
|
leaving the `with` block stops it. With no arguments you get the free trial
|
|
42
|
-
while it lasts:
|
|
46
|
+
while it lasts: 100 free hours, no card, up to eight sandboxes running at once.
|
|
43
47
|
The current default image includes NumPy, pandas and matplotlib; see the
|
|
44
48
|
[sandbox environment](https://withruntime.com/docs/sandbox-environment).
|
|
45
49
|
`AsyncRuntime` is the same client for asyncio, method for method:
|
|
@@ -25,7 +25,7 @@ with Sandbox.create() as sbx:
|
|
|
25
25
|
|
|
26
26
|
`Sandbox.create()` needs no arguments and returns once the sandbox is running;
|
|
27
27
|
leaving the `with` block stops it. With no arguments you get the free trial
|
|
28
|
-
while it lasts:
|
|
28
|
+
while it lasts: 100 free hours, no card, up to eight sandboxes running at once.
|
|
29
29
|
The current default image includes NumPy, pandas and matplotlib; see the
|
|
30
30
|
[sandbox environment](https://withruntime.com/docs/sandbox-environment).
|
|
31
31
|
`AsyncRuntime` is the same client for asyncio, method for method:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "withruntime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
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 = ["."]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""A blocked account gets its own error type (ARCHITECTURE.md section 11,
|
|
2
|
+
"A blocked account is told why")."""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import withruntime
|
|
8
|
+
from withruntime._errors import error_for
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AccountErrorsTest(unittest.TestCase):
|
|
12
|
+
def test_account_blocked_is_its_own_type(self) -> None:
|
|
13
|
+
error = error_for(402, {"error": {"code": "account_blocked",
|
|
14
|
+
"message": "This account cannot spend: a payment on it is disputed."}}, None)
|
|
15
|
+
self.assertIsInstance(error, withruntime.AccountBlockedError)
|
|
16
|
+
self.assertIsInstance(error, withruntime.RuntimeError)
|
|
17
|
+
self.assertEqual(error.code, "account_blocked")
|
|
18
|
+
self.assertEqual(error.status, 402)
|
|
19
|
+
self.assertFalse(error.retryable)
|
|
20
|
+
|
|
21
|
+
def test_empty_balance_stays_plain(self) -> None:
|
|
22
|
+
error = error_for(402, {"error": {"code": "insufficient_funds", "message": "Add credit."}}, None)
|
|
23
|
+
self.assertNotIsInstance(error, withruntime.AccountBlockedError)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AccountCloseTest(unittest.TestCase):
|
|
27
|
+
"""runtime.account.close (ARCHITECTURE.md section 3.1)."""
|
|
28
|
+
|
|
29
|
+
def test_close_posts_the_typed_name_once(self) -> None:
|
|
30
|
+
from withruntime._sync_client import Account
|
|
31
|
+
|
|
32
|
+
calls: list[tuple[Any, ...]] = []
|
|
33
|
+
|
|
34
|
+
class Transport:
|
|
35
|
+
def json(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
36
|
+
calls.append((method, path, kwargs))
|
|
37
|
+
return {"orgId": "o1", "name": "Acme", "closedAt": "2026-09-25T20:00:00Z", "released": True}
|
|
38
|
+
|
|
39
|
+
closed = Account(Transport()).close(confirm="Acme") # type: ignore[arg-type]
|
|
40
|
+
self.assertTrue(closed["released"])
|
|
41
|
+
self.assertEqual(calls, [("POST", "/v1/account:close", {"body": {"confirm": "Acme"}, "retry": False})])
|
|
42
|
+
self.assertTrue(hasattr(withruntime.Runtime(api_key="rk_x"), "account"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
unittest.main()
|
|
@@ -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,72 @@
|
|
|
1
|
+
"""runtime.billing.topup() and topup_status() against a stub API, sync and
|
|
2
|
+
async. The route is tested in packages/cloud (billing-api)."""
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
import unittest
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
|
|
9
|
+
from withruntime import AsyncRuntime, Runtime
|
|
10
|
+
|
|
11
|
+
TOPUP = {
|
|
12
|
+
"purchaseId": "3f0c2a4e-1b7d-4c5a-9e2f-0a1b2c3d4e5f",
|
|
13
|
+
"status": "open",
|
|
14
|
+
"amountUsd": "20.00",
|
|
15
|
+
"amount": "20.000000",
|
|
16
|
+
"payBy": "2026-09-25T13:00:00.000Z",
|
|
17
|
+
"networks": [{"network": "base", "address": "0xbase", "tokens": [{"currency": "usdc", "contract": "0xusdc"}]}],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Stub(BaseHTTPRequestHandler):
|
|
22
|
+
seen: list = []
|
|
23
|
+
|
|
24
|
+
def log_message(self, *args):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def answer(self):
|
|
28
|
+
length = int(self.headers.get("content-length") or 0)
|
|
29
|
+
body = json.loads(self.rfile.read(length)) if length else None
|
|
30
|
+
Stub.seen.append((self.command, self.path, body, self.headers.get("idempotency-key")))
|
|
31
|
+
data = json.dumps(TOPUP).encode()
|
|
32
|
+
self.send_response(200)
|
|
33
|
+
self.send_header("content-type", "application/json")
|
|
34
|
+
self.send_header("content-length", str(len(data)))
|
|
35
|
+
self.end_headers()
|
|
36
|
+
self.wfile.write(data)
|
|
37
|
+
|
|
38
|
+
do_GET = answer
|
|
39
|
+
do_POST = answer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BillingTest(unittest.TestCase):
|
|
43
|
+
@classmethod
|
|
44
|
+
def setUpClass(cls):
|
|
45
|
+
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), Stub)
|
|
46
|
+
threading.Thread(target=cls.server.serve_forever, daemon=True).start()
|
|
47
|
+
cls.url = f"http://127.0.0.1:{cls.server.server_address[1]}"
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def tearDownClass(cls):
|
|
51
|
+
cls.server.shutdown()
|
|
52
|
+
|
|
53
|
+
def setUp(self):
|
|
54
|
+
Stub.seen = []
|
|
55
|
+
|
|
56
|
+
def test_sync_topup_and_status(self):
|
|
57
|
+
with Runtime(api_key="rk_test", base_url=self.url, max_retries=0) as runtime:
|
|
58
|
+
self.assertEqual(runtime.billing.topup(20, idempotency_key="k1")["amount"], "20.000000")
|
|
59
|
+
self.assertEqual(runtime.billing.topup_status(TOPUP["purchaseId"])["status"], "open")
|
|
60
|
+
self.assertEqual(Stub.seen[0], ("POST", "/v1/billing/topups", {"usd": 20}, "k1"))
|
|
61
|
+
self.assertEqual(Stub.seen[1][:2], ("GET", f"/v1/billing/topups/{TOPUP['purchaseId']}"))
|
|
62
|
+
|
|
63
|
+
def test_async_topup(self):
|
|
64
|
+
async def go():
|
|
65
|
+
async with AsyncRuntime(api_key="rk_test", base_url=self.url, max_retries=0) as runtime:
|
|
66
|
+
return await runtime.billing.topup(20)
|
|
67
|
+
|
|
68
|
+
self.assertEqual(asyncio.run(go())["networks"][0]["address"], "0xbase")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
unittest.main()
|
|
@@ -84,6 +84,15 @@ class CapacityWait(unittest.TestCase):
|
|
|
84
84
|
self.assertEqual(len(set(wire.bodies)), 1)
|
|
85
85
|
self.assertEqual(json.loads(wire.bodies[0]), {"funding": "trial", "labels": {"ci": "1"}})
|
|
86
86
|
|
|
87
|
+
def test_a_create_says_why_it_waits_before_each_wait(self):
|
|
88
|
+
heard = []
|
|
89
|
+
busy = refusal("trial_busy")
|
|
90
|
+
for _, outcome, _ in create([busy, busy], on_capacity_wait=lambda error, seconds: heard.append(
|
|
91
|
+
(error.code, seconds))):
|
|
92
|
+
self.assertEqual(outcome.id, "sbx")
|
|
93
|
+
# Sync, then async: two waits each, of retryAfterMs.
|
|
94
|
+
self.assertEqual(heard, [("trial_busy", 0.005)] * 4)
|
|
95
|
+
|
|
87
96
|
def test_the_paid_limit_a_domain_limit_and_a_full_region_are_waited_out_too(self):
|
|
88
97
|
for code in ("quota_exceeded", "trial_domain_limit", "no_capacity"):
|
|
89
98
|
for wire, outcome, _ in create([refusal(code)]):
|
|
@@ -83,8 +83,10 @@ class Sync(unittest.TestCase):
|
|
|
83
83
|
self.assertEqual(seen, ["hello\n"])
|
|
84
84
|
|
|
85
85
|
def test_errors_are_typed_and_keys_never_leak(self):
|
|
86
|
+
# A misspelled field is refused before sending (test_exec_stream.CreateFields);
|
|
87
|
+
# a value out of range is the API's to refuse.
|
|
86
88
|
with self.assertRaises(InvalidRequestError) as caught:
|
|
87
|
-
client().sandboxes.create(
|
|
89
|
+
client().sandboxes.create(vcpu=0)
|
|
88
90
|
self.assertIn("issues", caught.exception.details)
|
|
89
91
|
self.assertNotIn("rk_test", str(caught.exception))
|
|
90
92
|
self.assertTrue(ServiceUnavailableError("x", status=503).retryable)
|
|
@@ -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()
|
|
@@ -340,11 +340,15 @@ class ForksPortsAndGaps(Base):
|
|
|
340
340
|
self.assertEqual(caught.exception.code, "fork_unavailable")
|
|
341
341
|
|
|
342
342
|
def test_ports(self):
|
|
343
|
+
# 25 September 2026: get_host raised, so E2B code did not run unchanged.
|
|
343
344
|
sbx = self.create()
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
self.
|
|
347
|
-
self.assertEqual(sbx.get_public_host(3000),
|
|
345
|
+
host = f"3000-{sbx.sandbox_id.replace('-', '')}.runtimehost.com"
|
|
346
|
+
self.assertEqual(sbx.get_host(3000), host)
|
|
347
|
+
self.assertEqual(sbx.get_host(3000), host)
|
|
348
|
+
self.assertEqual(sbx.get_public_host(3000), host)
|
|
349
|
+
self.assertEqual(len(self.world.called("previews.create")), 1)
|
|
350
|
+
with self.assertRaises(e2b.InvalidArgumentException):
|
|
351
|
+
sbx.get_host(0)
|
|
348
352
|
|
|
349
353
|
def test_gaps(self):
|
|
350
354
|
sbx = self.create()
|
|
@@ -356,6 +360,17 @@ class ForksPortsAndGaps(Base):
|
|
|
356
360
|
|
|
357
361
|
|
|
358
362
|
class Async(unittest.TestCase):
|
|
363
|
+
def test_get_host_answers_at_once_and_the_share_lands_beside_it(self):
|
|
364
|
+
world = World()
|
|
365
|
+
|
|
366
|
+
async def scenario():
|
|
367
|
+
async with await AsyncSandbox.create(client=world.async_client()) as sbx:
|
|
368
|
+
host = sbx.get_host(8000) # no await, as in E2B
|
|
369
|
+
self.assertEqual(host, f"8000-{sbx.sandbox_id.replace('-', '')}.runtimehost.com")
|
|
370
|
+
self.assertEqual(await sbx.get_public_host(8000), host)
|
|
371
|
+
self.assertEqual(len(world.called("previews.create")), 1)
|
|
372
|
+
asyncio.run(scenario())
|
|
373
|
+
|
|
359
374
|
def test_async_sandbox_end_to_end(self):
|
|
360
375
|
world = World()
|
|
361
376
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Exit codes in withruntime._eval_sandbox, which Harbor and Inspect share, as the
|
|
2
|
+
live API reports them (read on 25 September 2026): a command past its limit ends
|
|
3
|
+
with -9 and timedOut, and a command a signal ended with the negative signal
|
|
4
|
+
number. Harbor and Inspect expect what a shell reports. Needs neither."""
|
|
5
|
+
import asyncio
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from withruntime._eval_sandbox import _Shell, shell_exit_code
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Sandbox:
|
|
12
|
+
id = "sbx_exit"
|
|
13
|
+
|
|
14
|
+
def __init__(self, exit_event):
|
|
15
|
+
self.exit_event = exit_event
|
|
16
|
+
|
|
17
|
+
async def exec_stream(self, argv, *, env=None, stdin=None, timeout_ms=None):
|
|
18
|
+
yield {"type": "start", "processId": "p1"}
|
|
19
|
+
yield self.exit_event
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _run(exit_event):
|
|
23
|
+
shell = _Shell(_Sandbox(exit_event), "/tmp")
|
|
24
|
+
return asyncio.run(shell._stream(["sh", "-c", "x"], None, None, 1000, None, None))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ShellExitCodeTest(unittest.TestCase):
|
|
28
|
+
def test_a_timed_out_command_is_124(self):
|
|
29
|
+
result = _run({"type": "exit", "exitCode": -9, "state": "timed_out", "timedOut": True})
|
|
30
|
+
self.assertEqual((result.exit_code, result.timed_out), (124, True))
|
|
31
|
+
|
|
32
|
+
def test_a_command_a_signal_ended_is_128_plus_the_signal(self):
|
|
33
|
+
self.assertEqual(_run({"type": "exit", "exitCode": -15, "timedOut": False}).exit_code, 143)
|
|
34
|
+
self.assertEqual(_run({"type": "exit", "exitCode": -9, "timedOut": False}).exit_code, 137)
|
|
35
|
+
|
|
36
|
+
def test_ordinary_codes_are_kept(self):
|
|
37
|
+
self.assertEqual(_run({"type": "exit", "exitCode": 3, "timedOut": False}).exit_code, 3)
|
|
38
|
+
self.assertEqual(shell_exit_code(0, False), 0)
|
|
39
|
+
self.assertEqual(shell_exit_code(None, False), -1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
unittest.main()
|