withruntime 0.8.1__tar.gz → 0.8.3__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 (189) hide show
  1. {withruntime-0.8.1/withruntime.egg-info → withruntime-0.8.3}/PKG-INFO +37 -10
  2. {withruntime-0.8.1 → withruntime-0.8.3}/README.md +36 -9
  3. {withruntime-0.8.1 → withruntime-0.8.3}/pyproject.toml +1 -1
  4. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_blaxel.py +1 -1
  5. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_daytona.py +11 -0
  6. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_e2b_sandbox.py +3 -0
  7. withruntime-0.8.3/tests/test_files_upload_parallel.py +76 -0
  8. withruntime-0.8.3/tests/test_jobs.py +165 -0
  9. withruntime-0.8.3/tests/test_sandbox_sessions.py +149 -0
  10. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_secrets.py +3 -1
  11. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_vercel.py +2 -1
  12. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/__init__.py +2 -2
  13. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_client.py +75 -4
  14. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/__init__.py +2 -0
  15. withruntime-0.8.3/withruntime/_async_products/jobs.py +230 -0
  16. withruntime-0.8.3/withruntime/_async_products/secrets.py +197 -0
  17. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_errors.py +5 -0
  18. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_client.py +75 -4
  19. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/__init__.py +2 -0
  20. withruntime-0.8.3/withruntime/_sync_products/jobs.py +231 -0
  21. withruntime-0.8.3/withruntime/_sync_products/secrets.py +198 -0
  22. withruntime-0.8.3/withruntime/_version.py +1 -0
  23. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/__init__.py +7 -3
  24. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/_async_sandbox.py +81 -3
  25. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/_core.py +83 -3
  26. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/_sync_sandbox.py +81 -3
  27. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/daytona/_async_daytona.py +3 -3
  28. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/daytona/_core.py +2 -2
  29. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/daytona/_sync_daytona.py +3 -3
  30. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/__init__.py +1 -1
  31. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/inspect_ai.py +2 -1
  32. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/_core.py +1 -1
  33. {withruntime-0.8.1 → withruntime-0.8.3/withruntime.egg-info}/PKG-INFO +37 -10
  34. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime.egg-info/SOURCES.txt +5 -0
  35. withruntime-0.8.1/withruntime/_async_products/secrets.py +0 -58
  36. withruntime-0.8.1/withruntime/_sync_products/secrets.py +0 -59
  37. withruntime-0.8.1/withruntime/_version.py +0 -1
  38. {withruntime-0.8.1 → withruntime-0.8.3}/LICENSE +0 -0
  39. {withruntime-0.8.1 → withruntime-0.8.3}/setup.cfg +0 -0
  40. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_account_errors.py +0 -0
  41. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_async_stream_cleanup.py +0 -0
  42. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_audit.py +0 -0
  43. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_billing.py +0 -0
  44. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_capacity_wait.py +0 -0
  45. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_client.py +0 -0
  46. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_deepagents.py +0 -0
  47. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_e2b_code_interpreter.py +0 -0
  48. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_eval_sandbox.py +0 -0
  49. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_exec_stream.py +0 -0
  50. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_files_download.py +0 -0
  51. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_files_mode.py +0 -0
  52. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_harbor.py +0 -0
  53. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_http_tls_close.py +0 -0
  54. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_identity_token.py +0 -0
  55. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_images_v2.py +0 -0
  56. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_images_volumes_interpreter.py +0 -0
  57. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_inspect_ai.py +0 -0
  58. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_late_answer.py +0 -0
  59. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_lifecycle_settings.py +0 -0
  60. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_limits.py +0 -0
  61. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_mounts.py +0 -0
  62. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_network_funding.py +0 -0
  63. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_network_products.py +0 -0
  64. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_observability.py +0 -0
  65. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_openai_agents.py +0 -0
  66. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_previews_network_desktop.py +0 -0
  67. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_proxy.py +0 -0
  68. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_referrals.py +0 -0
  69. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_snapshot_after_fork.py +0 -0
  70. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_sso.py +0 -0
  71. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_stream_exit_close.py +0 -0
  72. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_switching.py +0 -0
  73. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_tools.py +0 -0
  74. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_unpack.py +0 -0
  75. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_volume_backups.py +0 -0
  76. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_wake_awake.py +0 -0
  77. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_wallet.py +0 -0
  78. {withruntime-0.8.1 → withruntime-0.8.3}/tests/test_watch.py +0 -0
  79. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/audit.py +0 -0
  80. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/billing.py +0 -0
  81. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/desktop.py +0 -0
  82. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/images.py +0 -0
  83. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/interpreter.py +0 -0
  84. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/limits.py +0 -0
  85. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/mcp.py +0 -0
  86. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/mounts.py +0 -0
  87. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/network.py +0 -0
  88. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/network_products.py +0 -0
  89. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/observability.py +0 -0
  90. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/previews.py +0 -0
  91. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/referrals.py +0 -0
  92. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/sso.py +0 -0
  93. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/switching.py +0 -0
  94. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/volumes.py +0 -0
  95. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_async_products/watch.py +0 -0
  96. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_clock.py +0 -0
  97. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_connection.py +0 -0
  98. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_eval_sandbox.py +0 -0
  99. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_http.py +0 -0
  100. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_proxy.py +0 -0
  101. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/audit.py +0 -0
  102. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/billing.py +0 -0
  103. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/desktop.py +0 -0
  104. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/images.py +0 -0
  105. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/interpreter.py +0 -0
  106. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/limits.py +0 -0
  107. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/mcp.py +0 -0
  108. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/mounts.py +0 -0
  109. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/network.py +0 -0
  110. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/network_products.py +0 -0
  111. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/observability.py +0 -0
  112. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/previews.py +0 -0
  113. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/referrals.py +0 -0
  114. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/sso.py +0 -0
  115. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/switching.py +0 -0
  116. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/volumes.py +0 -0
  117. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_sync_products/watch.py +0 -0
  118. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_tunnel.py +0 -0
  119. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_unpack.py +0 -0
  120. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/_ws.py +0 -0
  121. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/_async_io.py +0 -0
  122. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/_sync_io.py +0 -0
  123. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/__init__.py +0 -0
  124. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/agents.py +0 -0
  125. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/__init__.py +0 -0
  126. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/__init__.py +0 -0
  127. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/applications/__init__.py +0 -0
  128. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/applications/list_application_revisions.py +0 -0
  129. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/compute/__init__.py +0 -0
  130. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/compute/get_sandbox.py +0 -0
  131. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/compute/update_sandbox.py +0 -0
  132. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/functions/__init__.py +0 -0
  133. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/functions/create_function.py +0 -0
  134. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/images.py +0 -0
  135. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/api/workspaces.py +0 -0
  136. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/client.py +0 -0
  137. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/__init__.py +0 -0
  138. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/create_job_execution_request.py +0 -0
  139. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/create_job_execution_request_env.py +0 -0
  140. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/env.py +0 -0
  141. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/function.py +0 -0
  142. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/function_runtime.py +0 -0
  143. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/function_spec.py +0 -0
  144. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/metadata.py +0 -0
  145. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/models/workspace.py +0 -0
  146. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/client/types.py +0 -0
  147. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/common.py +0 -0
  148. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/drive.py +0 -0
  149. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/jobs.py +0 -0
  150. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/sandbox/__init__.py +0 -0
  151. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/core/sandbox/types.py +0 -0
  152. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/crewai.py +0 -0
  153. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/googleadk.py +0 -0
  154. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/langgraph.py +0 -0
  155. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/livekit.py +0 -0
  156. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/llamaindex.py +0 -0
  157. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/openai.py +0 -0
  158. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/pydantic.py +0 -0
  159. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/blaxel/telemetry.py +0 -0
  160. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/daytona/__init__.py +0 -0
  161. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/daytona/_async_io.py +0 -0
  162. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/daytona/_sync_io.py +0 -0
  163. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/deepagents.py +0 -0
  164. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/_async_io.py +0 -0
  165. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/_async_sandbox.py +0 -0
  166. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/_core.py +0 -0
  167. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/_sync_io.py +0 -0
  168. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/_sync_sandbox.py +0 -0
  169. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/code_interpreter/__init__.py +0 -0
  170. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
  171. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/code_interpreter/_models.py +0 -0
  172. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
  173. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/harbor.py +0 -0
  174. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/openai_agents.py +0 -0
  175. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/tools.py +0 -0
  176. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/__init__.py +0 -0
  177. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/_async_io.py +0 -0
  178. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/_async_sandbox.py +0 -0
  179. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/_sync_io.py +0 -0
  180. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/_sync_sandbox.py +0 -0
  181. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/api.py +0 -0
  182. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/sandbox/__init__.py +0 -0
  183. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/vercel/sandbox/sync.py +0 -0
  184. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/wallet.py +0 -0
  185. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime/webhooks.py +0 -0
  186. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime.egg-info/dependency_links.txt +0 -0
  187. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime.egg-info/entry_points.txt +0 -0
  188. {withruntime-0.8.1 → withruntime-0.8.3}/withruntime.egg-info/requires.txt +0 -0
  189. {withruntime-0.8.1 → withruntime-0.8.3}/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.8.1
3
+ Version: 0.8.3
4
4
  Summary: Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://withruntime.com
@@ -71,11 +71,18 @@ A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `forward_port` (any
71
71
  TCP port in the sandbox on a local port), `files` (read, write,
72
72
  list, glob, stat, move, remove, upload and download directories), `pause`,
73
73
  `wake`, `extend`, `update`, `keep_alive`, `fork` and `snapshot`, and the
74
- `interpreter`, `network`, `previews` and `desktop` products. A paused sandbox
74
+ `interpreter`, `network`, `previews`, `desktop`, `mounts` (your S3, R2 or Google
75
+ Cloud Storage bucket as a folder), `metrics` and `mcp` (servers from the MCP
76
+ catalog, run in the sandbox) products. A paused sandbox
75
77
  also wakes by itself on the next call, and `Sandbox.get_or_create(name)` returns
76
- the sandbox with that name or creates it. The client has `sandboxes`, `images`,
77
- `volumes`, `snapshots`, `limits`, `feedback` and `support`;
78
- `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
78
+ the sandbox with that name or creates it. A sandbox pauses itself after 60 seconds
79
+ with nothing happening in it, keeping its memory and processes, so an
80
+ idle sandbox costs no compute; `idle_pause_seconds` sets 10 to 86,400, or 0 for
81
+ never. The client has `sandboxes`, `images`, `volumes`, `snapshots`,
82
+ `secrets`, `limits`, `feedback` and `support`; `webhooks`, `events`, `otel` and
83
+ `audit` to watch the account; `domains`, `ports`, `addresses`, `tunnel` and
84
+ `network` to connect sandboxes to your own world (paid accounts); and `mcp`,
85
+ `sso`, `billing`, `referrals` and `switching`. `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
79
86
  what its agent may still spend today. Every write carries an
80
87
  idempotency key, so retries never do anything twice; errors are typed and carry
81
88
  `code`, `hint` and `request_id`.
@@ -159,8 +166,8 @@ How it maps:
159
166
  (every `/` becomes `-`; the tag is `latest` when none is given), so the
160
167
  code keeps Blaxel's image string. A missing one raises `NotSupportedError`
161
168
  with the build command, for example `npx withruntime image build
162
- --dockerfile Dockerfile --name my-company-agent-image -t
163
- my-company-agent-image:latest`. Volumes mount Runtime volumes of the same
169
+ --dockerfile Dockerfile --name my-company-agent-image -t
170
+ my-company-agent-image:latest`. Volumes mount Runtime volumes of the same
164
171
  name. On the free trial a sandbox has at most 4096 MB of memory (2 vCPUs).
165
172
  - **Standby.** A sandbox pauses after a minute with no call (Blaxel: about 15
166
173
  seconds). It keeps its memory and processes, and wakes on the next command,
@@ -200,17 +207,22 @@ Differences you can hit:
200
207
  (`/workspace`). File calls act as the sandbox user and fall back to `sudo`
201
208
  where only root may: they reach files a root process made, including private
202
209
  ones, and a file they write is the sandbox user's.
203
- - A sandbox is visible only to the key that created it: another key of the
204
- same account gets 404 for it
210
+ - A key sees the sandboxes it made, and another key's answer 404, unless an
211
+ owner or admin makes the key account-wide, as a Blaxel workspace key is
205
212
  ([teams](https://withruntime.com/docs/teams#keys-in-a-team)).
206
213
  - Public previews need a paid sandbox.
207
214
  - A process's `pid` is Runtime's process id, not an operating-system number.
215
+ - `sandbox.sessions` makes a Runtime sandbox session, and
216
+ `SandboxInstance.from_session` drives the sandbox with its token: commands,
217
+ files and previews of that one sandbox. A session lasts a day at most, and
218
+ `Access-Control-Allow-Origin` in `response_headers` names the page that may
219
+ use it.
208
220
 
209
221
  These raise `NotSupportedError` before anything happens, naming what to use:
210
222
  regions outside the US, read-only and ephemeral volumes, drives, `extra_args`
211
223
  other than `iptables`, egress and subnet settings, preview headers, custom
212
224
  domains and preview expiry, restoring a snapshot in place (fork from it
213
- instead), sessions, codegen, `system`, schedules, and Blaxel's agents, models,
225
+ instead), codegen, `system`, schedules, and Blaxel's agents, models,
214
226
  tools, jobs and applications. The sync module is generated from the async one
215
227
  by `scripts/generate_dropin_sync.py`.
216
228
 
@@ -223,6 +235,21 @@ gives LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK and any framework
223
235
  that takes typed functions four sandbox tools. See
224
236
  https://withruntime.com/docs/frameworks.
225
237
 
238
+ ## Reliability and support
239
+
240
+ - **Status:** https://withruntime.com/status checks the API from outside every
241
+ two minutes and starts a real sandbox every ten, with every day's record
242
+ kept; also as https://withruntime.com/status.md and
243
+ https://withruntime.com/status.json.
244
+ - **Uptime Promise:** paid accounts are promised 99% API uptime
245
+ each calendar month, and a month below it pays 10% of that
246
+ month's charges back as credit, automatically
247
+ (https://withruntime.com/legal/sla).
248
+ - **Security and compliance:** https://withruntime.com/docs/trust.
249
+ - **What changed:** https://withruntime.com/changelog.
250
+ - **Help:** `npx withruntime support "<question>"`, or
251
+ `npx withruntime feedback "<text>"` for anything that got in your way.
252
+
226
253
  Docs: https://withruntime.com/docs/python.
227
254
 
228
255
  The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
@@ -48,11 +48,18 @@ A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `forward_port` (any
48
48
  TCP port in the sandbox on a local port), `files` (read, write,
49
49
  list, glob, stat, move, remove, upload and download directories), `pause`,
50
50
  `wake`, `extend`, `update`, `keep_alive`, `fork` and `snapshot`, and the
51
- `interpreter`, `network`, `previews` and `desktop` products. A paused sandbox
51
+ `interpreter`, `network`, `previews`, `desktop`, `mounts` (your S3, R2 or Google
52
+ Cloud Storage bucket as a folder), `metrics` and `mcp` (servers from the MCP
53
+ catalog, run in the sandbox) products. A paused sandbox
52
54
  also wakes by itself on the next call, and `Sandbox.get_or_create(name)` returns
53
- the sandbox with that name or creates it. The client has `sandboxes`, `images`,
54
- `volumes`, `snapshots`, `limits`, `feedback` and `support`;
55
- `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
55
+ the sandbox with that name or creates it. A sandbox pauses itself after 60 seconds
56
+ with nothing happening in it, keeping its memory and processes, so an
57
+ idle sandbox costs no compute; `idle_pause_seconds` sets 10 to 86,400, or 0 for
58
+ never. The client has `sandboxes`, `images`, `volumes`, `snapshots`,
59
+ `secrets`, `limits`, `feedback` and `support`; `webhooks`, `events`, `otel` and
60
+ `audit` to watch the account; `domains`, `ports`, `addresses`, `tunnel` and
61
+ `network` to connect sandboxes to your own world (paid accounts); and `mcp`,
62
+ `sso`, `billing`, `referrals` and `switching`. `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
56
63
  what its agent may still spend today. Every write carries an
57
64
  idempotency key, so retries never do anything twice; errors are typed and carry
58
65
  `code`, `hint` and `request_id`.
@@ -136,8 +143,8 @@ How it maps:
136
143
  (every `/` becomes `-`; the tag is `latest` when none is given), so the
137
144
  code keeps Blaxel's image string. A missing one raises `NotSupportedError`
138
145
  with the build command, for example `npx withruntime image build
139
- --dockerfile Dockerfile --name my-company-agent-image -t
140
- my-company-agent-image:latest`. Volumes mount Runtime volumes of the same
146
+ --dockerfile Dockerfile --name my-company-agent-image -t
147
+ my-company-agent-image:latest`. Volumes mount Runtime volumes of the same
141
148
  name. On the free trial a sandbox has at most 4096 MB of memory (2 vCPUs).
142
149
  - **Standby.** A sandbox pauses after a minute with no call (Blaxel: about 15
143
150
  seconds). It keeps its memory and processes, and wakes on the next command,
@@ -177,17 +184,22 @@ Differences you can hit:
177
184
  (`/workspace`). File calls act as the sandbox user and fall back to `sudo`
178
185
  where only root may: they reach files a root process made, including private
179
186
  ones, and a file they write is the sandbox user's.
180
- - A sandbox is visible only to the key that created it: another key of the
181
- same account gets 404 for it
187
+ - A key sees the sandboxes it made, and another key's answer 404, unless an
188
+ owner or admin makes the key account-wide, as a Blaxel workspace key is
182
189
  ([teams](https://withruntime.com/docs/teams#keys-in-a-team)).
183
190
  - Public previews need a paid sandbox.
184
191
  - A process's `pid` is Runtime's process id, not an operating-system number.
192
+ - `sandbox.sessions` makes a Runtime sandbox session, and
193
+ `SandboxInstance.from_session` drives the sandbox with its token: commands,
194
+ files and previews of that one sandbox. A session lasts a day at most, and
195
+ `Access-Control-Allow-Origin` in `response_headers` names the page that may
196
+ use it.
185
197
 
186
198
  These raise `NotSupportedError` before anything happens, naming what to use:
187
199
  regions outside the US, read-only and ephemeral volumes, drives, `extra_args`
188
200
  other than `iptables`, egress and subnet settings, preview headers, custom
189
201
  domains and preview expiry, restoring a snapshot in place (fork from it
190
- instead), sessions, codegen, `system`, schedules, and Blaxel's agents, models,
202
+ instead), codegen, `system`, schedules, and Blaxel's agents, models,
191
203
  tools, jobs and applications. The sync module is generated from the async one
192
204
  by `scripts/generate_dropin_sync.py`.
193
205
 
@@ -200,6 +212,21 @@ gives LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK and any framework
200
212
  that takes typed functions four sandbox tools. See
201
213
  https://withruntime.com/docs/frameworks.
202
214
 
215
+ ## Reliability and support
216
+
217
+ - **Status:** https://withruntime.com/status checks the API from outside every
218
+ two minutes and starts a real sandbox every ten, with every day's record
219
+ kept; also as https://withruntime.com/status.md and
220
+ https://withruntime.com/status.json.
221
+ - **Uptime Promise:** paid accounts are promised 99% API uptime
222
+ each calendar month, and a month below it pays 10% of that
223
+ month's charges back as credit, automatically
224
+ (https://withruntime.com/legal/sla).
225
+ - **Security and compliance:** https://withruntime.com/docs/trust.
226
+ - **What changed:** https://withruntime.com/changelog.
227
+ - **Help:** `npx withruntime support "<question>"`, or
228
+ `npx withruntime feedback "<text>"` for anything that got in your way.
229
+
203
230
  Docs: https://withruntime.com/docs/python.
204
231
 
205
232
  The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "withruntime"
7
- version = "0.8.1"
7
+ version = "0.8.3"
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" }
@@ -1053,7 +1053,7 @@ class Interpreter(Base):
1053
1053
  class Unsupported(Base):
1054
1054
  def test_every_gap_is_importable_and_names_its_alternative(self):
1055
1055
  box = self.create()
1056
- calls = [lambda: box.sessions.create(), lambda: box.codegen.fastapply("a", "b"), lambda: box.system.upgrade(),
1056
+ calls = [lambda: box.codegen.fastapply("a", "b"), lambda: box.system.upgrade(),
1057
1057
  lambda: box.drives.mount("d", "/d"), lambda: box.schedules.list(),
1058
1058
  lambda: SyncSandboxInstance.from_session({}), lambda: bl.BlAgent("x"), lambda: bl.bl_model("m"),
1059
1059
  lambda: bl.settings.region, lambda: bl.SyncVolumeInstance.create({}),
@@ -238,6 +238,17 @@ class Process(Base):
238
238
  with self.assertRaises(NotSupportedError):
239
239
  sandbox.process.create_pty_session()
240
240
 
241
+ def test_refusals_point_at_what_runtime_has(self):
242
+ sandbox = self.daytona.create()
243
+ for call, text in ((sandbox.update_secrets, "withruntime secrets set"),
244
+ (sandbox.get_metrics, "sandbox.withruntime.metrics("),
245
+ (sandbox.create_ssh_access, "withruntime sandbox ssh"),
246
+ (lambda: self.daytona.create(CreateSandboxFromSnapshotParams(secrets={"A": "s"})),
247
+ "withruntime secrets set")):
248
+ with self.assertRaises(NotSupportedError) as caught:
249
+ call()
250
+ self.assertIn(text, caught.exception.alternative)
251
+
241
252
 
242
253
  class Files(Base):
243
254
  def test_files(self):
@@ -380,6 +380,9 @@ class ForksPortsAndGaps(Base):
380
380
  lambda: e2b.Secret.create("s"), lambda: e2b.wait_for_port(3000)):
381
381
  with self.assertRaises(NotSupportedException):
382
382
  call()
383
+ with self.assertRaises(NotSupportedException) as caught:
384
+ e2b.Secret.create("s")
385
+ self.assertIn("withruntime secrets set", str(caught.exception))
383
386
 
384
387
 
385
388
  class Async(unittest.TestCase):
@@ -0,0 +1,76 @@
1
+ """A large write keeps eight chunks in flight, in both client forms.
2
+
3
+ Each chunk's reply waits on the API and the guest, so the link idles while
4
+ every chunk in flight waits: four in flight took 100 MB 17-18 s from a
5
+ 12.6 MB/s home link, eight took 11 s (27 September 2026)."""
6
+ import asyncio
7
+ import threading
8
+ import time
9
+ import unittest
10
+ from withruntime._async_client import AsyncFiles
11
+ from withruntime._sync_client import Files
12
+
13
+ MIB = 1048576
14
+
15
+
16
+ class Counter:
17
+ def __init__(self):
18
+ self.now = self.most = self.chunks = 0
19
+ self.lock = threading.Lock()
20
+
21
+ def enter(self):
22
+ with self.lock:
23
+ self.now += 1
24
+ self.chunks += 1
25
+ self.most = max(self.most, self.now)
26
+
27
+ def leave(self):
28
+ with self.lock:
29
+ self.now -= 1
30
+
31
+
32
+ class Transport:
33
+ def __init__(self):
34
+ self.count = Counter()
35
+
36
+ def json(self, method, path, body):
37
+ return {'uploadId': 'upload', 'chunkBytes': MIB} if path.endswith('/uploads') else {}
38
+
39
+ def send(self, method, path, **kwargs):
40
+ self.count.enter()
41
+ time.sleep(0.02)
42
+ self.count.leave()
43
+ return self
44
+
45
+ def read(self):
46
+ return b''
47
+
48
+
49
+ class AsyncTransport(Transport):
50
+ async def json(self, *args, **kwargs):
51
+ return super().json(*args, **kwargs)
52
+
53
+ async def send(self, method, path, **kwargs):
54
+ self.count.enter()
55
+ await asyncio.sleep(0.02)
56
+ self.count.leave()
57
+ return self
58
+
59
+ async def read(self):
60
+ return b''
61
+
62
+
63
+ class UploadParallel(unittest.TestCase):
64
+ def test_sync_keeps_eight_chunks_in_flight(self):
65
+ transport = Transport()
66
+ Files(transport, 'sandbox').write('/workspace/big.bin', b'x' * (16 * MIB))
67
+ self.assertEqual((transport.count.chunks, transport.count.most), (16, 8))
68
+
69
+ def test_async_keeps_eight_chunks_in_flight(self):
70
+ transport = AsyncTransport()
71
+ asyncio.run(AsyncFiles(transport, 'sandbox').write('/workspace/big.bin', b'x' * (16 * MIB)))
72
+ self.assertEqual((transport.count.chunks, transport.count.most), (16, 8))
73
+
74
+
75
+ if __name__ == '__main__':
76
+ unittest.main()
@@ -0,0 +1,165 @@
1
+ """runtime.jobs and the jobs copy of runtime.secrets against a stub API, sync and
2
+ async. The routes run over Postgres in packages/cloud (product-api.test.ts)."""
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, InvalidRequestError, Runtime, SecretPartlyStoredError,
10
+ ServiceUnavailableError)
11
+
12
+ JOB_ID = "11111111-1111-4111-8111-111111111111"
13
+ RUN_ID = "22222222-2222-4222-8222-222222222222"
14
+ SECRET_ID = "33333333-3333-4333-8333-333333333333"
15
+ JOB = {"id": JOB_ID, "name": "nightly", "state": "active", "nextRunAt": 1}
16
+ RUN = {"id": RUN_ID, "jobId": JOB_ID, "state": "succeeded", "exitCode": 0}
17
+ COPY = {"id": SECRET_ID, "name": "DB_PASSWORD", "version": 2, "deletedAt": None}
18
+ EGRESS = {"name": "API_TOKEN", "hosts": ["api.example.com"], "placeholder": "rtsec_x", "enforced": True}
19
+
20
+
21
+ class Stub(BaseHTTPRequestHandler):
22
+ seen: list = []
23
+ mode = "ok"
24
+ copies: list = []
25
+
26
+ def log_message(self, *args):
27
+ pass
28
+
29
+ def answer(self, value, status=200):
30
+ data = json.dumps(value).encode()
31
+ self.send_response(status)
32
+ self.send_header("content-type", "application/json")
33
+ self.send_header("content-length", str(len(data)))
34
+ self.end_headers()
35
+ self.wfile.write(data)
36
+
37
+ def handle_any(self):
38
+ length = int(self.headers.get("content-length") or 0)
39
+ body = json.loads(self.rfile.read(length)) if length else None
40
+ path = self.path.split("?")[0]
41
+ Stub.seen.append((self.command, self.path, body, self.headers.get("idempotency-key")))
42
+ if Stub.mode == "off" or (Stub.mode == "jobs-off" and path.startswith("/v1/secrets")):
43
+ return self.answer({"error": {"code": "unavailable", "message": "This product is not enabled on this "
44
+ "deployment.", "hint": "not yet"}}, 503)
45
+ if path == "/v1/jobs" and self.command == "GET":
46
+ return self.answer({"items": [JOB], "nextCursor": None})
47
+ if path.endswith("/runs"):
48
+ return self.answer({"items": [RUN], "nextCursor": None})
49
+ if path.endswith("/logs"):
50
+ return self.answer({"chunks": [{"offset": 0, "stream": "stdout", "text": "hi\n"}], "nextCursor": 3,
51
+ "truncated": False, "complete": True})
52
+ if path.startswith("/v1/job-runs/"):
53
+ return self.answer(RUN)
54
+ if path.startswith("/v1/jobs"):
55
+ return self.answer(JOB)
56
+ if path == "/v1/secrets" and self.command == "GET":
57
+ return self.answer(Stub.copies)
58
+ if path.endswith(":reveal"):
59
+ return self.answer({"id": SECRET_ID, "version": 2, "value": "hunter2"})
60
+ if path.startswith("/v1/secrets"):
61
+ return self.answer({**COPY, "version": 1})
62
+ if path.startswith("/v1/egress-secrets"):
63
+ return self.answer(EGRESS if self.command == "PUT" else {"name": "x", "deleted": True, "enforced": True})
64
+ self.answer({"error": {"code": "not_found", "message": "no"}}, 404)
65
+
66
+ do_GET = do_POST = do_PUT = do_DELETE = handle_any
67
+
68
+
69
+ class JobsTest(unittest.TestCase):
70
+ @classmethod
71
+ def setUpClass(cls):
72
+ cls.server = ThreadingHTTPServer(("127.0.0.1", 0), Stub)
73
+ threading.Thread(target=cls.server.serve_forever, daemon=True).start()
74
+ cls.url = f"http://127.0.0.1:{cls.server.server_address[1]}"
75
+
76
+ @classmethod
77
+ def tearDownClass(cls):
78
+ cls.server.shutdown()
79
+
80
+ def setUp(self):
81
+ Stub.seen, Stub.mode, Stub.copies = [], "ok", []
82
+
83
+ def client(self):
84
+ return Runtime(api_key="rk_test", base_url=self.url, max_retries=2)
85
+
86
+ def test_create_fills_defaults_and_every_verb_calls_its_route(self):
87
+ with self.client() as runtime:
88
+ runtime.jobs.create("nightly", cron="0 3 * * *", timezone="Europe/Berlin", command=["echo", "hi"])
89
+ self.assertEqual([j["id"] for j in runtime.jobs.list()], [JOB_ID])
90
+ self.assertEqual(runtime.jobs.runs(JOB_ID).data[0]["id"], RUN_ID)
91
+ self.assertEqual(runtime.jobs.run(RUN_ID)["state"], "succeeded")
92
+ self.assertTrue(runtime.jobs.logs(RUN_ID, cursor=0)["complete"])
93
+ runtime.jobs.pause(JOB_ID)
94
+ runtime.jobs.resume(JOB_ID)
95
+ runtime.jobs.cancel(JOB_ID)
96
+ first = Stub.seen[0]
97
+ self.assertEqual(first[:2], ("POST", "/v1/jobs"))
98
+ self.assertTrue(first[3])
99
+ self.assertEqual(first[2]["compute"], {"region": "us-east-vin", "vcpu": 2, "cpuMode": "shared",
100
+ "cpuFloorMillis": 50, "memoryMiB": 4096, "diskMiB": 4096,
101
+ "durationSeconds": 1860})
102
+ self.assertEqual(first[2]["schedule"], {"kind": "cron", "expression": "0 3 * * *",
103
+ "timezone": "Europe/Berlin"})
104
+ self.assertEqual([s[1].split("?")[0] for s in Stub.seen[1:]], [
105
+ "/v1/jobs", f"/v1/jobs/{JOB_ID}/runs", f"/v1/job-runs/{RUN_ID}", f"/v1/job-runs/{RUN_ID}/logs",
106
+ f"/v1/jobs/{JOB_ID}:pause", f"/v1/jobs/{JOB_ID}:resume", f"/v1/jobs/{JOB_ID}:cancel"])
107
+
108
+ def test_mistakes_are_refused_before_sending(self):
109
+ with self.client() as runtime:
110
+ for kwargs, pattern in [
111
+ ({"at": 1, "compute": {"vcpu": 32}}, "at most 16 vCPU"),
112
+ ({"at": 1, "compute": {"diskMiB": 512}}, "at least 3072 MiB"),
113
+ ({"cron": "0 3 * *"}, "five fields"),
114
+ ({"at": "tomorrow"}, "must be a time"),
115
+ ({}, "at= for one run or cron="),
116
+ ({"at": 1, "timezone": "UTC"}, "timezone goes with cron"),
117
+ ]:
118
+ with self.assertRaisesRegex(InvalidRequestError, pattern):
119
+ runtime.jobs.create("x", command=["true"], **kwargs)
120
+ self.assertEqual(Stub.seen, [])
121
+
122
+ def test_switched_off_names_the_product_and_is_not_retried(self):
123
+ Stub.mode = "off"
124
+ with self.client() as runtime:
125
+ with self.assertRaises(ServiceUnavailableError) as caught:
126
+ runtime.jobs.list()
127
+ self.assertEqual(caught.exception.message, "Jobs are not enabled on this Runtime API yet.")
128
+ self.assertIn("retrying will not help", caught.exception.hint)
129
+ self.assertFalse(caught.exception.retryable)
130
+ self.assertEqual(len(Stub.seen), 1)
131
+
132
+ def test_secret_jobs_copy_set_rotate_reveal_delete(self):
133
+ with self.client() as runtime:
134
+ saved = runtime.secrets.set("DB_PASSWORD", value="hunter2", jobs=True)
135
+ self.assertEqual(saved["jobs"]["version"], 1)
136
+ Stub.copies = [COPY]
137
+ runtime.secrets.set("DB_PASSWORD", value="new", jobs=True)
138
+ self.assertEqual(runtime.secrets.reveal("DB_PASSWORD")["value"], "hunter2")
139
+ self.assertEqual(runtime.secrets.delete("DB_PASSWORD")["from"], ["sandboxes", "jobs"])
140
+ lines = [(s[0], s[1].split("?")[0]) for s in Stub.seen]
141
+ self.assertEqual(lines, [
142
+ ("GET", "/v1/secrets"), ("POST", "/v1/secrets"),
143
+ ("GET", "/v1/secrets"), ("POST", f"/v1/secrets/{SECRET_ID}:rotate"),
144
+ ("GET", "/v1/secrets"), ("POST", f"/v1/secrets/{SECRET_ID}:reveal"),
145
+ ("GET", "/v1/secrets"), ("DELETE", "/v1/egress-secrets/DB_PASSWORD"),
146
+ ("POST", f"/v1/secrets/{SECRET_ID}:delete")])
147
+ self.assertEqual(Stub.seen[3][2], {"value": "new", "expectedVersion": 2})
148
+
149
+ def test_both_copies_report_a_partial_store_and_the_retry_finishes(self):
150
+ Stub.mode = "jobs-off"
151
+
152
+ async def go():
153
+ async with AsyncRuntime(api_key="rk_test", base_url=self.url, max_retries=0) as runtime:
154
+ with self.assertRaises(SecretPartlyStoredError) as caught:
155
+ await runtime.secrets.set("API_TOKEN", value="t", hosts=["api.example.com"], jobs=True)
156
+ self.assertEqual(caught.exception.details["stored"], ["sandboxes"])
157
+ Stub.mode = "ok"
158
+ saved = await runtime.secrets.set("API_TOKEN", value="t", hosts=["api.example.com"], jobs=True)
159
+ self.assertEqual(saved["placeholder"], "rtsec_x")
160
+ self.assertEqual(saved["jobs"]["version"], 1)
161
+ asyncio.run(go())
162
+
163
+
164
+ if __name__ == "__main__":
165
+ unittest.main()
@@ -0,0 +1,149 @@
1
+ """Sandbox sessions (ARCHITECTURE.md section 3.12) against a stub API: the
2
+ backend makes, lists and revokes them with ``sbx.sessions``; a page's code
3
+ drives the sandbox with ``Sandbox.from_session`` and the token alone; and the
4
+ Blaxel adapter's ``sandbox.sessions`` and ``from_session`` map onto them. The
5
+ routes are tested in packages/cloud (sandbox-sessions-postgres.test.ts)."""
6
+ import asyncio
7
+ import json
8
+ import threading
9
+ import unittest
10
+ from datetime import datetime, timedelta, timezone
11
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
12
+
13
+ import withruntime.blaxel as bl
14
+ from withruntime import AsyncSandbox, Runtime, Sandbox
15
+ from withruntime.blaxel import NotSupportedError, SyncSandboxInstance
16
+
17
+ ID = "8a1f9c2e-0d1b-4c3a-9e8f-7a6b5c4d3e2f"
18
+
19
+
20
+ class Stub(BaseHTTPRequestHandler):
21
+ seen: list = []
22
+ sessions: list = []
23
+ url = ""
24
+
25
+ def log_message(self, *args):
26
+ pass
27
+
28
+ def answer(self, value, status=200):
29
+ data = json.dumps(value).encode()
30
+ self.send_response(status)
31
+ self.send_header("content-type", "application/json")
32
+ self.send_header("content-length", str(len(data)))
33
+ self.end_headers()
34
+ self.wfile.write(data)
35
+
36
+ def handle_any(self):
37
+ length = int(self.headers.get("content-length") or 0)
38
+ body = json.loads(self.rfile.read(length) or b"null") if length else None
39
+ path = self.path.split("?")[0]
40
+ Stub.seen.append((self.command, path, self.headers.get("authorization"), body))
41
+ if path == f"/v1/sandboxes/{ID}/sessions" and self.command == "POST":
42
+ sid = f"00000000-0000-4000-8000-{len(Stub.sessions):012d}"
43
+ made = {"id": sid, "sandboxId": ID, "name": None, "origins": body.get("origins", []),
44
+ "createdBy": "agent", "createdAt": "2026-09-28T10:00:00Z",
45
+ "expiresAt": (datetime.now(timezone.utc)
46
+ + timedelta(seconds=body.get("ttlSeconds", 3600))).isoformat(),
47
+ "revokedAt": None, "state": "active"}
48
+ Stub.sessions.insert(0, made)
49
+ return self.answer({**made, "token": f"rtsess_{sid}_{'t' * 43}", "apiUrl": Stub.url})
50
+ if path == f"/v1/sandboxes/{ID}/sessions":
51
+ return self.answer({"data": Stub.sessions, "nextCursor": None})
52
+ if path.endswith(":revoke"):
53
+ sid = path.split("/")[-1].split(":")[0]
54
+ for one in Stub.sessions:
55
+ if one["id"] == sid:
56
+ one.update(state="revoked", revokedAt="2026-09-28T10:30:00Z")
57
+ return self.answer(one)
58
+ return self.answer({"error": {"code": "not_found", "status": 404, "message": "x"}}, 404)
59
+ if path.endswith(":exec"):
60
+ return self.answer({"exitCode": 0, "stdout": "hi\n", "stderr": "", "timedOut": False})
61
+ return self.answer({"id": ID, "name": "web", "state": "running", "labels": {}, "autoWake": True,
62
+ "memoryMiB": 4096, "createdAt": "2026-09-28T10:00:00Z",
63
+ "expiresAt": "2026-09-28T11:00:00Z", "onLeaseEnd": "stop"})
64
+
65
+ do_GET = do_POST = do_PUT = handle_any
66
+
67
+
68
+ class SessionsTest(unittest.TestCase):
69
+ @classmethod
70
+ def setUpClass(cls):
71
+ cls.server = ThreadingHTTPServer(("127.0.0.1", 0), Stub)
72
+ threading.Thread(target=cls.server.serve_forever, daemon=True).start()
73
+ cls.url = Stub.url = f"http://127.0.0.1:{cls.server.server_address[1]}"
74
+
75
+ @classmethod
76
+ def tearDownClass(cls):
77
+ cls.server.shutdown()
78
+
79
+ def setUp(self):
80
+ Stub.seen, Stub.sessions = [], []
81
+
82
+ def test_backend_makes_lists_and_revokes(self):
83
+ with Runtime(api_key="rtcloud_k", base_url=self.url, max_retries=0) as runtime:
84
+ sbx = runtime.sandboxes.get(ID)
85
+ made = sbx.sessions.create(ttl_seconds=900, origins=["https://app.example.com"], name="u1")
86
+ self.assertTrue(made["token"].startswith("rtsess_"))
87
+ self.assertEqual(Stub.seen[-1][3], {"ttlSeconds": 900, "origins": ["https://app.example.com"],
88
+ "name": "u1"})
89
+ self.assertEqual([one["id"] for one in sbx.sessions.list()], [made["id"]])
90
+ self.assertEqual(sbx.sessions.revoke(made["id"])["state"], "revoked")
91
+
92
+ def test_page_uses_the_token_alone(self):
93
+ token = f"rtsess_{'0' * 8}-0000-4000-8000-{'0' * 12}_{'t' * 43}"
94
+ sbx = Sandbox.from_session(token, ID, self.url)
95
+ self.assertEqual(Stub.seen, [])
96
+ self.assertEqual(sbx.exec("echo hi").stdout, "hi\n")
97
+ self.assertEqual(Stub.seen[-1][2], f"Bearer {token}")
98
+ with self.assertRaises(Exception):
99
+ Sandbox.from_session("rtcloud_key", ID, self.url)
100
+
101
+ async def go():
102
+ page = AsyncSandbox.from_session(token, ID, self.url)
103
+ return (await page.exec("echo hi")).stdout
104
+
105
+ self.assertEqual(asyncio.run(go()), "hi\n")
106
+
107
+ def test_blaxel_sessions_and_from_session(self):
108
+ runtime = Runtime(api_key="rtcloud_k", base_url=self.url, max_retries=0)
109
+ bl.use_client(runtime)
110
+ try:
111
+ box = SyncSandboxInstance(_runtime=runtime.sandboxes.get(ID))
112
+ session = box.sessions.create({
113
+ "expires_at": datetime.now(timezone.utc) + timedelta(minutes=15),
114
+ "response_headers": {"Access-Control-Allow-Origin": "https://app.example.com",
115
+ "Access-Control-Allow-Methods": "GET"}})
116
+ self.assertTrue(session.token.startswith("rtsess_"))
117
+ self.assertEqual(session.url, f"{self.url}/v1/sandboxes/{ID}")
118
+ self.assertEqual(Stub.seen[-1][3]["origins"], ["https://app.example.com"])
119
+ self.assertIn(Stub.seen[-1][3]["ttlSeconds"], (899, 900))
120
+ box.sessions.create() # Blaxel's default: a day, Runtime's most.
121
+ self.assertEqual(Stub.seen[-1][3]["ttlSeconds"], 86_400)
122
+ self.assertEqual(len(box.sessions.list()), 2)
123
+ self.assertEqual(box.sessions.get(session.name)["url"], session.url)
124
+ box.sessions.delete(session.name)
125
+ self.assertNotIn(session.name, [one.name for one in box.sessions.list()])
126
+ kept = box.sessions.create_if_expired({}, delta_seconds=60)
127
+ self.assertEqual(box.sessions.create_if_expired({}, delta_seconds=60).token, kept.token)
128
+
129
+ page = SyncSandboxInstance.from_session(session)
130
+ self.assertEqual(page.metadata.name, "web")
131
+ self.assertTrue(any(auth == f"Bearer {session.token}" for _, _, auth, _ in Stub.seen))
132
+
133
+ before = len(Stub.seen)
134
+ for options in ({"response_headers": {"Access-Control-Allow-Origin": "*"}},
135
+ {"response_headers": {"X-Frame-Options": "DENY"}},
136
+ {"request_headers": {"Authorization": "x"}},
137
+ {"expires_at": datetime.now(timezone.utc) + timedelta(days=2)}):
138
+ with self.assertRaises(NotSupportedError):
139
+ box.sessions.create(options)
140
+ self.assertEqual(len(Stub.seen), before)
141
+ with self.assertRaises(NotSupportedError):
142
+ SyncSandboxInstance.from_session({"url": "https://blaxel.dev/x", "token": "bl_x"})
143
+ finally:
144
+ bl.use_client(None)
145
+ runtime.close()
146
+
147
+
148
+ if __name__ == "__main__":
149
+ unittest.main()
@@ -39,7 +39,8 @@ class Stub(BaseHTTPRequestHandler):
39
39
 
40
40
  def do_GET(self):
41
41
  Stub.seen.append((self.command, self.path, None))
42
- self.answer({"secrets": [SECRET]})
42
+ # The jobs copies: none here.
43
+ self.answer([] if self.path.startswith("/v1/secrets") else {"secrets": [SECRET]})
43
44
 
44
45
  def do_PUT(self):
45
46
  Stub.seen.append((self.command, self.path, self.body()))
@@ -75,6 +76,7 @@ class SecretsTest(unittest.TestCase):
75
76
  ("PUT", "/v1/egress-secrets/OPENAI_API_KEY", {"value": "sk-live", "hosts": ["api.openai.com"],
76
77
  "header": "Authorization", "format": "Bearer {value}"}),
77
78
  ("GET", "/v1/egress-secrets", None),
79
+ ("GET", "/v1/secrets?limit=100", None),
78
80
  ("DELETE", "/v1/egress-secrets/OPENAI_API_KEY", None),
79
81
  ])
80
82
 
@@ -95,8 +95,9 @@ class Create(Base):
95
95
  self.assertEqual(network_rules(NetworkPolicy.deny_all()), {"internet": False})
96
96
  self.assertEqual(network_rules(NetworkPolicy.allow_all()), {"internet": True})
97
97
  self.assertEqual(network_rules(NetworkPolicy.custom()), {"internet": False})
98
- with self.assertRaises(NotSupportedError):
98
+ with self.assertRaises(NotSupportedError) as caught:
99
99
  network_rules(NetworkPolicy.custom(allow={"api.github.com": ["rule"]}))
100
+ self.assertIn("withruntime secrets set NAME --host api.github.com", caught.exception.alternative)
100
101
 
101
102
 
102
103
  class Keys(unittest.TestCase):