feat(cli): coworkers set up their own cloud environment from the CLI
Summary
People were starting cloud workspaces on Satya's environment, which carries his personal tokens. This PR makes the setup self-serve from the CLI. It also removes the naming rules and name-based defaults that made using the wrong environment easy.
- CLI. Environment commands
create,save,replace,update,archiveandrepos. A newsuperset connectionsgroup for the credentials a person's cloud workspaces run with: agent sign-in, and GitHub as yourself. - API.
environment.promotecan replace an environment's golden in place. Saving as new copies the source's variables. Promote takes a scope. The tRPCmaxDurationgoes 60 → 300 s so a promote is not killed partway through. - Ids, not names. Environments are referenced by id everywhere (CLI, MCP, shared resolver), and the
(organization_id, name)unique index is dropped (migration 0128). - Release. The release rebuilds the internal environment by id, not by name.
internal-setup.shbecomessatya-setup.sh. - Setup. A cloud workspace runs
bun installon its first boot.
Why
- Workspaces were landing on Satya's environment.
ws createand the MCPworkspaces createtool fell back to "the first environment with repositories". That is alphabetical, and in our org it was "Superset", Satya's environment. - Nobody had another path. Every other route to your own environment needed the desktop.
- The name rules got in the way. Names were unique forever, archived ones included. Matching was case-insensitive, but uniqueness was case-sensitive.
- Renaming broke the release. The release looked the row up by name, so renaming it would have created a second environment.
What / How
CLI (internal audience, like environments)
| Command | Does |
|---|---|
environments repos | Repos the org's GitHub App can see; the values --repo takes. |
environments create --name --repo … [--hooks-repo] [--scope] [--region] | Image-based environment from owner/name repos. |
environments save --workspace --name [--scope] | Always a new environment from a ready box. |
environments replace <env-id> --workspace [--scope] [--yes] | Swaps that environment's golden for the box's, keeping id, name and variables. |
environments update <env-id> [--name] [--scope] | Rename, or change who can use it. |
environments archive <env-id> [--yes] | Archive; deletes the golden. |
connections agents set|list|remove | Agent sign-in (agentCredential.*). The value comes from a hidden prompt or stdin, never a flag. |
connections github connect|status|disconnect | Opens the authorize URL and waits until connected (githubUser.*). |
- Confirmation:
replaceandarchivemake you type the environment's name in a terminal, and refuse without--yeswhen there isn't one. That covers agents. - Timeouts:
saveandreplacetreat a 502/504 as "the server is still going". They then pollenvironment.listfor the result.savelooks for a row that was not there before the call, because names are no longer unique.
API: environment.promote
- New or replace. It takes either
name(new environment) orenvironmentId(replace in place). - Saving as new.
- The source environment's variables are copied: decrypted and re-encrypted, since each value is bound to its row.
- The new row and its repos are written in one transaction.
- Replacing in place.
- It updates the row's source, region, bundle and hooks repo, and rewrites its repos, in one transaction.
- The new golden gets its own name, because the row forks from the old one until the update lands.
- The old golden is deleted after the transaction.
- Scope.
scopeis optional. Settingpersonalrecords the caller as creator when the row has none (the release-made row). It refuses when someone else created it. This check runs before the snapshot, so a refusal can't leave an orphaned golden. - Desktop. The desktop's
{ cloudWorkspaceId, name }call still works unchanged.
Ids only
- Resolution.
selectCloudEnvironmentand the CLI'sresolveEnvironmentmatch by exact id. - Defaults. With nothing requested, an environment is picked only when exactly one qualifies. Otherwise the error lists
id (name)pairs.
Release
- Lookup by id.
SUPERSET_INTERNAL_ENVIRONMENT_ID(set insandbox.ymlto prod's row) selects the environment to update. Name, scope and creator are never touched, so a rename or making it personal survives releases. - No id set. Without the variable (a fresh dev database) the release creates one and logs its id.
- Default seed. The shared
Defaultseed usedON CONFLICT (organization_id, name). It now finds its row and then updates it, or inserts one.
Migration 0128
ALTER TABLE "environments" DROP CONSTRAINT "environments_organization_id_name_unique";, generated by drizzle-kit.
- Production size:
environmentsis 96 kB with 9 rows (read-only query, 2026-09-28). - No type change, no FK: the lock is ACCESS EXCLUSIVE on
environmentsonly, briefly. - Under load: rehearsed on this workspace's Neon branch, a production child created today. pgbench ran 8 clients reading and inserting into
environmentswhilePGOPTIONS="-c lock_timeout=5s -c statement_timeout=60s" drizzle-kit migrateran. It applied in 1 s; 5,700 transactions, 0 errors. That was one pass, since the migration can't be reapplied on the same branch. - Seed afterwards: ran
seed.tstwice on the migrated branch. There is oneDefaultrow, updated both times.
Manual QA
Verified against production with Satya's CLI login, on personal test environments that were archived afterwards:
-
environments reposlists the org's 22 repos. -
connections agents listandconnections github statusread correctly. -
create→update --name→archivework, and the repo and hooks repo are stored. -
createwith an unknown repo points toenvironments repos. -
connections agents set codexwith a bad key: the server refuses it and nothing is stored. -
archivewithout--yesand without a terminal refuses, and the environment stays. With--yesit archives. In a pty, typing the right name archives. A wrong name followed by Ctrl-C prints "Cancelled" and the environment stays. -
replacewithout--yesand without a terminal refuses before any server call. Satya's golden is unchanged. -
--environment Superset(a name) is rejected; the error lists the ids.
Not yet run for real: the new promote paths (save as new with variables, replace in place, scope). These need this API deployed, because a box can't reach a local API to become ready. The plan after deploy: create a box, save, fork it, replace, then confirm the old golden is gone and the variables came across.
Testing
bun run typecheckin cli, trpc, shared, mcp, sandbox, db and desktop.bun test: cli 281, trpc 396, shared 1147, mcp 11. New tests arepromoteWorkspace.test.ts(timeout polling, same-name environments, refusals) andcloud-environments.test.ts(ids only, single-choice default).biome checkis clean.
Risks / Rollout
- Breaking change for names. Scripts and agent prompts that pass an environment name, or rely on
ws createpicking one, now fail with a list of ids. Only the internal audience is affected. - Merging starts a production release, since the PR touches
packages/sandbox/**. The release updates1dcfb8ac…by id, so there is no rename race. After it runs, Satya's environment becomes "Satya's Superset", personal, viasuperset environments update. - Rollback: revert the PR. Migration 0128 is forward-only. Putting the unique index back would fail once two environments share a name, and nothing needs it any more.
Follow-ups
- Inside a cloud box, commands marked
sandbox: falsesay "Unknown command". A "run this from your machine" message needs a framework change. --env-filesets keys one by one, so a failure partway leaves a partial set.- There's no MCP tool for listing environments. An agent finds ids from the error message.
Summary by cubic
Lets coworkers create and manage their own cloud environments from the CLI, instead of workspaces falling back to Satya's. Environments are now addressed by id everywhere, not name, and the name-unique index is dropped.
New Features
- New
environments create|save|replace|update|archive|reposcommands, plusconnections agentsandconnections githubfor the credentials a person's cloud workspaces run with. environment.promotesaves a workspace as a new environment (re-encrypting its variables onto the new row, read before the snapshot) or swaps an existing one's golden in place; areplace/archiveneeds the name typed in a terminal, or--yes.saveandreplacepoll for the finished row on gateway timeouts, since names are no longer unique.- Promote cleans up on failure: an unused golden is deleted if the DB write fails, and a failed delete of a replaced golden is logged.
replacewon't update an environment archived underneath it (its new golden is then deleted), and requires the golden, scope and creator it loaded, so a concurrent change becomes a conflict.archivedeletes the golden the row points at when archived, andconnections github connectreports an existing connection instead of authorizing again. - The release rebuilds the internal environment by id (set in
sandbox.yml), so a rename no longer forks a second one; a cloud workspace runsbun installon first boot.
Migration
- Migration 0128 drops
environments_organization_id_name_unique; names are labels, and CLI and MCP now require environment ids (errors listid (name)pairs). - Merging starts a production release because it touches
packages/sandbox; rollback needs a revert, the migration is forward-only.
Summary by CodeRabbit
-
New Features
- Added CLI commands to connect and disconnect GitHub, check connection status, and list, sign in, or sign out agents for cloud workspaces.
- Added commands to create, update, archive, replace, save, and list cloud environments and connected repositories.
- Cloud workspaces can be saved to new or existing environments, with optional scope settings. Saving restarts the workspace.
-
Changes
- Environment selection now uses IDs; names are no longer accepted, and ambiguous selections require an explicit ID.
- Environments in the same organization can now share a name.
- Environment archive and replacement operations require confirmation unless
--yesis supplied.