Skip to content

[Feat] Add Box sandbox provider - #1233

Merged
mrubens merged 14 commits into
developfrom
feature/add-box-sandbox-provider-3tuht55ljpsvi
Aug 12, 2026
Merged

[Feat] Add Box sandbox provider#1233
mrubens merged 14 commits into
developfrom
feature/add-box-sandbox-provider-3tuht55ljpsvi

Conversation

@roomote-roomote

@roomote-roomote roomote-roomote Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

​Opened on behalf of @mrubens. Follow up by mentioning @roomote-roomote, in the web UI, or in Slack.

What changed

  • adds Box as a built-in hosted sandbox provider with API-key setup and small, default, or large machine sizing
  • implements Box Public API v1 lifecycle, commands, detached output polling, file upload, preview URL hosting, and /tmp worker bootstrap
  • supports same-Box standby resume, sleep-check shutdown, retention cleanup, usage accounting, and scheduler indexes
  • supports Roomote environment snapshots via Box named snapshots (template boxes): snapshotting an environment saves a roomote-snap-* template, and tasks that start from it fork the template into a fresh Box in seconds instead of re-running setup
  • adds setup and settings UI, production and self-host Compose forwarding, deployment configuration, and public operator documentation

Adapter and bootstrap hardening

  • Box API errors surface the server's error code and message (with the bearer key redacted and length-capped) instead of a bare HTTP status
  • provisioning refusals (box_starting / machine_not_running, in both their 400 and 409 forms) and snapshot_not_ready forks retry until ready, bounded by the readiness timeout
  • destroy/cleanup paths stop with force: true so a failed pre-stop snapshot cannot wedge cleanup; standby keeps the plain stop it needs for resume
  • BOX_TIMEOUT_MS defaults 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)
  • the sandbox-server port is hosted publicly (the worker enforces its own bearer auth, matching other providers' trust model) because Box's private-hosting gate rejects credential-less CORS preflights; app preview ports stay behind the token gate, and the preview proxy converts their _token query into the gate's _port_auth cookie when forwarding
  • install-worker.sh reclaims a root-owned /sandbox via 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-repairs
  • the worker release archive now ships the Dockerfile's PM2_VERSION pin so bring-your-own-image bootstraps install the same pm2 as the baked worker image

Why 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

Box sandbox configuration without a Preview badge or warning UI

@roomote-community

roomote-community Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

No code issues found. See task

  • DEFAULT_COMPUTE_PROVIDER=box is accepted by runtime environment validation.
  • Production and self-host Compose environments forward all BOX_* credentials, runtime settings, and retention policy values.
  • packages/compute-providers/src/adapters/box.ts retries documented early-provisioning 400 machine_not_running and 409 responses.
  • apps/preview-proxy/src/lib/proxy.ts strips all target query parameters before http-proxy joins the request path and moves Box auth into the upstream cookie.
  • apps/docs/providers/compute/box.mdx consistently documents Box named environment snapshots.
  • apps/dev/src/services/__tests__/build-worker-release-script.test.ts supplies the required worker Dockerfile fixture.

Reviewed 57852af

Comment thread packages/compute-providers/src/adapters/box.ts Fixed
Comment thread SELF_HOSTING.md
@roomote-roomote

roomote-roomote Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all live feedback in 137d97e: Box is accepted as the default provider, every BOX_* setting is forwarded through both production Compose anchors, and the CodeQL-flagged trailing-slash regex was replaced with linear trimming. Targeted tests, typechecks, and pre-push checks pass; Compose validation reached the expected Caddy step but Docker is unavailable in this sandbox.

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 &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment thread apps/preview-proxy/src/lib/proxy.ts Outdated
const search = url.searchParams.toString();
const pathname = url.pathname === '/' ? '' : url.pathname;
return {
target: `${url.origin}${pathname}${search ? `?${search}` : ''}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment thread apps/docs/providers/compute/box.mdx Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@mrubens
mrubens marked this pull request as ready for review August 12, 2026 05:12
@mrubens
mrubens merged commit 8748a40 into develop Aug 12, 2026
8 of 9 checks passed
@mrubens
mrubens deleted the feature/add-box-sandbox-provider-3tuht55ljpsvi branch August 12, 2026 05:12
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.

2 participants