revert(sandbox): remove shared agent sandboxes and the staging default - #5240
Merged
Conversation
Reverts #5112 (share agent sandboxes by branch), #5116 (always share agent sandboxes) and #5132 (use shared staging branch), restoring the pre-#5112 behaviour: hosted agent sandboxes are per-user again, and a new GitHub-backed thread gets a generated branch instead of `staging`. Reconstructed against current paths rather than reverted: #5120 split apps/mesh into apps/api + apps/web and hoisted branch-name.ts, runtime-defaults.ts and thread/schema.ts into packages/shared, and #5174 deleted vm-events.ts outright, so none of the three commits reverse-apply. Core change: SandboxId goes back to a flat `{ userId, projectRef }` and sandboxIdKey back to `userId:projectRef`, so every hosted claim handle changes. Live shared claims must be drained BEFORE this deploys. Also reverted, because they only exist inside the shared model: - #5225 withoutLegacyAgentSandboxEntries — left in place it would strip the sandboxMap entries the reverted server writes back, leaving shouldAutoStart permanently true (silent, compiles fine) - #5219 resolveSharedThreadVm and the shared-scope branch - #5183 stale-provisioning reset (lived inside the deleted storage) Deliberately preserved: - migrations 137/138 and their index.ts entries — Kysely's #ensureNoMissingMigrations runs unconditionally, so deleting an applied migration crash-loops every pod. The tables are dropped by a follow-up forward migration after the drain, not by this commit. - #5114/#5118 withClaimGitLock, #5126 org-context guard, #5143 analytics gate, #5171 owner-keyed thread graft, #5195/#5207/#5210/#5221 client auto-retry, the daemon status-code train and the settings-validation train - #5112's org-fs least-privilege narrowing and the start dedup key fix - #5116's squashed queue-tray pluralization (en + pt-br) - the remote-branch-name validations #5132 deleted, since this restores the dynamic origin/HEAD lookup that made them load-bearing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolved 3 conflicts toward main's post-branch work: - use-layout-state.ts / shell-layout.tsx: keep #5231's caller-passed `branch` param over the branch's parallel readCachedTaskBranch path. main's callers already omit the branch on agent-switch, which is the same guarantee the branch's carryBranch gate provided. - connection-tools.integration.test.ts: keep userModelPreferences stub, drop agentSandboxSessions (storage class removed by the revert).
decocms Bot
pushed a commit
that referenced
this pull request
Jul 27, 2026
PR: #5240 revert(sandbox): remove shared agent sandboxes and the staging default Bump type: patch - decocms (apps/api/package.json): 4.133.1 -> 4.133.2 - @decocms/e2e (packages/e2e/package.json): 1.22.0 -> 1.22.1 - @decocms/sandbox (packages/sandbox/package.json): 1.22.9 -> 1.22.10 - @decocms/shared (packages/shared/package.json): 0.4.0 -> 0.4.1 - deploy/helm/sandbox-env (chart 0.9.28) (deploy/helm/sandbox-env/values.yaml deploy/helm/sandbox-env/Chart.yaml): image.tag/appVersion -> 1.22.10 Deploy-Scope: both
pedrofrxncx
added a commit
that referenced
this pull request
Jul 27, 2026
…5240) (#5269) #5240 restored per-user hosted sandboxes and deleted every reader/writer of agent_sandbox_sessions and agent_sandbox_runner_state, but deliberately kept both tables so the rollout had a rollback window. This is R2: the forward migration that drops them. agent_sandbox_runner_state.state holds plaintext credentials (an x-access-token:ghs_... clone URL, submodule tokens, the resolved env bag) and the table has no organization FK, so nothing ever GCs a row -- dormant rows are a standing secret leak, not just dead weight. Co-authored-by: Pedro França <pedrofrxncx@deco.cx>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reverts #5112 (share agent sandboxes by branch), #5116 (always share agent sandboxes) and #5132 (use shared staging branch), restoring the pre-#5112 behaviour: hosted agent sandboxes are per-user again, and a new GitHub-backed thread gets a generated branch instead of
staging.This is R1 (code only). The two tables stay in place — see Migrations and Deploy order. It must not ship before the drain (below).
Why it isn't a
git revertNone of the three commits reverse-apply. Measured against
origin/main: reverting #5112 alone produces 33 conflicted paths, and resurrects three files at the deadapps/mesh/path.apps/meshintoapps/api+apps/web, and also hoistedbranch-name.ts,runtime-defaults.tsandthread/schema.tsintopackages/shared— which is whereDEFAULT_WORKSPACE_BRANCH = "staging"lives, i.e. the whole point of feat(sandbox): use shared staging branch #5132.vm-events.tsoutright, so feat(sandbox): share agent sandboxes by branch #5112's largest single-file hunk (+180) has no target.So each file was reconstructed against its current path, using
git show 5b0d71784:<old-path>as the specification, then diffed back against that baseline to prove convergence.The load-bearing change
SandboxIdreturns to a flat{ userId, projectRef }andsandboxIdKeyto`${userId}:${projectRef}`. The claim handle isslugify(branch)-sha256(idKey).slice(0,16), so every live shared claim gets a different address the moment this deploys.deleteHttpRouteIfManaged, whose call path this PR deletes → the Route leaks and 502s.Uncommitted work is mostly safe: the daemon registers
SIGTERM → shutdown() → publish(...)(packages/sandbox/daemon/entry.ts:919-960, covered bydaemon.git.e2e.test.ts:174), and the operator's teardown is an ordinary Delete → SIGTERM within the grace period.Required before deploy
provisioningrow to reachreadyso it has a handle.agent_sandbox_sessionsrow on the current code (the teardown paths are deleted by this PR). NoteSANDBOX_DELETEmarks rows terminal even when it deleted nothing — a NULL-handle row skipsrunner.delete, and a rejection is swallowed. "Zero non-terminal rows" is not a completion signal; only thekubectlcheck is.kubectl -n <ns> delete httproute -l 'studio.decocms.com/role=claimed,studio.decocms.com/org-id,!studio.decocms.com/user-id'(theorg-idterm matters —!user-idalone also matches tenant-less internal sandboxes).sandboxMapentries. fix(sandbox): stop trusting legacy agent-sandbox entries on the agent row #5225's message records ~1789 leftoveragent-sandboxcells on agent rows. They're inert today; after this PR they are authoritative again and point at long-reaped claims. Strip theagent-sandboxand legacyclusterkeys from each branch cell (keepuser-desktop) on the old code, where it's behaviourally a no-op.Migrations
Both migrations are pure
CREATE TABLE— neither alters a pre-existing table, so nothing is lossy. They are kept, byte-identical, along with theirindex.tsentries.Deleting them would crash-loop every pod. In
kysely@0.28'smigrator.js,#ensureNoMissingMigrationsruns at :447 — unconditionally, outside theallowUnorderedMigrationsgate at :448 — and throwscorrupted migrations: previously executed migration ... is missingat :495. SoallowUnorderedMigrations: truewould not help. Migrations run on boot (settings/pipeline.ts:50); nothing underdeploy/orselfhost/sets--skip-migrations.CI would not catch it:
storage-integration.ymlmigrates a fresh Postgres from an empty ledger, andmigrations/index.test.tsonly checks file↔registration parity, so deleting file and entry together passes both.The tables are dropped by a follow-up forward migration
145-drop-agent-sandbox-tables.tsin R2, after R1 has fully rolled out. Worth doing rather than leaving them dormant:agent_sandbox_runner_state.stateholds plaintext credentials (anx-access-token:ghs_…clone URL, submodule tokens, the env bag) and has no organization FK, so nothing ever GCs it.Deploy order
R1 = this PR (tables left in place, harmless — nothing reads them). R2 = migration 145. Same-release drops mean an old pod hits
relation "agent_sandbox_sessions" does not existon everySANDBOX_START; the chart runs migrations from the app CMD, so there's no natural barrier. Two releases also buy a rollback window.Also reverted
These exist only inside the shared model:
withoutLegacyAgentSandboxEntriesstrips exactly theagent-sandboxsandboxMap entries the reverted server writes back, while the session graft that compensated is gone —vmEntrypermanently null →shouldAutoStartpermanently true →SANDBOX_STARTon every mount and branch switch. It compiles fine; nothing but a deliberate decision catches it.resolveSharedThreadVm+ the shared-scope branch. Its hoist of the unconditional thread-map cleanup is kept — that bug is scope-independent and pre-dates sharing.provisioningreset; lived inside the deleted storage class.Deliberately preserved
withClaimGitLock, fix(virtual-mcp): require org context for id-scoped legacy MCP requests #5126 org-context guard, fix(threads): skip chat_started analytics on an id-collision replay #5143 analytics gate, fix(chat): refresh read-only teammate thread metadata so preview finds the repo #5171 owner-keyed thread graft, fix(sandbox): auto-retry transient SANDBOX_START failures without a refresh #5195/fix(sandbox): surface terminal claim failure so preview stops spinning forever #5207/fix(sandbox): auto-retry transient claim failures before showing errored #5210/fix(sandbox): scope the claim auto-retry budget to the active branch #5221 client auto-retry, the daemon status-code train (fix(sandbox): return 409 instead of 500 for publish refusals on detached HEAD/protected branch #5109/fix(sandbox): stop discard from deleting renamed files with no recovery #5124/fix(sandbox): return 409 instead of 500 for a blocked rebase #5134/fix(sandbox): return 400 instead of 500 when the diff base branch isn't on origin #5142/fix(sandbox): return 400 instead of 500 when the rebase base branch isn't on origin #5144/fix(sandbox): return 400 instead of 500 for non-string entries in git/discard filepaths #5150/fix(sandbox): reject empty base string in git/diff POST endpoint #5175/fix(sandbox): return 400 instead of 500 for empty-string entries in git/discard filepaths #5189/fix(sandbox): return 409 instead of 500 for tokenless GitHub publish #5211), the settings-validation train.{ self: ["ORG_FS_READ", "ORG_FS_WRITE"] }(reverting would re-bake a full-management key into every sandbox pod), andsandboxProviderKindstays in the start dedup key.isValidRemoteBranchNameinclone.ts,assertValidRemoteBranchNameincheckout-branch.ts). feat(sandbox): use shared staging branch #5132 removed them as dead once the base was a constant; this PR restores the dynamic, remote-controlledorigin/HEADlookup, so they are load-bearing again. This is the one place a naive revert silently drops hardening.Testing
bun run check,bun run lint(0 errors),bun run fmt— clean.apps/web: 23 failures — the identical set asorigin/main(pre-existing); the 6-test delta is exactly the deletedagent-sandbox-map.test.ts.apps/api+ packages: 245 pre-existing failures on both branches (integration tests needing Postgres/NATS). 0 new; 1 gone — the deleted shared-sandbox integration test.5b0d71784blob. Residuals are only refactor(studio): split API and web workspaces #5120 specifier rewrites plus the preserved work listed above.start.test.tsasserts{ userId, projectRef }again,handle.test.tsdrops the shared/user isolation cases,runtime-defaults.test.tsdrops thestaging/mainassertions, andchat-locked-thread.spec.tsis back to thepickWarmBranchFromSandboxMap → generateBranchName()wording.Not run locally: real-Postgres integration tests and the Playwright e2e suite — CI covers both.
Open items
branch = 'staging'(persisted column,storage/types.ts:945/:1032). Post-revert two members get separate pods on the same branch, and the second non-forcegit push -u origin stagingis rejected non-fast-forward — a failure mode that didn't exist pre-feat(sandbox): use shared staging branch #5132, when every thread had a unique generated branch. Do not null the column (that orphans theorigin/stagingwork and breaksexistingThread?.branch ?? input.branch). Either accept with a release note, or rewrite to per-thread names after pushing each staging workspace. Needs a decision + the row counts.gonefor orphaned thread sandboxes on shared scope #5219/fix(sandbox): stop trusting legacy agent-sandbox entries on the agent row #5225 intentionally lost? They fixed real symptoms ("preview stuck on Reserving sandbox", "works for me, stuck for you"). Confirm they can't recur in the per-user model.useAgentSandboxSessionpolled/api/:org/agent-sandbox-sessions/:vmidevery 5s. Already-loaded tabs will 404 until reload. Optionally leave the route mounted returning an empty list for one release.staging? The daemon'sprotectedBranches()is{main, master, origin/HEAD default}, sostagingwas freely pushable — if a customer has since protected it, post-revert publishes fail differently.🤖 Generated with Claude Code
Summary by cubic
Reverts shared agent sandboxes and the
stagingdefault. Sandboxes are per-user again, and new GitHub-backed threads pick a warm branch or get a generated name.Refactors
SandboxIdis{ userId, projectRef }andsandboxIdKeyisuserId:projectRef.computeClaimHandlenow hashesuserId:projectRef, so existing shared handles will change.agent-sandbox-sessionsAPI/routes/storage and related client hooks; provider resolution no longer looks up shared sessions and uses the acting user’ssandboxMapinstead.sandboxMap, falling back togenerateBranchName; the fixedstaging/maindefaults are removed; daemon base-branch logic readsorigin/HEADand validates remote ref names.dev-connectionresolves only to the acting user’s sandbox; branch picker/use-branches no longer depend on shared sessions; addsreadCachedTaskBranchto carry the active task’s branch into new threads; keeps fix(chat): New chat always creates, on the current branch #5231’s caller-passed branch param path; generates a branch on publish when missing.Migration
Written for commit d0c070b. Summary will update on new commits.