Repository navigation
Add baseten.sandbox package - #25
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Signed uploads can leak shared-client credentials, alongside additional correctness and compatibility issues.
6 open findings
Remove inherited Authorization when API key is absent · New Prevent shared client credentials leaking to signed storage URLs · New Detect explicit sandbox-api copy instead of substring matches · New Terminate cp options before user-controlled paths · New Offload in-memory build context compression from event loop · New Retain text/event-stream in generated Accept choices · New
What changed in this PR
Adds a comprehensive sync/async sandbox SDK, including control-plane, filesystem, process, image, authentication, and retry APIs.
Changes:
- Adds the handwritten
baseten.sandboxpackage and documentation. - Regenerates API clients with per-client transport options and updated schemas.
- Adds extensive unit and end-to-end coverage.
| File(s) | Description |
|---|---|
baseten/sandbox/*.py |
Implements sandbox clients, models, auth, retries, filesystem, processes, images, and builders. |
baseten/client/{managementapi,sandboxapi,inferenceapi}/* |
Updates generated clients and models. |
scripts/apigen/* |
Updates specifications, preprocessing, and client generation. |
scripts/docgen/* |
Adds sandbox API documentation support. |
tests/sandbox/* |
Adds sandbox unit and end-to-end coverage. |
tests/client/*, tests/__init__.py |
Updates generated-client and live API tests. |
README.md, CONTRIBUTING.md |
Documents sandbox usage and testing. |
pyproject.toml, uv.lock |
Adds HTTP/2 test tooling and lock data. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Arguments left ``None`` stay unchanged, and set ones replace their | ||
| previous values, including every entry of *envs*, *labels*, and the | ||
| lifecycle's expiration policies. Other lifecycle fields change only | ||
| when set. At least one argument besides *name* must be set. Other |
There was a problem hiding this comment.
This documents "At least one argument besides name must be set", but nothing enforces it — update(name=...) with no other argument sends an empty PATCH. Either raise ValueError here (as push does for its source check) or drop the sentence.
There was a problem hiding this comment.
I think this came from server side or contract docs relay, but agreed can remove this, it may not be/stay true.
There was a problem hiding this comment.
I forgot to do this before merging. Setting reminder to do it in a followup...
# Conflicts: # baseten/client/managementapi/__init__.py # baseten/client/managementapi/_models.py # baseten/client/modelconfig/_models.py # baseten/client/sandboxapi/_models.py


Stacked on #24
🚀 What
baseten.sandbox, a hand-written package for Baseten sandboxes covering the control plane (/v1/sandboxes) and each sandbox's execution API, sync and asyncbase_url,auth,headers,timeout, so one httpx client can serve every API💻 How
SandboxClient/AsyncSandboxClient:create,get,get_info,list,update,delete,sandbox_from_url, andimages(ImageClient:push,wait_built,logs,get_info,list,list_tags,delete,delete_tag,cleanup,list_library)Sandbox/AsyncSandbox:fs(text and byte read/write,list,mkdir,remove,find,grep,copy,write_tree,watch, multipart above 5MB) andprocess(exec,exec_stream,get,list,logs,stream_logs,wait,stop,kill,write_stdin,close_stdin)ImageBuilder: immutable Dockerfile builder, pushed withimages.push(builder=...)/v1/tokenfor a cached, refreshed token used on both planes;token_provideroverridesh2is installed (httpx[http2])raw_apihead_filesystem). Temporary preprocess patch (TODO) makesSandboxImage.tagsoptional until the spec drops it, as the server already has🔬 Testing
httpx.MockTransportper plane, sync and async, covering auth and revocation, retries and the transient classifier, multipart boundaries and concurrency (including a busy caller thread pool), zip, ignore files, the builder, sync/async signature parity, and field parity against the generated typestests/sandbox/test_e2e.py, env-gated): every wrapped call against staging on both clients, including byte-exact 12MB and ~10MB text multipart writes, recursive watch, and a sandbox run from a builder-pushed image. One image push per run, skipped on the 3.11 CI jobImportant review notes and deviations from the previous SDK
ManagementClient/AsyncManagementClient. Models and pure logic are shared; only I/O, retry and poll loops, and streams are written twice. A test checks every sync/async pair has the same methods and signaturesget_info(name=...)), instead of JS/Go's one request object. Adding a keyword never breaks callers. Builder methods are positional, as in JS/GoSandboxInfo,SandboxProcessInfo,ImageInfo), since Python callers import names unqualifiedget/createreturn aSandboxhandle (name, URL);get_info/listreturn data only.gettakesname=orinfo=(no call). The previous SDK returned clients carrying the recordkw_onlydataclasses, not pydantic: the generated models already validate the wire, and our types promise only fields. One type per concept for input and output, as the API uses one schema for bothLiteral[...] | straliases, so callers write string literals that still type-checkNonemeans unset on inputs and absent on outputs.updateis one call; nothing can be cleared, since the API takes no nulls.labels,envs, expiration policies replace, not mergetimedelta, dates timezone-awaredatetime. Execution API timestamps parse against an explicit format list and fail clearly on anything elselist,images.list,images.list_tagsare lazy (async) generators; a repeated cursor raiseslist/get_info/deletereturnImageInfoSandboxExpirationPolicyUnknownraw_apireaches them. Unlike JS, there are no placeholder accessors, since adding them later is additiveSandboxErrorbase for everything the SDK raises;SandboxAPIErroron both planes,SandboxGatewayErrorfor edge 502/503/504 on the execution API, typed errors for copy, process wait timeout, image upload, image build. httpx transport errors pass through after retriesSandboxProcessWaitTimeoutErrordoes not subclass builtinTimeoutError, which is anOSErrorTypeError(source combinations) orValueError(content), before anything is sentretry). All idempotent filesystem PUTs are retried, as the spec documents them idempotent;exec,stop,kill,write_stdin,removeare notConnectionError, OSTimeoutErrorwith an errno, httpx disconnect and HTTP/2 reset ec list; a caller's own read timeout is never retriedtoken_providergets the revoked tokenhttp_client_overrideis transport only: requests carry absolute URLs and their own auth and headersh2is installed (http2=None), required withTrue. The previous SDK never used HTTP/2 and made a new unpooled clientexecwithwait_for_completionsends headers only when the command exits. httpx's default 5s would fail longer commandsexec_stream,stream_logs,watch) returnSandboxStream/AsyncSandboxStream: iterators and context managers whoseclose()releases the connectionSandboxacross its uploads. Sync uploads use the calling thread plus the cliExecutor`), or a 2-thread pool per sandbox, created on first use. Async uses tasks. The abort is sent even on an interrupt or cancellation, on its own10s timeoutexec_streamrequests and parses NDJSON. Anerrorrecord raises with the server's messageexec_streamevents areSandboxProcessExecEventOutput(raw chunks, so prompts show at once) thenSandboxProcessExecEventExit;stream_logsswrite_stdintakesstrorbytes, not streams, so revocation resend stays possiblewaitpolls client-side and returns failed, killed, or stopped processes; an unknown status raises.timeout=Nonewaits with no limitwrite_bytessends permissions only when set, applied when the file is created. The multipartwrite_bytesis a raw request, since the generated client has no form for that endpointbytes, as beforesearchor stat yet: the API only just added themcopyrunscp -rwith quoted paths and waits; anything but exit 0 raisesSandboxFileSystemCopyErrorwatchyields full paths,opssplit from the server's|-joined ops, has arecursiveoption, and nowith_contentfind/greprequirepathPOST /v1/sandboxes/imagesplus upload, giving a reusable image (<name>:latest). The previous SDK's builder deployed a sandbox instbuilder,registry_image,zip,directory,files), one@overloadper source plus a runtime checkzipfile(ZIP64 automatic), modes kept, the same link rules as Go/JS. Local files are zipped to an anonymous temp file, which the OS rewithContent-Length(temp_file=Falseto opt out). Async zips and reads on a worker threaddirectoryhonors a root.dockerignoreviaignore_file_processor, so no parser dependency; otherwisedefault_image_ignore_file(the previous CLI's defaults, Docker semantics). On the async client, the ignore function and processor may be coroutine functionslogs()returns oldest first, capped at the 11,000 most recent lineslist_librarydrops always-false fields and flattens creation optionsImageBuilder.from_registrystarts one; nothing on it calls the server.dockerfile()matches JS's line for line, injected sandbox API copy and entrypoint includedValueErrorat the call, as JS throws (Go records errors and continues)env/labelkeep mapping order, as JS (Go sorts keys)