withruntime 0.5.1__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 (123) hide show
  1. withruntime-0.6.1/LICENSE +55 -0
  2. {withruntime-0.5.1 → withruntime-0.6.1}/PKG-INFO +11 -3
  3. {withruntime-0.5.1 → withruntime-0.6.1}/README.md +3 -2
  4. {withruntime-0.5.1 → withruntime-0.6.1}/pyproject.toml +9 -1
  5. withruntime-0.6.1/tests/test_async_stream_cleanup.py +269 -0
  6. withruntime-0.6.1/tests/test_deepagents.py +194 -0
  7. withruntime-0.6.1/tests/test_files_mode.py +74 -0
  8. withruntime-0.6.1/tests/test_harbor.py +554 -0
  9. withruntime-0.6.1/tests/test_identity_token.py +104 -0
  10. withruntime-0.6.1/tests/test_inspect_ai.py +234 -0
  11. withruntime-0.6.1/tests/test_mounts.py +69 -0
  12. withruntime-0.6.1/tests/test_network_funding.py +28 -0
  13. withruntime-0.6.1/tests/test_network_products.py +98 -0
  14. withruntime-0.6.1/tests/test_snapshot_after_fork.py +87 -0
  15. withruntime-0.6.1/tests/test_volume_backups.py +98 -0
  16. withruntime-0.6.1/tests/test_watch.py +144 -0
  17. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/__init__.py +43 -0
  18. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_client.py +91 -32
  19. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/__init__.py +10 -0
  20. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/desktop.py +61 -3
  21. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/interpreter.py +4 -2
  22. withruntime-0.6.1/withruntime/_async_products/mcp.py +64 -0
  23. withruntime-0.6.1/withruntime/_async_products/mounts.py +39 -0
  24. withruntime-0.6.1/withruntime/_async_products/network_products.py +154 -0
  25. withruntime-0.6.1/withruntime/_async_products/volumes.py +84 -0
  26. withruntime-0.6.1/withruntime/_async_products/watch.py +105 -0
  27. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_errors.py +2 -1
  28. withruntime-0.6.1/withruntime/_eval_sandbox.py +356 -0
  29. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_http.py +97 -36
  30. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_client.py +91 -32
  31. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/__init__.py +10 -0
  32. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/desktop.py +61 -3
  33. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/interpreter.py +4 -2
  34. withruntime-0.6.1/withruntime/_sync_products/mcp.py +65 -0
  35. withruntime-0.6.1/withruntime/_sync_products/mounts.py +40 -0
  36. withruntime-0.6.1/withruntime/_sync_products/network_products.py +155 -0
  37. withruntime-0.6.1/withruntime/_sync_products/volumes.py +85 -0
  38. withruntime-0.6.1/withruntime/_sync_products/watch.py +106 -0
  39. withruntime-0.6.1/withruntime/_version.py +1 -0
  40. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_ws.py +2 -3
  41. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/deepagents.py +15 -7
  42. withruntime-0.6.1/withruntime/harbor.py +309 -0
  43. withruntime-0.6.1/withruntime/inspect_ai.py +462 -0
  44. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime.egg-info/PKG-INFO +11 -3
  45. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime.egg-info/SOURCES.txt +24 -0
  46. withruntime-0.6.1/withruntime.egg-info/entry_points.txt +2 -0
  47. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime.egg-info/requires.txt +6 -0
  48. withruntime-0.5.1/tests/test_deepagents.py +0 -79
  49. withruntime-0.5.1/withruntime/_async_products/volumes.py +0 -43
  50. withruntime-0.5.1/withruntime/_sync_products/volumes.py +0 -44
  51. withruntime-0.5.1/withruntime/_version.py +0 -1
  52. {withruntime-0.5.1 → withruntime-0.6.1}/setup.cfg +0 -0
  53. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_audit.py +0 -0
  54. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_capacity_wait.py +0 -0
  55. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_client.py +0 -0
  56. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_daytona.py +0 -0
  57. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_e2b_code_interpreter.py +0 -0
  58. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_e2b_sandbox.py +0 -0
  59. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_images_v2.py +0 -0
  60. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_images_volumes_interpreter.py +0 -0
  61. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_lifecycle_settings.py +0 -0
  62. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_limits.py +0 -0
  63. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_observability.py +0 -0
  64. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_openai_agents.py +0 -0
  65. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_previews_network_desktop.py +0 -0
  66. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_proxy.py +0 -0
  67. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_referrals.py +0 -0
  68. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_secrets.py +0 -0
  69. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_switching.py +0 -0
  70. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_tools.py +0 -0
  71. {withruntime-0.5.1 → withruntime-0.6.1}/tests/test_vercel.py +0 -0
  72. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/audit.py +0 -0
  73. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/images.py +0 -0
  74. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/limits.py +0 -0
  75. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/network.py +0 -0
  76. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/observability.py +0 -0
  77. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/previews.py +0 -0
  78. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/referrals.py +0 -0
  79. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/secrets.py +0 -0
  80. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_async_products/switching.py +0 -0
  81. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_clock.py +0 -0
  82. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_connection.py +0 -0
  83. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_proxy.py +0 -0
  84. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/audit.py +0 -0
  85. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/images.py +0 -0
  86. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/limits.py +0 -0
  87. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/network.py +0 -0
  88. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/observability.py +0 -0
  89. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/previews.py +0 -0
  90. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/referrals.py +0 -0
  91. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/secrets.py +0 -0
  92. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_sync_products/switching.py +0 -0
  93. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/_tunnel.py +0 -0
  94. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/daytona/__init__.py +0 -0
  95. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/daytona/_async_daytona.py +0 -0
  96. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/daytona/_async_io.py +0 -0
  97. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/daytona/_core.py +0 -0
  98. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/daytona/_sync_daytona.py +0 -0
  99. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/daytona/_sync_io.py +0 -0
  100. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/__init__.py +0 -0
  101. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/_async_io.py +0 -0
  102. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/_async_sandbox.py +0 -0
  103. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/_core.py +0 -0
  104. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/_sync_io.py +0 -0
  105. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/_sync_sandbox.py +0 -0
  106. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/__init__.py +0 -0
  107. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
  108. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_models.py +0 -0
  109. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
  110. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/openai_agents.py +0 -0
  111. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/tools.py +0 -0
  112. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/__init__.py +0 -0
  113. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/_async_io.py +0 -0
  114. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/_async_sandbox.py +0 -0
  115. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/_core.py +0 -0
  116. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/_sync_io.py +0 -0
  117. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/_sync_sandbox.py +0 -0
  118. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/api.py +0 -0
  119. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/sandbox/__init__.py +0 -0
  120. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/vercel/sandbox/sync.py +0 -0
  121. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime/webhooks.py +0 -0
  122. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime.egg-info/dependency_links.txt +0 -0
  123. {withruntime-0.5.1 → withruntime-0.6.1}/withruntime.egg-info/top_level.txt +0 -0
@@ -0,0 +1,55 @@
1
+ Apache License
2
+
3
+ Version 2.0, January 2004
4
+
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
16
+
17
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
18
+
19
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
20
+
21
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
22
+
23
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
24
+
25
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
26
+
27
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
28
+
29
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
30
+
31
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
32
+
33
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
34
+
35
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
36
+
37
+ You must give any other recipients of the Work or Derivative Works a copy of this License; and
38
+
39
+ You must cause any modified files to carry prominent notices stating that You changed the files; and
40
+
41
+ You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
42
+
43
+ If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
44
+
45
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46
+
47
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48
+
49
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50
+
51
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52
+
53
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54
+
55
+ END OF TERMS AND CONDITIONS
@@ -1,13 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: withruntime
3
- Version: 0.5.1
3
+ Version: 0.6.1
4
4
  Summary: Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first.
5
+ License: Apache-2.0
5
6
  Requires-Python: >=3.10
6
7
  Description-Content-Type: text/markdown
8
+ License-File: LICENSE
7
9
  Provides-Extra: openai-agents
8
10
  Requires-Dist: openai-agents>=0.22.3; extra == "openai-agents"
9
11
  Provides-Extra: deepagents
10
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"
17
+ Dynamic: license-file
11
18
 
12
19
  # Runtime Cloud Python SDK
13
20
 
@@ -37,6 +44,8 @@ with Sandbox.create() as sbx:
37
44
  `Sandbox.create()` needs no arguments and returns once the sandbox is running;
38
45
  leaving the `with` block stops it. With no arguments you get the free trial
39
46
  while it lasts: 50 free hours, no card, up to eight sandboxes running at once.
47
+ The current default image includes NumPy, pandas and matplotlib; see the
48
+ [sandbox environment](https://withruntime.com/docs/sandbox-environment).
40
49
  `AsyncRuntime` is the same client for asyncio, method for method:
41
50
 
42
51
  ```python
@@ -131,5 +140,4 @@ https://withruntime.com/docs/frameworks.
131
140
  Docs: https://withruntime.com/docs/python.
132
141
 
133
142
  The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
134
- 0.3.0. Both names still work: `withruntime-cloud` installs this package and
135
- `import runtime_cloud` gives you the same classes.
143
+ 0.3.0. That name stopped at 0.5.1 and gets no new releases: use `withruntime`.
@@ -26,6 +26,8 @@ with Sandbox.create() as sbx:
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
28
  while it lasts: 50 free hours, no card, up to eight sandboxes running at once.
29
+ The current default image includes NumPy, pandas and matplotlib; see the
30
+ [sandbox environment](https://withruntime.com/docs/sandbox-environment).
29
31
  `AsyncRuntime` is the same client for asyncio, method for method:
30
32
 
31
33
  ```python
@@ -120,5 +122,4 @@ https://withruntime.com/docs/frameworks.
120
122
  Docs: https://withruntime.com/docs/python.
121
123
 
122
124
  The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
123
- 0.3.0. Both names still work: `withruntime-cloud` installs this package and
124
- `import runtime_cloud` gives you the same classes.
125
+ 0.3.0. That name stopped at 0.5.1 and gets no new releases: use `withruntime`.
@@ -4,9 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "withruntime"
7
- version = "0.5.1"
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
+ license = { text = "Apache-2.0" }
10
11
  requires-python = ">=3.10"
11
12
  dependencies = []
12
13
 
@@ -15,6 +16,13 @@ dependencies = []
15
16
  openai-agents = ["openai-agents>=0.22.3"]
16
17
  # withruntime.deepagents: a LangChain Deep Agents sandbox backend.
17
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"
18
26
 
19
27
  [tool.setuptools.packages.find]
20
28
  where = ["."]
@@ -0,0 +1,269 @@
1
+ """Real peer EOF proves stream/pool cleanup; a Python object disappearing does not."""
2
+ import asyncio
3
+ import unittest
4
+
5
+ from withruntime._async_client import _Transport
6
+
7
+
8
+ class AsyncStreamCleanupTest(unittest.IsolatedAsyncioTestCase):
9
+ async def asyncSetUp(self):
10
+ self.eof = asyncio.Event()
11
+ self.accepted = asyncio.Event()
12
+ self.writers = []
13
+ self.peer_eofs = []
14
+ self.responses = []
15
+ self.handlers = set()
16
+ self.body = b'{"event":1}\n{"event":2}\n'
17
+ self.partial = False
18
+ self.errors = []
19
+
20
+ async def answer(reader, writer):
21
+ task = asyncio.current_task()
22
+ self.handlers.add(task)
23
+ self.writers.append(writer)
24
+ peer_eof = asyncio.Event()
25
+ self.peer_eofs.append(peer_eof)
26
+ try:
27
+ await reader.readuntil(b'\r\n\r\n')
28
+ body = self.responses.pop(0) if self.responses else self.body
29
+ length = len(body) + (100 if self.partial else 0)
30
+ writer.write(b'HTTP/1.1 200 OK\r\nContent-Type: application/x-ndjson\r\nContent-Length: '
31
+ + str(length).encode() + b'\r\n\r\n' + body)
32
+ await writer.drain()
33
+ self.accepted.set()
34
+ await reader.read()
35
+ self.eof.set()
36
+ peer_eof.set()
37
+ except BaseException as error:
38
+ self.errors.append(error)
39
+ finally:
40
+ writer.close()
41
+ await writer.wait_closed()
42
+ self.handlers.discard(task)
43
+
44
+ self.server = await asyncio.start_server(answer, '127.0.0.1', 0)
45
+ self.transport = _Transport('fixture', f'http://127.0.0.1:{self.server.sockets[0].getsockname()[1]}', 5, 0)
46
+
47
+ async def asyncTearDown(self):
48
+ # Observe EOF before this fallback fixture cleanup, including on the
49
+ # unfixed implementation. Never count fixture shutdown as SDK cleanup.
50
+ await self.transport.close()
51
+ await asyncio.sleep(0)
52
+ await self.transport.close()
53
+ for writer in self.writers:
54
+ writer.close()
55
+ await writer.wait_closed()
56
+ self.server.close()
57
+ await self.server.wait_closed()
58
+ if self.handlers:
59
+ await asyncio.gather(*self.handlers)
60
+ self.assertEqual(self.errors, [])
61
+
62
+ async def assert_peer_closed(self):
63
+ await asyncio.wait_for(asyncio.gather(*(event.wait() for event in self.peer_eofs)), 0.5)
64
+ self.assertEqual(len(self.transport._http._idle), 0)
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
+
98
+ async def test_buffered_stream_close_then_transport_close_does_not_repool_later(self):
99
+ stream = self.transport.events('GET', '/events')
100
+ self.assertEqual(await anext(stream), {'event': 1})
101
+ await stream.aclose()
102
+ await self.transport.close()
103
+ await self.assert_peer_closed()
104
+ await asyncio.sleep(0)
105
+ self.assertEqual(len(self.transport._http._idle), 0)
106
+
107
+ async def test_partial_stream_close_immediately_closes_peer_without_pool_close(self):
108
+ self.partial = True
109
+ stream = self.transport.events('GET', '/events')
110
+ self.assertEqual(await anext(stream), {'event': 1})
111
+ await stream.aclose()
112
+ await self.assert_peer_closed()
113
+
114
+ async def test_json_decode_error_closes_partial_response(self):
115
+ self.body, self.partial = b'not-json\n', True
116
+ stream = self.transport.events('GET', '/events')
117
+ with self.assertRaises(ValueError):
118
+ await anext(stream)
119
+ await self.assert_peer_closed()
120
+
121
+ async def test_cancelled_pending_read_closes_peer(self):
122
+ self.body, self.partial = b'{"unfinished":', True
123
+ stream = self.transport.events('GET', '/events')
124
+ pending = asyncio.create_task(anext(stream))
125
+ await self.accepted.wait()
126
+ pending.cancel()
127
+ with self.assertRaises(asyncio.CancelledError):
128
+ await pending
129
+ await stream.aclose()
130
+ await self.assert_peer_closed()
131
+
132
+ async def test_completed_response_released_after_pool_close_is_not_kept(self):
133
+ response = await self.transport.send('GET', '/events')
134
+ self.assertEqual(await response._read_some(), self.body)
135
+ await self.transport.close()
136
+ await response.close()
137
+ await self.assert_peer_closed()
138
+
139
+ async def test_transport_close_closes_a_response_held_by_a_caller(self):
140
+ self.partial = True
141
+ response = await self.transport.send('GET', '/events')
142
+ await self.transport.close()
143
+ try:
144
+ await self.assert_peer_closed()
145
+ finally:
146
+ await response.close()
147
+
148
+ async def test_send_after_pool_close_never_opens_a_socket(self):
149
+ await self.transport.close()
150
+ with self.assertRaisesRegex(RuntimeError, "HTTP pool is closed"):
151
+ await self.transport.send('GET', '/events')
152
+ self.assertEqual(self.writers, [])
153
+
154
+
155
+ async def test_public_exec_stream_closes_partial_inner_stream_without_client_close(self):
156
+ from withruntime._async_client import AsyncSandbox
157
+ self.body = b'{"type":"stdout","offset":0,"data":"hello"}\n'
158
+ self.partial = True
159
+ retained = []
160
+ events = self.transport.events
161
+
162
+ def retain_stream(*args, **kwargs):
163
+ # Keep the inner iterator alive so this tests explicit ownership,
164
+ # not CPython's later async-generator finalizer scheduling.
165
+ stream = events(*args, **kwargs)
166
+ retained.append(stream)
167
+ return stream
168
+
169
+ self.transport.events = retain_stream
170
+ stream = AsyncSandbox(self.transport, {"id": "fixture"}).exec_stream(["echo", "hello"])
171
+ try:
172
+ self.assertEqual((await anext(stream))["data"], "hello")
173
+ await stream.aclose()
174
+ await self.assert_peer_closed()
175
+ finally:
176
+ await stream.aclose()
177
+ for inner in retained:
178
+ await inner.aclose()
179
+
180
+
181
+ async def test_public_process_output_and_exec_continuation_close_inner_streams(self):
182
+ from withruntime._async_client import AsyncProcess, AsyncSandbox
183
+ self.partial = True
184
+ self.body = b'{"type":"stdout","offset":0,"data":"hello"}\n'
185
+ retained = []
186
+ events = self.transport.events
187
+ def retain_stream(*args, **kwargs):
188
+ inner = events(*args, **kwargs)
189
+ retained.append(inner)
190
+ return inner
191
+ self.transport.events = retain_stream
192
+ for continuing in [False, True]:
193
+ if continuing:
194
+ self.responses = [b'{"type":"continue","processId":"process","cursor":0}\n', self.body]
195
+ stream = AsyncSandbox(self.transport, {"id":"fixture"}).exec_stream(["echo", "hello"])
196
+ else:
197
+ stream = AsyncProcess(self.transport, "fixture", {"id":"process"}).output()
198
+ try:
199
+ self.assertEqual((await anext(stream))["data"], "hello")
200
+ await stream.aclose()
201
+ await self.assert_peer_closed()
202
+ finally:
203
+ await stream.aclose()
204
+ for inner in retained:
205
+ await inner.aclose()
206
+ self.assertEqual(len(self.writers), 3)
207
+
208
+
209
+ class SyncStreamCleanupTest(unittest.TestCase):
210
+ def setUp(self):
211
+ import threading
212
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
213
+ from withruntime._sync_client import _Transport as SyncTransport
214
+ self.eof = threading.Event()
215
+ eof = self.eof
216
+ body = b'{"event":1}\n{"event":2}\n'
217
+
218
+ class Handler(BaseHTTPRequestHandler):
219
+ protocol_version = 'HTTP/1.1'
220
+ def log_message(self, *args):
221
+ pass
222
+ def do_GET(self):
223
+ self.send_response(200)
224
+ self.send_header('Content-Length', str(len(body)))
225
+ self.end_headers()
226
+ self.wfile.write(body)
227
+ self.wfile.flush()
228
+ if self.rfile.read(1) == b'':
229
+ eof.set()
230
+
231
+ self.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
232
+ self.thread = threading.Thread(target=self.server.serve_forever)
233
+ self.thread.start()
234
+ self.transport = SyncTransport('fixture', f'http://127.0.0.1:{self.server.server_port}', 5, 0)
235
+
236
+ def tearDown(self):
237
+ self.transport.close()
238
+ self.server.shutdown()
239
+ self.server.server_close()
240
+ self.thread.join()
241
+
242
+ def test_generator_close_closes_unread_response_without_pool_close(self):
243
+ stream = self.transport.events('GET', '/events')
244
+ self.assertEqual(next(stream), {'event': 1})
245
+ stream.close()
246
+ self.assertTrue(self.eof.wait(0.5), 'SDK did not close the peer before fixture cleanup')
247
+
248
+ def test_late_response_close_cannot_refill_closed_sync_pool(self):
249
+ response = self.transport.send('GET', '/events')
250
+ response._response.read()
251
+ self.transport.close()
252
+ response.close()
253
+ self.assertTrue(self.eof.wait(0.5), 'closed pool retained a late response')
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()