diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift index 07fee4edf9e3..933a587f6a33 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift @@ -1,6 +1,40 @@ import Foundation extension CmuxTuiSurfaceProvider { + /// Type text into a remote terminal without adding a newline. + func sendText(terminalID: String, text: String) async throws { + let connected = try await links.connected(machineID: machineID) + guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) } + _ = try await link.run(arguments: CloudTuiCommandLine.writeArguments(socketPath: connected.socketPath, terminalID: terminalID, text: text)) + } + + /// Press named keys in the remote terminal, in order. + func sendKeys(terminalID: String, keys: [String]) async throws { + let connected = try await links.connected(machineID: machineID) + guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) } + _ = try await link.run(arguments: CloudTuiCommandLine.keysArguments(socketPath: connected.socketPath, terminalID: terminalID, keys: keys)) + } + + /// Read the visible terminal grid reported by the daemon. + func readScreen(terminalID: String) async throws -> [String: Any] { + let connected = try await links.connected(machineID: machineID) + guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) } + let data = try await link.run(arguments: CloudTuiCommandLine.screenReadArguments(socketPath: connected.socketPath, terminalID: terminalID)) + return (try JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:] + } + + /// Wait for the screen pattern, with time for the link to return its result. + func waitForScreen(terminalID: String, pattern: String, timeoutMs: Int?) async throws -> [String: Any] { + let connected = try await links.connected(machineID: machineID) + guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) } + let effectiveMs = Self.clampedWaitTimeoutMs(timeoutMs) + let data = try await link.run( + arguments: CloudTuiCommandLine.screenWaitArguments(socketPath: connected.socketPath, terminalID: terminalID, pattern: pattern, timeoutMs: effectiveMs), + timeout: .milliseconds(effectiveMs + 5_000) + ) + return (try JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:] + } + nonisolated static let defaultWaitTimeoutMs = 30_000 nonisolated static let maxWaitTimeoutMs = 3_600_000 diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 049d2ba3b3d9..abc19b41a3b5 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -728,6 +728,35 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { ) } + /// Close a terminal, using its tab when the process has already exited. + func closeTerminal(_ id: SurfaceResourceID) async throws { + try await closeTerminal(id, fallbackTabID: nil) + } + + func closeTerminal(_ id: SurfaceResourceID, fallbackTabID: String?) async throws { + pendingRemoteCreations.removeValue(forKey: id) + do { + _ = try await runCloseCommand { + CloudTuiCommandLine.closeTerminalArguments(socketPath: $0, terminalID: id.key) + } + } catch { + guard let tabID = fallbackTabID ?? tabByTerminal[id.key], Self.isSelectorNotFound(error) else { throw error } + _ = try await runCloseCommand { + CloudTuiCommandLine.closeTabArguments(socketPath: $0, tabID: tabID) + } + } + closeLocalPanes(showing: [id]) + catalog.remove(id, from: self) + scheduleRefresh() + } + + private func closeLocalPanes(showing ids: [SurfaceResourceID]) { + let wanted = Set(ids) + for projection in catalog.snapshot.projections where wanted.contains(projection.resource) { + SurfacePaneFactory.close(panelID: projection.panelID, in: projection.workspaceID) + } + } + /// cmux-tui's `selector.not_found` error body, surfaced by `link.run` as the /// command's output text. static func isSelectorNotFound(_ error: Error) -> Bool { diff --git a/web/app/lib/request-origin.ts b/web/app/lib/request-origin.ts index f47151b5acbc..f6f0cd1a0390 100644 --- a/web/app/lib/request-origin.ts +++ b/web/app/lib/request-origin.ts @@ -1,4 +1,4 @@ -import { NextRequest } from "next/server"; +import { NextRequest, type NextResponse } from "next/server"; import { directDevBackendOrigin } from "./direct-dev-backend-origin"; /** @@ -36,3 +36,22 @@ export function requestWithOrigin( method: request.method, }); } + +/** Public URLs belong in redirects; local rewrites must stay inside Next. + * Rewriting through Tailscale Serve runs middleware again and loops when + * next-intl removes the default locale prefix from its own rewrite. */ +export function responseWithInternalRewrite( + response: NextResponse, + incomingRequest: NextRequest, + environment: Record = process.env, +): NextResponse { + const publicOrigin = directDevBackendOrigin(environment)?.origin; + const rewrite = response.headers.get("x-middleware-rewrite"); + if (!publicOrigin || !rewrite) return response; + const target = new URL(rewrite); + if (target.origin !== publicOrigin) return response; + target.protocol = incomingRequest.nextUrl.protocol; + target.host = incomingRequest.nextUrl.host; + response.headers.set("x-middleware-rewrite", target.toString()); + return response; +} diff --git a/web/proxy.ts b/web/proxy.ts index 8d0932271ba9..677c828637df 100644 --- a/web/proxy.ts +++ b/web/proxy.ts @@ -12,7 +12,7 @@ import { remoteTmuxDocsLocales, } from "./i18n/locale-availability"; import { buildAlternateLinkHeader } from "./i18n/seo"; -import { requestOrigin, requestWithOrigin } from "./app/lib/request-origin"; +import { requestOrigin, requestWithOrigin, responseWithInternalRewrite } from "./app/lib/request-origin"; import { DASHBOARD_RETURN_PATH_HEADER, dashboardReturnPathForRequest, @@ -28,6 +28,10 @@ const intlMiddleware = createMiddleware(routing); const localeSet = new Set(routing.locales); export default function middleware(incomingRequest: NextRequest) { + return responseWithInternalRewrite(routeRequest(incomingRequest), incomingRequest); +} + +function routeRequest(incomingRequest: NextRequest) { const request = requestWithOrigin(incomingRequest); const dashboardReturnPath = dashboardReturnPathForRequest( request.nextUrl.pathname, diff --git a/web/scripts/build-devbox-freestyle.ts b/web/scripts/build-devbox-freestyle.ts index a503968151e2..f1c20bcd8020 100644 --- a/web/scripts/build-devbox-freestyle.ts +++ b/web/scripts/build-devbox-freestyle.ts @@ -340,6 +340,8 @@ try { // Cache seeds cover root and the work user (uid 1000). await step("cmux-etc", "mkdir -p /etc/cmux /etc/skel"); await put("cmux-bashrc", "/etc/cmux/bashrc"); + await put("cmux-prompt.bash", "/etc/cmux/prompt.bash"); + await step("prompt-default-name", "echo cmux > /etc/cmux/vm-name"); await put("seed-history", "/etc/cmux/seed-history"); await put("cmux-terminfo.sh", "/etc/profile.d/cmux-terminfo.sh"); await put("cmux-terminfo.src", "/etc/cmux/terminfo.src"); diff --git a/web/scripts/devbox-image-common.ts b/web/scripts/devbox-image-common.ts index 5dd62acfbe1e..50bf77d23094 100644 --- a/web/scripts/devbox-image-common.ts +++ b/web/scripts/devbox-image-common.ts @@ -38,6 +38,7 @@ export const DEVBOX_TEMPLATE_FILES = [ "cmux-bashrc", "cmux-devbox-boot", "cmux-motd", + "cmux-prompt.bash", "cmux-terminfo.sh", "cmux-terminfo.src", "codex-managed.toml", diff --git a/web/services/vms/drivers/freestyle.ts b/web/services/vms/drivers/freestyle.ts index 5a4c0e783b2f..abf055e43db3 100644 --- a/web/services/vms/drivers/freestyle.ts +++ b/web/services/vms/drivers/freestyle.ts @@ -47,6 +47,7 @@ import { } from "../images/desktop"; import { recordSpanError, setSpanAttributes, withVmSpan } from "../telemetry"; import { GUEST_CMUX_SHIM, GUEST_CMUX_SHIM_PATH } from "../guestCli"; +import { guestPromptInstallCommand, type GuestPromptIdentity } from "../guestPrompt"; import { approveCmuxTuiEnrollment, CMUX_TUI_ATTACH_BUNDLE_NOT_READY_EXIT, @@ -965,7 +966,7 @@ export class FreestyleProvider implements VMProvider { // The in-VM shim is a separate convenience layer over the baked // daemon and is installed idempotently for agents and peer links. - await this.installGuestCli(vm, vmId); + await this.installGuestCli(vm, vmId, options.promptIdentity); await this.announcePrivateAddresses(vm, data); } catch (err) { // A VM that failed to size or configure must not survive as an @@ -1384,16 +1385,17 @@ export class FreestyleProvider implements VMProvider { // an invitation unless the caller is enrolled. Exit 3 means the daemon // was not ready inside the settle budget; heal, then run it again. const fingerprint = options?.deviceFingerprint; + const promptSetup = options?.promptIdentity ? `${guestPromptInstallCommand(options.promptIdentity)} && ` : ""; let bundleResult = await this.execResult( vm, - cmuxTuiAttachBundleCommand({ readyGate: freestyleDaemonSettledCommand(), deviceFingerprint: fingerprint }), + promptSetup + cmuxTuiAttachBundleCommand({ readyGate: freestyleDaemonSettledCommand(), deviceFingerprint: fingerprint }), DAEMON_SETTLE_TIMEOUT_MS + EXEC_OVERHEAD_TIMEOUT_MS + EXEC_DEFAULT_TIMEOUT_MS, ); let healed = false; if (!bundleResult || bundleResult.exitCode === CMUX_TUI_ATTACH_BUNDLE_NOT_READY_EXIT) { healed = true; await this.ensureCmuxTuiRunning(vm, vmId); - bundleResult = await this.execResult(vm, cmuxTuiAttachBundleCommand({ deviceFingerprint: fingerprint })); + bundleResult = await this.execResult(vm, promptSetup + cmuxTuiAttachBundleCommand({ deviceFingerprint: fingerprint })); } if (!healed && bundleResult?.exitCode === 0) { // The healthy fast path skips the heal, so this is where a machine @@ -1653,12 +1655,13 @@ export class FreestyleProvider implements VMProvider { * the adapter on older images; create/attach callers treat a failed install * as a failed heal. */ - private async installGuestCli(vm: Vm, vmId: string): Promise { + private async installGuestCli(vm: Vm, vmId: string, promptIdentity?: GuestPromptIdentity): Promise { const temporaryPath = `${GUEST_CMUX_SHIM_PATH}.tmp-${randomBytes(12).toString("hex")}`; try { await vm.fs.writeTextFile(temporaryPath, GUEST_CMUX_SHIM, { mode: 0o755 }); const result = await vm.exec({ - command: `chmod 0755 '${temporaryPath}' && mv -f '${temporaryPath}' '${GUEST_CMUX_SHIM_PATH}'`, + command: `chmod 0755 '${temporaryPath}' && mv -f '${temporaryPath}' '${GUEST_CMUX_SHIM_PATH}'` + + (promptIdentity ? ` && ${guestPromptInstallCommand(promptIdentity)}` : ""), timeoutMs: 30_000, linuxUser: GUEST_LINUX_USER, }); diff --git a/web/services/vms/drivers/types.ts b/web/services/vms/drivers/types.ts index b41cb4ffc5c0..e40664145084 100644 --- a/web/services/vms/drivers/types.ts +++ b/web/services/vms/drivers/types.ts @@ -2,6 +2,8 @@ // per-provider implementations behind an interface. Callers hold a `VMProvider` and never reach // into specifics. +import type { GuestPromptIdentity } from "../guestPrompt"; + export type ProviderId = "freestyle"; const PROVIDER_IDS: readonly ProviderId[] = ["freestyle"]; @@ -70,6 +72,8 @@ export type CreateOptions = { image: string; // provider-specific template/snapshot identifier /** Human-facing machine label; providers may ignore this cosmetic field. */ displayName?: string; + /** Current prompt name, written into the guest rather than a shell environment. */ + promptIdentity?: GuestPromptIdentity; providerMetadata?: Record; /** * Name of a persistent volume to mount as the machine's home directory. Providers that @@ -232,6 +236,8 @@ export type CmuxRemoteEndpoint = { }; export type CmuxRemoteAttachOptions = { + /** Authoritative display name and revision, refreshed before returning the terminal. */ + promptIdentity?: GuestPromptIdentity; /** * The caller's cmux-tui device fingerprint, when it already enrolled with this * VM's daemon. Lets the provider skip minting an invitation. diff --git a/web/services/vms/guestPrompt.ts b/web/services/vms/guestPrompt.ts new file mode 100644 index 000000000000..b7104f40bf7c --- /dev/null +++ b/web/services/vms/guestPrompt.ts @@ -0,0 +1,76 @@ +import { readFileSync } from "node:fs"; +import { shellQuote } from "./drivers/cmuxTuiDaemon"; + +export type GuestPromptIdentity = { + readonly machineId: string; + readonly name: string; + readonly revision: number; +}; + +/** A display label becomes a prompt slug; the machine's routing id stays stable. */ +export function vmPromptIdentity(row: { + readonly id: string; + readonly slug: string | null; + readonly displayName: string | null; + readonly updatedAt: Date; +}): GuestPromptIdentity { + const slug = (value: string) => value.normalize("NFKD").toLowerCase() + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-").slice(0, 63).replace(/^-+|-+$/g, ""); + return { + machineId: row.id, + name: slug(row.displayName ?? "") || slug(row.slug ?? "") || "cmux", + revision: row.updatedAt.getTime(), + }; +} + +const bashrc = readFileSync(new URL("./images/devbox/cmux-bashrc", import.meta.url), "utf8"); +const prompt = readFileSync(new URL("./images/devbox/cmux-prompt.bash", import.meta.url), "utf8"); + +// Runs on lifecycle operations, never during shell startup or prompt drawing. +// A lock serializes competing attaches/renames. Atomic replacement gives every +// shell either the old complete name or the new one. The machine id prevents a +// fork from inheriting its source's revision. User rc files are never touched. +const install = String.raw` +import fcntl, json, os, pathlib, sys, tempfile +directory = pathlib.Path(sys.argv[1]) +payload = json.loads(sys.argv[2]) +directory.mkdir(parents=True, exist_ok=True) +def replace(name, content): + target = directory / name + if target.is_file() and not target.is_symlink() and target.read_text() == content: + return + fd, temporary = tempfile.mkstemp(prefix=".prompt-", dir=directory) + try: + with os.fdopen(fd, "w") as stream: + stream.write(content) + os.fchmod(stream.fileno(), 0o644) + os.replace(temporary, target) + finally: + if os.path.exists(temporary): os.unlink(temporary) +with (directory / ".prompt-lock").open("a") as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + try: + current = json.loads((directory / ".prompt-identity").read_text()) + except (FileNotFoundError, ValueError): + current = {} + if not isinstance(current, dict) or not isinstance(current.get("revision", -1), int): + current = {} + incoming = payload["identity"] + if current.get("machineId") != incoming["machineId"] or current.get("revision", -1) <= incoming["revision"]: + replace("vm-name", incoming["name"] + "\n") + replace(".prompt-identity", json.dumps(incoming)) + for name, content in payload["files"].items(): + replace(name, content) +`; + +export function guestPromptInstallCommand(identity: GuestPromptIdentity, directory = "/etc/cmux"): string { + if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(identity.name) || !Number.isSafeInteger(identity.revision)) { + throw new Error("Invalid Cloud prompt identity"); + } + const files = { + "prompt.bash": prompt.replaceAll("/etc/cmux", directory), + bashrc: bashrc.replaceAll("/etc/cmux", directory), + }; + return `python3 -c ${shellQuote(install)} ${shellQuote(directory)} ${shellQuote(JSON.stringify({ identity, files }))}`; +} diff --git a/web/services/vms/images/devbox/Dockerfile b/web/services/vms/images/devbox/Dockerfile index b05a55dc7598..fe0b2d79bfa2 100644 --- a/web/services/vms/images/devbox/Dockerfile +++ b/web/services/vms/images/devbox/Dockerfile @@ -274,6 +274,8 @@ RUN curl -fsSL https://github.com/akinomyoga/ble.sh/releases/download/nightly/bl && rm -f /tmp/ble.tar.xz \ && test -f /usr/local/share/blesh/ble.sh COPY cmux-bashrc /etc/cmux/bashrc +COPY cmux-prompt.bash /etc/cmux/prompt.bash +RUN echo cmux > /etc/cmux/vm-name COPY seed-history /etc/cmux/seed-history COPY cmux-terminfo.sh /etc/profile.d/cmux-terminfo.sh # The cmux app's terminfo overlay (Resources/terminfo-overlay as infocmp diff --git a/web/services/vms/images/devbox/README.md b/web/services/vms/images/devbox/README.md index 439cb28a2049..4ef785cb5018 100644 --- a/web/services/vms/images/devbox/README.md +++ b/web/services/vms/images/devbox/README.md @@ -102,9 +102,27 @@ contract, not decoration: away, and the verifier proves both that the refusal is gone and that a pane the daemon opens really reports `cmux@cmux`. -`vm-devbox-image.test.ts` pins the shared files (`cmux-bashrc`, -`agent-config.sh`, `seed-history`, `chrome-managed-policy.json`) to their -chatmux counterparts, so edit both copies together. +The default Bash prompt shows `cmux@`. It reads `/etc/cmux/vm-name` +with Bash's built-in `read` before each prompt. Create and attach install +the current name. Rename updates it on a running machine. A paused or +unreachable machine gets the saved name on its next attach. The prompt name +uses the display label in lowercase with hyphens, or the generated slug when +there is no usable label. Routing ids do not change. + +Set `PS1` after the `/etc/cmux/bashrc` source line in `~/.bashrc` to customize +the prompt. For example, `PS1='\u@${__cmux_vm_name}:\w\$ '` keeps the live +name with a different layout. A fixed prompt or a prompt tool also works. +The line editor attaches at the first prompt, after these user settings load. +Remove the source line to replace the full cmux shell setup. Lifecycle updates +only write system defaults, never user rc files or prompt settings. The +name reader never rewrites `PS1`, runs Git, calls the network, or starts a +child process. The shell variable is local scratch state, not exported +configuration. After the first upgrade, open a new shell to load this setup. That shell then reads +later name changes at its next prompt without a restart. + +The Dockerfile and Freestyle recipe use these same shell files. +`vm-devbox-image.test.ts` checks the image contract; `vm-guest-prompt.test.ts` +checks live prompt updates and user overrides. ## Desktop layer (`desktop/`) diff --git a/web/services/vms/images/devbox/cmux-bashrc b/web/services/vms/images/devbox/cmux-bashrc index 5449256de97a..3321a4a44f99 100644 --- a/web/services/vms/images/devbox/cmux-bashrc +++ b/web/services/vms/images/devbox/cmux-bashrc @@ -1,9 +1,14 @@ # cmux Cloud devbox interactive shell setup (/etc/cmux/bashrc). # Sourced from /etc/bash.bashrc, /etc/skel/.bashrc, /root/.bashrc, and # /home/cmux/.bashrc. -# (byte-identical body below the header; vm-devbox-image.test.ts pins it) and -# chatmux infra/sandbox-images/devshell.bashrc. case $- in *i*) ;; *) return ;; esac +# A login shell can reach this file through both system and user rc files. +if [[ ${__cmux_bashrc_loaded:-} ]]; then + # Ubuntu's per-user rc can replace the system PS1 between the two sources. + [ -r /etc/cmux/prompt.bash ] && . /etc/cmux/prompt.bash + return +fi +__cmux_bashrc_loaded=1 # PTYs from the sandbox API do not reliably inherit these. export LANG="${LANG:-C.UTF-8}" @@ -45,7 +50,8 @@ if [ -d /etc/cmux/blesh-cache-seed/blesh ]; then fi if [ -f /usr/local/share/blesh/ble.sh ] && [ -z "${BLE_VERSION:-}" ]; then - source /usr/local/share/blesh/ble.sh --noattach + # Attach at the first prompt, after the user's .bashrc can customize PS1. + source /usr/local/share/blesh/ble.sh --attach=prompt bleopt prompt_eol_mark= exec_errexit_mark= exec_elapsed_mark= exec_exit_mark= # Minimal integration: token highlighting off entirely (the stock faces # paint colored backgrounds under mistyped commands and mid-word input, @@ -65,18 +71,9 @@ if [ -f /usr/local/share/blesh/ble.sh ] && [ -z "${BLE_VERSION:-}" ]; then ble-bind -f 'M-C-h' kill-backward-cword fi -# Palette colors, not 256-color cube indices: the pane renders in the Mac's -# Ghostty theme, so magenta/green/yellow/cyan/red land on whatever that theme -# says, exactly like the local zsh prompt (ESC[35m, ESC[32m, ESC[33m). A cube -# index such as 135 is the same fixed purple under every theme. The machine -# name stays in the prompt (\h): cloud machines are addressed by name. -__cmux_git() { - local b d="" - b=$(git symbolic-ref --short HEAD 2>/dev/null) || b=$(git rev-parse --short HEAD 2>/dev/null) || return 0 - git diff --quiet --ignore-submodules HEAD 2>/dev/null || d="*" - printf ' on \001\033[36m\002%s\001\033[31m\002%s\001\033[0m\002' "$b" "$d" -} -PS1='\[\e[35m\]\u@\h\[\e[0m\] in \[\e[32m\]\w\[\e[0m\]$(__cmux_git)\[\e[33m\] λ\[\e[0m\] ' +# Users can override PS1 later in ~/.bashrc. The default reads the current +# machine name with Bash builtins and no Git scan. +[ -r /etc/cmux/prompt.bash ] && . /etc/cmux/prompt.bash HISTSIZE=50000 HISTFILESIZE=50000 @@ -85,5 +82,3 @@ shopt -s histappend alias ls='ls --color=auto' alias grep='grep --color=auto' alias g=git - -[ -n "${BLE_VERSION:-}" ] && ble-attach diff --git a/web/services/vms/images/devbox/cmux-prompt.bash b/web/services/vms/images/devbox/cmux-prompt.bash new file mode 100644 index 000000000000..d45490f085a2 --- /dev/null +++ b/web/services/vms/images/devbox/cmux-prompt.bash @@ -0,0 +1,14 @@ +# Default only. Set PS1 after sourcing /etc/cmux/bashrc in ~/.bashrc to +# replace it, or remove that source line to use your own shell setup. +# Only builtins run before each prompt. The name lives in a file, so open +# shells see renames without an environment update or a child process. +if ! declare -F __cmux_prompt_name >/dev/null; then + __cmux_prompt_name() { + local status=$? + IFS= read -r __cmux_vm_name 2>/dev/null < /etc/cmux/vm-name || __cmux_vm_name=cmux + return "$status" + } + PROMPT_COMMAND=(__cmux_prompt_name "${PROMPT_COMMAND[@]}") +fi +__cmux_prompt_name +PS1='\[\e[35m\]\u@${__cmux_vm_name}\[\e[0m\] in \[\e[32m\]\w\[\e[0m\]\[\e[33m\] λ\[\e[0m\] ' diff --git a/web/services/vms/images/manifest.json b/web/services/vms/images/manifest.json index 7b3525f71331..ca43778cd1e5 100644 --- a/web/services/vms/images/manifest.json +++ b/web/services/vms/images/manifest.json @@ -8311,7 +8311,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "sm", "cpu": 2, @@ -8347,7 +8347,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "md", "cpu": 4, @@ -8383,7 +8383,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "lg", "cpu": 8, @@ -8419,7 +8419,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "lgx", "cpu": 12, @@ -8454,7 +8454,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "xl", "cpu": 16, @@ -8490,7 +8490,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "2xl", "cpu": 32, @@ -8526,7 +8526,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "sm", "cpu": 2, @@ -8534,7 +8534,7 @@ "storageMb": 16384, "freestyleBase": "freestyle/ubuntu-sm" }, - "defaultForLocalDev": true + "defaultForLocalDev": false }, { "provider": "freestyle", @@ -8563,7 +8563,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "md", "cpu": 4, @@ -8599,7 +8599,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "lg", "cpu": 8, @@ -8635,7 +8635,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "lgx", "cpu": 12, @@ -8670,7 +8670,7 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", - "defaultForKind": true, + "defaultForKind": false, "size": { "name": "xl", "cpu": 16, @@ -8706,6 +8706,437 @@ "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-12 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-307f0a6dea1240dc94ccf22ba5adcc60, lg=sh-d07f57f6e14144159c4f3df441f5f661, xl=sh-f43f2dc226514452a891e5ff3e3098b0, lgx=sh-14d5835919154833a4e088ebf0f5c018, md=sh-b8c25737393b4cee9177923c6c04f894, 2xl=sh-13efeb95e5894be781ec96c8d25430ac.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "2xl", + "cpu": 32, + "memoryMb": 65536, + "storageMb": 131072, + "freestyleBase": "freestyle/ubuntu-2xl" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-sm", + "imageId": "sh-285fd804575248e9a193561e6a71c07d", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "sm", + "cpu": 2, + "memoryMb": 4096, + "storageMb": 16384, + "freestyleBase": "freestyle/ubuntu-sm" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-md", + "imageId": "sh-a282ea94065f4e3f89e8b63ad5229292", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "md", + "cpu": 4, + "memoryMb": 8192, + "storageMb": 32768, + "freestyleBase": "freestyle/ubuntu" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-lg", + "imageId": "sh-23dde0a0a7cc4482970ea091df5e926b", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "lg", + "cpu": 8, + "memoryMb": 16384, + "storageMb": 65536, + "freestyleBase": "freestyle/ubuntu-lg" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-lgx", + "imageId": "sh-2d0a19e477e0419bbab5b4447a8cead2", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "lgx", + "cpu": 12, + "memoryMb": 24576, + "storageMb": 98304 + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-xl", + "imageId": "sh-302163dff19c439499ba6e78d6d34ed5", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "xl", + "cpu": 16, + "memoryMb": 32768, + "storageMb": 131072, + "freestyleBase": "freestyle/ubuntu-xl" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-2xl", + "imageId": "sh-324baafd44ad4010abac11bbc882123f", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "2xl", + "cpu": 32, + "memoryMb": 65536, + "storageMb": 131072, + "freestyleBase": "freestyle/ubuntu-2xl" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-sm-base", + "imageId": "sh-285fd804575248e9a193561e6a71c07d", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "sm", + "cpu": 2, + "memoryMb": 4096, + "storageMb": 16384, + "freestyleBase": "freestyle/ubuntu-sm" + }, + "defaultForLocalDev": true + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-md-base", + "imageId": "sh-a282ea94065f4e3f89e8b63ad5229292", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "md", + "cpu": 4, + "memoryMb": 8192, + "storageMb": 32768, + "freestyleBase": "freestyle/ubuntu" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-lg-base", + "imageId": "sh-23dde0a0a7cc4482970ea091df5e926b", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "lg", + "cpu": 8, + "memoryMb": 16384, + "storageMb": 65536, + "freestyleBase": "freestyle/ubuntu-lg" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-lgx-base", + "imageId": "sh-2d0a19e477e0419bbab5b4447a8cead2", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "lgx", + "cpu": 12, + "memoryMb": 24576, + "storageMb": 98304 + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-xl-base", + "imageId": "sh-302163dff19c439499ba6e78d6d34ed5", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", + "defaultForKind": true, + "size": { + "name": "xl", + "cpu": 16, + "memoryMb": 32768, + "storageMb": 131072, + "freestyleBase": "freestyle/ubuntu-xl" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-20260913-120438-2xl-base", + "imageId": "sh-324baafd44ad4010abac11bbc882123f", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "a741e72a04b6a8259cf2509fe4fbba81b2bbdd40", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "fb6123e9b3e24fc6f9d6b71e1300e0a2114cdeaa586e823a1ea6152396da05f7", + "schema": 2 + }, + "builtAt": "2026-09-13T12:09:07.262Z", + "builderScriptVersion": "7505e241a75d5a0808886f963fc84e814823963d0ecc46e4b533c26c7a9478e4", + "agentToolResolvedVersions": { + "@anthropic-ai/claude-code": "2.1.267", + "@openai/codex": "0.154.0", + "opencode-ai": "1.18.30", + "@earendil-works/pi-coding-agent": "0.85.1", + "agent-browser": "0.37.1" + }, + "validationStatus": "passed", + "notes": "cmux devbox epoch 2026-09-10-r2 Devbox on the Freestyle public platform (api.freestyle.sh) from freestyle/ubuntu-sm: the base's Node/Bun/Python/uv/Docker plus pinned agents, devtools, Chrome + cua-driver, ble.sh devshell, cmux login banner, and the desktop layer (openbox/TigerVNC 5901, noVNC 6901, Ghostty, Chrome, Thunar) run by the cmux-desktop systemd unit as cmux; cmux (uid 1000, NOPASSWD sudo) is the work user and the daemon's session user, so terminals are non-root; hostname cmux (static, live, 127.0.1.1 alias; SSH host keys regenerated under it; journal reset); baked cmux-tui daemon 01ab9d104d, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-13 with verify-devbox-image.ts by promote-devbox-image.ts (toolchain, agent pins, daemon contract, desktop on 5901/6901). Sizes derived and re-booted by derive-devbox-sizes.ts: sm=sh-285fd804575248e9a193561e6a71c07d, md=sh-a282ea94065f4e3f89e8b63ad5229292, lg=sh-23dde0a0a7cc4482970ea091df5e926b, lgx=sh-2d0a19e477e0419bbab5b4447a8cead2, xl=sh-302163dff19c439499ba6e78d6d34ed5, 2xl=sh-324baafd44ad4010abac11bbc882123f.", + "cmuxTuiCommit": "01ab9d104d263f9afd99d43bacbbc9adc1dc4898", + "cmuxTuiSha256": "bfd7894110f7a5d2b5031b2babeaf1b26950e87b54bfbc52de459e3d7a60116f", "defaultForKind": true, "size": { "name": "2xl", diff --git a/web/services/vms/repository.ts b/web/services/vms/repository.ts index f74171cab706..af7657be4827 100644 --- a/web/services/vms/repository.ts +++ b/web/services/vms/repository.ts @@ -2636,7 +2636,12 @@ export const vmRepositoryLiveShape: VmRepositoryShape = { const db = cloudDb(); const updated = await db .update(cloudVms) - .set({ displayName: input.displayName, updatedAt: new Date() }) + .set({ + displayName: input.displayName, + // Date exposes milliseconds. Keep renames strictly ordered even + // when two writers arrive within the same millisecond. + updatedAt: sql`greatest(${cloudVms.updatedAt} + interval '1 millisecond', clock_timestamp())`, + }) .where(and(eq(cloudVms.id, input.id), ne(cloudVms.status, "destroyed"))) .returning({ id: cloudVms.id }); return updated.length > 0; diff --git a/web/services/vms/workflows.ts b/web/services/vms/workflows.ts index 6ef5ff6d21f1..fdb8a463967c 100644 --- a/web/services/vms/workflows.ts +++ b/web/services/vms/workflows.ts @@ -101,6 +101,7 @@ import { type VmResizeReservation, } from "./repository"; import { measureVmEffect, type VmTimingSink } from "./timings"; +import { guestPromptInstallCommand, vmPromptIdentity } from "./guestPrompt"; export { homeVolumeNameForUser, @@ -325,8 +326,7 @@ export function getVm(input: { }); } -/** Sets or clears the user-facing label on a machine the caller owns. The - * provider VM id stays the machine's address; this is display-only. */ +/** Sets or clears the label and refreshes the guest prompt. Routing ids stay stable. */ export function renameVm(input: { readonly userId: string; readonly billingTeamId?: string | null; @@ -338,7 +338,22 @@ export function renameVm(input: { const repo = yield* VmRepository; const vm = yield* requireUserVm(input); yield* repo.setDisplayName({ id: vm.id, displayName: input.displayName }); - return vmEntryFromRow({ ...vm, displayName: input.displayName, updatedAt: new Date() }); + // Read the committed row so a concurrent rename and attach carry the + // database's revision, not the request's start time. + const updated = yield* requireUserVm(input); + if (updated.status === "running") { + const providers = yield* VmProviderGateway; + yield* providers.exec(updated.provider, input.providerVmId, guestPromptInstallCommand(vmPromptIdentity(updated)), { + timeoutMs: 10_000, + providerMetadata: updated.providerMetadata, + }).pipe( + Effect.flatMap((result) => result.exitCode === 0 ? Effect.void : Effect.fail(new Error(`prompt update exited ${result.exitCode}`))), + // A saved rename must remain available when a guest is unreachable. + // The next attach repairs it; paused machines are never woken here. + Effect.catchAll((error) => Effect.logWarning("Cloud prompt update deferred until attach", { vmId: updated.id, error })), + ); + } + return vmEntryFromRow(updated); }); } @@ -616,6 +631,7 @@ export function createVm(input: { providers.create(input.provider, { image: input.image, displayName: create.vm.slug ?? undefined, + promptIdentity: vmPromptIdentity(create.vm), providerMetadata: create.vm.providerMetadata, homeVolume: input.perMachineHome ? homeVolumeTemplateForUser(input.userId) @@ -896,6 +912,7 @@ function finishBaseCreate( providers.create(input.provider, { image: input.image, displayName: create.vm.slug ?? undefined, + promptIdentity: vmPromptIdentity(create.vm), providerMetadata: create.vm.providerMetadata, edgeRules: materials?.edgeRules, network: { id: network.providerNetworkId }, @@ -3264,6 +3281,7 @@ export function openVmCmuxRemote(input: { input.providerVmId, "attach", providers.openCmuxRemote(vm.provider, input.providerVmId, { + promptIdentity: vmPromptIdentity(vm), deviceFingerprint: input.deviceFingerprint, clientCapabilities: input.clientCapabilities, providerMetadata: vm.providerMetadata, diff --git a/web/tests/direct-dev-backend-host.test.ts b/web/tests/direct-dev-backend-host.test.ts index e00fc279172a..59c5d0c0b8da 100644 --- a/web/tests/direct-dev-backend-host.test.ts +++ b/web/tests/direct-dev-backend-host.test.ts @@ -1,13 +1,62 @@ import { describe, expect, test } from "bun:test"; -import type { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { directDevBackendHost } from "../app/lib/direct-dev-backend-origin"; -import { requestOrigin } from "../app/lib/request-origin"; +import { requestOrigin, responseWithInternalRewrite } from "../app/lib/request-origin"; +import middleware from "../proxy"; + +function withDirectOrigin(run: () => void) { + const values = { + CMUX_DEV_BACKEND_TRANSPORT: "direct", + CMUX_DEV_BACKEND_TAILSCALE_HOST: "cmux-dev-backend-1.tail137216.ts.net", + CMUX_WWW_ORIGIN: "https://cmux-dev-backend-1.tail137216.ts.net:3916/", + }; + const previous = Object.fromEntries(Object.keys(values).map((key) => [key, process.env[key]])); + Object.assign(process.env, values); + try { run(); } finally { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } +} function request(origin: string): NextRequest { return { nextUrl: { origin } } as unknown as NextRequest; } describe("direct dev backend host forwarding", () => { + test("preserves double-slash paths without treating them as a new host", () => { + withDirectOrigin(() => { + const response = responseWithInternalRewrite( + NextResponse.rewrite("https://cmux-dev-backend-1.tail137216.ts.net:3916//another.example/page?x=1"), + new NextRequest("http://0.0.0.0:3916/"), + ); + expect(response.headers.get("x-middleware-rewrite")) + .toBe("http://0.0.0.0:3916//another.example/page?x=1"); + }); + }); + + test("keeps page rewrites inside the server instead of sending them through middleware again", () => { + withDirectOrigin(() => { + for (const pathname of ["/", "/billing/success?session_id=test"]) { + const response = middleware(new NextRequest(`http://0.0.0.0:3916${pathname}`)); + expect(response.status).toBe(200); + expect(response.headers.get("location")).toBeNull(); + const rewritten = new URL(response.headers.get("x-middleware-rewrite")!); + expect(rewritten.origin).toBe("http://0.0.0.0:3916"); + expect(rewritten.pathname.startsWith("/en")).toBe(true); + } + }); + }); + + test("keeps browser redirects on the public HTTPS origin", () => { + withDirectOrigin(() => { + const response = middleware(new NextRequest("http://0.0.0.0:3916/en")); + expect(response.status).toBe(307); + expect(response.headers.get("location")).toBe("https://cmux-dev-backend-1.tail137216.ts.net:3916/"); + }); + }); + test("uses the configured Tailscale origin for direct transport", () => { expect( requestOrigin(request("https://0.0.0.0:3916"), { diff --git a/web/tests/vm-devbox-image.test.ts b/web/tests/vm-devbox-image.test.ts index 880687097428..75c9e558ec55 100644 --- a/web/tests/vm-devbox-image.test.ts +++ b/web/tests/vm-devbox-image.test.ts @@ -105,6 +105,7 @@ describe("devbox image template", () => { "cmux-bashrc", "cmux-devbox-boot", "cmux-motd", + "cmux-prompt.bash", "cmux-terminfo.sh", "cmux-terminfo.src", "codex-managed.toml", @@ -120,6 +121,7 @@ describe("devbox image template", () => { "cmux-bashrc", "cmux-devbox-boot", "cmux-motd", + "cmux-prompt.bash", "cmux-terminfo.sh", "cmux-terminfo.src", "codex-managed.toml", @@ -166,7 +168,7 @@ describe("devbox image template", () => { }); test("every shell file parses", () => { - for (const name of ["cmux-bashrc", "agent-config.sh", "cmux-terminfo.sh"]) { + for (const name of ["cmux-bashrc", "cmux-prompt.bash", "agent-config.sh", "cmux-terminfo.sh"]) { const result = spawnSync("bash", ["-n", path.join(templateDir, name)]); expect({ name, status: result.status }).toEqual({ name, status: 0 }); } diff --git a/web/tests/vm-guest-prompt.test.ts b/web/tests/vm-guest-prompt.test.ts new file mode 100644 index 000000000000..65e4189ff343 --- /dev/null +++ b/web/tests/vm-guest-prompt.test.ts @@ -0,0 +1,173 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { guestPromptInstallCommand, vmPromptIdentity } from "../services/vms/guestPrompt"; + +const directories: string[] = []; +afterEach(() => { + for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true }); +}); + +function fixture() { + const directory = mkdtempSync(path.join(tmpdir(), "cmux-prompt-")); + directories.push(directory); + return directory; +} + +function install(directory: string, name: string, revision: number, machineId = "vm-one") { + const result = spawnSync("sh", ["-c", guestPromptInstallCommand({ machineId, name, revision }, directory)], { encoding: "utf8" }); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); +} + +function bash(directory: string, command: string) { + const result = spawnSync("bash", ["--noprofile", "--norc", "-c", command], { + encoding: "utf8", + env: { NODE_ENV: "test", PATH: process.env.PATH!, HOME: directory }, + }); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + return result.stdout; +} + +describe("Cloud Bash prompt", () => { + test("attaches the line editor after user Bash settings have loaded", () => { + const directory = fixture(); + install(directory, "brave-blue-otter", 100); + const library = path.join(directory, "blesh"); + mkdirSync(library); + // The editor captures PS1 when it attaches. Its prompt-time mode defers + // that capture until the rest of the user's startup file has run. + writeFileSync(path.join(library, "ble.sh"), ` + BLE_VERSION=fixture + bleopt() { :; } + ble-face() { :; } + ble-bind() { :; } + ble-attach() { printf '%s' "$PS1" > "$HOME/captured-prompt"; } + [[ \${1-} == --noattach ]] || PROMPT_COMMAND+=(ble-attach) + `); + const rcPath = path.join(directory, "bashrc"); + writeFileSync(rcPath, readFileSync(rcPath, "utf8").replaceAll("/usr/local/share/blesh", library)); + const result = spawnSync("bash", ["--noprofile", "--norc", "-ic", ` + . '${rcPath}' + PS1='custom> ' + for command in "\${PROMPT_COMMAND[@]}"; do eval "$command"; done + `], { + encoding: "utf8", + env: { NODE_ENV: "test", PATH: process.env.PATH!, HOME: directory, USER: "cmux", TERM: "dumb" }, + }); + expect(result.status).toBe(0); + expect(readFileSync(path.join(directory, "captured-prompt"), "utf8")).toBe("custom> "); + }); + + test("uses the generated slug, then a shell-safe renamed label", () => { + const row = { id: "vm-one", slug: "brave-blue-otter", displayName: null, updatedAt: new Date(100) }; + expect(vmPromptIdentity(row)).toEqual({ machineId: "vm-one", name: "brave-blue-otter", revision: 100 }); + expect(vmPromptIdentity({ ...row, displayName: "My Build Box" }).name).toBe("my-build-box"); + expect(vmPromptIdentity({ ...row, displayName: "東京" }).name).toBe(row.slug); + expect(vmPromptIdentity({ ...row, displayName: "a".repeat(100) }).name).toHaveLength(63); + expect(vmPromptIdentity({ ...row, displayName: "$(touch /tmp/injected) `id` \\n" }).name).toMatch(/^[a-z0-9-]+$/); + }); + + test("an open shell reads a renamed machine using only builtins", () => { + const directory = fixture(); + install(directory, "brave-blue-otter", 100); + // Evaluate Bash's real prompt expansion in one shell. Empty PATH makes + // any accidental git/cat/hostname/network command fail the test. + const output = bash(directory, ` + . '${directory}/prompt.bash' + PATH=/does-not-exist + __cmux_prompt_name + eval 'printf "%s\\n" "'"$PS1"'"' + printf '%s\\n' renamed-box > '${directory}/vm-name' + __cmux_prompt_name + eval 'printf "%s\\n" "'"$PS1"'"' + printf 'hook=%s\\n' "\${PROMPT_COMMAND-}" + `); + expect(output).toContain("@brave-blue-otter"); + expect(output).toContain("@renamed-box"); + expect(output).toContain("hook=__cmux_prompt_name\n"); + }); + + test("preserves existing prompt commands, exit status, and repeated sourcing", () => { + const directory = fixture(); + install(directory, "brave-blue-otter", 100); + expect(bash(directory, ` + PROMPT_COMMAND=(':' 'printf user-hook') + . '${directory}/prompt.bash' + . '${directory}/prompt.bash' + false + __cmux_prompt_name + printf '%s|' "$?" + printf '%s|' "\${PROMPT_COMMAND[@]}" + `)).toBe("1|__cmux_prompt_name|:|printf user-hook|"); + }); + + test("renders successive prompts in a real interactive Bash terminal", () => { + const directory = fixture(); + install(directory, "brave-blue-otter", 100); + // System rc, Ubuntu user defaults, then the user's cmux source line. + writeFileSync(path.join(directory, "startup.bash"), `. '${directory}/bashrc'\nPS1='ubuntu> '\n. '${directory}/bashrc'\n`); + const result = spawnSync("python3", ["-c", String.raw` +import fcntl, os, pathlib, pty, select, signal, struct, subprocess, sys, termios, time +root = pathlib.Path(sys.argv[1]) +(root / ".inputrc").write_text("set enable-bracketed-paste off\n") +(root / ".hushlogin").touch() +master, slave = pty.openpty() +fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 120, 0, 0)) +def setup(): + os.setsid() + fcntl.ioctl(0, termios.TIOCSCTTY, 0) +shell = subprocess.Popen(["bash", "--noprofile", "--rcfile", str(root / "startup.bash"), "-i"], + stdin=slave, stdout=slave, stderr=slave, preexec_fn=setup, + env={"PATH": os.environ["PATH"], "HOME": str(root), "TERM": "xterm-256color"}) +os.close(slave) +def until(marker): + output = b"" + deadline = time.monotonic() + 3 + while marker not in output: + if time.monotonic() > deadline: raise AssertionError(repr(output)) + if select.select([master], [], [], 0.1)[0]: output += os.read(master, 65536) +try: + until(b"@brave-blue-otter") + (root / "vm-name").write_text("renamed-box\n") + os.write(master, b"false\n") + until(b"@renamed-box") + os.write(master, b"printf 'STATUS=%s\\n' \"$?\"\n") + until(b"STATUS=1") + os.write(master, b"PS1='custom> '\n") + until(b"\r\ncustom> ") + (root / "vm-name").write_text("third-name\n") + os.write(master, b":\n") + until(b"\r\ncustom> ") +finally: + os.killpg(shell.pid, signal.SIGKILL) + shell.wait() + os.close(master) +`, directory], { encoding: "utf8", timeout: 15_000 }); + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + }); + + test("user Bash settings and a custom prompt survive updates", () => { + const directory = fixture(); + const custom = `PS1='my custom prompt> '\nPROMPT_COMMAND=':'\n`; + writeFileSync(path.join(directory, ".bashrc"), custom); + install(directory, "brave-blue-otter", 100); + install(directory, "renamed-box", 200); + expect(readFileSync(path.join(directory, ".bashrc"), "utf8")).toBe(custom); + expect(bash(directory, `. '${directory}/prompt.bash'; . "$HOME/.bashrc"; printf '%s|%s' "$PS1" "$PROMPT_COMMAND"`)) + .toBe("my custom prompt> |:"); + }); + + test("a stale attach cannot undo a rename, and a clone takes its own identity", () => { + const directory = fixture(); + install(directory, "renamed-box", 200); + install(directory, "old-name", 100); + expect(readFileSync(path.join(directory, "vm-name"), "utf8")).toBe("renamed-box\n"); + install(directory, "clone-name", 50, "vm-two"); + expect(readFileSync(path.join(directory, "vm-name"), "utf8")).toBe("clone-name\n"); + }); +}); diff --git a/web/tests/vm-workflows.test.ts b/web/tests/vm-workflows.test.ts index 1d42480a275d..54bab65e86f0 100644 --- a/web/tests/vm-workflows.test.ts +++ b/web/tests/vm-workflows.test.ts @@ -61,6 +61,7 @@ import { revokeExpiredIdentityLeases, revokeUserIdentityLeasesForAccountDeletion, resetBaseVm, + renameVm, restoreVm, reconcileVmProviderStatuses, resizeVm, @@ -75,6 +76,55 @@ const runDbTests = process.env.CMUX_DB_TEST === "1"; const serialTest = (test as typeof test & { serial: typeof test }).serial; const dbTest = runDbTests ? serialTest : test.skip; +describe("Cloud prompt rename", () => { + test("publishes the committed name to the running guest", async () => { + let current = testCloudVmRow({ providerVmId: "vm-prompt", status: "running", slug: "brave-blue-otter" }); + const calls: string[] = []; + const repo: VmRepositoryShape = { + ...testWorkflowRepo({ vm: current }), + findUserVm: () => Effect.succeed(current), + setDisplayName: ({ displayName }) => Effect.sync(() => { + current = { ...current, displayName, updatedAt: new Date(200) }; + return true; + }), + }; + const result = await Effect.runPromise(renameVm({ + userId: current.userId, providerVmId: "vm-prompt", displayName: "My Build Box", + }).pipe(Effect.provide(Layer.merge( + Layer.succeed(VmRepository, repo), + Layer.succeed(VmProviderGateway, { + ...unusedProviderGateway(), + exec: (_provider, id, command) => Effect.sync(() => { + calls.push(id, command); + return { exitCode: 0, stdout: "", stderr: "" }; + }), + }), + )))); + expect(result.displayName).toBe("My Build Box"); + expect(result.slug).toBe("brave-blue-otter"); + expect(calls[0]).toBe("vm-prompt"); + expect(calls[1]).toContain('"name":"my-build-box","revision":200'); + }); + + test("renames a paused machine without waking it", async () => { + let current = testCloudVmRow({ providerVmId: "vm-prompt", status: "paused" }); + const repo: VmRepositoryShape = { + ...testWorkflowRepo({ vm: current }), + findUserVm: () => Effect.succeed(current), + setDisplayName: ({ displayName }) => Effect.sync(() => { + current = { ...current, displayName }; + return true; + }), + }; + // The provider rejects every guest operation, so a wake would fail here. + const result = await Effect.runPromise(renameVm({ + userId: current.userId, providerVmId: "vm-prompt", displayName: "Paused Box", + }).pipe(Effect.provide(Layer.succeed(VmRepository, repo)), Effect.provide(Layer.succeed(VmProviderGateway, unusedProviderGateway())))); + expect(result.displayName).toBe("Paused Box"); + expect(result.status).toBe("paused"); + }); +}); + let sql: Sql | null = null; type RecordedUsageEvent = Parameters[0]; @@ -144,6 +194,29 @@ afterAll(async () => { }); describe("VM Effect workflows", () => { + dbTest("keeps prompt revisions ordered across rapid renames and clock skew", async () => { + if (!sql) throw new Error("test database not initialized"); + const userId = "user-prompt-revisions"; + const providerVmId = "provider-prompt-revisions"; + await sql`delete from cloud_vms where user_id = ${userId}`; + const layer = providerLayer({ + ...unusedProviderGateway(), + create: () => Effect.succeed({ provider: "freestyle", providerVmId, image: "prompt-test", status: "running", createdAt: Date.now() }), + }); + await Effect.runPromise(createVm({ + userId, billingCustomerType: "team", billingTeamId: userId, billingPlanId: "free", provider: "freestyle", image: "prompt-test", maxActiveVms: 1, + }).pipe(Effect.provide(layer))); + const [{ id }] = await sql<{ id: string }[]>`update cloud_vms set updated_at = '2100-01-01T00:00:00Z' where user_id = ${userId} returning id`; + const revisions: number[] = []; + for (const displayName of ["first", "second"]) { + await Effect.runPromise(vmRepositoryLiveShape.setDisplayName({ id, displayName })); + const row = await Effect.runPromise(vmRepositoryLiveShape.findUserVm({ userId, billingTeamId: userId, providerVmId })); + revisions.push(row!.updatedAt.getTime()); + } + expect(revisions).toEqual([Date.parse("2100-01-01T00:00:00.001Z"), Date.parse("2100-01-01T00:00:00.002Z")]); + await sql`delete from cloud_vms where user_id = ${userId}`; + }); + test("repairs a legacy fork claim from provider CPU and memory stats", async () => { const source = testCloudVmRow({ id: "00000000-0000-4000-8000-000000000151",