Skip to content

feat: add the Roomote deployment-managed compute provider#402

Merged
mrubens merged 11 commits into
developfrom
feat/roomote-cloud-compute-provider
Jul 15, 2026
Merged

feat: add the Roomote deployment-managed compute provider#402
mrubens merged 11 commits into
developfrom
feat/roomote-cloud-compute-provider

Conversation

@mrubens

@mrubens mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What

Adds roomote as a first-class compute provider: managed sandboxes preconfigured by the hosting deployment, running on the existing Modal machinery with deployment-managed credentials (ROOMOTE_CLOUD_TOKEN_ID / ROOMOTE_CLOUD_TOKEN_SECRET) instead of an operator's own Modal account. Operators on deployments that carry these credentials get a zero-setup sandbox option in the wizard, while bring-your-own Modal/E2B/Daytona/Blaxel remain fully available alongside it.

Design

Catalog — the new provider's credentials are modeled as managed infrastructure fields: never operator form inputs. The wizard picker and Settings now hide providers that have no operator-editable fields unless the deployment already satisfies them, implementing the gating that the infrastructureSatisfied doc comment described. Deployments without the env vars never see the provider; deployments with them get it as a one-click choice.

Runtime — the factory and controller delegate to the Modal path with a parameterized vendor (ModalConfig.vendor), and task runs persist vendor='roomote'. Persisting the real vendor matters: sleep-check, task-log fetching, standby retention, and snapshots all resolve credentials per vendor through the setup catalog, so a masqueraded modal vendor would resolve the wrong (absent) credentials on deployments that only carry the managed pair.

Worker sidegetWorkerComputeProviderLabel maps the new provider to modal, so cgroup layout detection and usage polling behave identically; usage recording reuses the Modal policy for the same reason.

Base imageMODAL_BASE_IMAGE_REF stays shared: derivation from the deployment worker image (DOCKER_WORKER_IMAGE / baked RELEASE_VERSION) now covers both Modal-backed providers.

DB — the three sleep-check partial indexes gain the new vendor; migration 0012_great_corsair included.

Fallback semanticsgetDefaultAvailableComputeProvider no longer assumes deployment-managed providers are satisfiable when called without live status (they can only be satisfied by the runtime env).

Notes for reviewers

  • The roomote brand-icon key currently hits BrandIcon's empty-span fallback; needs a logo mark as a design follow-up.
  • DEFAULT_COMPUTE_PROVIDER env enum now accepts roomote so hosted provisioning can default to it.

Testing

  • pnpm lint, pnpm check-types, pnpm knip all pass.
  • Full pnpm test: all workspaces green except 19 pre-existing @roomote/web failures from a stale local test DB (llm_usage_events missing; reproduced identically on a clean tree, unrelated to this change).
  • New tests: factory resolves ROOMOTE_CLOUD_* (and refuses MODAL_TOKEN_* fallback) and reports its vendor; setup status satisfies the provider from runtime env alone and marks it unsatisfiable/hidden without the managed credentials.

Backend seam (second commit)

The managed provider is engine-neutral by contract: ROOMOTE_CLOUD_BACKEND (default modal, the only supported value today) selects which engine runs its sandboxes, so future deployments can run on E2B/Daytona under the same "Roomote" experience.

  • ROOMOTE_CLOUD_TOKEN_ID is now optional — engines differ in credential shape (Modal uses id+secret; API-key engines use only the secret slot); spawn-time validation is backend-specific.
  • The backend selector is a deployment-managed catalog field: resolved with the encrypted-env fallback, reserved from the generic editor, stripped from task sandboxes.
  • The factory and controller resolve the backend before delegating; an unsupported value throws rather than silently running on the wrong engine (the env schema also rejects it at boot).
  • Adding an engine = extend ROOMOTE_CLOUD_BACKENDS, add a branch at the two dispatch sites, and make the static per-provider metadata (capabilities, worker label, resource model, usage policy) backend-aware. The seam doc in compute-providers/roomote-cloud.ts covers this plus the drain-before-switch caveat: machine ids and snapshots are engine-specific, so changing a live deployment's backend requires draining roomote-vendor runs first.

@roomote-roomote

roomote-roomote Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

No new code issues found. See task

  • apps/controller/src/compute-providers/spawn-modal-worker.ts:372-381 passes modal to OIDC priming even for a roomote launch. primeSandboxOidcTargets creates its client from that logical provider, so a deployment with only ROOMOTE_CLOUD_TOKEN_* tries to resolve MODAL_TOKEN_* and fails before starting the worker whenever the environment has OIDC targets. Pass the persisted vendor through this call.
  • packages/db/drizzle/meta/_journal.json:89-94 introduces 0012_great_corsair, but the target branch already contains migration entries 0012-0014. The current journal replacement drops those deployed migrations and the PR is already conflicting; rebase onto develop and regenerate this index change as the next migration while preserving the existing history.
  • apps/web/src/components/settings/ComputeProviders.tsx:120-133 filters an unsatisfied managed provider out of providers but still uses it as effectiveDefaultProvider. If Roomote Cloud credentials are removed after it was persisted as the default, the select has no matching item and the existing missing-configuration alert disappears, while task dispatch continues choosing roomote. Keep the persisted default visible as unavailable, or fall back to a selectable provider with an explicit warning.
  • packages/types/src/setup-compute-config.ts:245-263 marks ROOMOTE_CLOUD_TOKEN_ID optional, so setup and configured-provider discovery accept a secret-only Roomote configuration. The only supported/default backend is Modal, and both the controller and client factory still require the token ID, so this configuration is offered as usable but every launch fails. Keep the ID required until readiness is backend-aware.
  • apps/web/src/components/settings/environments/Environments.tsx:77-80 now excludes roomote from allSnapshotProviders entirely. This avoids offering an unavailable provider, but it also removes the only create/refresh/clear controls for configured Roomote deployments and hides any existing Roomote snapshot. Gate the provider on configured status while still including it when the environment already has a Roomote snapshot.
  • packages/compute-providers/src/modal/create-modal-machine.ts:200-221 accepts a roomote client but continues emitting every create/bootstrap/cleanup mutation event as provider: 'modal'; apps/controller/src/compute-providers/spawn-modal-worker.ts:392-463 does the same for worker-launch events. Roomote task runs therefore persist misleading Modal event metadata. Emit the logical computeClient.vendor/vendor instead.

Reviewed 9332596

mrubens added 3 commits July 15, 2026 17:44
Adds 'roomote' as a first-class compute provider: managed sandboxes
preconfigured by the hosting deployment, running on the existing Modal
machinery with deployment-managed credentials (ROOMOTE_CLOUD_TOKEN_ID /
ROOMOTE_CLOUD_TOKEN_SECRET) instead of an operator's own Modal account.

- Setup catalog models the credentials as managed infrastructure fields:
  never operator form inputs, so the wizard and Settings offer the
  provider only when the deployment env already satisfies it. The picker
  and Settings now hide deployment-managed providers that cannot be
  configured from the UI (implements the documented
  infrastructureSatisfied gating).
- Factory/controller delegate to the Modal path with a parameterized
  vendor; task runs persist vendor='roomote' so sleep-check, log
  fetching, retention, and snapshots resolve the right credentials via
  the provider catalog.
- MODAL_BASE_IMAGE_REF derivation from the worker image now covers the
  new provider; the worker-side label maps to 'modal' so cgroup usage
  polling behaves identically.
- Sleep-check partial indexes gain the new vendor (migration included).
- getDefaultAvailableComputeProvider no longer assumes deployment-managed
  providers are satisfiable when called without live status.
Adds ROOMOTE_CLOUD_BACKEND (default 'modal', the only supported value
today) so the managed provider's engine is a per-deployment choice with
an engine-neutral contract: ROOMOTE_CLOUD_TOKEN_ID is now optional
(API-key engines use only the secret slot), the backend selector is a
deployment-managed catalog field (resolved with the DB fallback,
reserved from the editor, stripped from sandboxes), and the factory and
controller resolve the backend before delegating — an unsupported value
fails loudly at spawn instead of silently running on the wrong engine.

Future engines (E2B/Daytona) extend ROOMOTE_CLOUD_BACKENDS and add
their branch at the two dispatch sites, plus backend-aware capability/
label/resource/usage metadata; see compute-providers/roomote-cloud.ts
for the seam documentation including the drain-before-switch caveat.
…on renumber

- spawn-modal-worker: pass the persisted vendor to OIDC priming instead
  of the literal 'modal'. primeSandboxOidcTargets builds its compute
  client from that provider, so a roomote launch on a deployment with
  only ROOMOTE_CLOUD_TOKEN_* would otherwise fail to resolve
  MODAL_TOKEN_* whenever the environment has OIDC targets.
- ComputeProviders (Settings): resolve the effective default against the
  unfiltered provider list and keep it in the default-provider select as
  a disabled '(unavailable)' item when its section is hidden. Previously,
  removing Roomote Cloud credentials after it was persisted as the
  default made both the select value and the missing-configuration alert
  silently disappear while dispatch kept targeting it.
- db: regenerate the sleep-check index migration as 0015 on top of
  develop's 0012-0014, restoring the journal and 0012 snapshot that the
  rebase had displaced.
@mrubens
mrubens force-pushed the feat/roomote-cloud-compute-provider branch from 6b3412d to 7b529f2 Compare July 15, 2026 21:49
@mrubens

mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

All three review findings addressed in the rebase (7b529f22a):

  1. OIDC priming providerspawnModalWorker now passes the persisted vendor to primeEnvironmentOidcForMachine instead of the literal 'modal', so a roomote launch resolves ROOMOTE_CLOUD_* credentials when the environment has OIDC targets.

  2. Migration journal — rebased onto develop; the index change is regenerated as 0015_slimy_vengeance on top of 0012–0014, with the journal and existing snapshots preserved.

  3. Settings default-provider visibility — the effective default now resolves against the unfiltered provider list, and when it is a hidden deployment-managed provider it stays in the default select as a disabled "(unavailable)" item, so the missing-configuration alert renders instead of silently disappearing while dispatch keeps targeting it.

Validation after rebase: lint / check-types / knip pass; types, env, compute-providers, and controller test suites green. The @roomote/db environment-verification failures reproduce identically on clean develop (local test-DB staleness, unrelated).

…CLOUD_SLUG

The managed provider previously leaned on MODAL_APP_NAME for
per-deployment attribution, which is engine-specific and — being shared
with the plain Modal provider — would also rename the app in an
operator's own bring-your-own Modal workspace.

ROOMOTE_CLOUD_SLUG is the deployment identity; each backend maps it to
its native grouping mechanism. The Modal backend groups sandboxes under
the app roomote-<slug> (an explicit MODAL_APP_NAME still wins as an
escape hatch; the plain Modal provider ignores the slug entirely). The
slug is a deployment-managed catalog field: DB-fallback resolution,
reserved from the env editor, stripped from task sandboxes.
@mrubens

mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up per design discussion: per-deployment attribution now uses an engine-neutral ROOMOTE_CLOUD_SLUG instead of leaning on MODAL_APP_NAME. Each backend maps the slug to its native grouping — the Modal backend groups sandboxes under roomote-<slug> — and the plain Modal provider ignores it entirely, so a hosting operator's attribution can no longer bleed into a customer's bring-your-own Modal workspace via the shared env var. Explicit MODAL_APP_NAME still wins as an escape hatch.

mrubens added 4 commits July 15, 2026 18:07
- StepComputeProvider fixtures now carry a realistic operator-editable
  credential field (fields: [] represents a deployment-managed provider,
  which the picker deliberately hides when unsatisfied), plus a test
  covering that gating in both states.
- Environments: drop 'roomote' from the snapshot-provider list until it
  is gated on configured providers — an unconditional entry would offer
  roomote snapshot controls on deployments without the provider.
  Task-sleep snapshots are unaffected (snapshotCapableComputeProviders
  keeps the vendor).
The traced r.svg has hardcoded fills, so BrandIcon renders it as a CSS
mask over bg-current (the RoomoteWordmark technique) to inherit the
surrounding text color like the other brand icons — white inside the
picker buttons, foreground-colored in Settings.
Display strings only; the provider id, env vars, and persisted vendor
stay 'roomote'.
A secret-only deployment passed configSatisfied, so Roomote Sandbox was
offered (and defaulted) as usable while both the controller and the
client factory reject launches without the token id — every task would
fail to start. Keep the id required; future API-key backends should make
requiredness backend-aware instead of dropping it outright. Adds a test
pinning the secret-only-unsatisfied behavior.
@mrubens

mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Fourth finding addressed in e4cd246db: ROOMOTE_CLOUD_TOKEN_ID is required again. The optionality was premature future-proofing — with Modal as the only backend, a secret-only deployment read as configured while every launch would have failed on the missing id. The catalog comment now documents that future API-key backends should make requiredness backend-aware rather than dropping it, and a new test pins the secret-only-unsatisfied behavior.

Excluding roomote from the snapshot-provider list entirely removed the
only create/refresh/clear controls on deployments where the managed
provider IS configured, and hid any existing roomote snapshot. The list
now includes roomote when compute status reports it configured (via a
new useComputeProviderConfigured hook), and an environment that already
holds a roomote snapshot keeps its controls regardless, so stale
snapshots stay manageable after credentials are removed.
@mrubens

mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Environments snapshot gating addressed in the latest push: allSnapshotProviders now includes roomote when the compute status reports it configured (new useComputeProviderConfigured hook over compute.status, cache-shared with the sandbox settings page), and an environment that already holds a roomote snapshot keeps its refresh/clear controls even when the provider is no longer configured. Modal/E2B behavior is unchanged. Three new client tests cover configured, unconfigured, and stale-snapshot states.

roomote task runs persisted every create/bootstrap/cleanup and
worker-launch mutation event as provider 'modal'. createModalMachine now
emits computeClient.vendor and spawnModalWorker emits its vendor, so
event metadata matches the persisted task-run vendor. Client
construction and resource resolution keep the literal engine. Adds a
test asserting every event from a roomote-flavored launch carries the
roomote vendor.
@mrubens

mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Sixth finding addressed in the latest push: createModalMachine mutation events now emit computeClient.vendor and spawnModalWorker's worker-launch events emit its vendor, so roomote runs persist event metadata consistent with their task-run vendor. Client construction and resource resolution intentionally keep the literal engine. New test asserts every event from a roomote-flavored launch carries the roomote vendor.

cleanupModalInstance (invoked from the create path and worker spawn on
failure) still hardcoded provider 'modal' in its destroy_instance
events. It now emits the client's vendor; the computeClient param picks
up 'vendor' alongside 'destroyInstance', which both callers already
pass. Adds tests asserting the destroy events carry the roomote vendor
on success and failure paths.
@mrubens

mrubens commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the event-metadata finding: the residual path was cleanupModalInstance — its destroy_instance events (fired from the create path and worker spawn on bootstrap failure) still hardcoded provider: 'modal'. It now emits the client's vendor (the computeClient pick gains 'vendor'; both call sites already pass full clients), with tests covering the success and failed-cleanup event paths. The remaining 'modal' literals in the tree are deliberate: client construction, the resource model, the worker-context label (worker-side runtime handling must match the engine), and the usage-sample label that pairs with it.

@mrubens
mrubens merged commit bafcd94 into develop Jul 15, 2026
18 checks passed
@mrubens
mrubens deleted the feat/roomote-cloud-compute-provider branch July 15, 2026 23:32
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