Skip to content

feat(sandbox): add compositions and computer-use - #95

Draft
pratikbin wants to merge 9 commits into
mainfrom
feat/sandbox-compositions-computer
Draft

pratikbin wants to merge 9 commits into
mainfrom
feat/sandbox-compositions-computer

Conversation

@pratikbin

Copy link
Copy Markdown
Contributor

Supersedes #84 and #94, merged onto current main. The plugin side (createos-plugins#70) needs both, so they ship as one PR.

From #84

  • sandbox offload <dir> -- <cmd>: stage, run, fetch, destroy
  • sandbox matrix <dir>: build one golden sandbox, run --prepare once, fork it N times, run one job per clone
  • sandbox self pause|delete
  • sandbox fork --count N
  • retry on connection-setup failures; pause/resume now send a {} body; fork waits for pause to finish

From #94

  • sandbox desktop and sandbox computer: screenshot, click, type, key, open, windows

Conflict resolution against #86 (running-status preflight)

Cleanup (/simplify)

  • No behavior, flag, help-text or wire changes.
  • The disk-mount guard is shared by push and pull.
  • Offload and matrix share their run setup and teardown code.
  • desktop uses edit's sandbox picker instead of its own copy.
  • Computer-use GET requests go through one shared generic helper.
  • Leftover raw status strings are replaced with the api.SandboxStatus* constants.
  • Dead branches removed.

Verification

  • go build ./..., go test -count=1 ./... and golangci-lint run pass.
  • gosec: no findings in any file this PR changes. The 15 it reports are already on main (editor, vpn, herdr, devices).
  • govulncheck: only Go 1.26.3 standard-library CVEs, already on main. They are fixed by moving the toolchain to 1.26.6, which is a separate change.
  • Not re-run against a live control plane after the merge.

Known gap (already in #84)

sandbox pull <box> <remote> - streams to stdout and returns before the disk-mount guard runs, so a piped pull from a mounted path skips that check.

Introduce `sandbox offload` and `sandbox matrix` subcommands for one‑shot offload and parallel fork workflows. Add `sandbox self` for in‑sandbox signaling. Implement guards, retry logic, and improved error handling for lifecycle, connection setup, and disk operations.
fork --count was checked after resolving the source ref, so an invalid
count still paid for an API lookup before failing. Move the check to
the top of runFork.

Unknown command names (root or nested) printed a suggestion but
exited 0: urfave's CommandNotFoundFunc has no return value, so
ShowCommandHelp always returns nil after calling it. Root's own
custom Action also never reached that path at all, since urfave only
falls back to CommandNotFoundFunc through its default help Action.
Detect the unresolved name directly in each command's own Action
instead, and return a real error so app.Run (and main.go's exit code)
see the failure.

Claude-Session: https://claude.ai/code/session_019J9PwFePTzAhpPs1TY9ZGq
Go's stdlib flag parsing stops at the first positional argument, so a command like `fork <sandbox> --count 2` left `--count` unparsed and defaulted to 1. Added `forkCountFlag` that falls back to scanning raw os.Args, restoring the intended count.
The computer-use API has shipped in all three SDKs but never reached the
CLI, so the only way to drive a sandbox desktop from a shell was to talk
to the REST API by hand. The Claude Code plugin does exactly that in its
cos driver, which is the one place it bypasses this binary — and the
other integrations, which only shell out, cannot do it at all.

`sandbox desktop` turns on ingress, waits for the desktop stack to come
up, and prints a noVNC link. `sandbox computer` drives that desktop:
screenshot, screen, cursor, windows, move, click, type, key, open, plus
a hidden raw escape hatch for the routes not wrapped here.

Three things carried over from cos because each one costs a debugging
session to rediscover:

- The desktop stack starts after the sandbox reports running, and
  nothing upstream polls for it, so every caller writes the wait itself.
- fc answers 409 desktop_unavailable both while the desktop is booting
  and when an action fails on a live desktop. ComputerError.Retryable
  encodes which codes a waiter should keep trying, so the readiness wait
  neither gives up on a booting desktop nor spins on a permanent
  failure.
- Driving a non-desktop image fails early with the fix, rather than a
  bare 501 from the first call.

Flags are re-scanned by hand because urfave stops parsing them at the
first positional argument, so `computer screenshot my-box --out shot.png`
otherwise writes to the default path without reporting anything wrong —
the same workaround `sandbox edit` makes for --ingress. `--out` has no
short alias: -o is taken by the global output-format flag.

Verified against a live desktop:1 sandbox: link minted, Chrome driven to
a page by chord, typing and Return, pointer landed on the target link,
and every error path checked.
`sandbox desktop my-box --screen screen-1` dropped --screen, and --wait
with it, because urfave stops parsing flags at the first positional. The
computer subcommands already re-scanned their arguments by hand; desktop
read its flags straight off the context and so kept the bug.

Route desktop through the same parser and teach it --wait. A duration it
cannot parse now keeps the declared default instead of zeroing, which
would have turned the readiness wait into a single attempt.

Tests cover both flag positions, the equals form, the short alias, and
operands surviving around a flag.
CI runs golangci-lint, which this was not developed against. Two
classes: a type assertion on an error in the test, which breaks once
anything wraps it, and three `err` shadows in runDesktop.

Assert with errors.As, and assign to the existing err instead of
redeclaring it.
Combines #84 (offload, matrix, self, fork --count) with #94
(desktop, computer) on current main. Conflicts were against #86's
running-status preflight:

- fork.go: keep #84's forkN, using main's typed SandboxStatus consts
- push.go: running preflight first, then #84's disk-mount check;
  rename err to runErr to satisfy govet shadow, matching pull.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant