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
(adjusted from original PR description as changes came about -@cretz)
🚀 What
baseten sandbox list: lists sandboxes, filtered by --query and --status, up to --limit
baseten sandbox describe: shows one sandbox, with --show-secrets to reveal secret env values
baseten sandbox create: creates a sandbox, sending only the flags given
baseten sandbox update: changes a sandbox's image, env vars, or labels
baseten sandbox delete: deletes a sandbox after confirming
baseten sandbox exec: runs a command and streams its output, passing through its exit code
baseten sandbox connect: opens an interactive shell in a sandbox
baseten sandbox process
start: starts a command in the background
list: lists processes
describe: shows one process
logs: prints a process's output, with --tail to follow it
wait: waits for a process to exit, passing through its exit code
stop: asks a process to stop
kill: asks a process to be killed
baseten sandbox image
list: lists images, up to --limit
describe: shows one image
push: pushes an image from a directory or a registry, with --wait to wait for the build
delete: deletes an image after confirming
logs: prints an image's build logs
list-tags: lists an image's tags
list-library: lists the platform's starter images
baseten api sandbox: direct API calls to sandbox exec plane (matching management and inference forms that already exist for other APIs)
💻 How
Built on the high-level sandbox package from baseten-go#60
Control plane commands call the generated API through the sandbox client and print the API's own records
Execution API commands use the high-level client, which retries idempotent calls
Sandbox tokens are minted through the session's management client, so API keys and OAuth logins both work
Sandbox API errors map to the CLI's exit codes and JSON error envelope
connect speaks the sandbox's terminal WebSocket in internal/sandboxconnect
New dependencies: github.com/moby/patternmatcher (Docker's .dockerignore matcher) and github.com/creack/pty (tests only, not in the binary)
🔬 Testing
Unit tests per command file against the mock API, which also serves the sandbox's own API
E2E tests against staging: every command, connect through a real pseudo-terminal, and one image push from a directory with a .dockerignore
E2E runs on Linux only, as the rest of the suite does
Important review notes and deviations
Thin layer over the API
Control plane JSON is the generated API type as is, matching the rest of the CLI
Execution API JSON is the high-level client's record mapped back to the generated type, since those calls go through the high-level client for its retries. Timestamps come out as RFC 3339
No client-side checks for what the server rejects
--team defaults to your only team and is required if you belong to more than one, which is the API's behavior. Other commands default to the organization's default team
Naming
--name is always the command's own resource: the sandbox, or the image under image. Processes are --pid or --process-name
Flags take the API's field names (--memory), except --if-not-exists (shortened from create_if_not_exists) and the singular repeatable --env and --label
Only sandbox exec, not also process exec, since it is the most common action
image list-library, a verb like every other leaf command
--status values are lowercase like other CLI enums
Lists
list and image list collect pages up to --limit (default 1000) and print {"items": [...]}, like every other list
Blaxel's CLI instead shows one page of 200 with a next-page cursor hint
Streamed output would be a breaking change later, so we may want to discuss it for all list commands together
create
No --wait: create returns once the sandbox is deployed, and the SDKs have no wait either
--env values are secret (the server's default) and --plain-env values are not. Both need KEY=VALUE. A docker-style --env KEY read from the local environment would keep secrets out of shell history, but no other command does that yet
exec and processes
exec is basically process start then process wait, with streamed output and --stdin
One argument after -- is the whole command line for the sandbox's shell. Several are quoted so each stays one argument
With --stdin, output arrives line by line, since stdin can only be written once the process exists
process logs --tail replays from the start, then follows
Without --tail, log lines have no stream, since the API's output so far does not say which stream a line came from
stop and kill request termination and do not wait for it
No write-stdin or close-stdin commands, since exec --stdin covers piping
No check that a sandbox is deployed before using it. The API reports its own error
Images
push --dir leaves out paths per a root .dockerignore, read with Docker's own matcher, so the upload matches what Docker would build
Without a .dockerignore, the defaults are Blaxel's CLI's list, documented in the help as the equivalent .dockerignore
Blaxel's CLI reads .blaxelignore instead, and matches unanchored names at any depth. We follow Docker
The directory is zipped to a temporary file and uploaded with its length, as Blaxel's CLI does, so memory stays flat
push --wait is opt-in, since builds take minutes
--image became --registry-image so it does not read like the image's name
image logs prints at most the newest 11,000 lines, the API's limit
image list-library has no --team
connect
Fails before connecting unless both stdin and stdout are terminals
Each connect is a new shell
The terminal endpoint is not in the API spec, so it is not in the SDK
Windows has no resize signal, so the window keeps its starting size
Not included: filesystem commands, drives, and the other execution API features the SDK has
The timeout check occurs only after the blocking pipe read returns, so a missing echo can hang this test until the suite-wide timeout; time.After does not bound the read. Set a read deadline before entering the loop.
This bypasses the command's injected stderr and writes to the process-global stream, so callers using ExecuteOptions.Stderr cannot capture terminal error frames. It also violates the repository rule to route stderr through context-owned logging (CONTRIBUTING.md:9). Pass an io.Writer for stderr into Dial/Terminal and write the frame there.
Validate mutually exclusive source flags before client creation
cmd/command.sandbox.go:328
These flags promise exactly one source, but unlike other exclusive/required flag groups they omit the framework's oneof metadata (internal/cmd/command.go:667-677). Validation therefore happens only inside the SDK after credential resolution and after the command has printed “Waiting for image ...”, so invalid invocations can report an auth error or misleading progress instead of a usage error. Validate the pair before constructing the client, preferably via a shared oneof tag.
Text output omits environment variables with --show-secrets
internal/cmd/command.sandbox.go:84
--show-secrets is sent to the API, but the default text path immediately passes the result to outputSandboxInfo, which never renders info.Envs. As a result, baseten sandbox describe NAME --show-secrets succeeds without revealing any values unless the user also knows to request JSON. Render the environment variables in text mode (deterministically), or explicitly require/document structured output for this flag.
Display unavailable image creation time as -
internal/cmd/command.sandbox.go:440
ImageInfo.CreatedAt can be zero when the optional API field is absent (including the existing UPLOADING test fixture), so this renders 0001-01-01T00:00:00Z in the table. Match the sandbox list and detail renderer by showing - when the timestamp is unavailable.
Route terminal errors through the injected stderr writer
internal/sandboxconnect/terminal.go:165
This bypasses the command's injected stderr and writes to the process-global os.Stderr, so terminal protocol errors cannot be captured by ExecuteOptions.Stderr and violate the repository rule to route stderr through ctx.Log* (CONTRIBUTING.md:9). Pass an error writer into Terminal and use ctx.Stderr from the caller instead.
Preserve UTF-8 characters across terminal read chunks
internal/sandboxconnect/terminal.go:177
Converting each arbitrary Read chunk directly to a string can corrupt UTF-8 input. A paste larger than 512 bytes can split a multibyte character at the buffer boundary, and json.Marshal replaces each invalid fragment with U+FFFD before sending it. Buffer incomplete UTF-8 sequences (or read complete runes) so terminal input survives chunk boundaries.
Fix typo in test command
internal/cmd/command.sandbox_test.go:442
Correct the typo in this test command so the fixture reads naturally.
baseten sandbox list/describe/create/update/start/stop/delete/exec per
the M1 spec, over the baseten-go high-level sandbox client. Create
waits for DEPLOYED by default; exec shell-quotes argv into the command
string, streams output, and passes the command's exit code through.
Update keeps omitted --env unchanged rather than clearing. E2E cases
join the e2e suite, gated additionally on
BASETEN_E2E_TEST_SANDBOXES_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
baseten sandbox image list/describe/push/delete over the SDK's image
client, and sandbox connect NAME, a raw-mode terminal to a deployed
sandbox over its /terminal/ws WebSocket. Push waits for BUILT by
default; connect reuses the repo's coder/websocket dependency and
requires an interactive terminal.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The server's default session is shared, so a second connect rejoins a
shell an earlier one may have left dead; each dial now carries a fresh
session id and its window size, matching the reference client. The
round trip and the close path are unit tested against a local terminal
server.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Shell-quoting every argument broke the one-quoted-string style: the
whole command arrived wrapped in single quotes and the sandbox's shell
looked for a program by that whole name. One argument now passes as the
command string with its quoting intact; several arguments are still
re-quoted so word boundaries survive.
Co-Authored-By: Claude Code <noreply@anthropic.com>
A whole command string passes to the sandbox shell verbatim, so the
case exercises remote variable expansion and redirects.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Chad's spec refresh (baseten-go 7b8c438) serves
GET /v1/sandboxes/library_images through the Baseten management API,
which supersedes the Blaxel hub path and the client-side hidden and
coming-soon filtering (the server drops those entries). The Sandbox
record also lost its state field; the curated record and the list table
follow.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The served library_images endpoint is team-scoped like every other
sandbox command; the list table lost the STATE column with the state
field.
Co-Authored-By: Claude Code <noreply@anthropic.com>
- Render environment variables in describe's text output, sorted, so
--show-secrets shows values without --output json.
- Validate the image push source flags with the framework's oneof
group at parse time, before any client or progress output.
- Show '-' for a missing image creation time in the image list table.
- Route terminal protocol errors through the command's stderr instead
of the process global.
- Hold back partial UTF-8 sequences at read boundaries in the terminal
input loop, so pasted multi-byte characters survive the 512-byte
chunking; covered by a test that splits a rune across two reads.
- Bound the terminal test's blocking echo read with a pipe read
deadline, so a missing echo fails instead of hanging.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The catalog is available to every team; the endpoint's team parameter
is uniform spec plumbing, and --team implied wrong semantics.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The alias and process exec render the same help; one const, following
sandboxPreRelease and volumePreRelease.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Deletes internal/sandboxclient: the CLI consumes the sandbox
package from baseten-go#60 directly, now that it covers the whole
surface (create_if_not_exists, show_secrets, process list/logs,
image push with spooling, library listing).
Three contract points kept through the swap:
- JSON stays snake_case: the SDK types carry no tags, so the CLI
wraps its records in output DTOs, keeping --jq examples honest.
- Terminal output streams chunks as written; no synthetic newlines.
- The BASETEN_SANDBOXES_API_URL_OVERRIDE env var is gone: the
sandbox control plane serves on the management domain, verified
against staging.
The e2e suite follows: no separate sandboxes URL gate, and the
process phase reads the pid from JSON instead of the human output.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Rebased on latest main (drops the README bump, already upstream);
the alias and process exec share their output descriptions through
consts so they cannot drift.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
For every list thus far we have just collected the whole thing and shown it, but for metrics/usage/logs stuff, we stream it out to user (which has built-in backpressure if they are using a pager like less). Sandbox list is said to get pretty large, I wonder if we want to stream or if maybe we want to expose pagination details to users so they can repeatedly call.
The reason will be displayed to describe this comment to others. Learn more.
I agree. For CLIs would stream or page be more ergonomic (and also do you expect agents to use this)?
I think a lot of use cases pragmatically want stuff recency based, so a query filter for last active or created at might be more important than free form text.
The reason will be displayed to describe this comment to others. Learn more.
All of our list calls today do not stream for JSON (they do for text, and they do for JSON calls like logs), they page internally and we have --limits in some cases. So using a pager like less does work and provide backpressure I believe. Blaxel SDK does the same thing but exposes cursors to users to get successive pages.
What I think is best here is not to stream list (we don't anywhere else, but we will for logs) and provide a reasonable default --limit and if we need to support explicit user paging or streaming, we can discuss in the context of the CLI as a whole.
The reason will be displayed to describe this comment to others. Learn more.
If/when the API supports it, we can add to all of the interfaces no prob. What my current code is doing (will be in PR early tomorrow, e2e failing atm) is having --limit default to 1000, but you can override it. This encourages users to give a more specific query or status filter or explicitly override and allows us to do more in the future such as user-controlled pagination.
The reason will be displayed to describe this comment to others. Learn more.
sandbox.Info does not conform to our expected JSON form elsewhere. I think this just was meant to be SandboxInfoOutput or something.
So we need to decide, are we doing more raw calls like we do elsewhere or high-level client calls? I'm leaning to former (still using high-level client so you get token provider) so that we can use the proper JSON object the API uses like we do elsewhere in CLI (this applies to other commands too).
The reason will be displayed to describe this comment to others. Learn more.
I think my rationale was that this a resource distinct from the custom images (and e.lg. has no --teams flag either). The endpoint is /v1/sandboxes/library_images (which would be an argument to line up the command name). But I'm ok with making this a subcommand of image, if that plays well with leaving the team-flag out.
The reason will be displayed to describe this comment to others. Learn more.
The endpoint I think is /v1/sandboxes/library_images because it couldn't be put under /images/{image_name} because that accepts names on the path afterwards. In SDKs we put this under the images sub-service. Yeah, team flag is per call (even though every sandbox call besides this one will have it.
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.
(adjusted from original PR description as changes came about -@cretz)
🚀 What
baseten sandbox list: lists sandboxes, filtered by--queryand--status, up to--limitbaseten sandbox describe: shows one sandbox, with--show-secretsto reveal secret env valuesbaseten sandbox create: creates a sandbox, sending only the flags givenbaseten sandbox update: changes a sandbox's image, env vars, or labelsbaseten sandbox delete: deletes a sandbox after confirmingbaseten sandbox exec: runs a command and streams its output, passing through its exit codebaseten sandbox connect: opens an interactive shell in a sandboxbaseten sandbox processstart: starts a command in the backgroundlist: lists processesdescribe: shows one processlogs: prints a process's output, with--tailto follow itwait: waits for a process to exit, passing through its exit codestop: asks a process to stopkill: asks a process to be killedbaseten sandbox imagelist: lists images, up to--limitdescribe: shows one imagepush: pushes an image from a directory or a registry, with--waitto wait for the builddelete: deletes an image after confirminglogs: prints an image's build logslist-tags: lists an image's tagslist-library: lists the platform's starter imagesbaseten api sandbox: direct API calls to sandbox exec plane (matchingmanagementandinferenceforms that already exist for other APIs)💻 How
sandboxpackage from baseten-go#60connectspeaks the sandbox's terminal WebSocket ininternal/sandboxconnectgithub.com/moby/patternmatcher(Docker's.dockerignorematcher) andgithub.com/creack/pty(tests only, not in the binary)🔬 Testing
connectthrough a real pseudo-terminal, and one image push from a directory with a.dockerignoreImportant review notes and deviations
--teamdefaults to your only team and is required if you belong to more than one, which is the API's behavior. Other commands default to the organization's default team--nameis always the command's own resource: the sandbox, or the image underimage. Processes are--pidor--process-name--memory), except--if-not-exists(shortened fromcreate_if_not_exists) and the singular repeatable--envand--labelsandbox exec, not alsoprocess exec, since it is the most common actionimage list-library, a verb like every other leaf command--statusvalues are lowercase like other CLI enumslistandimage listcollect pages up to--limit(default 1000) and print{"items": [...]}, like every otherlistlistcommands togethercreate--wait: create returns once the sandbox is deployed, and the SDKs have no wait either--envvalues are secret (the server's default) and--plain-envvalues are not. Both needKEY=VALUE. A docker-style--env KEYread from the local environment would keep secrets out of shell history, but no other command does that yetexecand processesexecis basicallyprocess startthenprocess wait, with streamed output and--stdin--is the whole command line for the sandbox's shell. Several are quoted so each stays one argument--stdin, output arrives line by line, since stdin can only be written once the process existsprocess logs --tailreplays from the start, then follows--tail, log lines have no stream, since the API's output so far does not say which stream a line came fromstopandkillrequest termination and do not wait for itwrite-stdinorclose-stdincommands, sinceexec --stdincovers pipingpush --dirleaves out paths per a root.dockerignore, read with Docker's own matcher, so the upload matches what Docker would build.dockerignore, the defaults are Blaxel's CLI's list, documented in the help as the equivalent.dockerignore.blaxelignoreinstead, and matches unanchored names at any depth. We follow Dockerpush --waitis opt-in, since builds take minutes--imagebecame--registry-imageso it does not read like the image's nameimage logsprints at most the newest 11,000 lines, the API's limitimage list-libraryhas no--teamconnect