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.
- {withruntime-0.8.2/withruntime.egg-info → withruntime-0.8.4}/PKG-INFO +30 -6
- {withruntime-0.8.2 → withruntime-0.8.4}/README.md +29 -5
- {withruntime-0.8.2 → withruntime-0.8.4}/pyproject.toml +1 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_blaxel.py +1 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_daytona.py +11 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_e2b_sandbox.py +3 -0
- withruntime-0.8.4/tests/test_jobs.py +165 -0
- withruntime-0.8.4/tests/test_sandbox_sessions.py +149 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_secrets.py +3 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_vercel.py +2 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/__init__.py +2 -2
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_client.py +66 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/__init__.py +2 -0
- withruntime-0.8.4/withruntime/_async_products/jobs.py +230 -0
- withruntime-0.8.4/withruntime/_async_products/secrets.py +197 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_errors.py +5 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_client.py +66 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/__init__.py +2 -0
- withruntime-0.8.4/withruntime/_sync_products/jobs.py +231 -0
- withruntime-0.8.4/withruntime/_sync_products/secrets.py +198 -0
- withruntime-0.8.4/withruntime/_version.py +1 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/__init__.py +7 -3
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_async_sandbox.py +81 -3
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_core.py +83 -3
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_sync_sandbox.py +81 -3
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_async_daytona.py +3 -3
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_core.py +2 -2
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_sync_daytona.py +3 -3
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/__init__.py +1 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_core.py +1 -1
- {withruntime-0.8.2 → withruntime-0.8.4/withruntime.egg-info}/PKG-INFO +30 -6
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/SOURCES.txt +4 -0
- withruntime-0.8.2/withruntime/_async_products/secrets.py +0 -58
- withruntime-0.8.2/withruntime/_sync_products/secrets.py +0 -59
- withruntime-0.8.2/withruntime/_version.py +0 -1
- {withruntime-0.8.2 → withruntime-0.8.4}/LICENSE +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/setup.cfg +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_account_errors.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_async_stream_cleanup.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_audit.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_billing.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_capacity_wait.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_client.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_deepagents.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_e2b_code_interpreter.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_eval_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_exec_stream.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_files_download.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_files_mode.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_files_upload_parallel.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_harbor.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_http_tls_close.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_identity_token.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_images_v2.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_images_volumes_interpreter.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_inspect_ai.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_late_answer.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_lifecycle_settings.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_limits.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_mounts.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_network_funding.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_network_products.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_observability.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_openai_agents.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_previews_network_desktop.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_proxy.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_referrals.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_snapshot_after_fork.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_sso.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_stream_exit_close.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_switching.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_tools.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_unpack.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_volume_backups.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_wake_awake.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_wallet.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/tests/test_watch.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/audit.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/billing.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/desktop.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/images.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/interpreter.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/limits.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/mcp.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/mounts.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/network.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/network_products.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/observability.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/previews.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/referrals.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/sso.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/switching.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/volumes.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_async_products/watch.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_clock.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_connection.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_eval_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_http.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_proxy.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/audit.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/billing.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/desktop.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/images.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/interpreter.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/limits.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/mcp.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/mounts.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/network.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/network_products.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/observability.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/previews.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/referrals.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/sso.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/switching.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/volumes.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_sync_products/watch.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_tunnel.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_unpack.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/_ws.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_async_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/_sync_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/agents.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/applications/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/applications/list_application_revisions.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/compute/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/compute/get_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/compute/update_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/functions/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/functions/create_function.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/images.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/api/workspaces.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/client.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/create_job_execution_request.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/create_job_execution_request_env.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/env.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/function.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/function_runtime.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/function_spec.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/metadata.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/models/workspace.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/client/types.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/common.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/drive.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/jobs.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/sandbox/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/core/sandbox/types.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/crewai.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/googleadk.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/langgraph.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/livekit.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/llamaindex.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/openai.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/pydantic.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/blaxel/telemetry.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_async_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/daytona/_sync_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/deepagents.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_async_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_async_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_core.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_sync_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/_sync_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/_models.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/harbor.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/inspect_ai.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/openai_agents.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/tools.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_async_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_async_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_sync_io.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/_sync_sandbox.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/api.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/sandbox/__init__.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/vercel/sandbox/sync.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/wallet.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime/webhooks.py +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/dependency_links.txt +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/entry_points.txt +0 -0
- {withruntime-0.8.2 → withruntime-0.8.4}/withruntime.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
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
|
-
`
|
|
81
|
-
`
|
|
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),
|
|
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
|
|
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
|
-
`
|
|
58
|
-
`
|
|
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),
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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"]
|