Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmux-tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Mouse](docs/mouse.md)
- [Configuration](docs/configuration.md)
- [Machines and remote sessions](docs/machines.md)
- [Fly Sprites](docs/sprites.md)
- [Public CLI](spec/cli.md)
- [SDK contract](spec/bindings.md)
- [Public resource protocol](spec/resource-api-v1.md)
Expand Down
23 changes: 23 additions & 0 deletions cmux-tui/crates/cmux-tui-core/src/mux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16213,6 +16213,29 @@ mod tests {
assert_eq!(surface.test_cell_pixel_size(), (8, 16));
}

#[cfg(unix)]
#[test]
fn exited_host_cell_pixels_converge_without_a_live_host() {
let mux = test_mux();
let surface = Surface::exited_terminal_placeholder(
mux.next_id(),
mux.surface_options.lock().unwrap().clone(),
Arc::downgrade(&mux),
TerminalHostIdentity {
terminal_id: "00112233445566778899aabbccddeeff".into(),
incarnation: "11111111111111111111111111111111".into(),
},
)
.unwrap();
insert_surface_checked(&mut mux.state.lock().unwrap(), surface.clone()).unwrap();

let update = mux.set_cell_pixel_size(16, 32);

assert!(update.failures.is_empty(), "{:?}", update.failures);
assert_eq!(mux.cell_pixel_size(), (16, 32));
assert_eq!(surface.test_cell_pixel_size(), (16, 32));
}

#[test]
fn terminal_spawn_releases_cell_pixel_lifecycle_and_reconciles_before_publish() {
let mux = test_mux();
Expand Down
7 changes: 4 additions & 3 deletions cmux-tui/crates/cmux-tui-core/src/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5060,8 +5060,9 @@ impl PtySurface {
}
return Ok(true);
}
PtyRuntime::ExitedHosted => return Ok(false),
PtyRuntime::Local { .. } => {}
// The host is gone, but its retained terminal snapshot still
// renders for attached clients and must adopt their metrics.
PtyRuntime::ExitedHosted | PtyRuntime::Local { .. } => {}
}
}
let mut geometry = self.geometry.lock().unwrap();
Expand Down Expand Up @@ -5093,7 +5094,7 @@ impl PtySurface {
#[cfg(unix)]
PtyRuntime::Hosted(_) => None,
#[cfg(unix)]
PtyRuntime::ExitedHosted => return Ok(false),
PtyRuntime::ExitedHosted => None,
};
let has_attach_taps = {
let mut taps = self.taps.lock().unwrap();
Expand Down
1 change: 1 addition & 0 deletions cmux-tui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Configuration](configuration.md): full `cmux-tui.json` reference with defaults and a worked example.
- [Remote daemon](remote.md): authenticated clients, SSH, WebSocket, Iroh, relays, RPC, and port forwarding. The exact agent RPC wire schema is in the [remote RPC contract](../spec/remote-rpc.md).
- [Machines](machines.md): optional dual rails, static Unix/SSH targets, relay, `npx cmux` remote setup, and outbound `npx cmux machine-agent` registration.
- [Fly Sprites](sprites.md): credential-free base checkpoints, direct remote connections, and the management/device authentication boundary.
- [Raw control protocol](protocol.md): private protocol-v10 JSON-lines commands and compatibility rules.
- [Public resource protocol](../spec/resource-api-v1.md): stable opaque IDs, requests, mutations, streams, and errors.
- [Public CLI](../spec/cli.md): noun-first commands and selectors.
Expand Down
99 changes: 99 additions & 0 deletions cmux-tui/docs/sprites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Fly Sprites

Fly Sprites can host a persistent remote cmux session on a normal ext4
filesystem. The base checkpoint contains the pinned `cmux` npm distribution
and a Sprite service, but deliberately excludes every daemon identity,
invitation, client key, and provider credential.

## Build the base checkpoint

Install the Sprite CLI, source an owner-only token file, and run:

```sh
set -a
source ~/.secrets/fly-sprites.env
set +a
cmux-tui/scripts/build-sprite-base.sh --org manaflow
```

The script prints the Sprite name and checkpoint id. It leaves the `cmux-tui`
service stopped and `/home/sprite/.local/share/cmux-sprite/remote` absent.
Starting the service after a new Sprite is provisioned generates a unique
daemon identity.

Checkpoints currently restore one Sprite. Fly's cross-Sprite drive forking is
in beta and is not exposed by the public REST API. Until that API ships, this
checkpoint is a verified base artifact rather than a generally available
create-time template.

## Authentication boundary

There are two independent trust planes:

1. The cmux backend owns `SPRITE_TOKEN` and uses it for Sprite lifecycle,
service, filesystem, and exec APIs. The token never enters a Sprite, native
client, command argument, checkpoint, or database row.
2. Native clients connect directly through the public Sprite HTTPS edge, but
cmux authenticates the daemon key and device key inside an end-to-end Noise
session. The public URL is an untrusted carrier, not an authorization
boundary.

Do not put a static `--ws-token` in the checkpoint. Do not checkpoint
`/home/sprite/.local/share/cmux-sprite/remote`; clones would share the daemon
private key and enrolled-device database.

## Provision and enroll one client

Start the service after provisioning:

```sh
sprite exec -o <org> -s <sprite> -- sprite-env services start cmux-tui
```

Fetch the Sprite URL through the authenticated management API. Create a
single-device, short-lived invitation inside the Sprite:

```sh
cmux enroll create \
--session sprite \
--state-dir /home/sprite/.local/share/cmux-sprite/remote \
--advertise wss://<sprite-host>/v1/link \
--ttl 300 \
--json
```
Comment on lines +57 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Run invitation creation inside the Sprite.

This block runs cmux enroll create on the reader host, but --state-dir /home/sprite/... identifies daemon state inside the Sprite. The command will target a local admin socket or fail. Wrap this command in authenticated sprite exec -o <org> -s <sprite> -- ..., or state that the backend performs that exact operation before it returns the invitation.

The later approval step already specifies authenticated Sprite exec. The creation step needs the same execution boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmux-tui/docs/sprites.md` around lines 57 - 63, Update the
invitation-creation instructions around cmux enroll create so the command
executes inside the Sprite through authenticated sprite exec, using the
specified organization and Sprite, before returning the invitation; preserve the
existing state directory and enrollment arguments.


The backend returns the invitation once to the already authenticated cmux
user. The native client stores it in an owner-only file, claims it with its
persistent device key, and waits:

```sh
cmux connect \
--invite-file ~/.config/cmux/sprite.invite \
--device-name <device-name>
```

The backend then runs `cmux enroll pending` through authenticated Sprite exec.
It approves only the invitation id it issued for that user, VM, and expiry.
Never approve every pending request, and never identify a claimant only by its
display name.

After approval, the client reconnects with its device key. The invitation is
single-device and expires after five minutes. Revocation uses `cmux enroll
revoke`; it closes live sessions and rejects the device in the future.

For a team-owned Sprite, authorize invitation creation and approval against
the exact team and VM row before any provider call. Record the issued
invitation id, user id, VM id, expiry, and final device fingerprint. Store no
invitation URI or Sprite token.

## Why the URL is public

The current native WebSocket client does not attach a Fly organization token.
Keeping the Sprite URL private would require a cmux backend relay that adds the
provider credential. A public URL avoids that relay and preserves direct
interactive latency. Unauthenticated callers can reach the WebSocket edge but
cannot complete cmux's Noise handshake or access mux data.

If direct public carriers are unacceptable, implement a machine-provider
stream that keeps `SPRITE_TOKEN` server-side and forwards opaque cmux protocol
messages. Do not send the provider token to the native client.
148 changes: 148 additions & 0 deletions cmux-tui/scripts/build-sprite-base.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
printf '%s\n' \
'Usage: build-sprite-base.sh [options]' \
'' \
'Build a credential-free Fly Sprite checkpoint containing a pinned cmux remote' \
'daemon service.' \
'' \
'Options:' \
' --org <slug> Sprite organization (default: SPRITE_ORG or manaflow)' \
' --name <name> Base Sprite name (default: cmux-tui-base-<timestamp>)' \
' --cmux-version <version> Pinned npm cmux version (default: 0.9.11)' \
' --keep-running Start the daemon after creating the clean checkpoint' \
' --keep-on-failure Do not destroy a newly created Sprite after failure' \
' -h, --help Show this help' \
'' \
'Authentication:' \
' Source an owner-only file that exports SPRITE_TOKEN before running. The token' \
' is used only by the local Sprite CLI and is never copied into the Sprite or' \
' checkpoint.'
}

org="${SPRITE_ORG:-manaflow}"
name=""
cmux_version="0.9.11"
keep_running=0
keep_on_failure=0

while [[ $# -gt 0 ]]; do
case "$1" in
--org)
org="${2:?missing value for --org}"
shift 2
;;
--name)
name="${2:?missing value for --name}"
shift 2
;;
--cmux-version)
cmux_version="${2:?missing value for --cmux-version}"
shift 2
;;
--keep-running)
keep_running=1
shift
;;
--keep-on-failure)
keep_on_failure=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done

if [[ ! "$cmux_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
echo "--cmux-version must be an exact semver version" >&2
exit 2
fi
if [[ -z "${SPRITE_TOKEN:-}" ]]; then
echo "SPRITE_TOKEN is required" >&2
exit 2
Comment on lines +69 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the referenced script around the reported validation and the surrounding
# output behavior without executing repository code.
if [ -f cmux-tui/scripts/build-sprite-base.sh ]; then
  nl -ba cmux-tui/scripts/build-sprite-base.sh | sed -n '1,140p'
  printf '\n--- occurrences of SPRITE_TOKEN ---\n'
  rg -n 'SPRITE_TOKEN|sprite|echo ".*error|>&2' cmux-tui/scripts/build-sprite-base.sh || true
else
  printf 'Referenced file not found\n'
fi

printf '\n--- shell syntax safety probe ---\n'
sh -n cmux-tui/scripts/build-sprite-base.sh && echo "syntax ok" || echo "syntax error"

Repository: manaflow-ai/cmux

Length of output: 194


Information Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information

Reachability: Internal

Do not expose the credential environment-variable name in the error.

The script writes SPRITE_TOKEN is required to stderr, which exposes a credential configuration name to callers who run the script without credentials. Use product-level text instead, such as “A Sprite management credential is required. Source the authorized credential file and retry.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmux-tui/scripts/build-sprite-base.sh` around lines 69 - 71, Update the
missing-credential error in build-sprite-base.sh’s SPRITE_TOKEN validation to
use product-level wording without exposing the SPRITE_TOKEN environment-variable
name. Keep the stderr output and exit status unchanged, and instruct users to
source the authorized credential file before retrying.

Source: Coding guidelines

fi
if ! command -v sprite >/dev/null 2>&1; then
echo "sprite CLI is required: curl -fsSL https://sprites.dev/install.sh | bash" >&2
exit 2
fi

if [[ -z "$name" ]]; then
name="cmux-tui-base-$(date -u +%Y%m%d-%H%M%S)"
fi
if [[ ! "$name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
echo "--name must contain lowercase letters, digits, and hyphens" >&2
exit 2
fi

created=0
cleanup() {
local status=$?
if [[ "$status" -ne 0 && "$created" -eq 1 && "$keep_on_failure" -eq 0 ]]; then
sprite destroy -o "$org" -s "$name" --force >/dev/null 2>&1 || true
fi
exit "$status"
}
trap cleanup EXIT

sprite create -o "$org" --skip-console "$name"
created=1

# The Sprite edge is only the carrier. cmux authenticates and encrypts every
# connection with Noise and an enrolled device key. No Sprite management token
# or static cmux token is embedded in the image.
sprite url update -o "$org" -s "$name" --auth public

sprite exec -o "$org" -s "$name" -- bash -lc "
set -euo pipefail
npm install -g --omit=dev --no-audit --fund=false 'cmux@${cmux_version}'
sudo ln -sf \"\$(npm prefix -g)/bin/cmux\" /usr/local/bin/cmux
/usr/local/bin/cmux --version
install -d -m 700 /home/sprite/.local/share/cmux-sprite
sprite-env services create cmux-tui \
--cmd /usr/local/bin/cmux \
--args daemon,--session,sprite,--remote-ws,0.0.0.0:8080,--remote-ws-insecure-bind,--remote-state-dir,/home/sprite/.local/share/cmux-sprite/remote \
--dir /home/sprite \
--http-port 8080 \
--duration 10s
sprite-env services stop cmux-tui
rm -rf /home/sprite/.local/share/cmux-sprite/remote /tmp/cmux-tui-*
rm -f /.sprite/logs/services/cmux-tui.log
install -d -m 700 /home/sprite/.local/share/cmux-sprite
test ! -e /home/sprite/.local/share/cmux-sprite/remote
"

checkpoint_output="$(
sprite checkpoint create -o "$org" -s "$name" \
--comment "cmux ${cmux_version} clean base; no daemon identity, invitations, or credentials"
)"
printf '%s\n' "$checkpoint_output"
checkpoint="$(
printf '%s\n' "$checkpoint_output" |
awk '/Checkpoint v[0-9]+ created/ { for (i = 1; i <= NF; i++) if ($i ~ /^v[0-9]+$/) print $i }' |
tail -1
)"
if [[ -z "$checkpoint" ]]; then
echo "checkpoint creation returned no checkpoint id" >&2
exit 1
fi

if [[ "$keep_running" -eq 1 ]]; then
sprite exec -o "$org" -s "$name" -- sprite-env services start cmux-tui
fi

trap - EXIT
printf '\nSPRITE_BASE_ORG=%s\n' "$org"
printf 'SPRITE_BASE_NAME=%s\n' "$name"
printf 'SPRITE_BASE_CHECKPOINT=%s\n' "$checkpoint"
printf 'SPRITE_BASE_CMUX_VERSION=%s\n' "$cmux_version"
printf 'SPRITE_BASE_DAEMON_STATE=absent\n'
printf 'SPRITE_BASE_SERVICE=%s\n' "$([[ "$keep_running" -eq 1 ]] && echo running || echo stopped)"
Comment on lines +143 to +148

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Report the checkpoint build with the required tagged link.

Lines 143-148 report build metadata but do not emit http://127.0.0.1:17320/<tag>. Add the required tagged build-report link. Do not replace it with a file:// URL or a local artifact path.

As per coding guidelines, shell builds must use the tagged localhost report link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmux-tui/scripts/build-sprite-base.sh` around lines 143 - 148, Update the
build metadata output in the script’s reporting block to emit the required
tagged localhost report link at http://127.0.0.1:17320/<tag>. Derive the URL
from the existing build tag value, and preserve the current metadata lines
without substituting a file:// URL or local artifact path.

Source: Coding guidelines

Loading