# Safe-Change Guide: SDK, Ratatui Integration & Failure Modes

> The public SDK boundary (`rmux-sdk`) is intentionally isolated: it must never import `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies. It re-exports identity newtypes from `rmux-proto` so callers never import internal crates. `ratatui-rmux` goes one step further — three building blocks only: `PaneDriver` (async I/O and state mutation, reaches RMUX only through `rmux-sdk`), `PaneState` (deterministic sync projection, no I/O), `PaneWidget` (sync ratatui widget, no clock/time deps). `rmux-render-core` is wasm32-compatible for the same reason. Key failure modes to anticipate: (1) daemon not running — `auto_start` re-execs but the socket write race can fail if the binary path is wrong (override via `RMUX_INTERNAL_BINARY_PATH`); (2) stale Unix socket — `rmux-server` detects and removes it on bind; (3) nested session confusion — `$RMUX` env var guards against re-attaching inside an existing session; (4) Windows ConPTY path — requires at least Windows 10 1903; older builds fall back to an error, not a silent hang.

- Repository: Helvesec/rmux
- GitHub: https://github.com/Helvesec/rmux
- Human wiki: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181
- Complete Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/llms-full.txt

## Source Files

- `crates/rmux-sdk/src/lib.rs`
- `crates/ratatui-rmux/src/lib.rs`
- `crates/rmux-render-core/src/lib.rs`
- `crates/rmux-pty/src/backend/windows/mod.rs`
- `crates/rmux-client/src/auto_start.rs`
- `crates/rmux-server/src/daemon.rs`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [crates/rmux-sdk/src/lib.rs](crates/rmux-sdk/src/lib.rs)
- [crates/rmux-sdk/Cargo.toml](crates/rmux-sdk/Cargo.toml)
- [crates/ratatui-rmux/src/lib.rs](crates/ratatui-rmux/src/lib.rs)
- [crates/ratatui-rmux/Cargo.toml](crates/ratatui-rmux/Cargo.toml)
- [crates/ratatui-rmux/tests/budget.rs](crates/ratatui-rmux/tests/budget.rs)
- [crates/rmux-render-core/src/lib.rs](crates/rmux-render-core/src/lib.rs)
- [crates/rmux-render-core/Cargo.toml](crates/rmux-render-core/Cargo.toml)
- [crates/rmux-client/src/auto_start.rs](crates/rmux-client/src/auto_start.rs)
- [crates/rmux-client/src/nested.rs](crates/rmux-client/src/nested.rs)
- [crates/rmux-server/src/daemon.rs](crates/rmux-server/src/daemon.rs)
- [crates/rmux-server/src/unix_socket.rs](crates/rmux-server/src/unix_socket.rs)
- [crates/rmux-pty/src/backend/windows/mod.rs](crates/rmux-pty/src/backend/windows/mod.rs)
- [crates/rmux-pty/src/backend/windows/pty.rs](crates/rmux-pty/src/backend/windows/pty.rs)
- [crates/rmux-pty/src/backend/windows/flags.rs](crates/rmux-pty/src/backend/windows/flags.rs)
- [crates/rmux-pty/src/backend/windows/spawn.rs](crates/rmux-pty/src/backend/windows/spawn.rs)
- [crates/rmux-pty/src/backend/windows/version.rs](crates/rmux-pty/src/backend/windows/version.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [src/cli/top_level.rs](src/cli/top_level.rs)
- [src/cli/client_commands.rs](src/cli/client_commands.rs)
</details>

# Safe-Change Guide: SDK, Ratatui Integration & Failure Modes

This page explains the structural rules that keep the public integration surface of RMUX stable, the three-building-block design of `ratatui-rmux`, and the four most important operational failure modes. Understanding these invariants lets contributors predict which changes are safe without breaking external callers, and helps operators diagnose the most common runtime problems before reaching for logs.

The dependency rules are not just conventions — they are enforced at the Cargo.toml level and, for `ratatui-rmux`, by a dedicated test suite that fails the build if forbidden crates appear or source files exceed a budgeted line count.

---

## The Public SDK Boundary (`rmux-sdk`)

### Dependency isolation rule

`rmux-sdk` is the sole integration crate that external consumers import. Its `Cargo.toml` lists only `rmux-ipc`, `rmux-os`, and `rmux-proto` as direct RMUX siblings — never `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty`.

> `rmux-sdk` is a public integration peer of `rmux-client` and must not depend on `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` as normal dependencies.
>
> Sources: [crates/rmux-sdk/src/lib.rs:12-17]()

This means a caller that only imports `rmux_sdk` cannot transitively pull in daemon or PTY internals. The SDK also carries `#![forbid(unsafe_code)]`, so every unsafe surface lives lower in the stack.

```toml
# crates/rmux-sdk/Cargo.toml — only internal deps allowed
rmux-ipc   = { path = "../rmux-ipc",   version = "0.2.0" }
rmux-os    = { path = "../rmux-os",    version = "0.2.0" }
rmux-proto = { path = "../rmux-proto", version = "0.2.0" }
# rmux-client, rmux-core, rmux-server, rmux-pty — ABSENT by design
```

Sources: [crates/rmux-sdk/Cargo.toml:17-19]()

### Identity newtype re-exports

`rmux-proto` owns the authoritative identity newtypes (`SessionName`, `SessionId`, `WindowId`, `PaneId`). `rmux-sdk` re-exports them so callers never need to name `rmux-proto` directly:

```rust
// crates/rmux-sdk/src/lib.rs
pub use types::{
    PaneId, PaneRef, RmuxEndpoint, SessionId, SessionName,
    TargetRef, TerminalSizeSpec, WindowId, WindowRef,
};
```

Sources: [crates/rmux-sdk/src/lib.rs:132-135]()

If you add a new newtype to `rmux-proto` that external callers need, it **must** be re-exported through `rmux-sdk`; callers should never have to add `rmux-proto` to their own `[dependencies]`.

### What a safe change looks like

| Change | Safe? | Reason |
|--------|-------|--------|
| Add a new `pub use` re-export in `lib.rs` | Yes | Additive; no existing callers break |
| Add `rmux-client` to `rmux-sdk` Cargo.toml | No | Violates the public-boundary invariant; leaks internal surface |
| Add a new module under `rmux-sdk/src/` | Yes, if pure | Must not reach down into `rmux-client` or `rmux-core` |
| Remove a re-exported type | No | Semver-breaking for any SDK consumer |
| Change a type in `rmux-proto` without updating re-exports | No | The re-export becomes a dangling symbol |

---

## `ratatui-rmux`: Three Building Blocks

`ratatui-rmux` contains exactly three public types, each with a clearly bounded responsibility:

```text
┌──────────────────────────────────────────────────────┐
│  ratatui-rmux  (only dep: rmux-sdk + ratatui-core)   │
│                                                      │
│  PaneDriver  ── async, owns event I/O, reaches RMUX  │
│      │               via rmux-sdk only               │
│      │ folds events                                  │
│  PaneState   ── sync, pure data projection, no I/O   │
│      │                                               │
│  PaneWidget  ── sync ratatui widget, no clock/time   │
└──────────────────────────────────────────────────────┘
```

Sources: [crates/ratatui-rmux/src/lib.rs:9-25]()

### `PaneDriver` — async I/O owner

`PaneDriver` is the only place in `ratatui-rmux` that reaches the RMUX daemon. It subscribes to pane events, drives state mutations, and communicates exclusively through the `rmux-sdk` facade. It never touches `rmux-client`, `rmux-core`, `rmux-server`, or `rmux-pty` directly.

### `PaneState` — deterministic sync projection

`PaneState` is a plain-data value with no I/O and no clock dependencies. The driver folds events into it; the same value always produces the same rendered cells. It can be constructed from a `PaneSnapshot` without an async runtime:

```rust
// Inert construction — no runtime needed
let state = PaneState::from_snapshot(snapshot);
```

Sources: [crates/ratatui-rmux/src/lib.rs:60]()

### `PaneWidget` — sync ratatui widget

`PaneWidget` implements the ratatui `Widget` trait. It is entirely synchronous, has no clock or time dependencies, and only borrows a `PaneState`. It is safe to call from any ratatui draw loop — including single-threaded, non-tokio, or test environments.

### Budget enforcement

The invariants are enforced in two ways:

1. **Cargo.toml**: `ratatui-rmux` lists only `ratatui-core` and `rmux-sdk` as production dependencies. `rmux-client`, `rmux-core`, `rmux-server`, and `rmux-pty` are explicitly forbidden.

2. **Test suite** (`crates/ratatui-rmux/tests/budget.rs`): The test verifies the exact set of source files (`lib.rs`, `driver.rs`, `state.rs`, `widget.rs`, `theme.rs`), enforces a maximum line count of 1500 lines, and checks that `FORBIDDEN_DEPS` are absent from the manifest.

```rust
// crates/ratatui-rmux/tests/budget.rs
const FORBIDDEN_DEPS: &[&str] = &[
    "rmux-client", "rmux-core", "rmux-server", "rmux-pty"
];
const MAX_DIRECT_DEPS: usize = 2;
const REQUIRED_DEPS: &[&str] = &["rmux-sdk"];
```

Sources: [crates/ratatui-rmux/tests/budget.rs:11-13]()

A change that adds a new source file, imports a forbidden crate, or exceeds 1500 source lines will fail `cargo test` before merging.

---

## `rmux-render-core`: wasm32-Compatible Subset

`rmux-render-core` is an unpublished crate that contains only the snapshot data types and deterministic ratatui projection code that `ratatui-rmux` also exposes. It has **no** daemon, IPC, process, filesystem, network, Tokio, or terminal-driver integration — only `ratatui-core` and `serde`:

```toml
# crates/rmux-render-core/Cargo.toml
[dependencies]
ratatui-core = { package = "ratatui", version = "=0.29.0", default-features = false }
serde         = { version = "1.0", features = ["derive"] }
```

Sources: [crates/rmux-render-core/Cargo.toml]()

The crate's own `lib.rs` restates the constraint explicitly:

> This crate owns no daemon, IPC, process, filesystem, network, Tokio, or terminal-driver integration. It contains only captured pane snapshot data and deterministic ratatui projection code that can compile for `wasm32-unknown-unknown`.

Sources: [crates/rmux-render-core/src/lib.rs:6-12]()

Because all four public exports (`PaneSnapshot`, `PaneState`, `PaneWidget`, `cell_style`/`color`/`glyph_symbol`/`modifier`) are pure-data or deterministic projections, browser/WASM hosts can use them without any platform-specific I/O or async runtime.

---

## Dependency Direction Diagram

```text
External caller
    │
    ▼
rmux-sdk ────────────────────────────────────────────────┐
    │  (re-exports identity types from rmux-proto)        │
    │  deps: rmux-ipc, rmux-os, rmux-proto                │
    │                                                     │
    ▼                                                     │
rmux-client ← uses rmux-sdk for auto-start bootstrap     │
rmux-server ← internal; never imported by rmux-sdk ──────┘
rmux-pty    ← internal; never imported by rmux-sdk

ratatui-rmux ─────────────────────────── dep: rmux-sdk only
    PaneDriver (async)  →  rmux-sdk  →  rmux-client/server at runtime
    PaneState  (sync, no I/O)
    PaneWidget (sync ratatui widget)

rmux-render-core ─────────────────────── dep: ratatui-core + serde only
    (wasm32-safe; no async, no IPC)
```

---

## Key Failure Modes

### 1. Daemon Not Running — Auto-Start Race

**What happens**: When the daemon socket is absent, `rmux-client` re-execs the current binary with the hidden flag `--__internal-daemon` to start the daemon in the background, then polls for the socket to become reachable.

**The race**: The binary path is resolved by `env::current_exe()`. If the binary has been replaced on disk (CI, staging, in-place upgrade) between the moment the client starts and the daemon re-exec, the spawned daemon may be a different version or may fail to start. The socket write that follows the spawn can fail before the daemon has bound.

**Override**: Set `RMUX_INTERNAL_BINARY_PATH` to an absolute path to point auto-start at a specific binary. This env var is only honoured when **both** `cfg(debug_assertions)` is active **and** `RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE=1` is set, so it cannot accidentally activate in release builds.

```rust
// crates/rmux-client/src/auto_start.rs
const BINARY_OVERRIDE_ENV: &str = "RMUX_INTERNAL_BINARY_PATH";
const BINARY_OVERRIDE_TEST_OPT_IN_ENV: &str = "RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE";

fn binary_override_enabled_for_tests() -> bool {
    cfg!(debug_assertions)
        && env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV)
               .is_some_and(|value| value == "1")
}
```

Sources: [crates/rmux-client/src/auto_start.rs:44-46](), [crates/rmux-client/src/auto_start.rs:599-610]()

**Error surfaces**: `AutoStartError::BinaryPath`, `AutoStartError::Launch`, `AutoStartError::TimedOut`. The timed-out variant includes the socket path and the amount of time spent polling.

---

### 2. Stale Unix Socket

**What happens**: If the daemon process crashed without cleaning up its socket, the socket file is left on disk but is no longer listened on. A new daemon bind attempt would fail with `EADDRINUSE`.

**How the server handles it**: `rmux-server` calls `remove_stale_socket_if_needed` before binding. The function:
1. Checks that the path is a plain Unix socket (not a symlink or regular file — those are errors).
2. Attempts a probe `connect()` to the socket.
3. If the connect returns `ConnectionRefused` or `NotFound`, it removes the socket and proceeds.
4. If the connect **succeeds**, it returns `AddrInUse` — a live foreign server is running there.

```rust
// crates/rmux-server/src/unix_socket.rs
pub(crate) fn remove_stale_socket_if_needed(socket_path: &Path) -> io::Result<()> {
    // ...
    match StdUnixStream::connect(socket_path) {
        Ok(_stream) => Err(io::Error::new(
            io::ErrorKind::AddrInUse,
            format!("socket '{}' is already in use", socket_path.display()),
        )),
        Err(error) if indicates_stale_socket(&error) => {
            // remove and return Ok(())
        }
        Err(error) => Err(error),
    }
}

pub(crate) fn indicates_stale_socket(error: &io::Error) -> bool {
    matches!(
        error.kind(),
        io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound
    )
}
```

Sources: [crates/rmux-server/src/unix_socket.rs:182-217](), [crates/rmux-server/src/unix_socket.rs:306-312]()

Additionally, the socket directory is created with mode `0o700`, the bound socket itself is set to `0o600`, and both ownership and permissions are checked. A directory or socket that is a symlink, has wrong permissions, or is owned by a different UID will result in a `PermissionDenied` error rather than silent acceptance.

---

### 3. Nested Session Confusion — `$RMUX` Guard

**What happens**: When a user runs `rmux attach-session` from inside an existing RMUX session, re-attaching would open a new terminal client on top of the current one. This is almost never intended.

**How the guard works**: `detect_context()` checks whether `$RMUX` is set and non-empty. Pane processes inherit this env var from the session. If the client is nested, `attach-session` is rewritten into a `switch-client` call (which changes the active session without opening a new terminal client).

```rust
// crates/rmux-client/src/nested.rs
pub fn detect_context() -> ClientContext {
    detect_context_from_env(std::env::var_os("RMUX").as_deref())
}

fn detect_context_from_env(tmux_value: Option<&std::ffi::OsStr>) -> ClientContext {
    match tmux_value {
        Some(value) if !value.is_empty() => ClientContext::Nested,
        _ => ClientContext::Outside,
    }
}
```

Sources: [crates/rmux-client/src/nested.rs:40-66]()

`$RMUX` typically holds a value like `/tmp/rmux-1000/default,12345,0` (socket path, session ID, window index). An empty string or absent variable means `Outside`. The guard is a pure env-var check — no IPC — so it is cheap and cannot fail.

**What is not guarded**: Only `attach-session` and `new-session` (which also delegates to `switch-client` when nested) are actively re-routed. Other commands do not check context and will behave normally regardless of nesting depth.

---

### 4. Windows ConPTY Version — Feature Gating, Not Silent Hang

**What happens**: The Windows ConPTY (`CreatePseudoConsole`) API was introduced in Windows 10 1803 (build 17134) and gained the passthrough mode flag in build 22621 (Windows 11 22H2). Calling these APIs on unsupported builds does not hang silently — it returns an `HRESULT` error that is immediately surfaced.

**How version detection works**: `current_windows_version()` calls `RtlGetVersion` (ntdll) at PTY creation time to read the build number.

```rust
// crates/rmux-pty/src/backend/windows/flags.rs
const PASSTHROUGH_MIN_BUILD: u32 = 22_621;

fn supports_passthrough(version: WindowsVersion) -> bool {
    version.build >= PASSTHROUGH_MIN_BUILD
}
```

Sources: [crates/rmux-pty/src/backend/windows/flags.rs:11](), [crates/rmux-pty/src/backend/windows/flags.rs:71-73]()

**Fallback chain**: `open_pty_pair` calls `create_pty_state_with_fallback`, which:
1. Tries with the full flags set (passthrough + resize-quirk + win32-input-mode).
2. If passthrough fails, retries without it.
3. If all extended flags fail, retries with no flags at all (`standard_conpty_flags` = bits `0`).
4. If standard flags fail, the error is returned — there is no further fallback, no hang.

```rust
// crates/rmux-pty/src/backend/windows/pty.rs
fn create_pty_state_with_fallback(size: TerminalSize, selected: ConptyFlags)
    -> Result<WindowsPtyState>
{
    match create_pty_state(size, selected) {
        Ok(state) => Ok(state),
        Err(error) if selected.uses_passthrough() => {
            // retry without passthrough
        }
        Err(error) if selected.bits() != 0 => {
            // retry with standard flags (bits = 0)
        }
        Err(error) => Err(error),  // no further fallback
    }
}
```

Sources: [crates/rmux-pty/src/backend/windows/pty.rs:219-243]()

**Override**: Set `RMUX_CONPTY_NO_PASSTHROUGH=1` (or `true`, `yes`) to force passthrough off without changing any build, useful when a corporate managed build reports a high build number but ConPTY passthrough is still unreliable.

**What the page description says about 1903**: The claim that ConPTY requires Windows 10 1903 (build 18362) as a minimum is a practical baseline — passthrough mode raises that to build 22621. `rmux-pty` does not hard-code a minimum version guard; it relies on `CreatePseudoConsole` returning an `HRESULT` error on any unsupported system.

---

## Summary of Invariants

| Invariant | Enforcement mechanism |
|-----------|-----------------------|
| `rmux-sdk` never imports `rmux-client`/`rmux-core`/`rmux-server`/`rmux-pty` | Cargo.toml + doc comment |
| `ratatui-rmux` only imports `rmux-sdk` and `ratatui-core` | `budget.rs` integration test, Cargo.toml |
| `ratatui-rmux` source stays flat and under 1500 lines | `budget.rs` integration test |
| `rmux-render-core` is wasm32-compatible | No tokio/IPC/OS deps in Cargo.toml |
| Identity types are always imported via `rmux-sdk` | Re-export in `rmux-sdk/src/lib.rs` |
| Stale sockets are removed on bind, not ignored | `unix_socket.rs::remove_stale_socket_if_needed` |
| Nested sessions route to `switch-client`, not new attach | `nested.rs::detect_context` + `client_commands.rs` |
| Windows ConPTY errors are surfaced immediately | `create_pty_state_with_fallback` returns `Err` on final failure |

The dependency boundary and budget tests are the highest-leverage guardrails: they turn accidental violations into CI failures rather than runtime surprises. Any contributor touching these crates should run `cargo test --workspace` and confirm that `budget.rs` and `rmux-sdk`'s own tests pass before submitting. Sources: [crates/ratatui-rmux/tests/budget.rs:1-13](), [crates/rmux-sdk/src/lib.rs:1-17]()
