diff --git a/docs/cloud-cmux-tui-daemon.md b/docs/cloud-cmux-tui-daemon.md index 3d69af7f39ea..37fb62b86878 100644 --- a/docs/cloud-cmux-tui-daemon.md +++ b/docs/cloud-cmux-tui-daemon.md @@ -698,11 +698,31 @@ nothing else), and `/integrations` (what the machine can use, each with a `help` command). The shim resolves `cmux vm exec ` through `/peers` when no route file exists. See docs/vm-identity-edge-auth.md. +## Coding-agent hooks on a machine + +Every machine ships the cmux-tui hooks for Claude Code and Codex, installed +for the daemon user (`/home/cmux`): the bake and the create-time install both +run `cmux-tui agent hook install claude codex` right after the binary +(`cmuxTuiInstallCommand`), with the `cmux-tui-hook` helper downloaded from the +same manifest commit as the daemon and placed beside it. A machine whose daemon +is healthy but predates this gets the hooks on attach (`ensureAgentHooks` in +`freestyle.ts`), using the helper of the commit in `/etc/cmux/cmux-tui-pin`; +the daemon keeps running because it already exports `CMUX_TUI_HOOK` into +every pane. The readiness probe (`cmuxTuiHooksReadyCommand`) requires the +installed helper to be byte-equal to the pinned one and the cmux marker in +`~/.claude/settings.json`, `~/.codex/hooks.json`, and the `[hooks]` trust +table in `~/.codex/config.toml`. `agent-config.sh` adds the codex model +provider around that trust table at the first login that sees a boot env, so +the two writers of `config.toml` compose in either order. The bake's +`agent-hooks` step proves all of it on the snapshot. + ## Notifications from a machine `cmux notify` inside a machine is the guest shim (`web/services/vms/guestCli.ts`) -translating to `notification create --title … --body … [--level …] --terminal -$CMUX_TUI_TERMINAL_ID` on the machine's own session. The daemon appends it to +running `cmux-tui --session cloud --quiet notify …` with the arguments untouched +(`--quiet` is dropped when the caller passes `--json` or `--jsonl`); the +daemon's `notify` verb owns the macOS signature (subtitle, scoped `--clear`, +`--reply` refused, `CMUX_TUI_TERMINAL_ID` as the caller terminal). The daemon appends it to its durable notification ledger and the v2 `session.events` stream carries it as a delta: diff --git a/web/scripts/build-devbox-freestyle.ts b/web/scripts/build-devbox-freestyle.ts index 306cea355109..a503968151e2 100644 --- a/web/scripts/build-devbox-freestyle.ts +++ b/web/scripts/build-devbox-freestyle.ts @@ -86,6 +86,8 @@ import { VM_GUEST_MODEL_PLANE_ENV_PATH, renderVmGuestModelPlaneEnvFile, vmGuestM import { CMUX_TUI_LAYOUT_MARKER_PATH, CMUX_TUI_SESSION, + CMUX_TUI_HOOK_PROVIDERS, + cmuxTuiHooksReadyCommand, cmuxTuiInstallCommand, cmuxTuiPinCheckCommand, cmuxTuiRunCommand, @@ -465,6 +467,32 @@ try { `${cmuxTuiPinCheckCommand(cmuxTuiSource)} && mkdir -p /etc/cmux && printf '%s %s\n' ${cmuxTuiSource.sha256} ${cmuxTuiSource.commit} > /etc/cmux/cmux-tui-pin && cat /etc/cmux/cmux-tui-pin`, ); + // The install above also wrote the work user's Claude Code and Codex hooks + // (cmux-tui agent hook install), so a Stop, permission request, or question + // in either agent reaches the daemon journal and the owner's Mac as a + // notification with no per-machine setup. Prove the four artifacts and that + // the daemon user's own status verb agrees; then prove the two writers of + // ~/.codex/config.toml compose: hooks first (bake), then the provider block + // agent-config.sh adds at the first login that sees a boot env, with the + // trust state intact and the result still one TOML document. + await step( + "agent-hooks", + [ + cmuxTuiHooksReadyCommand(), + `${cmuxTuiRunCommand(`--json agent hook status ${CMUX_TUI_HOOK_PROVIDERS.join(" ")}`)} > /tmp/hook-status.json`, + `node -e 'const r = JSON.parse(require("fs").readFileSync("/tmp/hook-status.json","utf8")); const rows = r.providers || []; const by = Object.fromEntries(rows.map((p) => [p.provider, p])); for (const id of ${JSON.stringify([...CMUX_TUI_HOOK_PROVIDERS])}) { if (!by[id] || by[id].state !== "installed") { console.error(id, by[id]); process.exit(1); } }'`, + `test "$(stat -c %U ${WORK_HOME}/.claude/settings.json ${WORK_HOME}/.codex/hooks.json ${WORK_HOME}/.codex/config.toml | sort -u)" = ${WORK_USER}`, + `! grep -q '^model_provider = ' ${WORK_HOME}/.codex/config.toml`, + `rm -rf /tmp/hook-merge-check && mkdir -p /tmp/hook-merge-check/.codex && cp ${WORK_HOME}/.codex/config.toml /tmp/hook-merge-check/.codex/config.toml`, + `env HOME=/tmp/hook-merge-check OPENAI_BASE_URL=https://example.invalid/v1 OPENAI_API_KEY=cmux-vm-edge-placeholder CMUX_CODEROUTER_URL=https://example.invalid bash -lc 'true'`, + `head -c 200 /tmp/hook-merge-check/.codex/config.toml | grep -q '^model_provider = "cmux"'`, + `grep -q '^\\[hooks' /tmp/hook-merge-check/.codex/config.toml && grep -q '^\\[model_providers.cmux\\]' /tmp/hook-merge-check/.codex/config.toml`, + `python3 -c 'import tomllib,sys; d = tomllib.load(open("/tmp/hook-merge-check/.codex/config.toml","rb")); assert d["model_provider"] == "cmux" and "hooks" in d and d["history"]["persistence"] == "save-all", d'`, + `rm -rf /tmp/hook-merge-check /tmp/hook-status.json`, + "echo agent-hooks-ok", + ].join(" && "), + ); + // The Ghostty generation panes announce as TERM_PROGRAM_VERSION (the // supervisor exports it next to TERM_PROGRAM=ghostty; see cmux-devbox-boot). await step( diff --git a/web/scripts/verify-devbox-image.ts b/web/scripts/verify-devbox-image.ts index 7584d55a236e..d68835cd2716 100644 --- a/web/scripts/verify-devbox-image.ts +++ b/web/scripts/verify-devbox-image.ts @@ -26,8 +26,10 @@ import { agentLaunchCheck } from "./devbox-agent-launch"; import { DEFAULT_VM_EDGE_ALIAS_DOMAIN } from "../services/coderouter/vmGuestEnv"; import path from "node:path"; import { + CMUX_TUI_HOOK_PROVIDERS, CMUX_TUI_LAYOUT_MARKER_PATH, CMUX_TUI_SESSION, + cmuxTuiHooksReadyCommand, cmuxTuiLayoutSelector, cmuxTuiRunCommand, resolveCmuxTuiSource, @@ -112,6 +114,11 @@ const CHECKS: readonly string[] = [ // Quiet-marks smoke: the bashrc blanks ble.sh's status marks and pins USER // so no [ble: ...] or "insane environment" text ever renders. "tmux new-session -d -s marks -x 100 -y 24 && sleep 3 && tmux send-keys -t marks not-a-command Enter && sleep 2 && tmux send-keys -t marks 'printf no-newline' Enter && sleep 2 && out=$(tmux capture-pane -pt marks); tmux kill-session -t marks 2>/dev/null; printf '%s\\n' \"$out\" | grep -E '\\[ble:|ble\\.sh:' && exit 1; echo no-ble-marks", + // Coding-agent hooks: the work user's Claude Code and Codex hooks are + // installed and current (helper byte-equal to the pinned one, cmux marker + // in both provider configs, codex trust table), and the daemon user's own + // status verb reports both providers installed. + `${cmuxTuiHooksReadyCommand()} && ${cmuxTuiRunCommand(`--json agent hook status ${CMUX_TUI_HOOK_PROVIDERS.join(" ")}`)} > /tmp/hook-status.json && node -e 'const r = JSON.parse(require("fs").readFileSync("/tmp/hook-status.json","utf8")); for (const id of ${JSON.stringify([...CMUX_TUI_HOOK_PROVIDERS])}) { const p = (r.providers || []).find((x) => x.provider === id); if (!p || p.state !== "installed") { console.error(id, p); process.exit(1); } }' && rm -f /tmp/hook-status.json && echo agent-hooks-ok`, // Agent-config generator: a login shell under a throwaway HOME with fake // model-plane env (placeholder keys, never a token) materializes the codex // custom provider plus the pi openai-codex override (no route-token diff --git a/web/services/vms/drivers/cmuxTuiDaemon.ts b/web/services/vms/drivers/cmuxTuiDaemon.ts index f716160f4cfd..683286efc61c 100644 --- a/web/services/vms/drivers/cmuxTuiDaemon.ts +++ b/web/services/vms/drivers/cmuxTuiDaemon.ts @@ -114,9 +114,26 @@ export function cmuxTuiAsDaemonUser(command: string, options?: { readonly exec?: ); } -export type CmuxTuiSource = { url: string; sha256: string; commit: string; builtAt: string | null }; +/** + * One commit's Linux build: the daemon binary and its `cmux-tui-hook` helper, + * both from the same manifest so the hook records a daemon of its own + * generation writes into the journal. + */ +export type CmuxTuiSource = { + url: string; + sha256: string; + commit: string; + builtAt: string | null; + hookUrl: string; + hookSha256: string; +}; export const CMUX_TUI_LINUX_TARGET = "cmux-tui-x86_64-unknown-linux-musl"; +export const CMUX_TUI_HOOK_LINUX_TARGET = "cmux-tui-hook-x86_64-unknown-linux-musl"; +/** The marker every cmux-owned coding-agent hook entry carries (agent_hook_install.rs COMMAND_MARKER). */ +export const CMUX_TUI_HOOK_MARKER = "cmux-tui-journal-hook"; +/** Coding agents whose hooks every machine ships with; `cmux-tui agent hook install` names them. */ +export const CMUX_TUI_HOOK_PROVIDERS = ["claude", "codex"] as const; export const CMUX_TUI_DEFAULT_MANIFEST_URL = "https://files.cmux.com/cmux-tui/latest/manifest.json"; const CMUX_TUI_MANIFEST_CACHE_MS = 5 * 60 * 1000; @@ -143,27 +160,54 @@ export function parseCmuxTuiManifest( const record = manifest && typeof manifest === "object" ? manifest as Record : {}; const commit = typeof record.commit === "string" ? record.commit : ""; const binaries = record.binaries && typeof record.binaries === "object" ? record.binaries as Record : {}; - const sha256 = typeof binaries[CMUX_TUI_LINUX_TARGET] === "string" ? (binaries[CMUX_TUI_LINUX_TARGET] as string).toLowerCase() : ""; + const digest = (target: string): string => (typeof binaries[target] === "string" ? (binaries[target] as string).toLowerCase() : ""); + const sha256 = digest(CMUX_TUI_LINUX_TARGET); + const hookSha256 = digest(CMUX_TUI_HOOK_LINUX_TARGET); if (!/^[0-9a-f]{40}$/.test(commit)) { throw new ProviderError(provider, `cmux-tui manifest at ${manifestUrl} has no commit`); } if (!/^[0-9a-f]{64}$/.test(sha256)) { throw new ProviderError(provider, `cmux-tui manifest at ${manifestUrl} has no ${CMUX_TUI_LINUX_TARGET} sha256 — publish artifacts from a main with the musl target`); } + if (!/^[0-9a-f]{64}$/.test(hookSha256)) { + throw new ProviderError(provider, `cmux-tui manifest at ${manifestUrl} has no ${CMUX_TUI_HOOK_LINUX_TARGET} sha256 — the hook helper ships beside the daemon since cmux-tui-artifacts publishes both`); + } const base = manifestUrl.replace(/\/manifest\.json$/, ""); return { url: `${base}/${CMUX_TUI_LINUX_TARGET}`, sha256, commit, builtAt: typeof record.builtAt === "string" ? record.builtAt : null, + hookUrl: `${base}/${CMUX_TUI_HOOK_LINUX_TARGET}`, + hookSha256, }; } +/** The manifest of one published commit, a sibling of the rolling `latest` pointer. */ +export function cmuxTuiPinnedManifestUrl(commit: string, provider: ProviderId = "freestyle"): string { + if (!/^[0-9a-f]{40}$/.test(commit)) { + throw new ProviderError(provider, `cmux-tui pin commit ${JSON.stringify(commit)} is not a full sha`); + } + const url = new URL(cmuxTuiManifestUrl(provider)); + const segments = url.pathname.split("/"); + if (segments.at(-1) !== "manifest.json") { + throw new ProviderError(provider, `cmux-tui manifest URL ${url.href} does not end in /manifest.json`); + } + // `//manifest.json` -> `//manifest.json`; a + // root-level `/manifest.json` gains the commit segment. Origin and query survive. + if (segments.length >= 3) segments[segments.length - 2] = commit; + else segments.splice(segments.length - 1, 0, commit); + url.pathname = segments.join("/"); + return url.href; +} + let cmuxTuiSourceCache: { url: string; fetchedAt: number; source: CmuxTuiSource } | null = null; /** The Linux daemon build to install, from the manifest (cached 5 min per manifest URL). */ -export async function resolveCmuxTuiSource(provider: ProviderId = "freestyle"): Promise { - const manifestUrl = cmuxTuiManifestUrl(provider); +export async function resolveCmuxTuiSource( + provider: ProviderId = "freestyle", + manifestUrl: string = cmuxTuiManifestUrl(provider), +): Promise { if (cmuxTuiSourceCache && cmuxTuiSourceCache.url === manifestUrl && Date.now() - cmuxTuiSourceCache.fetchedAt < CMUX_TUI_MANIFEST_CACHE_MS) { return cmuxTuiSourceCache.source; } @@ -204,27 +248,109 @@ export function resetCmuxTuiSourceCache(): void { * Runs as root and installs into the daemon's own home, so a work-user machine * gets a binary its sessions can execute (/root is 0700) and a legacy machine * keeps the one it already has. + * + * The same command installs the coding-agent hooks (`cmuxTuiAgentHooksInstallCommand`), + * so a machine from the bake and a machine healed on attach both ship them. */ export function cmuxTuiInstallCommand(source: CmuxTuiSource): string { const bin = '"$CMUX_TUI_BIN"'; const tmp = '"$CMUX_TUI_TMP"'; - const pinned = (path: string) => `printf '%s %s\n' ${shellQuote(source.sha256)} ${path} | sha256sum -c >/dev/null 2>&1`; - const fetch = - `if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 --retry-delay 2 -o ${tmp} ${shellQuote(source.url)}; ` + - `elif command -v wget >/dev/null 2>&1; then wget -q -O ${tmp} ${shellQuote(source.url)}; ` + - `else false; fi`; return [ cmuxTuiLayoutSelector(), `CMUX_TUI_TMP="$CMUX_TUI_BIN.tmp"`, `mkdir -p "$(dirname "$CMUX_TUI_BIN")"`, - `if [ -x ${bin} ] && ${pinned(bin)}; then :; else ${fetch} && ${pinned(tmp)} && chmod 755 ${tmp} && mv -f ${tmp} ${bin}; fi`, + `if [ -x ${bin} ] && ${pinnedFile(source.sha256, bin)}; then :; else ${fetchTo(tmp, source.url)} && ${pinnedFile(source.sha256, tmp)} && chmod 755 ${tmp} && mv -f ${tmp} ${bin}; fi`, `ln -sfn ${bin} /usr/local/bin/cmux-tui`, + ...hookHelperInstallSteps(source), // Only the nodes this install created, never the daemon's state tree. - `if [ "$CMUX_TUI_USER" != root ]; then chown "$CMUX_TUI_USER:$CMUX_TUI_USER" "$CMUX_TUI_HOME/.cmux" "$CMUX_TUI_HOME/.cmux/bin" ${bin} 2>/dev/null || true; fi`, + `if [ "$CMUX_TUI_USER" != root ]; then chown "$CMUX_TUI_USER:$CMUX_TUI_USER" "$CMUX_TUI_HOME/.cmux" "$CMUX_TUI_HOME/.cmux/bin" ${bin} ${HOOK_BIN} 2>/dev/null || true; fi`, `${bin} --version`, + ...agentHooksInstallSteps(), ].join(" && "); } +const HOOK_BIN = '"$CMUX_TUI_HOOK_BIN"'; +const HOOK_TMP = '"$CMUX_TUI_HOOK_TMP"'; +/** Where `cmux-tui agent hook install` copies the helper for the daemon user (agent_hook_install.rs installed_helper). */ +const INSTALLED_HOOK = '"$CMUX_TUI_HOME/.local/share/cmux-tui/bin/cmux-tui-hook"'; + +function pinnedFile(sha256: string, path: string): string { + return `printf '%s %s\n' ${shellQuote(sha256)} ${path} | sha256sum -c >/dev/null 2>&1`; +} + +function fetchTo(path: string, url: string): string { + return ( + `if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 --retry-delay 2 -o ${path} ${shellQuote(url)}; ` + + `elif command -v wget >/dev/null 2>&1; then wget -q -O ${path} ${shellQuote(url)}; ` + + `else false; fi` + ); +} + +/** + * The `cmux-tui-hook` helper lands beside the daemon binary: that is the one + * place `cmux-tui agent hook install` looks for it without a PATH search + * (agent_hook_install.rs locate_helper_source). Same pin discipline as the daemon. + */ +function hookHelperInstallSteps(source: CmuxTuiSource): string[] { + return [ + `CMUX_TUI_HOOK_BIN="$(dirname "$CMUX_TUI_BIN")/cmux-tui-hook"`, + `CMUX_TUI_HOOK_TMP="$CMUX_TUI_HOOK_BIN.tmp"`, + `if [ -x ${HOOK_BIN} ] && ${pinnedFile(source.hookSha256, HOOK_BIN)}; then :; else ${fetchTo(HOOK_TMP, source.hookUrl)} && ${pinnedFile(source.hookSha256, HOOK_TMP)} && chmod 755 ${HOOK_TMP} && mv -f ${HOOK_TMP} ${HOOK_BIN}; fi`, + ]; +} + +/** + * Writes the Claude Code and Codex hook entries for the daemon user and copies + * the helper into that user's data dir, then proves it: the installed helper + * is byte-equal to the pinned one and every provider config carries the + * cmux marker. Idempotent (the installer rewrites nothing that already matches). + * The daemon exports CMUX_TUI_HOOK into every pane it spawns, so no restart is + * needed for an already running daemon; agents pick the hooks up at their + * next launch. + */ +function agentHooksInstallSteps(): string[] { + return [ + cmuxTuiAsDaemonUser(`"$CMUX_TUI_BIN" agent hook install ${CMUX_TUI_HOOK_PROVIDERS.join(" ")} >/dev/null`), + cmuxTuiHooksReadyCheck(), + ]; +} + +/** + * Readiness comes from the installer's own structured status, so a hook entry + * a user edited or reordered (reported `partial`) is repaired instead of + * passing a text grep; plus the helper beside the daemon must be byte-equal + * to the one the daemon user runs. + */ +function cmuxTuiHooksReadyCheck(): string { + const providers = CMUX_TUI_HOOK_PROVIDERS.join(" "); + const installed = JSON.stringify([...CMUX_TUI_HOOK_PROVIDERS]); + return [ + `test -x ${INSTALLED_HOOK}`, + `cmp -s ${HOOK_BIN} ${INSTALLED_HOOK}`, + cmuxTuiAsDaemonUser(`"$CMUX_TUI_BIN" --json agent hook status ${providers}`) + + ` | python3 -c 'import json, sys; r = json.load(sys.stdin); s = {p["provider"]: p["state"] for p in r.get("providers", [])}; sys.exit(0 if all(s.get(i) == "installed" for i in ${installed}) else 1)'`, + ].join(" && "); +} + +/** + * Hooks alone, for a machine whose daemon is healthy and pinned but predates + * hook installation: fetch the helper for the daemon's own commit and install + * the provider entries. Never touches the daemon binary or its state. + */ +export function cmuxTuiAgentHooksInstallCommand(source: CmuxTuiSource): string { + return [ + cmuxTuiLayoutSelector(), + ...hookHelperInstallSteps(source), + `if [ "$CMUX_TUI_USER" != root ]; then chown "$CMUX_TUI_USER:$CMUX_TUI_USER" ${HOOK_BIN} 2>/dev/null || true; fi`, + ...agentHooksInstallSteps(), + ].join(" && "); +} + +/** Exit 0 when the daemon user's coding-agent hooks are installed and current. */ +export function cmuxTuiHooksReadyCommand(): string { + return `${cmuxTuiLayoutSelector()} && CMUX_TUI_HOOK_BIN="$(dirname "$CMUX_TUI_BIN")/cmux-tui-hook" && ${cmuxTuiHooksReadyCheck()}`; +} + /** True when the installed binary matches the manifest pin (exit 0 from this command). */ export function cmuxTuiPinCheckCommand(source: CmuxTuiSource): string { return ( diff --git a/web/services/vms/drivers/freestyle.ts b/web/services/vms/drivers/freestyle.ts index 08df145a4b36..2cf6e7d259c1 100644 --- a/web/services/vms/drivers/freestyle.ts +++ b/web/services/vms/drivers/freestyle.ts @@ -56,7 +56,10 @@ import { cmuxTuiAttachBundleCommand, cmuxTuiDaemonBuild, cmuxTuiDaemonCommand, + cmuxTuiAgentHooksInstallCommand, + cmuxTuiHooksReadyCommand, cmuxTuiInstallCommand, + cmuxTuiPinnedManifestUrl, cmuxTuiLayoutSelector, cmuxTuiPinCheckCommand, cmuxTuiRunCommand, @@ -1376,6 +1379,12 @@ export class FreestyleProvider implements VMProvider { await this.ensureCmuxTuiRunning(vm, vmId); bundleResult = await this.execResult(vm, cmuxTuiAttachBundleCommand({ deviceFingerprint: fingerprint })); } + if (!healed && bundleResult?.exitCode === 0) { + // The healthy fast path skips the heal, so this is where a machine + // that predates hook installation gets its Claude Code and Codex + // hooks (best effort inside). + await this.ensureAgentHooks(vm, vmId); + } if (!bundleResult || bundleResult.exitCode !== 0) { throw new ProviderError( "freestyle", @@ -1555,7 +1564,10 @@ export class FreestyleProvider implements VMProvider { // Keep the shim present even when the baked daemon is already healthy. if (installGuestCli) await this.installGuestCli(vm); const healthy = await this.execResult(vm, freestyleDaemonSettledCommand(), DAEMON_SETTLE_TIMEOUT_MS + EXEC_OVERHEAD_TIMEOUT_MS); - if (healthy?.exitCode === 0) return; + if (healthy?.exitCode === 0) { + await this.ensureAgentHooks(vm, vmId); + return; + } const source = await this.deps.resolveDaemonSource("freestyle"); const pinned = await this.execResult(vm, freestylePinCheckCommand(source)); if (pinned?.exitCode !== 0) { @@ -1566,6 +1578,43 @@ export class FreestyleProvider implements VMProvider { } await this.execOrThrow(vm, vmId, freestyleStartDaemonCommand(), 60_000); await waitForCmuxTuiReady(this.cmuxTuiInvoke(vm), "freestyle", vmId); + // A repaired daemon whose binary was still pinned skipped the install + // (and with it the hooks); a resumed machine lands here while its + // supervisor re-keys the daemon. Same idempotent check as the healthy path. + await this.ensureAgentHooks(vm, vmId); + } + + /** + * A healthy daemon from a bake or create that predates hook installation + * has no Claude Code / Codex hooks, so its agents never post turn-completed + * or approval notifications. Install them for the daemon's own commit (the + * pin file the bake wrote, else the live pin the create used), the helper + * beside the binary so the two never disagree in generation. The daemon + * keeps running: it already exports CMUX_TUI_HOOK into every pane, and + * agents read hooks at their next launch. + */ + private async ensureAgentHooks(vm: Vm, vmId: string): Promise { + // Best effort throughout: a hook failure is logged and never costs the + // attach or the heal that called it. + try { + await this.installAgentHooks(vm, vmId); + } catch (err) { + console.warn(`[freestyle] ${vmId}: agent hooks not installed: ${errorMessage(err)}`); + } + } + + private async installAgentHooks(vm: Vm, vmId: string): Promise { + const ready = await this.execResult(vm, cmuxTuiHooksReadyCommand()); + if (ready?.exitCode === 0) return; + const pin = await this.execResult(vm, "cut -d' ' -f2 /etc/cmux/cmux-tui-pin 2>/dev/null"); + const commit = pin?.exitCode === 0 ? pin.stdout.trim() : ""; + // A pinned build published before the helper shipped throws here: the + // daemon is left as it is rather than paired with a helper from another + // generation. + const source = /^[0-9a-f]{40}$/.test(commit) + ? await this.deps.resolveDaemonSource("freestyle", cmuxTuiPinnedManifestUrl(commit)) + : await this.deps.resolveDaemonSource("freestyle"); + await this.execOrThrow(vm, vmId, cmuxTuiAgentHooksInstallCommand(source), CMUX_TUI_INSTALL_TIMEOUT_MS); } /** diff --git a/web/services/vms/guestCli.ts b/web/services/vms/guestCli.ts index c38a97b75b42..a95776a0338e 100644 --- a/web/services/vms/guestCli.ts +++ b/web/services/vms/guestCli.ts @@ -2555,40 +2555,19 @@ case "\${1:-}" in ;; notify) # Mac-CLI compatible \`cmux notify\` inside a machine (agent hooks call it - # with --title/--subtitle/--body). cmux-tui's verb is \`notification create\`; - # the notification lands in this machine's daemon ledger and the user's Mac - # picks it up from the session event stream it already follows. The Mac - # attributes it to the pane showing this terminal, so the daemon-assigned - # CMUX_TUI_TERMINAL_ID is the only selector that means anything here: Mac - # topology selectors are ignored, --subtitle folds into the body (cmux-tui - # has no such field), and only the levels the daemon accepts are forwarded. - # Nothing here can name a Mac workspace, surface, or socket. + # with --title/--subtitle/--body). cmux-tui's own \`notify\` verb takes the + # macOS signature: it stores --subtitle as its own field, scopes --clear, + # refuses --reply, and validates any selector it is handed, so the shim + # forwards the arguments verbatim and the daemon stays the one place that + # knows the grammar. Nothing here can name a Mac workspace, surface, or + # socket: those selectors are rejected by the daemon rather than mapped. shift - title=""; subtitle=""; body=""; level=""; terminal="\${CMUX_TUI_TERMINAL_ID:-}" - while [ "\$#" -gt 0 ]; do - case "\$1" in - --title) title="\${2:-}"; shift; [ "\$#" -gt 0 ] && shift ;; - --title=*) title="\${1#--title=}"; shift ;; - --subtitle) subtitle="\${2:-}"; shift; [ "\$#" -gt 0 ] && shift ;; - --subtitle=*) subtitle="\${1#--subtitle=}"; shift ;; - --body) body="\${2:-}"; shift; [ "\$#" -gt 0 ] && shift ;; - --body=*) body="\${1#--body=}"; shift ;; - --level) level="\${2:-}"; shift; [ "\$#" -gt 0 ] && shift ;; - --level=*) level="\${1#--level=}"; shift ;; - --terminal) terminal="\${2:-}"; shift; [ "\$#" -gt 0 ] && shift ;; - --terminal=*) terminal="\${1#--terminal=}"; shift ;; - --workspace|--surface|--window|--tab|--panel) shift; [ "\$#" -gt 0 ] && shift ;; - *) shift ;; - esac - done - [ -n "\$title" ] || title=Notification - if [ -n "\$subtitle" ]; then - if [ -n "\$body" ]; then body="\$subtitle — \$body"; else body="\$subtitle"; fi - fi - set -- notification create --title "\$title" --body "\$body" - case "\$level" in info|warning|error) set -- "\$@" --level "\$level" ;; esac - if [ -n "\$terminal" ]; then set -- "\$@" --terminal "\$terminal"; fi - exec "\$CMUX_TUI_BIN" --session "\$LOCAL_SESSION" --quiet "\$@" + # Silent on success like the Mac CLI, unless the caller asked for the + # JSON result: --quiet and --json are exclusive global output modes. + case " \$* " in + *" --json "*|*" --jsonl "*) exec "\$CMUX_TUI_BIN" --session "\$LOCAL_SESSION" notify "\$@" ;; + *) exec "\$CMUX_TUI_BIN" --session "\$LOCAL_SESSION" --quiet notify "\$@" ;; + esac ;; *) # Local daemon session. cmux-tui's own grammar is \`cmux \`. diff --git a/web/services/vms/guestSelfCli.ts b/web/services/vms/guestSelfCli.ts deleted file mode 100644 index 0c6c1f6e3502..000000000000 --- a/web/services/vms/guestSelfCli.ts +++ /dev/null @@ -1,177 +0,0 @@ -// The in-VM `cmux` command, self-discovery edition: `cmux self` says which -// machine this is, `cmux vm ls` lists the team's machines with this one -// marked. Both read `GET /api/vm/self` through the machine's TLS edge, which -// adds the VM-bound route token on the wire; the guest sends only the public -// placeholder bearer (services/coderouter/vmGuestEnv.ts). -// -// Installed by the driver on every attach and, when missing, in the same -// round trip as any `vm exec`, so it reaches machines created from any -// existing snapshot without a rebake. POSIX sh plus curl and jq, -// which the devbox image ships. Every other verb is refused with a pointer to -// the Mac CLI, so an agent learns the boundary from the tool, not from a doc. - -export const GUEST_CMUX_SELF_SHIM_PATH = "/usr/local/bin/cmux"; - -export const GUEST_CMUX_SELF_SHIM = `#!/bin/sh -# cmux — inside a cmux Cloud machine. Self-discovery only; every other verb -# runs on the Mac. Managed by cmux, reinstalled on attach; do not edit. -set -eu - -case "\${LC_ALL:-\${LC_MESSAGES:-\${LANG:-en}}}" in - ja*) cmux_lang=ja ;; - *) cmux_lang=en ;; -esac - -msg() { - key="$1"; shift - case "$cmux_lang/$key" in - en/help) printf '%s' 'cmux (inside a cmux Cloud machine) - - cmux self [--json] which machine this is: name, id, status, team - cmux vm ls [--json] the team'"'"'s machines, this one marked with * - cmux notify [flags] post a notification from this machine (Mac flags: --title, --subtitle, --body, --clear, --surface) - -Every other cmux verb (vm new, vm exec, vm push, …) runs on the Mac -cmux CLI. This machine holds no account token; the TLS edge authenticates it. -' ;; - ja/help) printf '%s' 'cmux(cmux Cloud マシン内) - - cmux self [--json] このマシンの名前、ID、状態、チーム - cmux vm ls [--json] チームのマシン一覧。このマシンには * が付きます - cmux notify [flags] このマシンから通知を送ります(Mac と同じフラグ: --title、--subtitle、--body、--clear、--surface) - -その他の cmux コマンド(vm new、vm exec、vm push など)は Mac の -cmux CLI で実行してください。このマシンはアカウントトークンを持たず、 -TLS エッジが認証します。 -' ;; - en/hostOnly) printf 'cmux: %s runs on the Mac cmux CLI, not inside a machine (try cmux --help)\\n' "$@" ;; - en/noDaemon) printf 'cmux: %s needs the cmux-tui daemon, which is not installed in this machine\\n' "$@" ;; - ja/noDaemon) printf 'cmux: %s には cmux-tui デーモンが必要ですが、このマシンにはインストールされていません\\n' "$@" ;; - ja/hostOnly) printf 'cmux: %s は Mac の cmux CLI で実行してください。マシン内では使えません(cmux --help を参照)\\n' "$@" ;; - en/noEdge) printf 'cmux: no cmux API endpoint in this machine (CMUX_CODEROUTER_URL is missing)\\n' ;; - ja/noEdge) printf 'cmux: このマシンに cmux API エンドポイントがありません(CMUX_CODEROUTER_URL が未設定)\\n' ;; - en/unreachable) printf 'cmux: cannot reach the cmux API from this machine (%s): %s\\n' "$@" ;; - ja/unreachable) printf 'cmux: このマシン(%s)から cmux API に到達できません: %s\\n' "$@" ;; - en/rejected) printf 'cmux: the cmux API rejected this machine (HTTP %s): %s\\n' "$@" ;; - ja/rejected) printf 'cmux: cmux API がこのマシンを拒否しました(HTTP %s): %s\\n' "$@" ;; - en/badOption) printf 'cmux: unknown option %s\\n' "$@" ;; - ja/badOption) printf 'cmux: 不明なオプション %s\\n' "$@" ;; - en/thisMachine) printf 'this machine' ;; - ja/thisMachine) printf 'このマシン' ;; - en/team) printf 'team' ;; - ja/team) printf 'チーム' ;; - en/machines) printf 'machines' ;; - ja/machines) printf 'マシン' ;; - esac -} - -die() { status="$1"; shift; msg "$@" >&2; exit "$status"; } - -load_env() { - if [ -f "\${HOME:-/root}/.config/cmux/model-plane.env" ]; then - . "\${HOME:-/root}/.config/cmux/model-plane.env" - elif [ -f /etc/cmux/model-plane.env ]; then - . /etc/cmux/model-plane.env - fi - [ -n "\${CMUX_CODEROUTER_URL:-}" ] || die 2 noEdge -} - -# One request; body on stdout, HTTP status in $http_status. A transport failure -# (DNS, TLS, timeout) exits 1 with curl's own reason so the cause is visible. -fetch_self() { - load_env - set -- --silent --show-error --connect-timeout 5 --max-time 20 \\ - --header "authorization: Bearer \${OPENAI_API_KEY:-cmux-vm-edge-placeholder}" \\ - --header "accept: application/json" \\ - --write-out '\\n%{http_code}' - if [ -f /usr/local/share/ca-certificates/freestyle-tls.crt ]; then - set -- --cacert /usr/local/share/ca-certificates/freestyle-tls.crt "$@" - fi - if ! raw="$(curl "$@" "\${CMUX_CODEROUTER_URL%/}/api/vm/self" 2>&1)"; then - # curl still appends the --write-out status ("000") after its error line; - # the error line is the reason worth showing. The edge becomes active - # about half a minute after a machine boots, so a fresh machine can land - # here once before the same command succeeds. - reason="$(printf '%s' "$raw" | grep -m 1 '^curl:' || printf '%s' "$raw" | sed '$d' | tail -n 1)" - die 1 unreachable "$(hostname 2>/dev/null || echo unknown)" "\${reason:-$raw}" - fi - http_status="$(printf '%s' "$raw" | tail -n 1)" - body="$(printf '%s' "$raw" | sed '$d')" - case "$http_status" in - 200) ;; - *) die 1 rejected "$http_status" "$(printf '%s' "$body" | jq -r '.message // .error // "unexpected response"' 2>/dev/null || echo "$body")" ;; - esac -} - -want_json() { - json=0 - for arg in "$@"; do - case "$arg" in - --json) json=1 ;; - --help|-h) msg help; exit 0 ;; - *) die 2 badOption "$arg" ;; - esac - done -} - -cmd_self() { - want_json "$@" - fetch_self - if [ "$json" -eq 1 ]; then printf '%s\\n' "$body"; return 0; fi - printf '%s' "$body" | jq -r --arg self "$(msg thisMachine)" --arg team "$(msg team)" --arg machines "$(msg machines)" ' - "\\(.machine.name)\\t\\(.machine.id)\\t\\(.machine.status)\\t(\\($self))", - "\\($team)\\t\\(.team.id)\\t\\(.machines | length) \\($machines)"' -} - -cmd_vm_ls() { - want_json "$@" - fetch_self - if [ "$json" -eq 1 ]; then printf '%s' "$body" | jq -c '{machines: .machines}'; printf '\\n'; return 0; fi - printf '%s' "$body" | jq -r '.machines[] | "\\(if .self then "*" else " " end) \\(.name)\\t\\(.id)\\t\\(.status)"' -} - -case "\${1:-}" in - self) shift; cmd_self "$@" ;; - notify|notification) - # Notifications live in this machine's cmux-tui daemon; the Mac derives - # its unread state from them. Same flags as the macOS cmux notify. Inside - # a daemon terminal CMUX_TUI_SOCKET is set; elsewhere (vm exec, cron) the - # daemon's own session socket is the target, never a default "main". - tui="\${CMUX_TUI_BIN:-/usr/local/bin/cmux-tui}" - if [ -x "$tui" ]; then - if [ -z "\${CMUX_TUI_SOCKET:-}" ]; then - run_dir="\${CMUX_TUI_RUN_DIR:-/tmp/cmux-tui-$(id -u)}" - if [ -S "$run_dir/cloud.sock" ]; then exec "$tui" --socket "$run_dir/cloud.sock" "$@"; fi - fi - exec "$tui" "$@" - fi - die 2 noDaemon "cmux $1" - ;; - vm) - shift - case "\${1:-}" in - ls|list) shift; cmd_vm_ls "$@" ;; - ""|--help|-h|help) msg help ;; - *) die 2 hostOnly "cmux vm $1" ;; - esac - ;; - ""|--help|-h|help) msg help ;; - --version|-v|version) printf 'cmux guest self-discovery CLI\\n' ;; - *) die 2 hostOnly "cmux $1" ;; -esac -`; - -/** - * One idempotent exec that installs the shim: base64 keeps the script - * byte-exact through any shell, and the temp-then-rename keeps a concurrent - * `cmux` invocation from ever seeing a half-written file. - */ -export function guestSelfCliInstallCommand(): string { - const encoded = Buffer.from(GUEST_CMUX_SELF_SHIM, "utf8").toString("base64"); - const tmp = `${GUEST_CMUX_SELF_SHIM_PATH}.tmp.$$`; - return [ - `printf '%s' '${encoded}' | base64 -d > ${tmp}`, - `chmod 0755 ${tmp}`, - `mv -f ${tmp} ${GUEST_CMUX_SELF_SHIM_PATH}`, - ].join(" && "); -} diff --git a/web/services/vms/images/devbox/agent-config.sh b/web/services/vms/images/devbox/agent-config.sh index f460b3dd4bd1..9e6d68f32e6f 100644 --- a/web/services/vms/images/devbox/agent-config.sh +++ b/web/services/vms/images/devbox/agent-config.sh @@ -64,11 +64,27 @@ cmux_write_agent_configs() { # can move under a codex upgrade. Auth comes from OPENAI_API_KEY at request # time (env_key); it is the public placeholder, and the edge adds the real # credential, so a rotated token needs no config rewrite. - if [ -n "${OPENAI_BASE_URL-}" ] && [ ! -e "$HOME/.codex/config.toml" ]; then + # + # config.toml has a second writer: `cmux-tui agent hook install codex` keeps + # its hook trust state under [hooks] there (the bake runs it before any + # shell has seen a boot env). A file that lacks model_provider is one of + # those: the provider goes in around it, top-level key first (TOML keeps + # bare keys above the first table header) and the tables last, so the + # trust state is preserved. A file that already names a provider, or that + # carries our tables, is left to whoever wrote it. + # TOML allows whitespace around keys, "=", and inside table headers, so the + # probes accept it; a key the user wrote as model_provider="openai" counts. + if [ -n "${OPENAI_BASE_URL-}" ] && ! grep -Eqs '^[[:space:]]*model_provider[[:space:]]*=' "$HOME/.codex/config.toml" \ + && ! grep -Eqs '^[[:space:]]*\[[[:space:]]*model_providers[[:space:]]*\.[[:space:]]*cmux[[:space:]]*\]' "$HOME/.codex/config.toml" \ + && ! grep -Eqs '^[[:space:]]*\[[[:space:]]*history[[:space:]]*\]' "$HOME/.codex/config.toml"; then mkdir -p "$HOME/.codex" 2>/dev/null && { echo "# generated by cmux from machine boot env; delete to manage manually" echo "model_provider = \"cmux\"" echo "" + if [ -f "$HOME/.codex/config.toml" ]; then + cat "$HOME/.codex/config.toml" + echo "" + fi echo "[model_providers.cmux]" echo "name = \"cmux\"" echo "base_url = \"$OPENAI_BASE_URL\"" @@ -79,7 +95,7 @@ cmux_write_agent_configs() { echo "" echo "[history]" echo "persistence = \"save-all\"" - } > "$HOME/.codex/config.toml" 2>/dev/null + } > "$HOME/.codex/config.toml.cmux-tmp" 2>/dev/null && mv -f "$HOME/.codex/config.toml.cmux-tmp" "$HOME/.codex/config.toml" 2>/dev/null fi # pi: override the built-in openai-codex provider, whose diff --git a/web/services/vms/images/manifest.json b/web/services/vms/images/manifest.json index ad4e3fc3c047..941771be2c87 100644 --- a/web/services/vms/images/manifest.json +++ b/web/services/vms/images/manifest.json @@ -6993,20 +6993,163 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-sm", - "imageId": "sh-cd099a44912648399e0420df9b4e7f4f", + "version": "freestyle-cmux-devbox-hooks-sm", + "imageId": "sh-434afeb0ba0c4e368ca432188ca9501d", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "desktop", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "sm", + "cpu": 2, + "memoryMb": 4096, + "storageMb": 16384, + "freestyleBase": "freestyle/ubuntu-sm" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-md", + "imageId": "sh-5d8fa1061b4d41ad819be2a803c99162", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "md", + "cpu": 4, + "memoryMb": 8192, + "storageMb": 32768, + "freestyleBase": "freestyle/ubuntu" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-lg", + "imageId": "sh-cc00c1314d75407486bad1c2db6b7941", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "lg", + "cpu": 8, + "memoryMb": 16384, + "storageMb": 65536, + "freestyleBase": "freestyle/ubuntu-lg" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-lgx", + "imageId": "sh-95d3d37fa8d24686b9b42e2f9147c887", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "lgx", + "cpu": 12, + "memoryMb": 24576, + "storageMb": 98304 + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-xl", + "imageId": "sh-4510d2b5069748c29e9dda0fc7d71b17", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7015,7 +7158,295 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "xl", + "cpu": 16, + "memoryMb": 32768, + "storageMb": 131072, + "freestyleBase": "freestyle/ubuntu-xl" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-2xl", + "imageId": "sh-f3c49f37968649f0abbd5ee42f7b3733", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "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-hooks-sm-base", + "imageId": "sh-434afeb0ba0c4e368ca432188ca9501d", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "sm", + "cpu": 2, + "memoryMb": 4096, + "storageMb": 16384, + "freestyleBase": "freestyle/ubuntu-sm" + }, + "defaultForLocalDev": false + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-md-base", + "imageId": "sh-5d8fa1061b4d41ad819be2a803c99162", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "md", + "cpu": 4, + "memoryMb": 8192, + "storageMb": 32768, + "freestyleBase": "freestyle/ubuntu" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-lg-base", + "imageId": "sh-cc00c1314d75407486bad1c2db6b7941", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "lg", + "cpu": 8, + "memoryMb": 16384, + "storageMb": 65536, + "freestyleBase": "freestyle/ubuntu-lg" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-lgx-base", + "imageId": "sh-95d3d37fa8d24686b9b42e2f9147c887", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "lgx", + "cpu": 12, + "memoryMb": 24576, + "storageMb": 98304 + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-xl-base", + "imageId": "sh-4510d2b5069748c29e9dda0fc7d71b17", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", + "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", + "defaultForKind": false, + "size": { + "name": "xl", + "cpu": 16, + "memoryMb": 32768, + "storageMb": 131072, + "freestyleBase": "freestyle/ubuntu-xl" + } + }, + { + "provider": "freestyle", + "version": "freestyle-cmux-devbox-hooks-2xl-base", + "imageId": "sh-f3c49f37968649f0abbd5ee42f7b3733", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "base", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "213e875f4a7664dbe5bca03cc60bf1e4ac1f675a", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "798f6ba9d4bfafb40d55c974c3aaa02d7bca04e79a998a9a585e5d82b82ed169", + "schema": 2 + }, + "builtAt": "2026-09-11T03:41:45.008Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-434afeb0ba0c4e368ca432188ca9501d, md=sh-5d8fa1061b4d41ad819be2a803c99162, lg=sh-cc00c1314d75407486bad1c2db6b7941, xl=sh-4510d2b5069748c29e9dda0fc7d71b17, lgx=sh-95d3d37fa8d24686b9b42e2f9147c887, 2xl=sh-f3c49f37968649f0abbd5ee42f7b3733.", + "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-hooks2-sm", + "imageId": "sh-d65ecd5fd6614ded9483a70d5e68ed28", + "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", + "kind": "desktop", + "cmuxdRemoteCommit": "none-cmux-tui", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", + "epoch": "2026-09-10-r2", + "devboxSource": { + "layers": "desktop", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", + "schema": 2 + }, + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", + "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7029,20 +7460,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-md", - "imageId": "sh-f5be56c7d50048278a66f1b654153bd3", + "version": "freestyle-cmux-devbox-hooks2-md", + "imageId": "sh-1a67fb62b8bb456ca56164819941f450", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "desktop", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7051,7 +7482,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7065,20 +7496,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-lg", - "imageId": "sh-50d26e0ace92459f9741708b1a312e0a", + "version": "freestyle-cmux-devbox-hooks2-lg", + "imageId": "sh-dfda28a462c943cca0a8a58153dabd58", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "desktop", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7087,7 +7518,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7101,20 +7532,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-lgx", - "imageId": "sh-6835b528b6a949e38c8fc0c2bc8164ba", + "version": "freestyle-cmux-devbox-hooks2-lgx", + "imageId": "sh-defeb7e51dd3455caecbafc79c13d30f", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "desktop", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7123,7 +7554,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7136,20 +7567,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-xl", - "imageId": "sh-882fde940e3440ffaa150a0a266f8d94", + "version": "freestyle-cmux-devbox-hooks2-xl", + "imageId": "sh-c6dad4da08e9431b966f0b36c06b01c8", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "desktop", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7158,7 +7589,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7172,20 +7603,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-2xl", - "imageId": "sh-5896573bcc4e4d54b7b72f63737632be", + "version": "freestyle-cmux-devbox-hooks2-2xl", + "imageId": "sh-679228fc42ea499095004e2004864bdb", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "desktop", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7194,7 +7625,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7208,20 +7639,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-sm-base", - "imageId": "sh-cd099a44912648399e0420df9b4e7f4f", + "version": "freestyle-cmux-devbox-hooks2-sm-base", + "imageId": "sh-d65ecd5fd6614ded9483a70d5e68ed28", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "base", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7230,7 +7661,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7245,20 +7676,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-md-base", - "imageId": "sh-f5be56c7d50048278a66f1b654153bd3", + "version": "freestyle-cmux-devbox-hooks2-md-base", + "imageId": "sh-1a67fb62b8bb456ca56164819941f450", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "base", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7267,7 +7698,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7281,20 +7712,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-lg-base", - "imageId": "sh-50d26e0ace92459f9741708b1a312e0a", + "version": "freestyle-cmux-devbox-hooks2-lg-base", + "imageId": "sh-dfda28a462c943cca0a8a58153dabd58", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "base", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7303,7 +7734,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7317,20 +7748,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-lgx-base", - "imageId": "sh-6835b528b6a949e38c8fc0c2bc8164ba", + "version": "freestyle-cmux-devbox-hooks2-lgx-base", + "imageId": "sh-defeb7e51dd3455caecbafc79c13d30f", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "base", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7339,7 +7770,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7352,20 +7783,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-xl-base", - "imageId": "sh-882fde940e3440ffaa150a0a266f8d94", + "version": "freestyle-cmux-devbox-hooks2-xl-base", + "imageId": "sh-c6dad4da08e9431b966f0b36c06b01c8", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "base", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7374,7 +7805,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, @@ -7388,20 +7819,20 @@ }, { "provider": "freestyle", - "version": "freestyle-cmux-devbox-20260911-032526-2xl-base", - "imageId": "sh-5896573bcc4e4d54b7b72f63737632be", + "version": "freestyle-cmux-devbox-hooks2-2xl-base", + "imageId": "sh-679228fc42ea499095004e2004864bdb", "envVar": "FREESTYLE_SANDBOX_SNAPSHOT", "kind": "base", "cmuxdRemoteCommit": "none-cmux-tui", - "repoCommit": "3774a642b2d4d96e42de5ee965726fc93ee05db9", + "repoCommit": "d5a15cc5dad82abbdafadcf334498750b9e47736", "epoch": "2026-09-10-r2", "devboxSource": { "layers": "desktop", - "digest": "789b829baf9e61b69f74d6d33b008f989cf5ea26ff0ac0795a8a96cbe22823db", + "digest": "30efe8c3c44afe55f5729756997d2f32500f6031d91cba4eff223633f37b8f7f", "schema": 2 }, - "builtAt": "2026-09-11T03:30:51.644Z", - "builderScriptVersion": "1fc0c4bac3dae2dd6c1b4ac3037568cd693ec13b37f12d4c459e6ad14956a5aa", + "builtAt": "2026-09-11T04:27:41.357Z", + "builderScriptVersion": "67b11dde0e8da54b80c1d7be017d4b5e6dabd869f4ff46eca2dc718b1ed31cbc", "agentToolResolvedVersions": { "@anthropic-ai/claude-code": "2.1.267", "@openai/codex": "0.154.0", @@ -7410,7 +7841,7 @@ "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 897bb7a9c0, identity bound to the instance id, no create-time bootstrap. Validated 2026-09-11 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-cd099a44912648399e0420df9b4e7f4f, md=sh-f5be56c7d50048278a66f1b654153bd3, lg=sh-50d26e0ace92459f9741708b1a312e0a, lgx=sh-6835b528b6a949e38c8fc0c2bc8164ba, xl=sh-882fde940e3440ffaa150a0a266f8d94, 2xl=sh-5896573bcc4e4d54b7b72f63737632be.", + "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-11 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-d65ecd5fd6614ded9483a70d5e68ed28, md=sh-1a67fb62b8bb456ca56164819941f450, lg=sh-dfda28a462c943cca0a8a58153dabd58, xl=sh-c6dad4da08e9431b966f0b36c06b01c8, lgx=sh-defeb7e51dd3455caecbafc79c13d30f, 2xl=sh-679228fc42ea499095004e2004864bdb.", "cmuxTuiCommit": "897bb7a9c0d7efd6407dbe229d1c30f8b0158589", "cmuxTuiSha256": "551da27dc459d1d2a399d235a81de8d704d40ad39f91bcc2628aaed99b272f96", "defaultForKind": true, diff --git a/web/tests/freestyle-cloud-shell-repair.test.ts b/web/tests/freestyle-cloud-shell-repair.test.ts index c62d09790c7a..811a09c8a6d3 100644 --- a/web/tests/freestyle-cloud-shell-repair.test.ts +++ b/web/tests/freestyle-cloud-shell-repair.test.ts @@ -15,7 +15,7 @@ const SOURCE = { url: "https://files.cmux.com/cmux-tui/test/cmux-tui-x86_64-unknown-linux-musl", sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", commit: "0123456789abcdef0123456789abcdef01234567", - builtAt: null, + builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64), } as const; describe("Freestyle Cloud VM daemon repair", () => { diff --git a/web/tests/vm-cmux-tui.test.ts b/web/tests/vm-cmux-tui.test.ts index eb168c3af3b6..a5011574c66f 100644 --- a/web/tests/vm-cmux-tui.test.ts +++ b/web/tests/vm-cmux-tui.test.ts @@ -7,7 +7,11 @@ import { CMUX_TUI_DAEMON_TERMINAL_ENV, CMUX_TUI_LAYOUT_MARKER_PATH, cmuxTuiDaemonCommand, + cmuxTuiAgentHooksInstallCommand, + cmuxTuiAsDaemonUser, + cmuxTuiHooksReadyCommand, cmuxTuiInstallCommand, + cmuxTuiPinnedManifestUrl, cmuxTuiLayoutSelector, cmuxTuiPinCheckCommand, cmuxTuiManifestUrl, @@ -22,6 +26,8 @@ const SHA = "c7a3155341a85a2f10a873d69a041bdf1855ec059a802e58e0779a7a6bdec607"; const COMMIT = "5a4780614cecd8e8ef040a24478f928ef31cc4ae"; const MANIFEST = `https://files.cmux.com/cmux-tui/${COMMIT}/manifest.json`; const URL = `https://files.cmux.com/cmux-tui/${COMMIT}/cmux-tui-x86_64-unknown-linux-musl`; +const HOOK_SHA = "9f2e4c1a7b3d5e6f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7"; +const HOOK_URL = `https://files.cmux.com/cmux-tui/${COMMIT}/cmux-tui-hook-x86_64-unknown-linux-musl`; function withEnv(values: Record, run: () => void) { const previous: Record = {}; @@ -53,21 +59,36 @@ describe("cmux-tui daemon source", () => { const source = parseCmuxTuiManifest(MANIFEST, { commit: COMMIT, builtAt: "2026-08-19T07:05:35Z", - binaries: { "cmux-tui-aarch64-apple-darwin": "a".repeat(64), "cmux-tui-x86_64-unknown-linux-musl": SHA.toUpperCase() }, + binaries: { + "cmux-tui-aarch64-apple-darwin": "a".repeat(64), + "cmux-tui-x86_64-unknown-linux-musl": SHA.toUpperCase(), + "cmux-tui-hook-x86_64-unknown-linux-musl": HOOK_SHA.toUpperCase(), + }, }); - expect(source).toEqual({ url: URL, sha256: SHA, commit: COMMIT, builtAt: "2026-08-19T07:05:35Z" }); + // The hook helper comes from the same commit as the daemon: a machine + // never pairs a daemon with a helper of another generation. + expect(source).toEqual({ url: URL, sha256: SHA, commit: COMMIT, builtAt: "2026-08-19T07:05:35Z", hookUrl: HOOK_URL, hookSha256: HOOK_SHA }); }); - test("fails closed on a manifest without a commit or without the musl build", () => { - expect(() => parseCmuxTuiManifest(MANIFEST, { binaries: { "cmux-tui-x86_64-unknown-linux-musl": SHA } })).toThrow(/commit/); - expect(() => parseCmuxTuiManifest(MANIFEST, { commit: COMMIT, binaries: { "cmux-tui-x86_64-unknown-linux-gnu": SHA } })).toThrow(/musl/); + test("fails closed on a manifest without a commit, without the musl build, or without the hook helper", () => { + const both = { "cmux-tui-x86_64-unknown-linux-musl": SHA, "cmux-tui-hook-x86_64-unknown-linux-musl": HOOK_SHA }; + expect(() => parseCmuxTuiManifest(MANIFEST, { binaries: both })).toThrow(/commit/); + expect(() => parseCmuxTuiManifest(MANIFEST, { commit: COMMIT, binaries: { "cmux-tui-x86_64-unknown-linux-gnu": SHA, "cmux-tui-hook-x86_64-unknown-linux-musl": HOOK_SHA } })).toThrow(/musl/); + expect(() => parseCmuxTuiManifest(MANIFEST, { commit: COMMIT, binaries: { "cmux-tui-x86_64-unknown-linux-musl": SHA } })).toThrow(/cmux-tui-hook/); expect(() => parseCmuxTuiManifest(MANIFEST, "nonsense")).toThrow(); }); + + test("a pinned manifest is the commit's sibling of the rolling pointer", () => { + expect(cmuxTuiPinnedManifestUrl(COMMIT)).toBe(MANIFEST); + withEnv({ CMUX_VM_CMUX_TUI_MANIFEST_URL: "https://files.example/tui/deadbeef/manifest.json" }, () => + expect(cmuxTuiPinnedManifestUrl(COMMIT)).toBe(`https://files.example/tui/${COMMIT}/manifest.json`)); + expect(() => cmuxTuiPinnedManifestUrl("abc")).toThrow(/full sha/); + }); }); describe("cmux-tui install and daemon commands", () => { test("installs into the daemon's own home, verifies the pin before and after download, and probes the binary", () => { - const command = cmuxTuiInstallCommand({ url: URL, sha256: SHA, commit: COMMIT, builtAt: null }); + const command = cmuxTuiInstallCommand({ url: URL, sha256: SHA, commit: COMMIT, builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64) }); // One runtime selection, shared with the daemon launch, so install and // launch can never disagree about where the binary lives. expect(command).toContain(cmuxTuiLayoutSelector()); @@ -82,20 +103,66 @@ describe("cmux-tui install and daemon commands", () => { // Only the nodes the install created; never a walk of the state tree. expect(command).toContain('chown "$CMUX_TUI_USER:$CMUX_TUI_USER" "$CMUX_TUI_HOME/.cmux" "$CMUX_TUI_HOME/.cmux/bin" "$CMUX_TUI_BIN"'); expect(command).not.toContain("chown -R"); - expect(command.endsWith('"$CMUX_TUI_BIN" --version')).toBe(true); + expect(command).toContain('"$CMUX_TUI_BIN" --version'); + }); + + test("installs the hook helper beside the daemon from the same pin and writes the Claude Code and Codex hooks as the daemon user", () => { + const source = { url: URL, sha256: SHA, commit: COMMIT, builtAt: null, hookUrl: HOOK_URL, hookSha256: HOOK_SHA }; + const command = cmuxTuiInstallCommand(source); + // Beside the binary: the one place `agent hook install` finds it without a PATH search. + expect(command).toContain('CMUX_TUI_HOOK_BIN="$(dirname "$CMUX_TUI_BIN")/cmux-tui-hook"'); + expect(command).toContain(`'${HOOK_SHA}' "$CMUX_TUI_HOOK_BIN" | sha256sum -c >/dev/null 2>&1; then :; else`); + expect(command).toContain(`curl -fsSL --retry 3 --retry-delay 2 -o "$CMUX_TUI_HOOK_TMP" '${HOOK_URL}'`); + expect(command).toContain(`'${HOOK_SHA}' "$CMUX_TUI_HOOK_TMP" | sha256sum -c >/dev/null 2>&1 && chmod 755`); + expect(command).toContain('"$CMUX_TUI_BIN" "$CMUX_TUI_HOOK_BIN" 2>/dev/null || true'); + // The hooks are the daemon user's (HOME=/home/cmux), never root's: root's + // settings are invisible to the terminals the daemon spawns. + const install = cmuxTuiAsDaemonUser('"$CMUX_TUI_BIN" agent hook install claude codex >/dev/null'); + expect(command).toContain(install); + expect(command.indexOf('"$CMUX_TUI_BIN" --version')).toBeLessThan(command.indexOf(install)); + // And proven, not assumed: helper installed and byte-equal to the pin, + // every provider config carrying the cmux marker, codex trust state written. + expect(command).toContain('test -x "$CMUX_TUI_HOME/.local/share/cmux-tui/bin/cmux-tui-hook"'); + expect(command).toContain('cmp -s "$CMUX_TUI_HOOK_BIN" "$CMUX_TUI_HOME/.local/share/cmux-tui/bin/cmux-tui-hook"'); + // Structured status, not a text grep: a user-edited entry reports partial and is repaired. + expect(command).toContain(cmuxTuiAsDaemonUser('"$CMUX_TUI_BIN" --json agent hook status claude codex')); + expect(command).toContain('all(s.get(i) == "installed" for i in ["claude","codex"])'); + expect(command).not.toContain("grep -q cmux-tui-journal-hook"); + }); + + test("the pinned manifest URL keeps the mirror's origin and query and handles a root-level pointer", () => { + withEnv({ CMUX_VM_CMUX_TUI_MANIFEST_URL: "https://mirror.example/manifest.json?token=abc" }, () => + expect(cmuxTuiPinnedManifestUrl(COMMIT)).toBe(`https://mirror.example/${COMMIT}/manifest.json?token=abc`)); + withEnv({ CMUX_VM_CMUX_TUI_MANIFEST_URL: "https://files.example/tui/latest/manifest.json?x=1" }, () => + expect(cmuxTuiPinnedManifestUrl(COMMIT)).toBe(`https://files.example/tui/${COMMIT}/manifest.json?x=1`)); + withEnv({ CMUX_VM_CMUX_TUI_MANIFEST_URL: "https://files.example/tui/latest/index.json" }, () => + expect(() => cmuxTuiPinnedManifestUrl(COMMIT)).toThrow(/manifest\.json/)); + }); + + test("the hooks-only install never touches the daemon binary", () => { + const source = { url: URL, sha256: SHA, commit: COMMIT, builtAt: null, hookUrl: HOOK_URL, hookSha256: HOOK_SHA }; + const command = cmuxTuiAgentHooksInstallCommand(source); + expect(command).toContain(cmuxTuiLayoutSelector()); + expect(command).toContain(HOOK_URL); + expect(command).not.toContain(URL); + expect(command).not.toContain("ln -sfn"); + expect(command).not.toContain("--version"); + expect(command).toContain("agent hook install claude codex"); + expect(cmuxTuiHooksReadyCommand()).toContain(cmuxTuiLayoutSelector()); + expect(cmuxTuiHooksReadyCommand()).toContain('test -x "$CMUX_TUI_HOME/.local/share/cmux-tui/bin/cmux-tui-hook"'); }); // Regression: `sha256sum -c -s` is BusyBox-only. GNU coreutils (the xfce-vnc desktop // image) rejects `-s` ("invalid option -- 's'"), which failed every create with a 502. test("the pin check never uses the BusyBox-only sha256sum -s flag", () => { - const command = cmuxTuiInstallCommand({ url: URL, sha256: SHA, commit: COMMIT, builtAt: null }); + const command = cmuxTuiInstallCommand({ url: URL, sha256: SHA, commit: COMMIT, builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64) }); expect(command).not.toMatch(/sha256sum[^|&;]*\s-s\b/); expect(command).not.toContain("--status"); expect(command).toContain("sha256sum -c >/dev/null 2>&1"); }); test("the pin check reads the same binary the daemon runs", () => { - const command = cmuxTuiPinCheckCommand({ url: URL, sha256: SHA, commit: COMMIT, builtAt: null }); + const command = cmuxTuiPinCheckCommand({ url: URL, sha256: SHA, commit: COMMIT, builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64) }); expect(command).toContain(cmuxTuiLayoutSelector()); expect(command).toContain('test -x "$CMUX_TUI_BIN"'); expect(command).toContain(`'${SHA}' "$CMUX_TUI_BIN" | sha256sum -c`); diff --git a/web/tests/vm-devbox-image.test.ts b/web/tests/vm-devbox-image.test.ts index fda478c8e10a..591bfa895327 100644 --- a/web/tests/vm-devbox-image.test.ts +++ b/web/tests/vm-devbox-image.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { spawn, spawnSync } from "node:child_process"; -import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import { tmpdir } from "node:os"; @@ -704,6 +704,56 @@ describe("devbox image template", () => { } }); + test("agent config generator adds the codex provider around hook trust state another writer left first", () => { + // The bake runs `cmux-tui agent hook install codex` before any shell has + // seen a boot env, so ~/.codex/config.toml already exists with only the + // hook trust table. The provider block goes in around it: bare key on + // top, tables at the end, trust state untouched, one TOML document. + const home = mkdtempSync(path.join(tmpdir(), "cmux-devbox-agent-config-merge-")); + try { + mkdirSync(path.join(home, ".codex"), { recursive: true }); + const hooks = [ + "[hooks]", + "", + '[hooks.state."/home/cmux/.codex/hooks.json:Stop:0:0"]', + 'trusted_hash = "3f0c"', + "", + ].join("\n"); + writeFileSync(path.join(home, ".codex/config.toml"), hooks); + const env = { + ...process.env, + HOME: home, + OPENAI_BASE_URL: "https://example.invalid/v1", + OPENAI_API_KEY: "cmux-vm-edge-placeholder", + CMUX_CODEROUTER_URL: "https://example.invalid", + }; + expect(spawnSync("bash", ["-c", `. ${path.join(templateDir, "agent-config.sh")}`], { env }).status).toBe(0); + const merged = readFileSync(path.join(home, ".codex/config.toml"), "utf8"); + const parsed = Bun.TOML.parse(merged) as Record; + expect(parsed.model_provider).toBe("cmux"); + expect(parsed.hooks).toEqual({ state: { "/home/cmux/.codex/hooks.json:Stop:0:0": { trusted_hash: "3f0c" } } }); + expect(parsed.model_providers).toEqual({ + cmux: { name: "cmux", base_url: "https://example.invalid/v1", env_key: "OPENAI_API_KEY", wire_api: "responses", requires_openai_auth: false, supports_websockets: false }, + }); + expect(parsed.history).toEqual({ persistence: "save-all" }); + // The bare key precedes the first table header, or TOML would file it under [hooks]. + expect(merged.indexOf('model_provider = "cmux"')).toBeLessThan(merged.indexOf("[hooks]")); + expect(existsSync(path.join(home, ".codex/config.toml.cmux-tmp"))).toBe(false); + // Idempotent: a second login sees the provider and rewrites nothing. + expect(spawnSync("bash", ["-c", `. ${path.join(templateDir, "agent-config.sh")}`], { env }).status).toBe(0); + expect(readFileSync(path.join(home, ".codex/config.toml"), "utf8")).toBe(merged); + // A config that already names a provider is the user's, even without + // ours, however the key is spaced (TOML allows none around "="). + for (const theirs of ['model_provider = "openai"\n', 'model_provider="openai"\n', ' model_provider\t= "openai"\n', '[ model_providers . cmux ]\nname = "x"\n', ' [history]\npersistence = "none"\n']) { + writeFileSync(path.join(home, ".codex/config.toml"), theirs); + expect(spawnSync("bash", ["-c", `. ${path.join(templateDir, "agent-config.sh")}`], { env }).status).toBe(0); + expect(readFileSync(path.join(home, ".codex/config.toml"), "utf8")).toBe(theirs); + } + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); + test("agent config generator materializes the coderouter plane from boot env", () => { const home = mkdtempSync(path.join(tmpdir(), "cmux-devbox-agent-config-")); try { diff --git a/web/tests/vm-freestyle-provider.test.ts b/web/tests/vm-freestyle-provider.test.ts index cbc7af59f1c7..18e2d5978767 100644 --- a/web/tests/vm-freestyle-provider.test.ts +++ b/web/tests/vm-freestyle-provider.test.ts @@ -87,7 +87,7 @@ function providerWith(fake: { readonly client: Freestyle }): FreestyleProvider { url: "https://files.cmux.com/cmux-tui/abc/cmux-tui-linux-x64", sha256: "0".repeat(64), commit: "abc", - builtAt: null, + builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64), }), }); } @@ -267,7 +267,7 @@ describe("Freestyle platform contract", () => { }); test("pin check trusts the pin recorded at bake time, falling back to the live pin on older images", () => { - const source = { url: "https://files.cmux.com/x", sha256: "f".repeat(64), commit: "abc", builtAt: null }; + const source = { url: "https://files.cmux.com/x", sha256: "f".repeat(64), commit: "abc", builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64) }; const check = freestylePinCheckCommand(source); expect(check).toContain("if [ -s /etc/cmux/cmux-tui-pin ]; then"); expect(check).toContain("cut -d' ' -f1 /etc/cmux/cmux-tui-pin"); @@ -388,7 +388,7 @@ describe("Freestyle platform contract", () => { url: "https://files.cmux.com/cmux-tui/abc/cmux-tui-linux-x64", sha256: "0".repeat(64), commit: "abc", - builtAt: null, + builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64), }), }); @@ -601,7 +601,7 @@ describe("FreestyleProvider resume policy", () => { url: "https://files.cmux.com/cmux-tui/abc/cmux-tui-linux-x64", sha256: "0".repeat(64), commit: "abc", - builtAt: null, + builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64), }), }); @@ -707,7 +707,7 @@ describe("Freestyle machine sizing", () => { // its own, so a machine outside a private network gets no URL at all. describe("Freestyle openCmuxRemote: the trusted-listener heal", () => { const PRIVATE = { publicIpv6: "2602:f75c:0:1::2a", vpcs: [{ ipv4: "10.4.0.7", ipv6: "fd00:4::7" }] }; - const SOURCE_OK = { url: "https://files.cmux.com/cmux-tui/abc/cmux-tui-linux-x64", sha256: "0".repeat(64), commit: "abc", builtAt: null }; + const SOURCE_OK = { url: "https://files.cmux.com/cmux-tui/abc/cmux-tui-linux-x64", sha256: "0".repeat(64), commit: "abc", builtAt: null, hookUrl: "https://files.cmux.com/cmux-tui/test/cmux-tui-hook-x86_64-unknown-linux-musl", hookSha256: "1".repeat(64) }; /** The attach bundle's fenced stdout with the trusted-listener probe printing `trusted`. */ function bundleStdout(trusted: "0" | "1"): string { @@ -779,6 +779,110 @@ describe("Freestyle openCmuxRemote: the trusted-listener heal", () => { }); }); +describe("Freestyle openCmuxRemote: agent hooks on a healthy daemon", () => { + const PRIVATE = { publicIpv6: "2602:f75c:0:1::2a", vpcs: [{ ipv4: "10.4.0.7", ipv6: "fd00:4::7" }] }; + const PIN_COMMIT = "5a4780614cecd8e8ef040a24478f928ef31cc4ae"; + const SOURCE = { url: "https://files.cmux.com/cmux-tui/abc/cmux-tui-linux-x64", sha256: "0".repeat(64), commit: PIN_COMMIT, builtAt: null, hookUrl: `https://files.cmux.com/cmux-tui/${PIN_COMMIT}/cmux-tui-hook-x86_64-unknown-linux-musl`, hookSha256: "1".repeat(64) }; + + /** + * A machine whose daemon is healthy and trusted; `hooksReady` is what the + * hooks-ready probe exits, `pin` what /etc/cmux/cmux-tui-pin holds. Records + * every manifest URL the driver resolved. + */ + function hooksFake(input: { readonly hooksReady: number; readonly pin: string; readonly manifest?: "ok" | "missing-helper" }) { + const execs: string[] = []; + const manifests: (string | undefined)[] = []; + const vm = { + data: async () => PRIVATE, + exec: async ({ command }: { command: string }) => { + execs.push(command); + if (command.includes("__CMUX_PROBE__")) { + return { statusCode: 0, stdout: ["__CMUX_PROBE__", JSON.stringify({ build_identity: "abc", remote_protocol: 12, version: "0.1.0" }), "__CMUX_DEVICES__", "[]", "__CMUX_TRUSTED__", "1", "__CMUX_END__"].join("\n"), stderr: "" }; + } + if (command.includes("cmux-tui-pin")) return { statusCode: 0, stdout: `${input.pin}\n`, stderr: "" }; + if (command.includes(".local/share/cmux-tui/bin/cmux-tui-hook") && !command.includes("agent hook install")) { + return { statusCode: input.hooksReady, stdout: "", stderr: "" }; + } + return { statusCode: 0, stdout: "", stderr: "" }; + }, + }; + const client = { vms: { ref: () => vm } } as unknown as Freestyle; + const provider = new FreestyleProvider({ + client: () => client, + resolveDaemonSource: async (_provider, manifestUrl) => { + manifests.push(manifestUrl); + if (input.manifest === "missing-helper") throw new ProviderError("freestyle", "manifest has no cmux-tui-hook"); + return SOURCE; + }, + }); + return { provider, execs, manifests }; + } + + test("a healthy daemon with hooks already installed is left alone", async () => { + const fake = hooksFake({ hooksReady: 0, pin: PIN_COMMIT }); + await fake.provider.openCmuxRemote(VM_ID, { clientCapabilities: [] }); + expect(fake.execs.some((command) => command.includes("agent hook install"))).toBe(false); + expect(fake.manifests).toEqual([]); + }); + + test("a healthy daemon without hooks gets the helper of its own pinned commit and the Claude Code and Codex hooks, with no restart", async () => { + const fake = hooksFake({ hooksReady: 1, pin: PIN_COMMIT }); + await fake.provider.openCmuxRemote(VM_ID, { clientCapabilities: [] }); + // The bake's pin, not the rolling pointer: helper and daemon share a generation. + expect(fake.manifests).toEqual([`https://files.cmux.com/cmux-tui/${PIN_COMMIT}/manifest.json`]); + const install = fake.execs.find((command) => command.includes("agent hook install claude codex")); + expect(install).toBeDefined(); + expect(install).toContain(SOURCE.hookUrl); + expect(install).not.toContain(SOURCE.url); + expect(fake.execs.some((command) => command.includes("systemctl restart cmux-tui-daemon"))).toBe(false); + }); + + test("a machine created from the live pin (no pin file) takes the live manifest", async () => { + const fake = hooksFake({ hooksReady: 1, pin: "" }); + await fake.provider.openCmuxRemote(VM_ID, { clientCapabilities: [] }); + expect(fake.manifests).toEqual([undefined]); + expect(fake.execs.some((command) => command.includes("agent hook install claude codex"))).toBe(true); + }); + + test("a daemon that only needed a restart still gets its hooks, and a hook failure never fails the heal", async () => { + // The first attach bundle reports the daemon not ready (exit 3); the heal + // finds the pin intact, restarts, and must still reconcile hooks. The hook + // install itself fails here, and the attach still returns its route. + const execs: string[] = []; + let bundles = 0; + const vm = { + data: async () => PRIVATE, + fs: { writeTextFile: async () => {}, remove: async () => {} }, + exec: async ({ command }: { command: string }) => { + execs.push(command); + if (command.includes("__CMUX_PROBE__")) { + bundles += 1; + if (bundles === 1) return { statusCode: 3, stdout: "", stderr: "" }; + return { statusCode: 0, stdout: ["__CMUX_PROBE__", JSON.stringify({ build_identity: "abc", remote_protocol: 12, version: "0.1.0" }), "__CMUX_DEVICES__", "[]", "__CMUX_TRUSTED__", "1", "__CMUX_END__"].join("\n"), stderr: "" }; + } + if (command.includes("agent hook install")) return { statusCode: 1, stdout: "", stderr: "helper download failed" }; + if (command.includes("cmux-tui-pin")) return { statusCode: 0, stdout: `${PIN_COMMIT}\n`, stderr: "" }; + if (command.includes(".local/share/cmux-tui/bin/cmux-tui-hook")) return { statusCode: 1, stdout: "", stderr: "" }; + if (command.includes("pgrep -f 'cmux-tui server [s]tart'") && !command.includes("systemctl restart")) return { statusCode: 1, stdout: "", stderr: "" }; + return { statusCode: 0, stdout: "", stderr: "" }; + }, + }; + const client = { vms: { ref: () => vm } } as unknown as Freestyle; + const provider = new FreestyleProvider({ client: () => client, resolveDaemonSource: async () => SOURCE }); + const endpoint = await provider.openCmuxRemote(VM_ID, { clientCapabilities: [] }); + expect(endpoint.trustedCarrier).toBe(true); + expect(execs.some((command) => command.includes("systemctl restart cmux-tui-daemon"))).toBe(true); + expect(execs.some((command) => command.includes("agent hook install claude codex"))).toBe(true); + }); + + test("a pinned build published before the helper existed still attaches, without hooks", async () => { + const fake = hooksFake({ hooksReady: 1, pin: PIN_COMMIT, manifest: "missing-helper" }); + const endpoint = await fake.provider.openCmuxRemote(VM_ID, { clientCapabilities: [] }); + expect(endpoint.trustedCarrier).toBe(true); + expect(fake.execs.some((command) => command.includes("agent hook install"))).toBe(false); + }); +}); + describe("Freestyle port open: the private address, the desktop healed", () => { const PRIVATE = { publicIpv6: "2602:f75c:0:1::2a", vpcs: [{ ipv4: "10.4.0.7", ipv6: "fd00:4::7" }] }; diff --git a/web/tests/vm-guest-cli.test.ts b/web/tests/vm-guest-cli.test.ts index 3bf643f96cc4..bab3a3998240 100644 --- a/web/tests/vm-guest-cli.test.ts +++ b/web/tests/vm-guest-cli.test.ts @@ -325,15 +325,16 @@ esac }); }); - // `cmux notify` is what agent hooks run inside a machine. cmux-tui has no - // `notify` verb, so the shim must translate to `notification create` and tag - // the daemon-assigned terminal so the Mac can attribute the notification to - // the pane showing it. Nothing Mac-side (workspace/surface ids, sockets) may - // travel in the other direction. + // `cmux notify` is what agent hooks run inside a machine. cmux-tui's own + // `notify` verb carries the macOS signature (subtitle field, scoped --clear, + // --reply refused, selectors validated), so the shim forwards the arguments + // untouched on the machine's session. A translation here would fork the + // grammar: https://github.com/manaflow-ai/cmux/pull/12131 moved it into the + // daemon and the daemon's tests pin it. describe("notify", () => { - test("maps to notification create on the daemon session, tagged with this terminal", () => { + test("forwards every argument verbatim to the daemon's notify verb on the local session", () => { const run = runShim( - ["notify", "--title", "Build done", "--subtitle", "api", "--body", "3 tests passed", "--tab", "0", "--panel", "1", "--reply"], + ["notify", "--title", "Build done", "--subtitle", "api", "--body", "3 tests passed", "--surface", "current"], { CMUX_TUI_TERMINAL_ID: TERMINAL_ID }, ); expect(run.stderr).toBe(""); @@ -342,59 +343,42 @@ esac "--session", "cloud", "--quiet", - "notification", - "create", + "notify", "--title", "Build done", + "--subtitle", + "api", "--body", - "api — 3 tests passed", - "--terminal", - TERMINAL_ID, + "3 tests passed", + "--surface", + "current", ]); }); - test("omits --terminal outside a daemon PTY, drops levels the daemon rejects, defaults the title", () => { - const withoutTerminal = runShim(["notify", "--body", "hi", "--level", "success"], { CMUX_TUI_TERMINAL_ID: undefined }); - expect(withoutTerminal.status).toBe(0); - expect(withoutTerminal.argv).toEqual(["--session", "cloud", "--quiet", "notification", "create", "--title", "Notification", "--body", "hi"]); + test("does not fold, drop, or rewrite flags: --clear and --reply reach the daemon for it to decide", () => { + const clear = runShim(["notify", "--clear", "--workspace", "current"], { CMUX_TUI_TERMINAL_ID: TERMINAL_ID }); + expect(clear.status).toBe(0); + expect(clear.argv).toEqual(["--session", "cloud", "--quiet", "notify", "--clear", "--workspace", "current"]); + const reply = runShim(["notify", "--title=T", "--reply"], { CMUX_TUI_TERMINAL_ID: undefined }); + expect(reply.argv).toEqual(["--session", "cloud", "--quiet", "notify", "--title=T", "--reply"]); + }); - const withLevel = runShim(["notify", "--title=T", "--body=B", "--level=error", "--surface", "surface:3"], { - CMUX_TUI_TERMINAL_ID: TERMINAL_ID, - }); - expect(withLevel.status).toBe(0); - expect(withLevel.argv).toEqual([ - "--session", - "cloud", - "--quiet", - "notification", - "create", - "--title", - "T", - "--body", - "B", - "--level", - "error", - "--terminal", - TERMINAL_ID, - ]); + test("drops --quiet when the caller wants the JSON result, since the two output modes exclude each other", () => { + const json = runShim(["notify", "--title", "T", "--json"], { CMUX_TUI_TERMINAL_ID: TERMINAL_ID }); + expect(json.argv).toEqual(["--session", "cloud", "notify", "--title", "T", "--json"]); + const jsonl = runShim(["notify", "--jsonl", "--title=T"], {}); + expect(jsonl.argv).toEqual(["--session", "cloud", "notify", "--jsonl", "--title=T"]); }); - test("never forwards Mac socket or topology identity into the daemon", () => { - const run = runShim(["notify", "--title", "T", "--workspace", "workspace:1", "--surface", "surface:2", "--window", "window:1"], { + test("never adds Mac socket identity from the environment", () => { + const run = runShim(["notify", "--title", "T"], { CMUX_TUI_TERMINAL_ID: TERMINAL_ID, CMUX_SOCKET_PATH: "/tmp/should-not-leak.sock", CMUX_WORKSPACE_ID: "11111111-1111-1111-1111-111111111111", CMUX_SURFACE_ID: "22222222-2222-2222-2222-222222222222", }); expect(run.status).toBe(0); - const joined = run.argv.join(" "); - expect(joined).not.toContain("workspace:1"); - expect(joined).not.toContain("surface:2"); - expect(joined).not.toContain("window:1"); - expect(joined).not.toContain("1111"); - expect(joined).not.toContain("2222"); - expect(joined).not.toContain(".sock"); - expect(run.argv).toEqual(["--session", "cloud", "--quiet", "notification", "create", "--title", "T", "--body", "", "--terminal", TERMINAL_ID]); + expect(run.argv).toEqual(["--session", "cloud", "--quiet", "notify", "--title", "T"]); }); }); @@ -425,7 +409,7 @@ esac }); expect(result.stderr).toBe(""); expect(result.status).toBe(0); - expect(result.stdout).toContain("home-fake --session cloud --quiet notification create --title T --body --terminal " + TERMINAL_ID); + expect(result.stdout).toContain("home-fake --session cloud --quiet notify --title T"); }, 20_000); test("install command is a safe atomic base64 write", () => { diff --git a/web/tests/vm-guest-self-cli.test.ts b/web/tests/vm-guest-self-cli.test.ts deleted file mode 100644 index bf0ad0ecb5c9..000000000000 --- a/web/tests/vm-guest-self-cli.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { spawnSync } from "node:child_process"; -import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { createServer } from "node:net"; -import path from "node:path"; -import { GUEST_CMUX_SHIM } from "../services/vms/guestCli"; -import { - GUEST_CMUX_SELF_SHIM, - GUEST_CMUX_SELF_SHIM_PATH, - guestSelfCliInstallCommand, -} from "../services/vms/guestSelfCli"; - -// The guest `cmux` shim, run for real under /bin/sh against a fake `curl` on -// PATH that records the request and replays a canned /api/vm/self body. jq -// must be present, as it is in the devbox image. - -const SELF_BODY = { - schema: 1, - machine: { id: "fs-self", vmId: "v-self", name: "builder", displayName: "builder", slug: "sleepy-red-fox", status: "running", createdAt: "2026-09-01T00:00:00.000Z", self: true }, - team: { id: "team-1" }, - machines: [ - { id: "fs-sibling", vmId: "v-sib", name: "quiet-teal-otter", displayName: null, slug: "quiet-teal-otter", status: "paused", createdAt: "2026-09-02T00:00:00.000Z", self: false }, - { id: "fs-self", vmId: "v-self", name: "builder", displayName: "builder", slug: "sleepy-red-fox", status: "running", createdAt: "2026-09-01T00:00:00.000Z", self: true }, - ], -}; - -let root = ""; -let bin = ""; -let home = ""; -let requestLog = ""; - -const fakeCurl = (body: string, status: string, fail = false) => `#!/bin/sh -printf '%s\\n' "$*" >> "${requestLog}" -${fail ? `echo "curl: (6) Could not resolve host: coderouter.cmux.internal" >&2; printf '\\n000'; exit 6` : `printf '%s\\n%s' '${body.replace(/'/g, `'\\''`)}' '${status}'`} -`; - -const run = (args: string[], env: Record = {}) => - spawnSync("/bin/sh", [path.join(bin, "cmux"), ...args], { - encoding: "utf8", - env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, HOME: home, LANG: "en_US.UTF-8", LC_ALL: "", ...env }, - }); - -beforeAll(() => { - root = mkdtempSync(path.join(tmpdir(), "cmux-guest-self-")); - bin = path.join(root, "bin"); - home = path.join(root, "home"); - requestLog = path.join(root, "requests.log"); - mkdirSync(bin); - mkdirSync(path.join(home, ".config", "cmux"), { recursive: true }); - writeFileSync(path.join(bin, "cmux"), GUEST_CMUX_SELF_SHIM); - chmodSync(path.join(bin, "cmux"), 0o755); - writeFileSync( - path.join(home, ".config", "cmux", "model-plane.env"), - "export CMUX_CODEROUTER_URL='https://coderouter.cmux.internal'\nexport OPENAI_API_KEY='cmux-vm-edge-placeholder'\n", - ); -}); - -afterAll(() => { - rmSync(root, { recursive: true, force: true }); -}); - -const withCurl = (body: string, status: string, fail = false) => { - writeFileSync(path.join(bin, "curl"), fakeCurl(body, status, fail)); - chmodSync(path.join(bin, "curl"), 0o755); - writeFileSync(requestLog, ""); -}; - -describe("guest cmux self-discovery shim", () => { - test("is valid POSIX sh and installs atomically to /usr/local/bin/cmux", () => { - const syntax = spawnSync("/bin/sh", ["-n", path.join(bin, "cmux")], { encoding: "utf8" }); - expect(syntax.status).toBe(0); - expect(GUEST_CMUX_SELF_SHIM.startsWith("#!/bin/sh\n")).toBe(true); - const install = guestSelfCliInstallCommand(); - expect(install).toContain(`mv -f ${GUEST_CMUX_SELF_SHIM_PATH}.tmp.$$ ${GUEST_CMUX_SELF_SHIM_PATH}`); - expect(install).toContain("base64 -d"); - expect(install).not.toContain("crt_"); - }); - - test("cmux self prints this machine and calls the edge with the placeholder bearer", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - const result = run(["self"]); - expect(result.status).toBe(0); - expect(result.stdout).toBe("builder\tfs-self\trunning\t(this machine)\nteam\tteam-1\t2 machines\n"); - const request = spawnSync("cat", [requestLog], { encoding: "utf8" }).stdout; - expect(request).toContain("https://coderouter.cmux.internal/api/vm/self"); - expect(request).toContain("authorization: Bearer cmux-vm-edge-placeholder"); - expect(request).not.toContain("crt_"); - }); - - test("cmux self --json passes the body through unchanged", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - const result = run(["self", "--json"]); - expect(result.status).toBe(0); - expect(JSON.parse(result.stdout)).toEqual(SELF_BODY); - }); - - test("cmux vm ls lists the team's machines with this one marked", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - const result = run(["vm", "ls"]); - expect(result.status).toBe(0); - expect(result.stdout).toBe(" quiet-teal-otter\tfs-sibling\tpaused\n* builder\tfs-self\trunning\n"); - const json = run(["vm", "ls", "--json"]); - expect(JSON.parse(json.stdout)).toEqual({ machines: SELF_BODY.machines }); - }); - - test("the full guest CLI retains self discovery without requiring a daemon", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - const fullCLI = path.join(bin, "cmux-full"); - writeFileSync(fullCLI, GUEST_CMUX_SHIM); - for (const args of [["self", "--json"], ["vm", "ls", "--json"], ["vm", "list", "--json"]]) { - const result = spawnSync("/bin/sh", [fullCLI, ...args], { - encoding: "utf8", - env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, HOME: home, CMUX_TUI_BIN: path.join(root, "absent-daemon") }, - }); - expect(result.status).toBe(0); - expect(JSON.parse(result.stdout)).toEqual(args[0] === "self" ? SELF_BODY : { machines: SELF_BODY.machines }); - } - }); - - test("an unreachable edge exits 1 with the hostname and curl's reason", () => { - withCurl("", "000", true); - const result = run(["self"]); - expect(result.status).toBe(1); - expect(result.stdout).toBe(""); - // Real curl prints its error line and then the --write-out status; the - // reason shown is the error line, never the trailing "000". - expect(result.stderr).toBe("cmux: cannot reach the cmux API from this machine (" + result.stderr.split("(")[1]?.split(")")[0] + "): curl: (6) Could not resolve host: coderouter.cmux.internal\n"); - expect(result.stderr).not.toContain("000"); - }); - - test("a rejected machine exits 1 with the API's message", () => { - withCurl(JSON.stringify({ error: "unauthorized", message: "This machine's cmux credential expired or was revoked." }), "401"); - const result = run(["vm", "ls"]); - expect(result.status).toBe(1); - expect(result.stderr).toBe("cmux: the cmux API rejected this machine (HTTP 401): This machine's cmux credential expired or was revoked.\n"); - }); - - test("host-only verbs are refused with a pointer to the Mac CLI", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - for (const args of [["vm", "new"], ["vm", "exec", "fs-self", "--", "ls"]]) { - const result = run(args); - expect(result.status).toBe(2); - expect(result.stderr).toContain("runs on the Mac cmux CLI"); - } - // notify belongs to the machine's daemon; without one installed the shim - // says so instead of pointing at the Mac. - const notify = run(["notify", "--title", "x"]); - expect(notify.status).toBe(2); - expect(notify.stderr).toContain("needs the cmux-tui daemon"); - }); - - test("notify delegates to the daemon CLI with the machine's session socket", async () => { - const fakeTui = path.join(root, "fake-cmux-tui"); - writeFileSync(fakeTui, "#!/bin/sh\nprintf '%s\\n' \"$@\"\n", { mode: 0o755 }); - const runDir = path.join(root, "run"); - mkdirSync(runDir, { recursive: true }); - const sock = path.join(runDir, "cloud.sock"); - const server = createServer().listen(sock); - await new Promise((resolve) => server.once("listening", resolve)); - try { - // Outside a daemon terminal the shim points the CLI at the daemon's session socket. - const outside = run(["notify", "--title", "x", "--subtitle", "y"], { CMUX_TUI_BIN: fakeTui, CMUX_TUI_RUN_DIR: runDir }); - expect(outside.status).toBe(0); - expect(outside.stdout.split("\n").filter(Boolean)).toEqual(["--socket", sock, "notify", "--title", "x", "--subtitle", "y"]); - // Inside a daemon terminal the injected socket already applies. - const inside = run(["notification", "list"], { CMUX_TUI_BIN: fakeTui, CMUX_TUI_RUN_DIR: runDir, CMUX_TUI_SOCKET: "/tmp/injected.sock" }); - expect(inside.stdout.split("\n").filter(Boolean)).toEqual(["notification", "list"]); - } finally { - server.close(); - } - expect(spawnSync("cat", [requestLog], { encoding: "utf8" }).stdout).toBe(""); - const help = run(["--help"]); - expect(help.status).toBe(0); - expect(help.stdout).toContain("cmux self [--json]"); - expect(run(["self", "--bogus"]).status).toBe(2); - }); - - test("speaks Japanese when the locale asks for it", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - const result = run(["self"], { LANG: "ja_JP.UTF-8" }); - expect(result.stdout).toContain("(このマシン)"); - expect(run(["--help"], { LC_ALL: "ja_JP.UTF-8" }).stdout).toContain("cmux Cloud マシン内"); - }); - - test("missing edge env exits 2 without any request", () => { - withCurl(JSON.stringify(SELF_BODY), "200"); - const result = run(["self"], { HOME: root }); - expect(result.status).toBe(2); - expect(result.stderr).toContain("CMUX_CODEROUTER_URL"); - expect(spawnSync("cat", [requestLog], { encoding: "utf8" }).stdout).toBe(""); - }); -});