You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sandbox / AsyncSandbox: fs (text read/write) and process (exec, list, logs); api reaches the generated execution client for anything unwrapped
API key exchanged at /v1/token for a cached, refreshed token used on both planes; token_provider overrides
Generated clients reachable as api / sandbox_api on the client
🔬 Testing
Unit: a routing transport serving both planes, covering token minting and revocation, retries, pagination, and wire shapes
Parity: every translated generated type is pinned, so a new server field fails until the surface forwards it or lists it under an explicit exception
E2E (env-gated): lifecycle and exec against a live environment
Important review notes and deviations (running list of decisions)
API shape
Matches the JS and Go SDKs' behavior with Python conventions: keyword-only options, sync and async twins, Optional meaning "leave unset" on wire types. The Go and JS SDK PRs are the source of what to build; this list tracks the same decisions here
create/get return a Sandbox handle (name, URL); get_info/list return data only. create also carries info, the record as of creation
No public Sandbox constructor: sandbox_from_url builds one without a call, so a sandbox always shares its client's authentication, connections, and retry budgets, and closes with it
get_info takes show_secrets; the server masks values for callers without the workspace administrator role even then
create sends only what the caller set; the server owns defaults and conflicts (create_if_not_exists returns the existing live sandbox or recreates a dying one). create fails loudly when the served record carries no URL, which the served spec says is always present
SandboxInfo has no state and no display_name: the served spec drops both. create takes create_if_not_exists as a flag, not a tri-state
list is a lazy generator; a repeated server cursor errors rather than paging forever
Env values default to secret=True, matching the server's default; secret=False opts out and unmasks the value in responses. The previous draft's False default would have unmasked everything
Execution-plane timestamps parse an explicit layout list (ISO 8601, RFC 1123) and fail clearly on anything else
library_images drops nothing client-side: the served endpoint already excludes hidden and coming-soon entries. Creation options are flattened to creation_extra_args and creation_volumes, documented as suggestions since create cannot pass volumes or extra args yet
Not implemented yet: update, the image client (push, wait, logs, tags, cleanup), process breadth (exec_stream, wait, stop, kill, stdin), filesystem breadth (bytes, multipart, list, mkdir, find, grep, copy, write_tree, watch), drives, codegen, system, network. api and sandbox_api reach them
Errors and retries
SandboxApiError on both planes, SandboxGatewayError for edge 502/503/504 on the execution API
Control-plane calls are not retried. Execution-plane calls retry idempotent ones only, with the same budgets as Go and JS: reads 5, gateway 2, uploads 3, settable via SandboxRetryOptions
A revoked token (401 plus x-blaxel-error-code: TOKEN_REVOKED) is re-minted and the request resent; later resends wait 1s past the revocation's rounded-up cutoff second, since a token minted right after the revoking event can still fall before it. Past two resends the rejection is returned
One token exchange is bounded by 30s, so a stalled mint is not what every caller waits on
Transport
httpx, HTTP/2 when the optional h2 package is installed (http2=None auto-detects). The default timeout bounds the connect time only: exec with wait_for_completion holds the response open until the command exits, so a response-header timeout would fail every long command
The reason will be displayed to describe this comment to others. Learn more.
I am holding off reviewing this because I think it's a bit premature. basetenlabs/baseten-js#23 I think needs to get further along first, and then can be used as inspiration for this PR.
Add the missing http2 package extra or document installing h2
baseten/sandbox/_auth.py:50
The installation command in this error does not work: pyproject.toml defines no http2 optional dependency, so pip install baseten[http2] will not install h2. Add the advertised package extra (or direct users to install h2) so http2=True has an actionable failure path.
Reject bare status strings in synchronous API
baseten/sandbox/_client.py:327
str satisfies Sequence[str], so the public annotation accepts statuses="DEPLOYED"; the comprehension then sends one status per character and the API rejects the request. Reject a bare string before constructing the query (and keep accepting actual sequences).
This issue also appears in the following locations of the same file:
Sync and async SandboxClient for the control plane: create, get, get_info,
list (cursor-paginated), delete. Sandbox/AsyncSandbox for the execution
plane with fs (read with retry, write) and process (exec) subsystems;
drives, codegen, system, and network raise NotImplementedError until
wrapped.
API keys are exchanged for a sandbox token at POST /v1/token, cached until
shortly before expiry, and replaced on 401 TOKEN_REVOKED by re-sending
the request. One shared transport pools connections across both planes;
HTTP/2 is best-effort via the optional h2 package. Records convert to
curated types (SandboxInfo, SandboxProcessInfo) with open string
statuses. Creation sends only the fields the caller set, so server-side
defaults apply.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The surface exposes no generated types: methods take keyword arguments of
primitives and curated dataclasses, and the generated models are built
internally. The parity tests fail when an upstream spec field appears,
until the surface forwards it or lists it as deliberately not translated.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The sandbox management API can be served on a different host than the
token exchange (today: api.blaxel.dev serves /v1/sandboxes while
api.staging.baseten.co mints the tokens, until staging.b10.co is live),
so the client accepts a separate base URL for the sandboxes calls.
The e2e suite mirrors the JS suite's environment contract
(BASETEN_E2E_TEST_API_KEY, BASETEN_E2E_TEST_DOMAIN,
BASETEN_E2E_TEST_SANDBOXES_DOMAIN) and skips without them. Verified live
against staging + blaxel dev: create (server applied its image and
memory defaults), exec with waitForCompletion, fs write/read, delete.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…rchive
Exercises the raw execution client against a live sandbox: stdin piping
with delete_process_stdin and the text/plain logs stream, multipart
initiate/part/complete followed by an fs read-back, and a dry-run archive
export (the 200 ExportResult branch; the 202 branch needs a presigned
upload URL, which stays unit-tested).
The stdin test is xfail: the server sends logs: null on a still-running
process while the spec declares logs required, so pydantic rejects the
response. Reported for a server-side fix.
Co-Authored-By: Claude Code <noreply@anthropic.com>
library_images() on both clients lists the platform's starter images
over the regenerated typed client, dropping hidden and coming-soon
entries to match the console's create form. Named after the
library_models consumer catalog, not Blaxel's hub.
Co-Authored-By: Claude Code <noreply@anthropic.com>
library_images() on both clients lists the platform's starter images
over the served GET /v1/sandboxes/library_images, which drops hidden
and coming-soon entries server-side. Named after the library_models
consumer vocabulary, not Blaxel's hub.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…ains create_if_not_exists
The curated SandboxInfo loses state and display_name with the spec
forward-patch; create() drops display_name and gains the
create_if_not_exists idempotent form the CLI already exposes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- sandbox_from_url on both clients; the standalone Sandbox constructors
are gone, so a sandbox always shares its client's authentication,
connections, and retry budgets.
- get_info takes show_secrets.
- A revoked token's later resends wait out the revocation's rounded-up
cutoff second; one token exchange is bounded by 30s.
- create fails loudly when the served record carries no URL.
- Env values default to secret, matching the server's default; the old
False default would have unmasked values.
- LibraryImage flattens the catalog's creation options, matching Go.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- The async token exchange carries the same 30s bound as sync.
- Process list and logs retry on the read and gateway budgets, as
idempotent execution-plane reads.
- A same-content file write is idempotent, so fs write retries on the
upload budgets.
- LibraryImageVolume exports with the other curated library types.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- The async auth transport buffers with aread, since the request body
can be an async stream.
- SandboxApiError keeps an error-only body's error as its code and
description; the gate checked the wrong field.
- Sandbox drops its context-manager methods: a sandbox closes with its
client, so the protocol advertised cleanup it does not own.
- Async fs read and write retry paths covered by tests, mirroring the
sync pair.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Advertised http2 extra does not install the h2 dependency
baseten/sandbox/_auth.py:59
The suggested installation command cannot work because this package defines no http2 optional dependency in pyproject.toml; pip will not install h2 for baseten[http2]. Point users to an installation command that actually provides h2, or add the advertised extra.
Second-resolution sandbox names collide across concurrent runs
tests/sandbox/test_sandbox_e2e.py:36
Rounding wall-clock time to one second does not produce a unique sandbox name. Concurrent E2E runs can create the same resource, and each test's unconditional cleanup may then delete another run's sandbox. Use a UUID or equivalent random suffix, as the existing E2E suite does in tests/test_e2e.py:144.
Test assumes deployment completes before execution calls
tests/sandbox/test_sandbox_e2e.py:54
The served create contract explicitly allows deployment to continue after the response (scripts/apigen/specs/management.json:10632), so create() may return DEPLOYING. This strict assertion and the immediate execution calls make the live test flaky; poll get_info() with a bounded timeout until DEPLOYED before using the execution API.
Test does not tolerate transitional DEPLOYING status
tests/sandbox/test_sandbox_e2e.py:98
This test also assumes the first follow-up read must already report DEPLOYED, although the create endpoint is allowed to return while deployment continues. Poll to readiness, or assert a contract that permits the transitional status, so a valid DEPLOYING response does not fail the E2E suite.
Process starts before sandbox deployment is ready
tests/sandbox/test_sandbox_e2e.py:142
The process API is called immediately after creation even though creation may return before deployment is ready. A gateway failure here is especially misleading because this test is marked xfail and will be accepted as the expected server bug without exercising stdin/log behavior. Wait for DEPLOYED before starting the process.
Multipart execution starts before sandbox deployment completes
tests/sandbox/test_sandbox_e2e.py:177
Multipart initiation is attempted immediately after create(), but the create response may still be DEPLOYING under the served API contract. Wait for the sandbox to reach DEPLOYED before calling its execution API to avoid intermittent gateway failures.
This execution-plane write can race sandbox deployment because create() is allowed to return while status is still DEPLOYING. Poll with a bounded timeout until DEPLOYED before writing/exporting so the E2E test does not intermittently fail at the gateway.
Matching the Go rule: only a body that can be replayed is ever sent
again. An in-memory body re-sends on a revoked token; one still backed
by a stream, such as a raw multipart upload, is sent once, since
replaying it would mean materializing it. The eager read/aread that
buffered every body is gone.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Public handles remain directly constructible in an invalid state, readiness guidance is incomplete, and HTTP/2 installation instructions reference a nonexistent extra.
The documented baseten[http2] extra does not exist in pyproject.toml, so following this recovery instruction does not install h2 and http2=True continues to fail. Point users to the actual package (or add the advertised extra).
Document that sandbox creation may return before deployment completes
baseten/sandbox/_client.py:280
The served create contract says deployment may continue after the response and only DEPLOYED sandboxes accept execution calls (scripts/apigen/specs/management.json:10632, baseten/sandbox/_info.py:10). The current docstring omits this, making the natural client.create(...).process.exec(...) flow race deployment. Document polling get_info() (the handle's info is only the creation snapshot), or make create wait.
Document async sandbox creation readiness and polling
baseten/sandbox/_client.py:586
The async create call has the same readiness contract: the response may still be DEPLOYING, but this docstring does not warn callers before they await an execution operation. Document polling get_info() (because AsyncSandbox.info is a snapshot), or make creation wait consistently.
Add parity checks for all nested translated model types
tests/sandbox/test_parity.py:151
This only pins the top-level library-image model. The converter also translates SandboxPort, SandboxLibraryImageCreationOptions, and SandboxLibraryImageVolume, so new fields on any of those nested generated types would still be silently dropped despite the parity suite's stated guarantee. Add field-set parity assertions for each nested translated type (and SandboxEnv, which is translated in create/info paths).
With no __init__, Sandbox() built an uninitialized object that failed
later on access; construction now names the client entry points. _shared
bypasses it with __new__.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Synchronous create returns handles before deployment is ready
baseten/sandbox/_client.py:302
The generated control-plane contract says creation may return while status is still DEPLOYING, with the URL assigned before readiness, but this immediately hands out an execution handle. Since process.exec() is deliberately not retried, the common create-then-exec flow can fail against an allowed successful create response. Poll to DEPLOYED before returning, or expose a documented readiness/wait operation so callers can safely use the handle.
This issue also appears on line 605 of the same file.
Status filters incorrectly expand bare strings
baseten/sandbox/_client.py:350
A plain string satisfies Sequence[str], so client.list(statuses="DEPLOYED") is accepted by type checkers but this implementation emits one status per character. Reject string input explicitly (or use an input type that excludes strings) rather than sending a malformed filter.
This issue also appears on line 651 of the same file.
Synchronous exec serializes omitted options as null
baseten/sandbox/_sandbox.py:397
All optional process fields are passed explicitly even when None. The generated serializer preserves explicitly set nulls, so exec("cmd") sends workingDir, env, name, timeout, and waitForCompletion as null instead of leaving them unset, contrary to this PR's stated wire contract and potentially overriding server defaults. Build the request from only the arguments the caller supplied, as _sandbox_create_request already does.
This issue also appears on line 473 of the same file.
CI omits the sandbox domain secret
tests/sandbox/test_sandbox_e2e.py:27
These new E2E tests cannot run in the repository CI configuration: .github/workflows/ci.yml:41-47 injects the API key and management domain but never BASETEN_E2E_TEST_SANDBOXES_DOMAIN, so this condition always skips the entire sandbox suite even on main. Wire the sandbox-domain secret into the workflow so the claimed live coverage executes.
Recovery instructions reference an undefined http2 extra
baseten/sandbox/_auth.py:59
This recovery instruction names a package extra that the project does not define, so pip install baseten[http2] will not install h2. Point users to an installation command that actually provides the imported package.
A stream-backed body that meets a revocation is still sent only once,
but its token now leaves the cache, so the next request mints instead
of reusing the known-bad one; the exhausted replayable attempts do too.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚀 What
baseten.sandbox, a hand-written SDK surface for Baseten sandboxes over the generated clients from 💥 Add sandbox API client and split query params from request bodies #21, covering the control plane (/v1/sandboxes) and each sandbox's execution APIhttpx💻 How
SandboxClient/AsyncSandboxClient:create,get,get_info,sandbox_from_url,list,delete,library_imagesSandbox/AsyncSandbox:fs(textread/write) andprocess(exec,list,logs);apireaches the generated execution client for anything unwrapped/v1/tokenfor a cached, refreshed token used on both planes;token_provideroverridesapi/sandbox_apion the client🔬 Testing
Important review notes and deviations (running list of decisions)
Optionalmeaning "leave unset" on wire types. The Go and JS SDK PRs are the source of what to build; this list tracks the same decisions herecreate/getreturn aSandboxhandle (name, URL);get_info/listreturn data only.createalso carriesinfo, the record as of creationSandboxconstructor:sandbox_from_urlbuilds one without a call, so a sandbox always shares its client's authentication, connections, and retry budgets, and closes with itget_infotakesshow_secrets; the server masks values for callers without the workspace administrator role even thencreatesends only what the caller set; the server owns defaults and conflicts (create_if_not_existsreturns the existing live sandbox or recreates a dying one).createfails loudly when the served record carries no URL, which the served spec says is always presentSandboxInfohas nostateand nodisplay_name: the served spec drops both.createtakescreate_if_not_existsas a flag, not a tri-statelistis a lazy generator; a repeated server cursor errors rather than paging foreversecret=Falseopts out and unmasks the value in responses. The previous draft's False default would have unmasked everythinglibrary_imagesdrops nothing client-side: the served endpoint already excludes hidden and coming-soon entries. Creation options are flattened tocreation_extra_argsandcreation_volumes, documented as suggestions sincecreatecannot pass volumes or extra args yetupdate, the image client (push, wait, logs, tags, cleanup), process breadth (exec_stream,wait,stop,kill, stdin), filesystem breadth (bytes, multipart,list,mkdir,find,grep,copy,write_tree,watch), drives, codegen, system, network.apiandsandbox_apireach themSandboxApiErroron both planes,SandboxGatewayErrorfor edge 502/503/504 on the execution APISandboxRetryOptionsx-blaxel-error-code: TOKEN_REVOKED) is re-minted and the request resent; later resends wait 1s past the revocation's rounded-up cutoff second, since a token minted right after the revoking event can still fall before it. Past two resends the rejection is returnedhttpx, HTTP/2 when the optionalh2package is installed (http2=Noneauto-detects). The default timeout bounds the connect time only:execwithwait_for_completionholds the response open until the command exits, so a response-header timeout would fail every long command