Skip to content

Iroh: full-stack coverage, a client that builds without Zig, and an iOS app - #9237

Closed
lawrencecchen wants to merge 13 commits into
feat-cmux-tui-remote-daemonfrom
feat-tui-iroh-ios
Closed

lawrencecchen wants to merge 13 commits into
feat-cmux-tui-remote-daemonfrom
feat-tui-iroh-ios

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8667. Targets feat-cmux-tui-remote-daemon, not main.

The goal was to get the Iroh transport onto a phone. Three things were in the way, and each is a commit.

Iroh had no end-to-end test

direct_ws_e2e, direct_wss_e2e, relay_e2e, and pty_reconnect_e2e each drive the real daemon through invitation enrollment, Noise, and the service layer. Iroh had none. Its inline tests cover carrier mechanics well, but every one of them binds with RelayMode::Disabled on loopback and stops at raw frames, so the two things a phone depends on were never exercised: a relayed path, and a PTY driven across one.

crates/cmux-remote/tests/iroh_e2e.rs adds both, against a relay server spawned in-process over plaintext HTTP so the tests need no network and no certificate handling.

test iroh_direct_path_drives_a_real_daemon_pty ... ok
test iroh_relay_path_drives_a_real_daemon_pty ... ok

A client could not build without a Zig toolchain

cmux-remote depended on ghostty-vt unconditionally, but the only code that touches it is the workspace service's server-side terminal model. On iOS that is not merely dead weight: libghostty-vt is a Zig build, so a Rust toolchain and the iOS SDK were not enough to build a client.

The new default-on daemon-services feature carries the workspace service, the handlers built on it, and the local bridge. MessageStream and ServicesError move to a new message module, since both endpoints need them and only their old home was daemon-side. Without the feature the carrier, the authenticated session, and WorkspaceClient are intact and no Zig is involved.

ghostty-vt-sys also gains CMUX_GHOSTTY_VT_LIB_DIR, which links a prebuilt archive instead of invoking zig, mirroring how the Swift side consumes a prebuilt GhosttyKit. Worth knowing: zig 0.15.2 cannot link a host executable on macOS 26, and zig build builds its own runner for the host before anything else, so a Mac on 26 cannot run it even to cross-compile.

The phone should not reimplement the protocol

Enrollment is a PSK-authenticated Noise handshake, sessions are mutually authenticated and resumable, frames carry per-lane sequence numbers with bounded replay, and Iroh adds path selection and relay fallback underneath. A Swift reimplementation would be a second set of bugs in the parts hardest to test.

crates/cmux-remote-mobile is a C ABI over the same Rust client the TUI uses: connect, attach a shell, move bytes, resize, and read a snapshot. apps/ios is a SwiftUI app on top of it. It carries no VT parser either, because the daemon answers SnapshotProcessTerminal with styled runs, so the app lays out a monospaced grid and nothing more.

Its status bar names the selected path and counts session resumes, and the path picker forces direct-only or relay-only, so a failure can be attributed to one path instead of to "the network".

Verification

  • Both new e2e tests pass, and the default build is unchanged.
  • cmux-remote compiles with --no-default-features --features iroh-transport with zig removed from PATH entirely.
  • cmux-remote-mobile builds for aarch64-apple-ios, producing an arm64 archive that exports all ten entry points.
  • Every iOS source type-checks under Swift 6 for arm64-apple-ios.

Rust verification ran in a Linux container, because this Mac is on macOS 26 where zig cannot link at all. That is the same reason the iOS app bundle is unbuilt here: xcodebuild is blocked locally by the repository guard, so the app has been compiled but not run on a device. That is the one gap.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Brings the Iroh transport to iOS with a Zig‑free client build, full‑stack tests, a new cmux-remote-mobile C ABI, and a SwiftUI app. Also defers provider selection until connectable, normalizes workspace mode constants, makes initial route timeouts owner‑driven, adds cancellable SSH bootstrap tests, updates Cloudflare relay tooling to wrangler 4.120.0, and stabilizes the TUI smoke test by waiting for published browser tab state.

  • New Features

    • cmux-remote-mobile: a static C ABI over the Rust client (connect, attach shell, IO, resize, snapshots). Builds for aarch64-apple-ios and simulator.
    • iOS app in apps/ios: uses the C ABI, shows selected path and resume count, lets you force auto/direct‑only/relay‑only, and renders the terminal from styled runs.
    • Full‑stack Iroh E2E tests (crates/cmux-remote/tests/iroh_e2e.rs) covering direct and relay paths with a real PTY via an in‑process relay.
    • EnrollmentInvitation::daemon_public_key_bytes to support pinning the daemon key at connect.
    • Cancellable SSH bootstrap (SshBootstrapCancellation) with a test that shuts it down at its child owner.
    • Memory‑instrumented transport workload injected into interactive_latency_e2e to track RAM under load.
  • Migration

    • New daemon-services feature gates host‑only services and Zig. Client‑only builds: cargo build -p cmux-remote --no-default-features --features iroh-transport.
    • To link a prebuilt libghostty-vt.a instead of invoking Zig, set CMUX_GHOSTTY_VT_LIB_DIR in ghostty-vt-sys’s build environment.
    • Fix: ghostty-vt-sys build now accepts borrowed paths in its Zig build configuration.

Written for commit d3b8c2c. Summary will update on new commits.

Review in cubic


Note

Medium Risk
Touches authenticated remote session/Iroh transport and splits cmux-remote features; default daemon builds stay the same, but client-only and iOS linking paths need CI coverage. iOS app is largely new surface (network, enrollment, terminal I/O) and was not run on device in the author’s environment.

Overview
Adds an iOS cmux remote app that enrolls via cmux://enroll/…, opens a shell over Iroh, and renders the terminal from daemon styled-run snapshots (no on-device VT parser). Swift talks to cmux-remote-mobile, a static Rust library with a small C ABI over the same client stack as the TUI.

cmux-remote now gates host-only workspace/VT work behind daemon-services (default on) so iroh-transport alone can build a mobile client without pulling ghostty-vt / Zig. Shared MessageStream / ServicesError move to message. ghostty-vt-sys can link a prebuilt archive via CMUX_GHOSTTY_VT_LIB_DIR. EnrollmentInvitation::daemon_public_key_bytes supports pinning at connect.

New iroh_e2e tests drive a real daemon PTY over Iroh on direct and relay paths (in-process plaintext relay), matching coverage other transports already had.

Reviewed by Cursor Bugbot for commit a7e63ee. Bugbot is set up for automated code reviews on this repo. Configure here.

Every other provider has an end-to-end test that drives the real daemon
through invitation enrollment, Noise, and the service layer. Iroh had
none: its inline tests cover carrier mechanics but all bind with
RelayMode::Disabled on loopback and stop at raw frames, so a relayed
path and a PTY driven across one were never exercised.

Both tests run against a relay server spawned in-process over plaintext
HTTP, so they need no network and no certificate handling.
cmux-remote pulled in libghostty-vt unconditionally, but the only code
that uses it is the workspace service's server-side terminal model. A
client dialing a daemon needs none of that, and on iOS the dependency is
not merely dead weight: it makes a Rust toolchain and the iOS SDK
insufficient to build a client, because libghostty-vt is a Zig build.

The new default-on daemon-services feature carries the workspace
service, the handlers built on it, and the local bridge. MessageStream
and ServicesError move to a new message module, since both endpoints
need them and only their old home was daemon-side.

Building without the feature leaves the carrier, the authenticated
session, and WorkspaceClient intact, and needs no Zig at all.
The daemon protocol is not something a phone should reimplement:
enrollment is a PSK-authenticated Noise handshake, sessions are mutually
authenticated and resumable, frames carry per-lane sequence numbers with
bounded replay, and Iroh adds path selection and relay fallback under all
of it. A second implementation in Swift would be a second set of bugs in
the parts hardest to test. The phone links the same Rust client the TUI
uses, and this crate is only the boundary: connect, attach a shell, move
bytes, resize, and read a connection snapshot.

Verified by building the crate for aarch64-apple-ios, which produces an
arm64 archive exporting all nine entry points.

ghostty-vt-sys also gains CMUX_GHOSTTY_VT_LIB_DIR, which links a prebuilt
archive instead of invoking zig. zig 0.15.2 cannot link a host executable
on macOS 26, and `zig build` builds its own runner for the host before
anything else, so a Mac on 26 cannot run it even to cross-compile.
The transport's interesting failures happen on the network a phone has:
NAT'd, moving between cellular and Wi-Fi, often unable to reach the
daemon directly at all. That is what relay fallback and session resume
are for, and none of it occurs on a loopback test.

The app implements no part of the daemon protocol. It links
cmux-remote-mobile, so enrollment, Noise, reliability, and path
selection are the same code the TUI runs. It carries no VT parser
either: the daemon answers SnapshotProcessTerminal with styled runs, so
the phone lays out a monospaced grid and nothing more.

The status bar names the selected path and counts session resumes, and
the path picker forces direct-only or relay-only, so a failure can be
attributed to one path rather than to "the network".

Verified by type-checking every source under Swift 6 for
arm64-apple-ios. A full Xcode build is blocked locally by the
repository's xcodebuild guard, so the app bundle itself is unbuilt here.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 8771b119-d42a-4dd2-973c-21bc16c7dc85

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​wrangler@​4.113.0 ⏵ 4.120.09810092 +196 +1100
Addedcargo/​toml@​1.1.3%2Bspec-1.1.010010093100100

View full report

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 5 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 41058ee. Configure here.

Comment thread cmux-tui/crates/cmux-remote-mobile/src/lib.rs
Comment thread cmux-tui/crates/cmux-remote-mobile/src/lib.rs
LIBRARY_SEARCH_PATHS: >-
$(SRCROOT)/../../target/$(CMUX_RUST_TARGET)/$(CMUX_RUST_PROFILE)
OTHER_LDFLAGS: -lcmux_remote_mobile
CMUX_RUST_PROFILE: debug

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release links wrong Rust archive

High Severity

CMUX_RUST_PROFILE is hardcoded to debug while build-rust.sh passes --release for non-Debug Xcode configurations. Release builds search target/.../debug for libcmux_remote_mobile.a after cargo wrote the archive under release, so linking fails or picks a stale debug library.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41058ee. Configure here.

}
connection = await client.connection()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idle pump still polls RPCs

Medium Severity

The pump comment claims an idle shell costs nothing, but after every readOutput timeout it still awaits terminal() and connection(), each of which crosses into Rust and hits the daemon. On a phone that becomes a SnapshotProcessTerminal plus diagnostics RPC about every 250ms while the shell is idle.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41058ee. Configure here.

text.foregroundColor((run.fg ?? snapshot.defaultFg).swiftUI),
at: CGPoint(x: x, y: y),
anchor: .topLeading)
column += run.text.count

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wide cells use character count

Medium Severity

Column advancement uses run.text.count instead of the daemon-provided width_hint on each styled run. Wide glyphs and combining sequences then misalign the grid relative to the server-side terminal model, so cursor and background fills drift for non-narrow text.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41058ee. Configure here.

lawrencecchen and others added 7 commits August 11, 2026 04:52
* fix(tui): normalize workspace mode constants

* fix(tui): specify normalized mode type

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
* Test SSH bootstrap cancellation at child owner

* Fix Future bounds under pinned Clippy
* test: cover deferred provider selection readiness

* fix: retain provider selection until ready

* test: hold ready provider through observation

* test: cover explicit provider switch override

* fix: cancel deferred selection after switch

* test: prove provider reconnect is one-shot

* test: retain unready provider selection intent

* fix: commit provider selection cancellation
* test: await published browser tab state

* test: use the close-tab owner action
@lawrence703

Copy link
Copy Markdown
Collaborator

Closing this stale stacked implementation. Head 4f327a13b9aba398a6fc9db54ad4fb2662439590 targets the obsolete feat-cmux-tui-remote-daemon branch and its cmux-remote-mobile/apps/ios path is not in current main.
The transport direction is now the merged PR 10889, with current mobile pairing work in PR 11431 and relay work in PR 10963 / PR 11071.
The direct/relay E2E and Zig-free client goals need a new current-main PR if still required. No unique safe delta is retained here.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Fleet instruction update for head d3b8c2c942f4f5c7dfb177079ad33708680d7b20: this PR is classified other. No macOS build tag is claimed. The current controller app recipe does not establish iOS/test readiness; that requires the appropriate validated recipe. Use cmux-ci for supported jobs, retain the returned ID and receipt, and wait on the same ID after any timeout. Do not use retired maclease allocation or post credentials. Exact-head tags will be posted only after the applicable build succeeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants