Skip to content

feat(sandbox): add computer and desktop commands - #94

Draft
pratikbin wants to merge 3 commits into
mainfrom
feat/sandbox-computer-desktop
Draft

pratikbin wants to merge 3 commits into
mainfrom
feat/sandbox-computer-desktop

Conversation

@pratikbin

Copy link
Copy Markdown
Contributor

What

Adds createos sandbox desktop and createos sandbox computer — the computer-use surface that already exists in all three SDKs but never reached the CLI.

createos sandbox create --rootfs desktop:1 --name my-desktop
createos sandbox desktop my-desktop          # ingress on, wait for the stack, print a noVNC link
createos sandbox computer screenshot my-desktop --out shot.png
createos sandbox computer key my-desktop ctrl l
createos sandbox computer type my-desktop "example.com"

Why

Computer-use shipped in fc-sdk, createos-go-sdk and createos-python-sdk, but not here. That left the CLI the only client that couldn't drive a desktop — so the Claude Code plugin's cos driver talks to the REST API directly for these two verbs, the single place it bypasses this binary. It carries its own auth precedence and error-code map to do it.

Every other integration (pi-extension, opencode-plugin, herdr-plugin) only shells out to this CLI, so none of them can do computer-use at all today. This closes that.

Follows ADR-0001 in createos-plugin, which records the intent to fold the driver's engine into this CLI and keep the plugins thin.

Commands

command
sandbox desktop [<sandbox>] ingress on, wait for readiness, print the noVNC link
computer screenshot PNG to a file
computer screen / cursor / windows read state
computer move / click pointer
computer type / key keyboard, key presses a chord
computer open URL or path in the desktop browser
computer raw hidden escape hatch for unwrapped routes

Three behaviors carried over from cos

Each of these costs a debugging session to rediscover, so they're encoded rather than left to callers:

  1. The readiness wait. The desktop stack (Xvfb → XFCE → x11vnc → websockify) starts after the sandbox reports running, and nothing upstream polls for it. Every caller ends up writing this wait; now it's in sandbox desktop.
  2. 409 is ambiguous by design. fc returns desktop_unavailable both while the desktop is booting and when an action fails on a healthy desktop. ComputerError.Retryable() encodes which codes a waiter should keep retrying, so the wait neither abandons a booting desktop nor spins for the full timeout on a permanent failure. This is what the tests pin.
  3. Rootfs guard. Pointing these at a non-desktop image fails immediately with the fix, instead of a bare 501 from the first computer call.

Flag parsing

parseComputerArgs re-scans arguments by hand because urfave/cli v2 stops parsing flags at the first positional. Without it, computer screenshot my-box --out shot.png silently writes to the default path and reports success — found while testing. Same workaround sandbox edit already makes for --ingress.

--out deliberately has no short alias: -o is the global output-format flag.

Verification

Against a live desktop:1 sandbox (created, driven, destroyed):

  • desktop → ingress enabled, readiness wait, noVNC link with expiry and a "anyone with this link can control it" warning
  • rootfs guard correctly refused a devbox:1 sandbox before touching ingress
  • screen 1280×800, cursor, windows (28)
  • screenshot verified by eye — Chrome on example.com
  • openkey ctrl ltypekey Return navigated to example.com/index.html
  • move 300 337 landed on the target link (hand cursor, status bar confirmed)
  • raw GET clipboard reaches unwrapped routes
  • interactive picker lists and cancels cleanly
  • error paths: one coordinate, non-numeric coordinates, empty text, malformed JSON, unknown sandbox, invalid screen — all actionable

Wire format cross-checked against createos-go-sdk rather than trusting cos alone: screen_id, /computer/screen, /cursor, /mouse/move, /mouse/click, /keyboard/type {text}, /keyboard/press {keys}, /open {target}, /screens/{id}/connect all match.

gofmt, go vet clean; 71 tests pass. golangci-lint and gosec were not available on the machine this was developed on.

Draft — open questions

  • Clipboard. The server exposes GET/PUT /computer/clipboard and it works (raw GET clipboard returns {"text":""}). For agent use it beats type for long strings — one call instead of per-character input. Worth a first-class computer clipboard command in this PR, or separate?
  • Should desktop create sandboxes? cos desktop creates one if none exists, but that relies on its per-repo statefile, which is plugin state and shouldn't move into a stateless CLI. Currently create --rootfs desktop:1 then desktop <ref>. A --create flag would restore the one-liner.
  • Screenshot default path is ./screenshot.png. cos used its state directory; the CLI has none.

Mesh

repo status
fc-sdk already-present
createos-go-sdk already-present
createos-python-sdk already-present
createos-cli this PR
createos-v2-landing needs a CLI reference entry
createos-plugin cos can drop api()/api_auth() and shell out like every other verb

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.
pratikbin added a commit to NodeOps-app/createos-plugins that referenced this pull request Sep 16, 2026
The CLI grew `sandbox desktop` and `sandbox computer`, so the REST layer
this driver carried for them has no reason to exist. Delete api(),
api_auth(), api_check() and desktop_wait(), and shell out like every
other verb.

That layer was the one place cos bypassed the CLI, and the cost of it
was a second implementation of things the CLI already knew: its own auth
precedence, its own error-code map, its own readiness poll. It also held
a false premise — a comment claiming the CLI does not read
CREATEOS_API_KEY, which it has (cmd/root/root.go). The same code now
serves pi, opencode and herdr, which could not do computer-use at all.

cos keeps what the CLI cannot know: which box belongs to this project,
and creating one on a desktop image when there is none. The rootfs guard
stays too, because the CLI tells you to create a new sandbox, and for a
project box the fix is to replace the one you have.

Two details preserved for callers:

- `cos computer screenshot -o file` still works. The CLI spells it
  --out, since -o is its global output-format flag, so cos translates.
- `cos resume` now says to re-run `cos desktop` on a box that had one. A
  resumed desktop answers reads before its input stack is back, so a
  click can 409 for a moment after the screen route already succeeds.

Requires a createos CLI carrying those commands. cos probes for them and
tells the user to upgrade rather than failing with "unknown command".

Blocked on NodeOps-app/createos-cli#94.
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