[Feat] Add Box sandbox provider - #1233
Conversation
|
No code issues found. See task
Reviewed 57852af |
|
Addressed all live feedback in 137d97e: Box is accepted as the default provider, every |
Box API failures previously threw only the bare status (e.g. "failed with status 400"), hiding actionable server messages like the free-trial TTL limit. Include the error payload's code and message in the thrown message, falling back to the nested error object, with the bearer key redacted and the text capped at 300 chars before it reaches logs and the task UI.
install-worker.sh silently exited 0 when /sandbox was unwritable (run_phase's if-condition suppresses errexit and the last phase's exit code won), leaving boxes with no worker and tasks failing with claim timeouts. It now reclaims a root-owned /sandbox via non-interactive sudo (Box stock images ship it root-owned), propagates mkdir/tar failures, verifies worker.js exists after install, and fails fast on the first failing phase. Box adapter changes from the platform guide: - Retry provisioning 409s (box_starting / machine_not_running) for any method, bounded by the readiness timeout; these are documented as refused-without-effect and safe to retry. - Pass force:true on stop for destroy/cleanup paths so a failed pre-stop snapshot cannot wedge cleanup; standby keeps the non-forced stop since it needs the snapshot. - Map box_starting / machine_not_running states to pending.
| // rejected the request without acting on it. Poll until the box comes | ||
| // up, bounded by the same budget as explicit readiness waits. | ||
| if ( | ||
| response.status === 409 && |
There was a problem hiding this comment.
machine_not_running is also documented by Box as a retryable 400 while a Box is in early provisioning; the public API guide explicitly tells callers to wait and retry in that case. This branch only retries the 409 form, so bootstrap/command launch still fails immediately when the API returns its 400 variant. Treat this code as retryable regardless of whether the status is 400 or 409 (and add the 400 case to the test).
There was a problem hiding this comment.
This is still unresolved at the current head: the provisioning retry guard only accepts HTTP 409, so Box's documented 400 machine_not_running response still falls through to BoxApiError immediately. Please include the 400 form and cover it in the retry test.
Box's private-hosting gate authenticates via a ?_token query param and rejects credential-less CORS preflights, so the web UI could never reach the worker's session endpoint (stuck on Connecting), and the server-side reachability probe failed the same way (sandboxSession 503s) — both compose URLs by appending paths to the stored domain, which mangled the token. Host the SANDBOX_SERVER port with --public instead: the sandbox server enforces its own bearer auth, matching the trust model of the other providers' publicly reachable domains. User app ports stay behind the token gate. Also strip the trailing slash URL normalization adds so bare-origin domains compose cleanly with appended paths.
Free-trial Box accounts reject creates with ttlSeconds above 2 hours (trial_auto_stop_required), so an unconfigured deployment previously failed out of the box by requesting the 5-hour SANDBOX_TIMEOUT_MS default. Default BOX_TIMEOUT_MS to 2 hours so Box works with no configuration; paid accounts can raise it explicitly. The worker already snapshots to standby just before the provider deadline and resumes on the next prompt, so the shorter TTL does not strand conversations. Documented in Settings help text and docs.
Box private-hosted domains carry a ?_token query param, which the preview proxy passed through verbatim as the http-proxy target — the gate rejected the mangled request and previews showed its Access denied page. Strip the token from the target and send it as the _port_auth cookie the gate accepts (merged with the browser's cookies), for both HTTP and WebSocket upgrades. Targets without a token are unchanged.
The worker's detached process manager requires pm2 and resolves /usr/local/bin/pm2 first. Worker Docker images bundle it, but bring-your-own images (Box) ship without it, so environment setup failed with 'pm2 is not installed or on PATH' and detached web servers never started. Install it next to node-pty during bootstrap and expose a launcher at the path the worker resolves; a no-op on images that already have pm2.
| const search = url.searchParams.toString(); | ||
| const pathname = url.pathname === '/' ? '' : url.pathname; | ||
| return { | ||
| target: `${url.origin}${pathname}${search ? `?${search}` : ''}`, |
There was a problem hiding this comment.
Keeping the remaining upstream query string in target reintroduces the path-joining bug described above. http-proxy joins target.path with req.url, so a target such as /base?keep=1 and an incoming /page?x=2 becomes /base?keep=1/page?x=2; the upstream receives a malformed keep value instead of the requested path. Either reject/strip all target query parameters after extracting _token, or proxy the preserved parameters without using target.path. The helper-only test does not exercise this request path.
Map Roomote environment snapshots onto Box named snapshots: createSnapshot saves a roomote-snap-* template (POST /named-snapshots), waits for it to materialize, persists the id, then force-stops the source box; resumeFromSnapshot forks the template into a fresh box (POST /boxes with from), which is usable in seconds and skips environment setup. Snapshot environment runs now launch fresh with the worker snapshot command, and standard runs fork their environment's template when one exists. The roomote-snap- prefix distinguishes template ids from bx_ box ids on resume (the standby path is unchanged). Forks still refresh the shipped worker runtime during bootstrap. Box accounts cap named snapshots at 10; documented in the provider docs along with cleanup guidance.
Verified against the real API: named snapshots list as status 'saving' while the capture is in flight, and forking one too early is refused with 409 snapshot_not_ready. Treat 'saving' as pending in the completion wait and add snapshot_not_ready to the retryable 409 codes so a fork racing the save simply waits it out.
| template, or snapshot during Roomote setup | ||
| - retaining the same stopped task sandbox for a follow-up is sufficient | ||
|
|
||
| Box does not support Roomote environment snapshots. A retained Box can resume |
There was a problem hiding this comment.
The new Environment snapshots section says Box snapshots are supported, but this paragraph (and the final Common issues entry) still says the exact opposite. Operators will reasonably conclude the feature is unavailable; remove or update the stale unsupported-snapshot text.
pm2 was npm-installed --no-save into the shared $DATA_DIR prefix, and the worker's own runtime npm installs into that prefix prune untracked packages — pm2 vanished after bootstrap while its launcher wrapper survived, and the existence-only check skipped reinstalling on later bootstraps. Install pm2 into a dedicated $DATA_DIR/roomote-pm2 prefix that nothing else manages, and validate by executing pm2 (repairing a broken wrapper) instead of checking that a file exists.
The worker Dockerfile already bakes a pinned pm2 (ARG PM2_VERSION) into /usr/local/bin/pm2; only bring-your-own-image bootstraps installed pm2, and they pulled latest, drifting from the image pin. Ship the Dockerfile's pin as PM2_VERSION in the worker release archive (like NODE_PTY_VERSION) and have install-worker.sh install that exact version, falling back to latest for older archives without the file.
| # The worker image's Dockerfile ARG is the source of truth for the pm2 | ||
| # version; ship it in the release so bring-your-own-image bootstraps | ||
| # (install-worker.sh) install the same pin instead of drifting to latest. | ||
| PM2_VERSION="$(sed -n 's/^ARG PM2_VERSION=//p' apps/worker/Dockerfile | head -1)" |
There was a problem hiding this comment.
The script test creates a minimal temporary checkout with apps/worker/package.json, but not apps/worker/Dockerfile (apps/dev/src/services/__tests__/build-worker-release-script.test.ts:29-39). This new required sed input is therefore empty and makes that test's local archive build exit before pnpm build; please add the Dockerfile fixture or obtain the pin from an input available in that test.
- Retry machine_not_running provisioning refusals on their 400 form too, not just 409 (the platform guide documents both), still gated on the error code so real 400s fail fast. - Never keep a query string on preview-proxy targets: http-proxy joins target.path with req.url, so preserved params would land mid-path upstream. Strip the whole query while converting _token to the auth cookie; stray params are dropped rather than mangled. - Remove the stale 'Box does not support environment snapshots' copy from the provider docs and point the common-issues entry at the 10-template account limit instead. - Give the build-worker-release script test the worker Dockerfile fixture its new PM2_VERSION extraction reads.
What changed
/tmpworker bootstraproomote-snap-*template, and tasks that start from it fork the template into a fresh Box in seconds instead of re-running setupAdapter and bootstrap hardening
box_starting/machine_not_running, in both their 400 and 409 forms) andsnapshot_not_readyforks retry until ready, bounded by the readiness timeoutforce: trueso a failed pre-stop snapshot cannot wedge cleanup; standby keeps the plain stop it needs for resumeBOX_TIMEOUT_MSdefaults to 2 hours, the free-trial maximum auto-stop, so an unconfigured deployment works out of the box; paid accounts can raise it (the worker already snapshots to standby before the provider deadline, so tasks resume cleanly across it)_tokenquery into the gate's_port_authcookie when forwardinginstall-worker.shreclaims a root-owned/sandboxvia non-interactive sudo (Box stock images ship it root-owned), fails fast per phase, verifies the worker actually landed, and installs a pinned pm2 into an isolated npm prefix on images that lack it — validated by execution so a broken launcher self-repairsPM2_VERSIONpin so bring-your-own-image bootstraps install the same pm2 as the baked worker imageWhy this change was made
Box provides hosted sandboxes with Docker Compose, private port hosting, persistent filesystems, and fast stop/resume behavior. The adapter uses conservative lifecycle handling where the current API does not provide atomic create deduplication or permanent deletion.
Impact
Self-hosted Roomote deployments can select Box as their sandbox provider using only
BOX_API_KEY, including through the shared production and self-host Compose environments, without building a provider-side worker image. Task follow-ups resume the retained Box, and environments can be snapshotted to templates for fast task starts. Cleanup currently stops and archives Boxes, which pauses billing but retains provider-side state until Box's permanent-delete API returns.Known Box account limits that shape behavior: free trials cap boxes at a 2-hour auto-stop TTL and 2 concurrent boxes, and all accounts cap named snapshots at 10 (stale
roomote-snap-*templates must currently be deleted from the Box dashboard). The end-to-end flow — spawn, claim, live session, preview, snapshot, and template fork — was verified against the live Box API during development.Screenshots