Repository navigation
🤖 feat: Daytona sandboxes as a workspace runtime - #7100
ibetitsmike wants to merge 233 commits into
Conversation
…sh config Part A of the Daytona runtime provider (ssh + daytona sub-config): - runtime schema: optional `daytona` sub-config on the ssh variant, mutually exclusive with `coder`; `daytona` runtime enablement ID (UI-only, like coder) and a shared enablement-ID -> RuntimeMode record. - shared size presets (small/medium/large) in src/common/constants/daytona.ts. - credentials in <xum home>/daytona.json (0600, atomic), with DAYTONA_API_KEY / DAYTONA_API_URL fallbacks; the UI only sees a masked view. - `daytona` oRPC namespace: getConfig, setConfig, testConnection, listSandboxes, listSnapshots. - thin typed fetch client with timeouts and typed errors (no @daytona/sdk). - DaytonaService: labels with the installation identity, per-sandbox SSH token cache with refresh-then-revoke, startAndWait with bounded polling. - daytonaSshConfig: 0600 include file with one Host block per sandbox, gateway known_hosts, a managed `Host *.xum--daytona` + Include block in ~/.ssh/config (shared marker-block writer extracted from the Coder writer), and an `ssh -G` check that the token is the resolved user. - sshConfigParser (ssh2 path) now resolves Include directives.
…parsing into small units
…and wiring Part B of the Daytona runtime provider (ssh + daytona sub-config): - DaytonaSSHRuntime (mirrors CoderSSHRuntime): finalizeConfig names a new sandbox before persist (Daytona takes a name wherever it takes an id) or attaches a given one as shared; validateBeforePersist checks credentials, preset size and name/attach existence; postCreateSetup creates with preset resources and lifecycle intervals, falls back to create-then-resize on HTTP 400, waits, writes ssh access, seeds the agent home and deletes the sandbox it created on failure; ensureReady caches a ready result for 60 s per sandbox, shares one in-flight start, reports "Starting sandbox" and maps a missing sandbox to runtime_not_ready; forkWorkspace marks both shared; deleteWorkspace starts a stopped sandbox so the dirty/unpushed checks run. - DaytonaService: create/resize/stop/delete/refreshActivity, single-flight ensureSandboxReady, ssh include ownership with token refresh, ready cache. - Agent-home seeding of tool_env/tool_pre/tool_post and global skills to the paths remote lookups read (~/.mux, ~/.agents/skills), never for attached boxes. - runtimeFactory selects DaytonaSSHRuntime; checkRuntimeAvailability reports Daytona availability; bash AI proxy forwards are not restored at startup. - Archive reuses the Coder keep/stop/delete behavior for dedicated sandboxes. - DaytonaReconciler (service container, 60 s, never blocks startup): keepalive for active workspaces, token refresh, stop-only orphan sweep with archived workspaces counted as references, per-call timeouts. - Gated TEST_DAYTONA=1 real test (skipped by default).
…er the CRAP limit
…e in archive refusals, report sandbox states Archive with the delete behavior now runs the same dirty and unpushed checks as deleteWorkspace on a dedicated Daytona sandbox (starting a stopped one for them). When they fail, the sandbox is stopped instead and the archive result carries a warning the sidebar and menu bar show. Archive refusals name the Daytona sandbox instead of a Coder workspace, and getRuntimeStatuses reports Daytona sandbox states (starting, running, stopped, archived, error).
…ontrols Settings > Runtimes gets a Daytona row: a global access form (write-only API key in its own file, API URL, target, test connection) and a project-scope defaults form (snapshot and size preset, saved to lastRuntimeConfig.ssh.daytona). Workspace creation gets a Daytona runtime button with New/Existing sandbox, snapshot and size controls, prefilled from the project defaults. Runtime badges and the footer chip show the sandbox state. Stories cover the forms at desktop and 390px widths as play-only contracts (Pixel budget has no headroom).
…g a shared-checkout child Side chats and isolation:none sub-agents copy the parent's runtime config, sandbox ID included, without the shared flag, so archiving one stopped or deleted the parent's sandbox.
…ook path The hooks page said remote user-level hooks live in ~/.xum; SSH remotes read ~/.mux and Docker reads /var/mux (RemoteRuntime/DockerRuntime getXumHome).
… and docs index Rebuild the runtime-choice context inside the auto-switch effect so it depends on its fields, not a per-render object; format; sync the xum-docs skill index.
Normalizing the project's default runtime in render broke the compiler's memoization, so selectedRuntime changed identity on every keystroke (useDraftWorkspaceSettings.compiler.test.tsx). The config type already admits daytona, so the normalization was unnecessary.
…RAP limits Extract the SSH badge, runtime-choice table, Coder/Daytona creation validation, Daytona access hooks and components, archive result helpers, the remembered-SSH merge, the delete-check exit-code mapping and the Daytona status probe. The creation form now loads Daytona lists itself while mounted. Covers the archive warning popover path with a test.
…s from starting sandboxes - Background git and PR status wait for a Daytona sandbox to run, as for devcontainers, so opening a workspace no longer starts a stopped sandbox. - RuntimeStatusStore refreshes when a turn starts a stopped runtime and when that start ends (runtime-status events from the workspace chat stream). - The API key status sits beside its label instead of inside it.
…clear errors - An invalid API URL throws DaytonaConfigError, which daytona.setConfig returns as BAD_REQUEST with its reason instead of Internal Server Error. - Client errors drop HTML tags, collapse whitespace and keep 200 chars of the server message.
…e findings - Delete refuses a dirty worktree under the unresolved ~/... path: checkWorkspaceDeletable expands a leading ~ remotely (also fixes Coder), and Daytona resolves the path first and fails closed when it cannot. - Daytona aliases keep their pinned gateway key: the headless OpenSSH override skips *.xum--daytona, and ssh2 checks the server key against the pinned daytona_known_hosts entries. - Daytona ssh tokens are redacted (<daytona-token>) from ssh stderr, errors, init output, pool probe errors and reverse-forward logs. - A fork reuses its source's sandbox (`forked`) without marking either workspace attached; the last referencing workspace, archived ones included, deletes it. Attached sandboxes are never deleted; their last workspace only releases Xum's ssh access. Archive skips a sandbox another workspace references. - A turn's runtime restarts a sandbox that stopped within the ready cache and retries the command once; transport failures invalidate the cache. - Tokens are revoked on settings changes and failed installs; a failed ssh -G user check reuses the token on retry. - Later turns after a failed setup report the setup failure and its reason. - Keepalive checks whether background jobs still run, skips stopped or unknown sandboxes, and skips the sweep when the sandbox list fails.
`new URL` reads "http:///api" and "https:api" as host "api", so they were saved as http://api. normalizeApiUrl now requires a written host and throws DaytonaConfigError ("has no host") otherwise.
…ings - Removing the last workspaces on one sandbox at the same time deletes it exactly once: the fate decision runs under a per-sandbox lock, and a removal that passed its dirty check is recorded so the next one does not count it. A refused (dirty) removal is never recorded and keeps the sandbox. - Startup loads every token from daytona.conf into the redactor and the token cache (due for refresh), so ssh errors after a restart never show it, and the next mint, a settings change or an archive revokes it. - When the gateway refuses a token (revoked outside Xum), the runtime mints a new one, rewrites the include file, resets the pool backoff and retries the command once. - A forced delete whose sandbox DELETE fails returns a removal warning that names the sandbox to delete by hand. - Delete waits (up to 60 s) for a sandbox Daytona is already destroying instead of failing with "is in state destroying".
…token A stopped or deleted sandbox also answers "Permission denied", so a command sent within the ready cache after a stop minted and revoked a token for nothing and then failed, and the first command after a delete outside Xum showed the refusal instead of "no longer exists". The runtime now reads the sandbox state first on an ssh failure: a stopped sandbox takes the restart path (turn runtimes), a missing one reports that it no longer exists, and only a started sandbox gets a new token.
…l on their claimed breaks - await the background-runtime rejection so its no-start assertion runs after the command settles - assert both concurrent ensureReady checks report the shared start (the old test passed without the dedupe) - assert the clean-delete test runs the SSH dirty check - round-trip snapshot, target and forked through RuntimeConfigSchema
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 26fb323354
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26fb323354
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
setConfig revoked only cached tokens; a mint in flight finished afterwards and wrote and cached an old-account token. A credentials generation is captured at mint start: a mint that spans a change revokes its token with the client that minted it, never writes or caches it, and mints again for the waiting callers.
…ours A name taken between validateBeforePersist and the create made Daytona refuse the create, and the cleanup then deleted the other install's sandbox. A failed create now deletes only a sandbox carrying this install's and workspace's labels; a successful create is still deleted when a later setup step fails.
The 256-token cap evicted tokens that were still live. Tokens now expire by age only.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07c785d9e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The workspace config holds the sandbox name, which another sandbox can take between the label check and the delete. Delete by the id the check read, and send no delete for a sandbox that is already gone.
Daytona's ssh gateway refuses every remote port forward (OpenSSH -R with any bind address, ssh2 forwardIn; checked live). Trying anyway made a bash command wait up to 10 s every 5 minutes while Xum tried 4 ports. A Daytona runtime now gets no forward target, as Docker does.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 270e373356
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The workspace config holds the sandbox name, which another sandbox can take. The resize fallback now stops, resizes and restarts the id that the create returned, and startAndWait starts the id of the sandbox whose label it checked. Also regenerates the built-in docs for the providers page change in 270e373.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bdcf0f0dac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
OpenSSH names the user, which is the gateway token, in its fatal Permission denied line, and an editor's or a native terminal's ssh output passes no Xum redactor. The managed ~/.ssh/config block now sets LogLevel QUIET for sandbox aliases. Xum's own ssh reads only the include file, so its messages (redacted) stay.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebe478135b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Review loop paused (AGENTS.md early stop, case 2: the loop does not converge). This is not a ready state. Merge stays paused. Codex rounds on the cut tree found 6, 3, 2, 2, 2, 5, 1, 2 and now 2 findings. The list stopped getting shorter, and the last rounds keep finding new cases of the same two design properties:
Both open findings go through the reconciler's token refresh. The fixes so far were small, but the next ones add mechanism: coordinating a release with a mint is the kind of tracking that this revision removed. So I stop here and ask for a decision:
The two open threads stay open until this is decided. |
# Conflicts: # src/browser/stores/GitStatusStore.test.ts # src/cli/serverSecurity.ts # src/node/services/serviceContainer.test.ts # src/node/services/workspaceService.ts
The workspace config held only the sandbox name, which another sandbox can take once the workspace's own is gone. The create now records Daytona's id as daytona.sandboxId (the name moves to daytona.sandboxName, which the ssh alias keeps using), and every control-plane request about a workspace's sandbox goes to that id: state, start, stop, resize, delete, activity, and ssh token mint and revoke. A sandbox without a recorded id is not Xum's to reach; the delete looks one up by name only to clean up a create whose answer never came, and only with this install's label. A workspace removed during the create records nothing, and its new sandbox is deleted again. By id, Daytona keeps answering for a deleted sandbox (deleting, then destroyed), so those states count as gone (found by the live suite). Cuts the reconciler's token refresh. ensureReady and the transport repair already mint a fresh token when one is needed, and the reconciler's mint ran outside the readiness join, so a release could not wait for it (both open Codex P1s on ebe4781).
|
Resuming the review loop with the recorded sandbox id (option C from #7100 (comment)), in 69803bc. @codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🟢 e2e stories (Haiku): 14 passed22 agent steps · 7 replayed from cache · 15 model calls · 46.2k tokens (24% cached) All 14 tests in 9 files
e2e 0.20.0 · 1m 15s · web · run artifacts 🟢 e2e Electron stories (Haiku): 3 passed3 agent steps · 3 model calls · 8.0k tokens (22% cached) All 3 tests in 3 files
e2e 0.20.0 · 18.2s · electron · run artifacts 🟢 e2e agent repros (Haiku): no tests selectedSpend: $0.00 of the $1.00 cap (proxy ledger) e2e 0.20.0 · 5.3s · 2 targets (web, phone) · run artifacts |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
The reconciler no longer refreshes tokens in the background, so the token is replaced only when Xum brings the sandbox up (or an ssh command fails). Say so, and that the user's own ssh clients can be refused after the hour until Xum uses the sandbox again (UAT round 8 observation).
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
|
@codex review |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Summary
Adds Daytona Cloud as a workspace runtime. A Daytona workspace is an
sshworkspace with adaytonasub-config: Xum creates a sandbox through Daytona's REST API and reaches it through Daytona's SSH gateway, so bash, file tools, git, background jobs, terminals and sub-agents run in the sandbox instead of on the Xum host. It reuses all ofSSHRuntime, as Coder does.What the user gets
DAYTONA_API_KEY;DAYTONA_API_URLandDAYTONA_TARGETpick a self-hosted API and a region.docs/runtime/daytona.mdx.Size
Down from +15.0k/-0.7k to +10.2k/-0.5k: production +5.5k, tests +4.6k, docs and stories +0.2k. An earlier revision removed about 5.5k lines of review-driven edge-case handling (unowned-sandbox paths, connection retirement, the account lease, generation counters, ssh2
Includeparsing, project defaults, event-driven status refresh). The commit messages say what each cut gives up. 69803bc adds the recorded sandbox id (+0.3k) and cuts the reconciler's token refresh.Testing
tests/runtime/daytonaRuntime.test.ts(create, exec, files, fork, archive, delete through the real gateway) passes on 69803bc. On that head it found that Daytona keeps answering by id for a deleted sandbox (statedeleting, thendestroyed); those states now count as gone. Earlier it found one bug, fixed in e7da6a3:SSHRuntimespawnedgit bundle createbefore the remotecatstarted, so a slow SSH start (Daytona's gateway) uploaded an empty bundle. Red-green test inSSHRuntime.syncContract.test.ts.sandboxIdand the alias assandboxName; a Haiku turn and the terminal ran in the sandbox. A2 a stop outside Xum, then the next turn started the sandbox and kept its files. A3 name takeover: Xum reports the sandbox gone and never starts, keeps alive or deletes the new sandbox with the old name. A4 a fork shares the sandbox; only the creator's archive deletes it. A5 an archive during the create leaves no sandbox. A6 a server restart keeps the id. A7 no key or token in any log or attachment. A8 no sandbox, ssh access or config left behind. In the fault-injection runs, 38 requests that change a sandbox went by id and 0 by name. The commits after 69803bc change only the docs (and the docs skill generated from them).make static-check,make lint-eslint,make crap-check, and the Daytona, SSH, terminal and touched browser suites pass (bun 1.4.3).Review record
LogLevel=QUIET, so ssh never prints the token into the terminal (the PTY redactor and its hold timer are gone). A settings save during a create now fails that create and deletes its sandbox in the old account.startAndWaitstarts the id whose label it checked, so a sandbox that takes the name is never changed.~/.ssh/configblock setsLogLevel QUIETfor sandbox aliases, so an editor's or a native terminal's ssh never prints the token in an error. 1 P1 answered as intended: the archive deletes the sandbox before it persistsarchivedAt, as Coder's delete-on-archive hook does, so a failed delete blocks the archive; the shared gap after a failed config write is 🤖 fix: an archive whose config write fails after a hook deleted the remote box leaves the workspace active #7465.daytona.sandboxId; the alias name is nowdaytona.sandboxName), and every control-plane request goes to that id, so a sandbox that takes the name is never reached. The reconciler's token refresh is cut: every mint now runs in the readiness join that an access release waits for.sandboxIdhand-edited to another Xum sandbox's id makes the delete act on that sandbox (Xum acts on the recorded id by design), and a turn sent while Daytona destroys the sandbox makes one failed ssh attempt before it reports the sandbox gone.Follow-ups
#7230 (cap API response size), #7232 (record the creating account), #7236 (one keepalive per shared sandbox), #7250 (rename on a stopped sandbox), #7252 (stale connection-test result), #7254 (status chip refresh after a turn, terminal or editor starts the sandbox), #7451 (SSH access after a failed delete), #7446 (a user fork gets its own sandbox), #7457 (fork removal on a stopped sandbox), #7462 (settings call without a key), #7463 (start after the deadline), #7465 (archive after a failed config write).
Risks
~path fixes and the managed~/.ssh/configblock writer. Host-key pinning, redaction and token handling act only on Daytona aliases.