withruntime 0.8.2__tar.gz → 0.8.4__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.2/withruntime.egg-info → withruntime-0.8.4}/PKG-INFO +30 -6
  2. {withruntime-0.8.2 → withruntime-0.8.4}/README.md +29 -5
  3. {withruntime-0.8.2 → withruntime-0.8.4}/pyproject.toml +1 -1
  4. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_blaxel.py +1 -1
  5. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_daytona.py +11 -0
  6. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_e2b_sandbox.py +3 -0
  7. withruntime-0.8.4/tests/test_jobs.py +165 -0
  8. withruntime-0.8.4/tests/test_sandbox_sessions.py +149 -0
  9. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_secrets.py +3 -1
  10. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_vercel.py +2 -1
  11. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/__init__.py +2 -2
  12. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_client.py +66 -1
  13. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/__init__.py +2 -0
  14. withruntime-0.8.4/withruntime/_async_products/jobs.py +230 -0
  15. withruntime-0.8.4/withruntime/_async_products/secrets.py +197 -0
  16. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_errors.py +5 -0
  17. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_client.py +66 -1
  18. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/__init__.py +2 -0
  19. withruntime-0.8.4/withruntime/_sync_products/jobs.py +231 -0
  20. withruntime-0.8.4/withruntime/_sync_products/secrets.py +198 -0
  21. withruntime-0.8.4/withruntime/_version.py +1 -0
  22. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/__init__.py +7 -3
  23. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_async_sandbox.py +81 -3
  24. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_core.py +83 -3
  25. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_sync_sandbox.py +81 -3
  26. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_async_daytona.py +3 -3
  27. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_core.py +2 -2
  28. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_sync_daytona.py +3 -3
  29. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/__init__.py +1 -1
  30. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_core.py +1 -1
  31. {withruntime-0.8.2 → withruntime-0.8.4/withruntime.egg-info}/PKG-INFO +30 -6
  32. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/SOURCES.txt +4 -0
  33. withruntime-0.8.2/withruntime/_async_products/secrets.py +0 -58
  34. withruntime-0.8.2/withruntime/_sync_products/secrets.py +0 -59
  35. withruntime-0.8.2/withruntime/_version.py +0 -1
  36. {withruntime-0.8.2 → withruntime-0.8.4}/LICENSE +0 -0
  37. {withruntime-0.8.2 → withruntime-0.8.4}/setup.cfg +0 -0
  38. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_account_errors.py +0 -0
  39. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_async_stream_cleanup.py +0 -0
  40. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_audit.py +0 -0
  41. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_billing.py +0 -0
  42. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_capacity_wait.py +0 -0
  43. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_client.py +0 -0
  44. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_deepagents.py +0 -0
  45. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_e2b_code_interpreter.py +0 -0
  46. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_eval_sandbox.py +0 -0
  47. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_exec_stream.py +0 -0
  48. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_files_download.py +0 -0
  49. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_files_mode.py +0 -0
  50. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_files_upload_parallel.py +0 -0
  51. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_harbor.py +0 -0
  52. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_http_tls_close.py +0 -0
  53. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_identity_token.py +0 -0
  54. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_images_v2.py +0 -0
  55. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_images_volumes_interpreter.py +0 -0
  56. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_inspect_ai.py +0 -0
  57. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_late_answer.py +0 -0
  58. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_lifecycle_settings.py +0 -0
  59. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_limits.py +0 -0
  60. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_mounts.py +0 -0
  61. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_network_funding.py +0 -0
  62. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_network_products.py +0 -0
  63. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_observability.py +0 -0
  64. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_openai_agents.py +0 -0
  65. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_previews_network_desktop.py +0 -0
  66. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_proxy.py +0 -0
  67. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_referrals.py +0 -0
  68. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_snapshot_after_fork.py +0 -0
  69. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_sso.py +0 -0
  70. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_stream_exit_close.py +0 -0
  71. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_switching.py +0 -0
  72. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_tools.py +0 -0
  73. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_unpack.py +0 -0
  74. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_volume_backups.py +0 -0
  75. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_wake_awake.py +0 -0
  76. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_wallet.py +0 -0
  77. {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_watch.py +0 -0
  78. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/audit.py +0 -0
  79. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/billing.py +0 -0
  80. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/desktop.py +0 -0
  81. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/images.py +0 -0
  82. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/interpreter.py +0 -0
  83. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/limits.py +0 -0
  84. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/mcp.py +0 -0
  85. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/mounts.py +0 -0
  86. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/network.py +0 -0
  87. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/network_products.py +0 -0
  88. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/observability.py +0 -0
  89. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/previews.py +0 -0
  90. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/referrals.py +0 -0
  91. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/sso.py +0 -0
  92. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/switching.py +0 -0
  93. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/volumes.py +0 -0
  94. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/watch.py +0 -0
  95. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_clock.py +0 -0
  96. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_connection.py +0 -0
  97. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_eval_sandbox.py +0 -0
  98. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_http.py +0 -0
  99. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_proxy.py +0 -0
  100. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/audit.py +0 -0
  101. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/billing.py +0 -0
  102. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/desktop.py +0 -0
  103. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/images.py +0 -0
  104. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/interpreter.py +0 -0
  105. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/limits.py +0 -0
  106. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/mcp.py +0 -0
  107. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/mounts.py +0 -0
  108. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/network.py +0 -0
  109. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/network_products.py +0 -0
  110. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/observability.py +0 -0
  111. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/previews.py +0 -0
  112. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/referrals.py +0 -0
  113. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/sso.py +0 -0
  114. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/switching.py +0 -0
  115. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/volumes.py +0 -0
  116. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/watch.py +0 -0
  117. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_tunnel.py +0 -0
  118. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_unpack.py +0 -0
  119. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_ws.py +0 -0
  120. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_async_io.py +0 -0
  121. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_sync_io.py +0 -0
  122. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/__init__.py +0 -0
  123. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/agents.py +0 -0
  124. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/__init__.py +0 -0
  125. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/__init__.py +0 -0
  126. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/applications/__init__.py +0 -0
  127. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/applications/list_application_revisions.py +0 -0
  128. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/compute/__init__.py +0 -0
  129. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/compute/get_sandbox.py +0 -0
  130. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/compute/update_sandbox.py +0 -0
  131. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/functions/__init__.py +0 -0
  132. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/functions/create_function.py +0 -0
  133. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/images.py +0 -0
  134. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/workspaces.py +0 -0
  135. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/client.py +0 -0
  136. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/__init__.py +0 -0
  137. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/create_job_execution_request.py +0 -0
  138. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/create_job_execution_request_env.py +0 -0
  139. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/env.py +0 -0
  140. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/function.py +0 -0
  141. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/function_runtime.py +0 -0
  142. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/function_spec.py +0 -0
  143. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/metadata.py +0 -0
  144. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/workspace.py +0 -0
  145. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/types.py +0 -0
  146. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/common.py +0 -0
  147. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/drive.py +0 -0
  148. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/jobs.py +0 -0
  149. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/sandbox/__init__.py +0 -0
  150. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/sandbox/types.py +0 -0
  151. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/crewai.py +0 -0
  152. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/googleadk.py +0 -0
  153. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/langgraph.py +0 -0
  154. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/livekit.py +0 -0
  155. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/llamaindex.py +0 -0
  156. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/openai.py +0 -0
  157. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/pydantic.py +0 -0
  158. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/telemetry.py +0 -0
  159. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/__init__.py +0 -0
  160. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_async_io.py +0 -0
  161. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_sync_io.py +0 -0
  162. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/deepagents.py +0 -0
  163. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_async_io.py +0 -0
  164. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_async_sandbox.py +0 -0
  165. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_core.py +0 -0
  166. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_sync_io.py +0 -0
  167. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_sync_sandbox.py +0 -0
  168. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/__init__.py +0 -0
  169. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
  170. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/_models.py +0 -0
  171. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
  172. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/harbor.py +0 -0
  173. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/inspect_ai.py +0 -0
  174. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/openai_agents.py +0 -0
  175. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/tools.py +0 -0
  176. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/__init__.py +0 -0
  177. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_async_io.py +0 -0
  178. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_async_sandbox.py +0 -0
  179. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_sync_io.py +0 -0
  180. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_sync_sandbox.py +0 -0
  181. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/api.py +0 -0
  182. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/sandbox/__init__.py +0 -0
  183. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/sandbox/sync.py +0 -0
  184. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/wallet.py +0 -0
  185. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/webhooks.py +0 -0
  186. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/dependency_links.txt +0 -0
  187. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/entry_points.txt +0 -0
  188. {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/requires.txt +0 -0
  189. {withruntime-0.8.2 → withruntime-0.8.4}/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.2
3
+ Version: 0.8.4
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,14 +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
78
  the sandbox with that name or creates it. A sandbox pauses itself after 60 seconds
77
79
  with nothing happening in it, keeping its memory and processes, so an
78
80
  idle sandbox costs no compute; `idle_pause_seconds` sets 10 to 86,400, or 0 for
79
- never. The client has `sandboxes`, `images`,
80
- `volumes`, `snapshots`, `limits`, `feedback` and `support`;
81
- `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
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
82
86
  what its agent may still spend today. Every write carries an
83
87
  idempotency key, so retries never do anything twice; errors are typed and carry
84
88
  `code`, `hint` and `request_id`.
@@ -208,12 +212,17 @@ Differences you can hit:
208
212
  ([teams](https://withruntime.com/docs/teams#keys-in-a-team)).
209
213
  - Public previews need a paid sandbox.
210
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.
211
220
 
212
221
  These raise `NotSupportedError` before anything happens, naming what to use:
213
222
  regions outside the US, read-only and ephemeral volumes, drives, `extra_args`
214
223
  other than `iptables`, egress and subnet settings, preview headers, custom
215
224
  domains and preview expiry, restoring a snapshot in place (fork from it
216
- instead), sessions, codegen, `system`, schedules, and Blaxel's agents, models,
225
+ instead), codegen, `system`, schedules, and Blaxel's agents, models,
217
226
  tools, jobs and applications. The sync module is generated from the async one
218
227
  by `scripts/generate_dropin_sync.py`.
219
228
 
@@ -226,6 +235,21 @@ gives LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK and any framework
226
235
  that takes typed functions four sandbox tools. See
227
236
  https://withruntime.com/docs/frameworks.
228
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
+
229
253
  Docs: https://withruntime.com/docs/python.
230
254
 
231
255
  The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
@@ -48,14 +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
55
  the sandbox with that name or creates it. A sandbox pauses itself after 60 seconds
54
56
  with nothing happening in it, keeping its memory and processes, so an
55
57
  idle sandbox costs no compute; `idle_pause_seconds` sets 10 to 86,400, or 0 for
56
- never. The client has `sandboxes`, `images`,
57
- `volumes`, `snapshots`, `limits`, `feedback` and `support`;
58
- `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
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
59
63
  what its agent may still spend today. Every write carries an
60
64
  idempotency key, so retries never do anything twice; errors are typed and carry
61
65
  `code`, `hint` and `request_id`.
@@ -185,12 +189,17 @@ Differences you can hit:
185
189
  ([teams](https://withruntime.com/docs/teams#keys-in-a-team)).
186
190
  - Public previews need a paid sandbox.
187
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.
188
197
 
189
198
  These raise `NotSupportedError` before anything happens, naming what to use:
190
199
  regions outside the US, read-only and ephemeral volumes, drives, `extra_args`
191
200
  other than `iptables`, egress and subnet settings, preview headers, custom
192
201
  domains and preview expiry, restoring a snapshot in place (fork from it
193
- instead), sessions, codegen, `system`, schedules, and Blaxel's agents, models,
202
+ instead), codegen, `system`, schedules, and Blaxel's agents, models,
194
203
  tools, jobs and applications. The sync module is generated from the async one
195
204
  by `scripts/generate_dropin_sync.py`.
196
205
 
@@ -203,6 +212,21 @@ gives LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK and any framework
203
212
  that takes typed functions four sandbox tools. See
204
213
  https://withruntime.com/docs/frameworks.
205
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
+
206
230
  Docs: https://withruntime.com/docs/python.
207
231
 
208
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.2"
7
+ version = "0.8.4"
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,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", "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):
@@ -13,7 +13,7 @@ from ._async_client import (AsyncFeedback, AsyncFiles, AsyncPage, AsyncProcess,
13
13
  AsyncSandboxes, AsyncSnapshots, AsyncSupport, AsyncTerminal, CommandResult)
14
14
  from ._errors import (AccountBlockedError, AuthenticationError, CommandError, ConflictError, ConnectionError,
15
15
  InvalidRequestError, NotFoundError, PermissionDeniedError, RateLimitError, RuntimeError,
16
- ServiceUnavailableError)
16
+ SecretPartlyStoredError, ServiceUnavailableError)
17
17
  from ._sync_client import Feedback, Files, Page, Process, Runtime, Sandboxes, Snapshots, Support, Terminal
18
18
  from ._sync_client import Sandbox as _Sandbox
19
19
  from ._tunnel import AsyncPortForward, PortForward
@@ -98,6 +98,6 @@ __all__ = [
98
98
  "Runtime", "AsyncRuntime", "Sandbox", "AsyncSandbox", "Sandboxes", "AsyncSandboxes", "Files", "AsyncFiles",
99
99
  "Process", "AsyncProcess", "Terminal", "AsyncTerminal", "Page", "AsyncPage", "Feedback", "AsyncFeedback",
100
100
  "Support", "AsyncSupport", "Snapshots", "AsyncSnapshots", "CommandResult", "RuntimeError", "AuthenticationError", "PermissionDeniedError",
101
- "NotFoundError", "ConflictError", "InvalidRequestError", "RateLimitError", "ServiceUnavailableError", "AccountBlockedError",
101
+ "NotFoundError", "ConflictError", "InvalidRequestError", "RateLimitError", "ServiceUnavailableError", "AccountBlockedError", "SecretPartlyStoredError",
102
102
  "ConnectionError", "CommandError", "verify_webhook", "WebhookVerificationError", "PortForward", "AsyncPortForward", "VERSION", "__version__",
103
103
  ]
@@ -778,6 +778,57 @@ class AsyncFiles:
778
778
  pass
779
779
 
780
780
 
781
+ class AsyncSandboxSessions:
782
+ """Sessions: a short-lived token your backend makes and hands to your own
783
+ frontend, which then runs commands, uses files and reaches previews in this
784
+ one sandbox directly. A session cannot stop, pause, extend, fork, snapshot
785
+ or change the sandbox, create anything, or reach anything else. It lasts an
786
+ hour unless asked (a day at most), ends when the key that made it is
787
+ revoked, and is revocable at once."""
788
+
789
+ def __init__(self, t: _Transport, sandbox_id: str) -> None:
790
+ self._t, self._id = t, sandbox_id
791
+
792
+ def _path(self, suffix: str = "") -> str:
793
+ return f"/v1/sandboxes/{_enc(self._id)}/sessions{suffix}"
794
+
795
+ async def create(self, ttl_seconds: Optional[int] = None, origins: Optional[list[str]] = None,
796
+ name: Optional[str] = None, idempotency_key: Optional[str] = None) -> dict[str, Any]:
797
+ """A new session. ``origins`` are the exact pages that will use it
798
+ (``https://app.example.com``, or ``http://localhost:5173`` while
799
+ developing): the API answers CORS for them and refuses other pages.
800
+ ``token`` is in this answer only."""
801
+ body: dict[str, Any] = {}
802
+ if ttl_seconds is not None:
803
+ body["ttlSeconds"] = ttl_seconds
804
+ if origins is not None:
805
+ body["origins"] = list(origins)
806
+ if name is not None:
807
+ body["name"] = name
808
+ made = await self._t.json("POST", self._path(), body=body, idempotency_key=idempotency_key)
809
+ if made.get("token"):
810
+ return made
811
+ # A replay of a create whose answer was lost: a token is shown once, so
812
+ # end that session and make a fresh one.
813
+ try:
814
+ await self.revoke(made["id"])
815
+ except RuntimeError:
816
+ pass
817
+ fresh = await self._t.json("POST", self._path(), body=body)
818
+ if not fresh.get("token"):
819
+ raise RuntimeError("The session was made but its token did not arrive.", code="session_token_lost",
820
+ status=0, hint=f"Revoke session {fresh['id']} and create another.")
821
+ return fresh
822
+
823
+ async def list(self) -> list[dict[str, Any]]:
824
+ """Sessions still active, and those that ended in the last day, newest first."""
825
+ return (await self._t.json("GET", self._path()))["data"]
826
+
827
+ async def revoke(self, session_id: str) -> dict[str, Any]:
828
+ """Ends a session now; its next request is refused."""
829
+ return await self._t.json("POST", self._path(f"/{_enc(session_id)}:revoke"), body={})
830
+
831
+
781
832
  class AsyncSandbox:
782
833
  """A sandbox. ``async with await runtime.sandboxes.create() as sbx:`` stops it at the end."""
783
834
 
@@ -786,6 +837,7 @@ class AsyncSandbox:
786
837
  self.info = info
787
838
  self._keep_alive: Optional[Callable[[], None]] = None
788
839
  self.files = AsyncFiles(t, info["id"])
840
+ self.sessions = AsyncSandboxSessions(t, info["id"])
789
841
  # Imported here, not at the top, so a product module may itself import
790
842
  # this one without a cycle.
791
843
  from ._async_products import SANDBOX as SANDBOX_PRODUCTS
@@ -800,6 +852,19 @@ class AsyncSandbox:
800
852
  def state(self) -> str:
801
853
  return self.info.get("state", "")
802
854
 
855
+ @staticmethod
856
+ def from_session(token: str, sandbox_id: str, api_url: Optional[str] = None, *,
857
+ timeout: float = 300) -> "AsyncSandbox":
858
+ """A sandbox reached with a session token instead of an API key: its
859
+ commands, processes, files and previews, and nothing else. Makes no
860
+ call. The backend makes the session with ``sbx.sessions.create()``."""
861
+ if not token.startswith("rtsess_"):
862
+ raise RuntimeError("That is not a sandbox session token (they start rtsess_).",
863
+ code="invalid_request", status=0,
864
+ hint="Make one on your backend with sbx.sessions.create() and pass its token.")
865
+ t = _Transport(token, api_url or os.environ.get("RUNTIME_API_URL", DEFAULT_BASE_URL), timeout, 4)
866
+ return AsyncSandbox(t, {"id": sandbox_id})
867
+
803
868
  def _path(self, suffix: str = "") -> str:
804
869
  return f"/v1/sandboxes/{_enc(self.id)}{suffix}"
805
870
 
@@ -1310,5 +1375,5 @@ class AsyncRuntime:
1310
1375
  await self.close()
1311
1376
 
1312
1377
 
1313
- __all__ = ["AsyncRuntime", "AsyncSandbox", "AsyncSandboxes", "AsyncFiles", "AsyncProcess", "AsyncTerminal",
1378
+ __all__ = ["AsyncRuntime", "AsyncSandbox", "AsyncSandboxes", "AsyncSandboxSessions", "AsyncFiles", "AsyncProcess", "AsyncTerminal",
1314
1379
  "AsyncPage", "AsyncFeedback", "AsyncSupport", "AsyncSnapshots", "CommandResult"]