feat(trpc,db,desktop): environments as the starting point for cloud workspaces
Stacked on #7043 — review that one first; this branches off it because both touch provision.ts.
A cloud workspace used to start from BLAXEL_SANDBOX_IMAGE: one floating image name shared by every sandbox, so a rebuild silently changed what new sandboxes got and nothing recorded which build any of them ran. That env var becomes a row.
Environments
A named starting point owned by an organization, carrying the provider's reference as a discriminated pair — source_kind: 'image' creates from a container image, 'fork' copies a sandbox someone already configured. Provider specifics live in that one row rather than on every workspace, so changing sandbox provider touches one table.
source_ref for 'fork' is the source sandbox name, not a snapshot id — the endpoint is POST /sandboxes/{name}/fork, so a bare snapshot id isn't forkable. Blaxel auto-snapshots on fork, which removes snapshot lifecycle management entirely.
Why fork at all
There is no commit-from-sandbox path to an image: archiving is hibernation ("a sandbox keeps a single archive", memory excluded), and images build only from Dockerfiles. So forking is the only way to capture an environment an agent configured interactively.
Verified against real sandboxes before building on it — a fork of a source idle past standby kept its prepared filesystem and warm repo, and per-process env overrode the inherited identity:
fork (source idle 45s) → 200 {"name":"env-fork-spike","snapshotId":"8f951ba3-…"}
prepared-state in fork → configured-by-agent / 2026-08-31T21:52:14Z
inherited-env in fork → ws=GOLDEN-IDENTITY
override-in-fork → ws=FORK-WORKSPACE-42
repo-warm in fork → main, 235M
A fork inherits the source's env vars and the fork request cannot override them, so identity is written in a second call (PUT /sandboxes/{name}). That update reaches only processes started afterwards — which is why the source must be snapshotted with host-service stopped, and start.sh exec'd only after the update lands. Captured running, the fork would restore a live host-service holding the golden identity and serve the wrong workspace silently.
Both calls go through the pinned @blaxel/core 0.3.11, which already exposes SandboxInstance.fork and updateSandbox — no version bump needed. (An earlier revision hand-rolled these as fetch on the assumption it didn't; that was wrong and is corrected in 540148e4.)
Secrets — reviving #6431
Environments carry their own variables, which resurrects the secrets surface deleted three weeks ago. It was cut for being "URL-only reachable, zero API calls in 90 days" — not for being wrong, but for having no reason to exist. It has one now.
crypto.ts (AES-256-GCM) ports verbatim. The reserved-key list did not. The old one guarded MODAL_API_SECRET, SANDBOX_AUTH_TOKEN — a previous provider's vocabulary — and none of ours. Since these land in the same spec.runtime.envs payload as identity, a user typing ORGANIZATION_ID into the settings form would have overridden a sandbox's organization: a cross-tenant failure reachable from a form. It now blocks by prefix (SUPERSET_, HOST_SERVICE_, BLAXEL_) plus the bare identity names, and is re-checked at resolve time, so a row written before a name became reserved can't shadow identity either.
cloud_workspaces
| column | why |
|---|---|
environment_id | not null; the migration clears the two internal sandboxes rather than backfilling a guess |
host_version | what a sandbox was last observed running, from host.info — never what we deployed |
deleted_at | replaces overwriting status with "deleted", which destroyed why a workspace ended and carried no timestamp |
⚠️ The migration contains DELETE FROM cloud_workspaces. Authorized, and the two live Blaxel sandboxes were deleted at the provider first so nothing is orphaned. environment_id is not null because there was no honest value to backfill — the two rows came from different image builds, so any constant would have made the staleness query confidently wrong.
Deploy requirements
SECRETS_ENCRYPTION_KEYin the API environment, or everysetSecretthrows- At least one environment row before a cloud workspace can be created — creation fails until then, by design
Verification
bun run typecheck clean across db, trpc and desktop; biome clean; bun test packages/trpc 122 pass. Fork behaviour tested against live sandboxes, all torn down. The 27 new strings are untranslated — translate-catalogs.yml fills those on PR open.
Not done
Environment picker in the create modal (currently takes the org's first environment), deleted_at isn't written by the delete path yet, and host_version is never populated.
Settings reachability is done (c4d5ab92c) — worth noting it needed two registrations, not one: the sidebar filters through getAllowedSectionsForVariant, which builds from SETTINGS_ITEMS, so a nav entry alone would still have been filtered out and left the page URL-only. Which is precisely how the surface this revives died.
https://claude.ai/code/session_01HmfE4Qm77AwKUxJeFVMetY
Summary by cubic
Cloud workspaces now start from named, pinned environments instead of the mutable BLAXEL_SANDBOX_IMAGE, so rebuilding an image no longer silently changes future workspaces. The PR adds environment management and selection across clients, safer sandbox provisioning, and authenticated Desktop access.
Environments and workspaces
- Settings can create and archive environments, manage encrypted variables and secrets, import
.envfiles, and search the new section. - Shared environments are readable across organizations but remain read-only.
- Ready cloud workspaces can be promoted to frozen environments after workspace identity, credentials, host data, and bootstrap state are removed.
- Desktop and mobile let users choose and remember an environment, falling back to the first available one.
- Cloud workspaces no longer carry projects; every sandbox clones the same repository and project pickers are removed.
- Cloud creation requires
environmentIdand rejects archived environments. - Sandbox workspaces can open an authenticated Desktop pane from the context menu, + menu, or hotkey, with access scoped to the active workspace.
- Reserved identity and provider variables are rejected, and environment data is capped at 128KB.
Migration and rollout
- Set
SECRETS_ENCRYPTION_KEYandSENTRY_DSN_SANDBOX, then run the manual shared-environment seed workflow once after deployment. - Migration 0105 drops
cloud_workspaces.project_idand clears existing cloud workspace rows because their environments cannot be backfilled safely. BLAXEL_SANDBOX_IMAGEremains an override for the seeded shared image; new 8GB sandboxes include agent templates and a display server, retry bootstrap safely, omit the monorepo, and report tagged crashes.- Run
bun run sandbox:smoke; use--keepandSMOKE_WORKSPACE_IDto inspect a live sandbox. - i18n typecheck now reads committed catalogs only; regeneration and strict validation remain in the lint checks.
Summary by CodeRabbit
- New Features
- Added cloud environment management in Settings, including creation, archiving, promotion, and secret management.
- Added
.envimport, searching, editing, revealing, and deleting environment variables. - Added environment selection for new cloud workspaces on desktop and mobile.
- Added desktop VNC panes for supported sandboxes, with menu actions and keyboard shortcuts.
- Security
- Environment secrets are encrypted, validated, and protected according to sensitivity.
- Bug Fixes
- Improved sandbox telemetry configuration and startup reliability.
- Documentation
- Documented cloud sandbox limitations and telemetry behavior.
- Localization
- Added translations for environment and desktop features.