Repository navigation
feat(runtime): bundle tmux with packaged Agent Code - #125
Conversation
Packaged Agent Code now ships its own tmux 3.6a so terminal-pane persistence works on user machines without any Homebrew install. Same shape as #119 (mitmdump bundling): manifest-pinned, hash-verified, fetched on demand, never committed. Key design decision — use upstream's prebuilt binary, NOT a custom static build. The original plan was to build statically from source (tmux + libevent + ncurses + utf8proc) per the review-agent's risk flag about terminfo/ncurses gotchas. While researching, found `tmux/tmux-builds` — the official upstream prebuilt-binary repo maintained by the tmux project itself. Their macOS binaries link ONLY to /usr/lib/libSystem.B.dylib and /usr/lib/libresolv.9.dylib (guaranteed present on every macOS install), and pass the full lifecycle proof (`tmux -V`, new-session, send-keys, capture-pane with `tput cols`, kill-session) cleanly without external dependencies. Owning a custom build for what already exists upstream is unnecessary maintenance; we adopt the upstream artifact as-is. What changes: - `third_party/tmux/manifest.json` — pins tmux 3.6a with per-arch archive sha256 + bytes from `tmux/tmux-builds` release v3.6a. Schema is identical to mitmproxy's manifest; reviewers see the same shape. - `scripts/runtime-tools/fetch-tmux.mjs` — downloads pinned archive, verifies sha256, extracts the inner `tmux` binary to `cache/<platform>/tmux`, sets executable bit, discards the archive. Idempotent — re-runs hit the version cache. - `scripts/runtime-tools/verify-tmux.mjs` — rewritten: verifies binary is executable, `tmux -V` reports the pinned version, and `otool -L` shows NO Homebrew dylib linkage. Strict mode is the CI gate; default mode warns. - `scripts/runtime-tools/build-tmux.mjs` — DELETED. No custom build needed. - `src/main/setup/runtimeTools.ts` — implements `resolveBundledTool ('tmux')` and extends `isBundledArchiveAvailable` for tmux. Because tmux is a single self-contained binary (no .app, no symlinks), the resolver returns the asar-unpacked path directly — no extraction, no userData copy, no marker. Defensive chmod 0o755 on each call to rule out electron-builder signing dropping the executable bit. - `src/main/tmux/TmuxRegistry.ts` — adds `getBinary()` accessor so SessionManager can forward the registry's resolved binary into TerminalSession; using the SAME binary for `new-session` and `attach` is critical when the bundled and system tmuxes diverge. - `src/main/index.ts` — `new TmuxRegistry({ tmuxBinary: await resolveBundledTool('tmux') })`. Bundled-only; if the binary is missing (dev without `runtime:prepare:mac`), tmux availability resolves to false and terminals fall back to direct PTY mode — same behaviour as a machine without tmux. No silent PATH fallback. - `src/main/sessionManager.ts` — threads `tmuxRegistry.getBinary()` into `TerminalSession`. Setup-gate cleanup (tmux is no longer user-installable): - `src/shared/types/setup.ts` — removes 'tmux' from `SetupToolId` and from `SetupInstallTarget`. - `src/main/setup/prerequisites.ts` — drops tmux from `TOOL_META` and `CHECK_ORDER`. The runtime resolver is the sole source of truth for tmux availability now. - `src/main/setup/binaryResolver.ts` — drops the tmux command entry. - `src/main/setup/homebrewInstaller.ts` — drops the tmux Homebrew install line. - `src/main/ipc/setup.ts` — narrows the `setup:install` target check to `mitmproxy` only. - `src/renderer/src/features/setup/ui/SetupGate.tsx` — drops the tmux entry from `OPTIONAL_INSTALL_TARGET`. Follow-up tracked: mitmproxy will get the same setup-gate cleanup treatment as part of the runtime-tools-cleanup PR. tmux landed first because #120 demanded it; mitmproxy was already bundled in #119, so the cleanup is a pure remove-PATH-fallback PR. Verified (run in worktree): - `npm run runtime:fetch:tmux -- --all` — clean, both arches. - `npm run runtime:verify:tmux -- --strict` — darwin-arm64 reports "3.6a, no Homebrew linkage"; darwin-x86_64 presence-only OK. - `npm run build:app` — clean; `out/main/runtime/tmux/<arch>/tmux` ends up at a real filesystem path with the executable bit set. - Lifecycle proof against the upstream binary (the original review ask): `tmux -L probe new-session -d -s t /bin/zsh`, `send-keys`, `capture-pane` returning `term=tmux-256color cols=80`, `kill-session` — all green. Terminfo handling works without any embedded TERMINFO_DIRS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l path Three fixes from the review on #125, all must-fix: 1. fetch-tmux.mjs spawned the extracted binary for both cache-hit detection and final validation, even on cross-arch fetches under `--all`. On a clean Apple-Silicon host without Rosetta, running an x86_64 Mach-O fails with "Bad CPU type in executable" — the release builder would crash trying to validate the x86_64 artifact it just downloaded. Cache-hit detection by `tmux -V` was also too loose: a corrupted-but-still-runnable binary that prints the right version string would pass. The fix: pin `binarySha256` per platform in `third_party/tmux/manifest.json` and use it for ALL identity checks. Cache hits, post-extract validation, and cross-arch `verify-tmux --strict` are now hash-based and platform-agnostic. The native arch additionally runs `tmux -V` + `otool -L` as a smoke check, but the load-bearing identity is the hash. A swapped or corrupted cross-arch binary that previously passed presence-only verification will now fail strict. 2. verify-tmux.mjs's cross-arch path was presence-only ("the file exists"), letting a corrupted x86_64 binary on an arm64 builder slip past strict mode. Same fix as #1: hash the cached binary against `binarySha256`. Strict mode now actually verifies content identity for every arch. 3. Bundled-only policy was leaky: src/main/index.ts passed the sentinel string `'tmux-bundled-missing'` to TmuxRegistry, which then spawned that bare command name. `child_process.spawn` PATH- searches bare names, so in the (extremely unlikely) case of a name collision the system would invoke whatever happened to be on PATH — exactly the silent fallback the bundled-only stance is meant to prevent. The fix: TmuxRegistry's `tmuxBinary` is now genuinely optional. When undefined, `detectAvailability()` short-circuits to false WITHOUT spawning anything. `getBinary()` returns `string | null`. src/main/index.ts passes `undefined` when bundled resolution returns null. No sentinel, no PATH search, no fallback. Also: softened the `chmod 0o755` comment in resolveTmux per the review — it no longer claims electron-builder definitively drops the exec bit, just notes that the defensive chmod is a cheap safety net for a class of "works in dev, broken in DMG" bugs that's much easier to keep than to re-add later. Verified (run in worktree): - `npm run runtime:fetch:tmux -- --all` — both platforms cache-hit on rerun via the new binarySha256 path (no binary spawn for either arch). - `npm run runtime:verify:tmux -- --strict` — darwin-arm64: `OK (3.6a, hash + linkage clean)` darwin-x86_64: `OK (hash matches; cross-arch)` ← was presence-only - `npm run build:app` — clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed 1. Cross-arch binary execution → hash-based identity ( Pinned 2. Loose cross-arch verification → real cross-arch verification ( Same fix as #1. Strict mode's 3. Sentinel path → genuine optional ( Replaced the Also softened the Verification on this commit
Pre-merge test plan items (clean-packaged-app, dev-without-prepare, SetupGate visual) still owed per your review. |
Closes #120. Companion to #119 (mitmdump bundling). With this merged, both core helpers ship inside packaged Agent Code — no Homebrew install required for proxy streaming or terminal-pane persistence.
Key design decision
The original plan for #120 was to build static tmux from source (tmux + libevent + ncurses + utf8proc), which the review-agent on #121 flagged as the higher-risk of the two bundling efforts because of ncurses terminfo and utf8proc static-linking gotchas.
Research turned up
tmux/tmux-builds— the official upstream prebuilt-binary repo maintained by the tmux project itself. macOS binaries:Owning a custom static build for what already exists upstream is unnecessary maintenance, so this PR adopts the upstream artifact as-is. The static-build path stays documented in case upstream ever stops publishing.
Repo plumbing
Bundled artifact (mirrors mitmproxy's shape from #119)
Runtime resolver
Wiring
Setup-gate cleanup
Verification
Size impact
DMG grows by ~1.3 MiB total (`tmux-3.6a-macos-arm64.tar.gz` 624 KB + `tmux-3.6a-macos-x86_64.tar.gz` 650 KB). No userData footprint — we spawn from the asar-unpacked path directly.
Follow-up
A separate cleanup PR will give mitmproxy the same treatment (drop the setup-gate visibility + PATH fallback now that the bundled artifact is canonical, per the discussion that motivated this PR's setup-gate cleanup). tmux landed first because #120 demanded it; mitmproxy was already bundled in #119, so the cleanup is a pure "remove redundant fallback" change.
Test plan
🤖 Generated with Claude Code