Skip to content

refactor(cloud): boot timing off the workspace row; repositories unordered; provision traced - #7551

Merged
saddlepaddle merged 5 commits into
sandbox-v2from
sandbox-v2-boot-timing-sentry
Sep 14, 2026
Merged

saddlepaddle merged 5 commits into
sandbox-v2from
sandbox-v2-boot-timing-sentry

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Decisions from Satya's migration review of #7548.

One migration. The six incremental migrations sandbox-v2 accumulated (boot stamps added then dropped, position added then dropped, a cascade rebuilt) are regenerated as one, 0115_cloud_environments_repositories, before any reach main; a second drizzle-kit generate finds no changes. The dev Neon branch's journal was rewritten to the single entry after its six recorded hashes matched the six old files and its schema matched the new migration (migrate is a no-op there). This PR's preview Neon branch, which had run the old six, was deleted so CI recreates it from production.

Boot timing leaves the row. No stamp columns. The desktop's cloud_workspace_opened event, its create-start record, the terminal attach notifier and the measure script are removed (the P0 study is done; its numbers are in the start-time plan). The provision job is one Sentry transaction with a span per stage (claim, create, settle), sampled by transaction name so nothing else in the API is traced, flushed by the job since it runs after its request has answered. No fallback: whether it arrives is checked after the production deploy.

No repository order. No position column. Repositories read alphabetically everywhere; the workspace opens on the environment's config location, else the first by name; cloudWorkspace.repositories marks that one primary. Opening multi-repository workspaces at /workspace with a sidebar repository picker is a recorded TODO.

No environment hooks override. environments.hooks is dropped with the identity's SUPERSET_SANDBOX_HOOKS: its only value read back was the internal environment's start, which is now the monorepo's cloud-only "start": ["superset-dev-stack"] in .superset/config.json; the release still runs internal-setup.sh itself and builds its golden from SUPERSET_INTERNAL_BRANCH (default main). A redesign of config.json for cloud is a recorded TODO.

No stored checkout branch. cloud_workspace_repositories.branch is dropped: it only fed a box's first checkout and went stale after. The primary checks out the workspace's own branch and the rest their default branch, both read when the box is claimed. Verified on a new two-repository box (identity lists both on main, both checkouts on main, no hooks key).

Verified. Typecheck, lint, i18n (one changed string, 16 locales), sandbox lib and self-seed tests. On the dev stack: a single-repository create came up ready with the checkout at the root marked primary (stages claim 0.7 s, create 10.5 s, settle 1.3 s). Migrations review page: https://app.superset.sh/page/sandbox-v2-migrations-mhf14m

https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

…dered; provision traced

The five boot-stamp columns are dropped (migration 0119) and the desktop's
cloud_workspace_opened event, its create-start record and the terminal
attach notifier are removed with the measure script: the P0 study is
done and its numbers are in the plan. The provision job is now one Sentry
transaction with a span per stage (claim, create, settle), sampled by
name so nothing else in the API is traced, flushed by the job itself
since it runs after its request has answered, and a structured log line
records the same durations in Vercel whether or not the span arrives.

position leaves both repository link tables (migration 0120): repositories
read alphabetically everywhere and the workspace opens on the environment's
config location, else the first by name; the workspace-repositories
endpoint marks that one primary. Multi-repository workspaces opening at
the root with a sidebar picker is a recorded TODO.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP
@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 7395d7f1-e0c5-4c50-9f89-39f9b3805098

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

The six incremental migrations sandbox-v2 accumulated (boot stamps added
then dropped, position added then dropped, a cascade rebuilt) are
regenerated as one, 0115_cloud_environments_repositories, before any of
them reach main; a second generate finds no changes. The dev Neon branch's
journal was rewritten to the single entry after its six recorded hashes
matched the six old files and its schema matched the new migration.

The provision job's structured log line is gone: the Sentry transaction is
the record, and whether it arrives is checked after the production deploy.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP
…anch

environments.hooks held one value anything read back: the internal
environment's start hook. It is now the monorepo's own cloud-only `start`
key in .superset/config.json, the release still runs internal-setup.sh
itself, and the identity's SUPERSET_SANDBOX_HOOKS goes with the column.
The release builds its golden from SUPERSET_INTERNAL_BRANCH (default main)
so a branch can be released before it merges.

cloud_workspace_repositories.branch only fed a box's first checkout and
went stale after. The primary checks out the workspace's own branch and
the rest their default branch, both read when the box is claimed.

The branch migration is regenerated again as one file; the dev Neon
branch had both columns dropped and its journal entry repointed.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP
@saddlepaddle
saddlepaddle merged commit 6718e43 into sandbox-v2 Sep 14, 2026
23 checks passed
@saddlepaddle
saddlepaddle deleted the sandbox-v2-boot-timing-sentry branch September 14, 2026 22:39
saddlepaddle added a commit that referenced this pull request Sep 15, 2026
…ts, brokered credentials (#7548)

* docs(sandbox): record the sandbox layout decisions, timing plan and build checklist

Twenty-four decisions on how a cloud workspace sandbox is laid out, provisioned,
credentialed and updated, taken directory by directory against the reference
design; the start-time plan they serve; and the per-PR build and test checklist
the sandbox-v2 branch follows.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(sandbox): measure every stage of a cloud workspace boot (P0) (#7527)

One trace per cloud workspace boot, from cloudWorkspace.create to the first
host-service 200 and the desktop's first terminal, and a script that turns five
creates and five reopens into a stage table. The start-time plan's estimates
are replaced with what it measured.

start.sh stamps every phase to /data/boot.log with millisecond epochs and
host-service reports the current boot's stamps and its runtime on health.check
in sandbox mode, adding its own process-start and listening marks. The
provisioning job writes its clock onto the cloud_workspaces row (job start,
sandbox create start and end, boot fired) with `ready`, and the first wake that
sees host-service answer writes first_healthy_at and nudges the list. The
desktop emits cloud_workspace_opened once per open with create→ready,
ready→access, ready→first-200 and ready→first-terminal, plus the row's
job-side deltas; a terminal attach broadcast in the transport gives it the
last of those. `bun run sandbox:measure` runs the job in-process against the
newest fork environment, probes health every 100 ms beside the API's own
one-second poll, stops and wakes each sandbox, prints medians and maxes and
deletes what it created.

Measured against a rebuilt dev golden: create to first 200 is 19.6 s median,
of which the boot script is 2 s (the checkout) and host-service 1.1 s; the
first SDK call after Sandbox.fork blocks about 15 s in most boots, and a
resume is about 15 s regardless. Reopen to first 200 is 15.4 s median.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(sandbox): the box as a bundle — packages/sandbox, the ubuntu user, the boot runner, the env push and header-rule credentials (#7528)

* feat(sandbox): the box as a bundle: packages/sandbox, the ubuntu user, the boot runner and the env push

A cloud workspace's box is now produced by packages/sandbox: an image that
carries the OS, toolchain and desktop packages, and a content-addressed
bundle (rootfs overlay, hashed assets from cdn.superset.sh, versioned
steps) the box installs and updates itself from at boot. The control
plane writes one identity file, starts superset-boot through the sandbox
API with the host secret in that command's env, and pushes the
environment's variables into host-service after boot; every outbound
credential, GitHub included, is a firewall header rule the box never
sees. host-service, the desktop, the checkout and every hook run as
ubuntu; the desktop stream is websockify on its own published port with
its own gate ticket, and host-service no longer proxies VNC.

The release rebuilds the runtime asset and the golden, probes a fork
(including a stop and a wake) and writes the environment rows only after
the probe passes; environments gain a bundle pin and hook overrides
(migration 0115). CI boots the image twice on every sandbox PR.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): boot through sudo with the secret preserved, log a missing runtime, and phase-token stamps

Vercel's own sudo option resets the env, so the host secret never reached
the boot runner on a real box; the image's sudoers now grants SETENV and
the control plane runs sudo --preserve-env itself. The boot-twice test
boots the same way. A missing host-service runtime is logged rather than
refused so the desktop still comes up. The runner's fail-open sentinel
no longer silences steps before a disabled one, and a new version of the
disabled step retries it; runner-check.ts covers the runner against a
synthetic bundle and runs in CI. Boot stamps are dotted phase tokens so
health.check can report them.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* test(host-service): the pushed environment reaches new terminals only

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* style(sandbox): format the boot-twice regex

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* chore(sandbox): drop a scratch inspection script that slipped into the tree

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* chore(sandbox): host-service asset row from the merged runtime

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): the setup hook runs with no USER in its env

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): give the dev gate a per-port origin so a workspace's two tickets never share a URL

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* ci(sandbox): the bucket client needs CDN_URL too

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): the release probe finds the dev stack on the env file's port; a golden's logs are stripped before its snapshot

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): the visible Chrome profile gets its own directory, since Chrome 136+ refuses remote debugging on the default one

The desktop entry now runs the wrapper instead of copying its flags, and
the login profile turns the agent CLIs' self-updater off (installed by
root at image build, it had nowhere to write).

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): record the real-box and desktop-pane evidence

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(sandbox): terminals inherit the desktop session's bus address

The reference captures its session env for shells; the desktop init now
starts the session bus first and writes its address for the login
profile to source, so a GUI app launched from a terminal joins the
session. Release and rollback evidence recorded.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* perf(sandbox): one call to boot a box; a create settles instead of re-waking

A wake paid the platform's resume on writeFiles and then made three more
sequential calls. The identity now rides in the boot command's env beside
the host secret and the runner writes the file itself; the policy update
goes out together with that command; the health poll runs at 100 ms; and
a create, whose box is already booting, only waits and pushes the
environment instead of re-running the wake. The provisioning stamps that
the merge dropped are back on the row.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the runner already on the box handles the boot, so runner-input changes ship with the image

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the v2 layout's stage table beside the P0 baseline

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(cloud-workspace): one guarded status transition; a delete during provisioning wins

Every status write goes through transitionCloudWorkspace, which only
moves a row out of the states it may leave. The provisioning job marks
ready only from provisioning and tears its box down when a delete got
there first; a readiness timeout keeps the box for diagnosis.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(host-service): name the managed-env module file, since Node cannot import the directory

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* style(host-service): format the managed-env import

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* revert(sandbox): the identity is a file the API writes before boot (#7534)

Satya chose the file over the boot command's env: what a person reads on
the box stays legible, it has no size ceiling, and the runner's inputs
keep one shape; the extra provider call is about 0.3 s on a 15 s path.
The policy update still goes out alongside the write, and a create still
settles instead of re-waking. The box now receives only the start and
ports hook overrides; setup is the release's and could be a whole script.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(sandbox): the runner sequences the start hook; a failed box is kept and stopped (#7535)

* feat(sandbox): the boot runner sequences the start hook; host-service runs it on request

Settled with Satya after the fact: the environment the hook needs is
pushed into host-service and never leaves it, so host-service spawns the
hook; the runner decides when, once host-service answers, the push has
landed and the checkout is in, through sandbox.runStartHook, so every
boot-time action reads in one log. boot-twice pushes an environment and
checks the log's hook line.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(cloud-workspace): a box whose host-service never answered is kept and stopped

Settled with Satya after the fact: the boot log, desktop and shell stay a
resume away for diagnosis, and the box costs storage rather than compute
until someone looks or deletes it. Any other provisioning failure still
deletes the box.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(environments): multi-repository environments, scope and the creation dialog (#7538)

* feat(environments): multi-repository environments, scope and the creation dialog

An environment lists its repositories in order (the first is the one a
workspace opens on), names which checkout's .superset/config.json it acts
on, and is scoped to the organization or to its creator. A workspace fixes
its checkouts at create; the box lays every repository out under
/workspace/<name>, host-service seeds a project per checkout, and the
firewall carries one installation token scoped to exactly those
repositories.

The desktop's environment dialog takes name, repositories (with a resync),
config location and scope, and can hand a new environment to an agent that
installs what the project needs and writes its hooks. The new-workspace
form picks repositories for an environment that fixes none, and reads
branches from the primary repository. Mobile reads the same rows.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(environments): the cloud form's base branch, the asset fetch around a cached 404, and the records

The base-branch pill now renders for a cloud workspace and its choice is
the branch the box checks out (mobile already sent one); an untyped choice
lets the API resolve the primary repository's default. The host-service
asset moves to this checkout's build. The runner's asset fetch retries with
a cache-busting query when the plain content-addressed URL answers 404: the
CDN caches a 404 for four hours, so an image build that ran before the
publish poisoned the edge. Decisions 25 and 26, the checklist's PR 6 with
the CDP and real-box evidence, and the layout mismatch are recorded.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* test(host-service): the self-seed test carries the multi-repository identity

The integration test built a single-checkout identity by hand; it now lists
two repositories and expects the primary under the cloud workspace id and
the sibling under the derived one.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(db): a workspace's repository rows go with the repository (#7540)

`cloud_workspace_repositories.repository_id` had no delete rule, so
disconnecting the GitHub integration (which deletes the installation and
its repositories) was blocked by any cloud workspace that had checked one
out. Migration 0118 (drizzle-kit) rebuilds the constraint with cascade, as
`environment_repositories` already had.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): the managed env reaches host-service's spawns; install-host takes the bundle's tarball (#7541)

* fix(host-service): the managed environment reaches host-service's own spawns

The control plane's push only reached terminals; gh, git, the credential
helper and anything else host-service spawns itself ran on the login-shell
snapshot, so on a sandbox `gh` refused to call GitHub for lack of the
placeholder token even though the firewall rule was in place. The push now
mirrors into the process environment (a key dropped by the next push leaves
it) and the tool environment merges the managed set over the cached shell
snapshot, the way terminals already did.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): install-host takes the tarball this bundle declares; sync-assets prunes stale media

On a box woken onto a bundle with a new host-service, `install-host` ran
and left the old build current: it took the first `host-service-*.tar.gz`
in the media directory, and the earlier build's tarball was still staged
beside the new one. The step now picks the archive whose hash the bundle's
manifest lists, and `sync-assets` prunes any staged archive the manifest
no longer names before it fetches. runner-check covers the prune; the
checklist records the live wake that proved the flow and gh through the
firewall.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(api): the GitHub install callback runs the initial sync directly in development (#7542)

The queue cannot reach a localhost API, so a dev install ended with
sync_queue_failed and no repositories; triggerSync already calls the job
endpoint directly in development, and the callback now does the same.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(sandbox): a lone repository is the workspace root; a promoted environment's repositories are fixed (#7544)

* feat(sandbox): a lone repository is the workspace root; a promoted environment's repositories are fixed

A workspace on one repository has it at /workspace, as the reference
machine had; several sit at /workspace/<name>. The path is data on the
workspace row ("." for the root), so the runner, the seeding and the hooks
read it rather than count. Decided with Satya over "always
/workspace/<name>".

An environment that has a golden cannot change its repositories: the golden
was cloned, set up and snapshotted for that set, a repository added later
would be a bare clone with no setup, and under the root layout a sibling
would land inside the first checkout. update refuses it and the dialog
shows the set read-only with "promote again to change". Image-backed
environments change freely. An update that changed only the repositories
used to throw "No values to set" after writing them.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the root-layout boxes and the freeze on the checklist

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): the runner clones beside the target and moves the checkout into place

host-service seeds the workspace row before the checkout lands and starts
git activity in that directory; on the monorepo golden it raced the
runner's clone into the same path (shallow.lock held by another process,
packs vanishing mid-fetch) and the checkout never finished. The clone now
lands in a sibling temporary directory and is moved in, the worktree first
and .git last, so the directory becomes a repository in one rename; the
directory itself is emptied rather than replaced so a shell sitting in it
keeps its cwd.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the clone race on the checklist

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the golden rebuild on the root layout

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(cloud): the settings cloud pages and mobile's environments query follow the cloud flag (#7549)

* fix(cloud): the settings cloud pages and mobile's environments query follow the cloud flag

The Settings "Environments" and "Agents" entries were offered to every v2
user and mobile queried environments for every signed-in user; the server
refuses both without the cloud-workspaces flag, so they showed an error
state instead of nothing. Both now check the same flag the sidebar and
the device picker do.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(cloud): environment rows without repositories never throw into a render

A client newer than the API would receive environment rows without the
repositories field; the desktop form, the settings list and the mobile
widget read it null-safely now.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* refactor(cloud): boot timing off the workspace row; repositories unordered; provision traced (#7551)

* refactor(cloud): boot timing off the workspace row; repositories unordered; provision traced

The five boot-stamp columns are dropped (migration 0119) and the desktop's
cloud_workspace_opened event, its create-start record and the terminal
attach notifier are removed with the measure script: the P0 study is
done and its numbers are in the plan. The provision job is now one Sentry
transaction with a span per stage (claim, create, settle), sampled by
name so nothing else in the API is traced, flushed by the job itself
since it runs after its request has answered, and a structured log line
records the same durations in Vercel whether or not the span arrives.

position leaves both repository link tables (migration 0120): repositories
read alphabetically everywhere and the workspace opens on the environment's
config location, else the first by name; the workspace-repositories
endpoint marks that one primary. Multi-repository workspaces opening at
the root with a sidebar picker is a recorded TODO.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the unordered create and the provision log line on the checklist

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* refactor(db): one migration for the branch; no provision log line

The six incremental migrations sandbox-v2 accumulated (boot stamps added
then dropped, position added then dropped, a cascade rebuilt) are
regenerated as one, 0115_cloud_environments_repositories, before any of
them reach main; a second generate finds no changes. The dev Neon branch's
journal was rewritten to the single entry after its six recorded hashes
matched the six old files and its schema matched the new migration.

The provision job's structured log line is gone: the Sentry transaction is
the record, and whether it arrives is checked after the production deploy.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* refactor(cloud): no environment hooks override, no stored checkout branch

environments.hooks held one value anything read back: the internal
environment's start hook. It is now the monorepo's own cloud-only `start`
key in .superset/config.json, the release still runs internal-setup.sh
itself, and the identity's SUPERSET_SANDBOX_HOOKS goes with the column.
The release builds its golden from SUPERSET_INTERNAL_BRANCH (default main)
so a branch can be released before it merges.

cloud_workspace_repositories.branch only fed a box's first checkout and
went stale after. The primary checks out the workspace's own branch and
the rest their default branch, both read when the box is claimed.

The branch migration is regenerated again as one file; the dev Neon
branch had both columns dropped and its journal entry repointed.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* docs(sandbox): the start hook from config.json on the checklist

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* feat(cloud): a person's own GitHub connection; cloud workspaces commit, push and open PRs as them (#7555)

* feat(cloud): a person's own GitHub connection; cloud workspaces commit, push and open PRs as them

Settings → Connections gets a GitHub row. Connecting authorizes the GitHub
App for the person (a user-to-server token): the API stores the tokens
encrypted in github_user_connections (migration 0116, drizzle-kit output),
refreshes them under a per-user lock since a refresh rotates the refresh
token, and removes a connection GitHub no longer honours. Disconnecting
deletes the row and revokes the grant.

A cloud workspace's firewall rule for github.com and api.github.com now
carries the creator's own token when they have connected, so pushes and
pull requests are theirs and reach only what both they and the App can; the
App's installation token otherwise. Create refuses an environment with a
repository the connected person cannot see, instead of failing at clone.
Commits on the box carry GIT_AUTHOR/COMMITTER from the connected account's
no-reply address (GitHub attributes it and it passes "block pushes that
expose my email"), else the Superset account's name and email.

A running box now keeps its credential rules fresh: host-service asks the
API every 30 minutes with its host secret, and the API re-applies the
policy. Before, a box nobody reopened lost GitHub access when the
hour-long installation token expired.

GH_APP_CLIENT_ID and GH_APP_CLIENT_SECRET are optional; without them the
row says the connection is unavailable and workspaces use the App's token.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(desktop): the GitHub connection row polls after Connect until it appears

Returning from the browser to an Electron window never makes the page
visible again, so the focus refetch never fired and the row stayed on
"Not connected" until a reload. It now polls every two seconds from the
Connect click until the connection shows up, for at most five minutes.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(sandbox): the release probe's credentials carry a git author

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* ci(sandbox): production release from CI; the image push logs itself in

The Sandbox workflow gains a production-release dispatch (main only) that
runs `bun run release --production` on a runner: image, internal golden,
production environment rows. The image push mints a registry credential from
VERCEL_SANDBOX_TOKEN and logs Docker in itself, the exchange `vercel vcr
login` makes, because the CLI needs a user account and the project token has
none. No 12-hour login to renew on a laptop either.

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

* fix(cloud): review fixes before main: personal environments, flag gating, provisioning and refresh races

- Personal environments are enforced in the one loader every environment
  route uses (get, update, archive, secrets) and at workspace create; update
  no longer takes a sourceRef or bundleSha from clients, which could point an
  environment at any sandbox in the shared project; a config-repository-only
  update must name one of the environment's repositories.
- Without the cloud workspaces flag the settings hid "Agent commands" instead
  of the cloud Agents page; the flag now filters sections and search counts
  through one helper.
- A duplicate provision delivery deleted the first delivery's box; it now
  deletes only when the row was deleted.
- A transient GitHub refresh failure removed the person's connection; only a
  rejected refresh token does now.
- The start hook honors the config's cwd; malformed repository JSON no longer
  throws at boot.
- The release resolves the monorepo before writing rows and writes the
  internal environment and its repositories in one transaction.
- Considerations doc: a visitor to a workspace acts on GitHub as its creator
  (multiplayer by default).

Claude-Session: https://claude.ai/code/session_01FK6rLzBEUiHHXZWNmpQ2BP

This branch was previously deployed

1 inactive deployment
preview — 9d722f35 Deployed Sep 14, 2026 by saddlepaddle via Deploy Docs #9874
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