Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
112 changes: 112 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,115 @@ jobs:

- name: Verify
run: npm run verify

- name: Write verify receipt
run: |
mkdir -p .ci-receipts
node scripts/cua/write-release-receipt.mjs verify .ci-receipts/verify-node-${{ matrix.node-version }}.json ubuntu-22.04

- name: Upload verify receipt
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: verify-receipt-node-${{ matrix.node-version }}
path: .ci-receipts
if-no-files-found: error
include-hidden-files: true
retention-days: 14

bundle:
name: Canonical server bundle
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Set up Node.js 22
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Build canonical server bundle
run: npm run server:bundle

- name: Write bundle receipt
run: |
mkdir -p .ci-receipts
node scripts/cua/write-release-receipt.mjs bundle .ci-receipts/bundle-node22.json ubuntu-22.04 release/server
rm -rf release/server

- name: Upload bundle receipt
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: bundle-receipt-node-22
path: .ci-receipts
if-no-files-found: error
include-hidden-files: true
retention-days: 14

fleet-release:
name: Fleet release evidence
# Release-grade browser/desktop QA runs after merge on main; per-PR runners
# starve the fixture startup budgets without proving anything the required
# Verify gates do not already cover.
if: github.event_name == 'push'
needs: [verify, bundle]
runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Install isolated graphical CI dependencies
run: |
sudo apt-get update
sudo apt-get install --yes tmux xvfb gnome-shell gnome-screenshot dbus-x11 at-spi2-core python3 python3-pip

- name: Install pinned Python browser driver
run: |
python3 -m pip install --disable-pip-version-check --no-cache-dir 'playwright==1.49.1'
python3 -c 'from playwright.sync_api import sync_playwright; print(sync_playwright.__module__)'

- name: Set up Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: npm

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

- name: Install dependencies
run: npm ci

- name: Download CI parity receipts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: '*-receipt-node-*'
path: .cua-release-evidence/ci
merge-multiple: true

- name: Fleet browser and full release QA
env:
CUA_EVIDENCE_DIR: ${{ github.workspace }}/.cua-release-evidence
run: npm run cua:release

- name: Upload fleet release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: fleet-release-evidence-node-24
path: .cua-release-evidence
if-no-files-found: error
include-hidden-files: true
retention-days: 14
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,20 @@ future server artifacts are published only through

## Unreleased

### Added

- Multi-PC fleet management now supports one browser-facing hub plus up to nine full
peer installations. Owner-only Hosts settings provide single-use enrollment,
explicit reconnect/sync state, local-first revocation, and direct-peer recovery
over Tailscale HTTPS/WSS or an explicitly created literal-loopback SSH forward.

### Changed

- Fleet operations keep peer data, tmux ownership, installation keys, updates, and
recovery local to each PC. Releases are updated hub first and then one peer at a
time; ChatMux provides no relay, transport downgrade, cloud sync, automatic
failover, fleet updater, remote desktop/IDE, or zero-configuration reachability.

- Cursor CLI integration now uses the documented `agent` command for chat,
authentication, model discovery, tmux spawning, resume terminals, and live
process discovery while retaining the legacy `cursor-agent` fallback.
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ chatmux/
- Documentation improvements are always welcome
- Keep language clear and concise
- Keep installation and self-hosting instructions aligned with [the self-hosting guide](docs/SELF-HOST.md)
- Multi-PC documentation must preserve the shipped contract in [REMOTE-ACCESS.md §8](docs/REMOTE-ACCESS.md#8-multi-pc-fleet-one-hub-and-full-peers): one hub plus at most nine full peers, owner-only enrollment, Tailscale HTTPS/WSS by default, and only literal loopback `ws://` behind an owner-created SSH forward. Do not describe a relay, downgrade, automatic failover, fleet updater, cloud sync, remote desktop/IDE, arbitrary commands, or zero-configuration reachability.

## Commit Convention

Expand Down Expand Up @@ -151,6 +152,14 @@ ChatMux uses trunk-based development: `main` plus short-lived branches. There is

## Releases

Fleet-capable releases are rolled out **hub first**, then one full peer at a time.
Verify the hub's direct UI and peer inventory after the hub update. After each peer
update, wait for it to leave **Syncing** and return **Online** before continuing.
**Incompatible** is a hard stop: use a supported version pairing or the documented
manual recovery; never downgrade the transport or bulk-update the remaining peers.
There is no fleet-wide update command, and every peer retains its own updater and
direct recovery UI.

Cutting a release (maintainers):

1. On up-to-date `main`, add the compatibility declaration for the new version to `packaging/release/update-compatibility.json` and commit it as `chore(release): declare X.Y.Z database rollback compatibility`. When the database schema is unchanged, carry forward **every** prior version sharing that schema in `rollbackCompatibleFrom` (the release workflow proves each declared entry); reset the list to only the immediately preceding version when a release actually migrates the schema. A single-entry declaration on an unchanged schema strands any install more than one release behind in `manual_required` (#49).
Expand Down
89 changes: 89 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ChatMux Design System

## 1. Atmosphere & Identity

A compact, quiet command deck for long-running coding-agent work. The signature is host-aware operational density: muted layered surfaces keep transcripts and terminals primary, while state, target, and destructive scope stay explicit at the action edge.

## 2. Color

All UI color is expressed through the HSL variables in `src/index.css` and their Tailwind semantic aliases.

| Role | Token | Usage |
| --- | --- | --- |
| Page | `--background` / `background` | App shell and navigation |
| Surface | `--card` / `card` | Panels and elevated controls |
| Text | `--foreground` / `foreground` | Primary text |
| Muted | `--muted`, `--muted-foreground` | Secondary surfaces and metadata |
| Primary | `--primary`, `--primary-foreground` | Primary actions and active state |
| Accent | `--accent`, `--accent-foreground` | Hover and selected affordances |
| Destructive | `--destructive`, `--destructive-foreground` | Termination and irreversible actions |
| Boundary | `--border`, `--input`, `--ring` | Dividers, controls, focus |

Status colors use the existing Tailwind emerald, amber, blue, and red ramps. They communicate machine state only; they are not decorative.

## 3. Typography

- Primary: `Pretendard Variable`, Pretendard, Encode Sans, system UI fallbacks.
- Terminal: the xterm stack declared by `TERMINAL_FONT_FAMILY`.
- Existing scale: 11px dense metadata, 12px captions, 14px controls/body, 16px primary body, and larger headings from Tailwind's standard scale.
- Labels remain short enough to avoid orphaned CJK particles; long machine identifiers truncate visually while remaining available in accessible names or titles.

## 4. Spacing & Layout

The base unit is 4px. Existing Tailwind spacing steps are the source of truth: 1/1.5 for dense inline controls, 2/3 for control padding, 4 for content gutters, and larger standard steps for page regions.

The app owns viewport scrolling through a fixed `inset-0` shell. Sidebar and main content own their inner scrolling; every flex scroll child uses `min-h-0`/`min-w-0`. Mobile surfaces respect safe-area variables from `src/index.css` and reflow at the existing `sm`/`md` breakpoints.

## 5. Components

### Action button
- Structure: native `button` with Lucide icon and concise label/title.
- Variants: neutral, primary, destructive.
- States: default, hover, focus-visible ring, disabled opacity/cursor, pending.
- Accessibility: native disabled semantics and a target-specific accessible name.
- Motion: color/opacity only, existing 150-200ms transitions.

### Host status badge
- Structure: compact semantic status text adjacent to the host label.
- Variants: online, connecting, syncing, degraded, offline, revoked, incompatible.
- States: stale/unavailable state disables all descendant actions.
- Accessibility: state is readable text, never color alone.

### Terminal target bar
- Structure: terminal icon, tmux name, host label, process generation, action cluster, close control.
- Variants: local and remote exact-pane target.
- States: ready, stale, syncing, offline, pending action, unknown outcome.
- Accessibility: every action names its host and termination scope; destructive actions require an explicit confirmation.
- Layout: wrapping cluster on mobile; single compact row when space permits.

### Destructive confirmation
- Structure: selected scope description plus confirm/cancel buttons.
- Variants: process termination, pane destruction, tmux-session destruction.
- Accessibility: copy states the distinct impact before confirmation; focus remains keyboard reachable.

## 6. Motion & Interaction

- Micro state transitions: 150ms ease-out.
- Standard panel transitions: 200ms ease-in-out.
- Animate only transform and opacity; existing color transitions remain allowed for interaction feedback.
- No decorative motion. Reduced-motion preferences are honored by the global stylesheet.

## 7. Depth & Surface

Mixed, restrained depth: tonal surface shifts and low-contrast borders define the operational shell; shadows are reserved for overlays and floating controls already established in `src/index.css`. Terminal and transcript content remain visually dominant.

## 8. Accessibility Constraints & Accepted Debt

### Constraints

- WCAG 2.2 AA target; body contrast at least 4.5:1 and large text/UI boundaries at least 3:1.
- Full keyboard reachability, visible focus rings, native disabled semantics, and text equivalents for all icon controls.
- Host, pane generation, stale state, and destructive scope must never be conveyed by color alone.
- Desktop, tablet, and 375px mobile layouts must avoid primary-content horizontal scrolling.

### Accepted Debt

| Item | Location | Why accepted | Owner / Exit |
| --- | --- | --- | --- |
| Legacy raw Tailwind status ramps | Existing application surfaces | Extracted system preserves shipped behavior; consolidation is outside Todo 17 | Address in a dedicated design-token cleanup |
| React dev-inspection tools are not installed | Project tooling | Dependency changes are outside the fleet terminal integration scope | Add in a dedicated frontend-tooling change |
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ chatmux access enable tailscale # switch modes after Tailscale is available

User allowlists, longer sessions, VPN mode, SSH tunnels, and public TLS options are covered in the [remote access guide](docs/REMOTE-ACCESS.md).

## Multiple PCs

One ChatMux installation can be the **hub** for as many as nine other **full ChatMux installations** (ten PCs total). Open the hub in your browser to see the hub and enrolled peers together; each peer still owns its tmux sessions, database, keys, updater, and direct browser UI.

Enrollment is owner-only in **Settings → Hosts**. Generate a single-use, 10-minute pairing code on the peer, then add it from the hub. Use the peer's Tailscale Serve endpoint as `wss://<peer-host>.ts.net:<serve-port>/fleet-ws` by default. ChatMux has no cloud relay and never falls back to plaintext. The only supported plaintext exception is an owner-created SSH local forward saved as `ws://127.0.0.1:<local-port>/fleet-ws` or `ws://[::1]:<local-port>/fleet-ws`.

Remote hosts can show **Offline** or **Syncing**. Syncing suspends remote writes until a fresh snapshot completes; Offline never redirects an action to the hub or another peer. Open that PC's own ChatMux address for direct recovery. See [multi-PC setup and recovery](docs/REMOTE-ACCESS.md#8-multi-pc-fleet-one-hub-and-full-peers).

## CLI

The `chatmux` command manages the installed service:
Expand All @@ -114,14 +122,16 @@ chatmux status # version, addresses, data locations
chatmux access users # allowed Tailscale accounts
chatmux access allow user@example.com
chatmux sandbox ~/my-project # run inside a Docker sandbox
chatmux fleet identity # public installation ID and fingerprint
chatmux fleet diagnose # redacted peer reachability
```

## Security and data boundaries

- ChatMux links tmux process ancestry to native transcript identifiers. A matching working directory alone is never enough to authorize a destructive action, and the tmux session identifier is rechecked before relay or termination.
- The backend binds to loopback. Tailscale mode trusts Serve identity headers only from loopback on the expected HTTPS origin; the installer never enables Funnel or a public listener, and unapproved users fail closed.
- Password mode uses `HttpOnly`, `SameSite=Strict` cookies with persistent logout revocation.
- State and indexes live below `~/.chatmux`. Back up `~/.chatmux/data` before migration or upgrade.
- State and indexes live below `~/.chatmux`. Back up `~/.chatmux/data` before migration or upgrade. Multi-PC federation does not replicate or cloud-sync that data.

## Development

Expand Down
82 changes: 82 additions & 0 deletions docs/FLEET-FEDERATION-RFC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Fleet federation RFC

`shared/fleet.ts` is the normative wire-contract source. This RFC fixes its product
and security semantics. Later work MUST NOT widen the surfaces below without a new
RFC revision.

## Topology and authority

- A fleet MUST contain one designated hub and at most nine enrolled full peers (ten
installations total). Every installation MUST retain its own tmux, provider,
SQLite, PTY, updater, and direct recovery UI.
- Browsers MUST use the hub's same-origin surface only. The hub MUST dial peers
directly. ChatMux MUST NOT operate a relay, signaling service, mesh, replicated
database, consensus system, automatic failover, or second agent artifact.
- Discovery is display and routing data only. The owning peer MUST re-run its local
lineage, protection, pane, and process-generation checks before every action.
`VerifiedTmuxActionTarget` MUST NOT cross the protocol.
- Remote file/editor/Git/project mutation, peer administration, fleet updates,
arbitrary command/argv/path RPC, remote plain-shell creation, and per-frame
signatures are out of scope.

## Reachability and trust

- Direct peers MUST use HTTPS/WSS; Tailscale Serve is the default documented path.
Plain `ws://` is valid only for a saved `ssh-loopback` peer targeting immediate
`127.0.0.1` or `[::1]` through an owner-managed SSH local forward. No automatic
downgrade is permitted.
- Each installation MUST own a durable random UUID and Ed25519 key pair. Hostname,
URL, IP, label, tmux socket, and provider ID MUST NOT identify an installation.
- Enrollment MUST be owner-only. A peer token MUST be 32 random bytes, hash-only at
rest, single-use, atomically consumed, and expire in ten minutes. Both sides MUST
pin the other installation key; a peer MAY have only one active hub grant.
- `/fleet-ws` MUST reject browser origin headers, cookies, JWTs, and query
credentials. After transport setup, peers MUST finish a five-second mutual-key
challenge, negotiate a protocol/capability intersection, and supersede stale
connection generations. TLS or the explicit SSH tunnel protects transport.

## Identity, descriptors, and keys

- Session and project references MUST be `{ hostId, localId }`. Pane references MUST
add lane and exact `TmuxPaneIdentity`; generation references MUST also add
`{ pid, startedAtMs }`. `shared/tmux.ts` preserves the canonical local identity
and adds host-only wrappers.
- IDs MUST be nonempty scalar strings of at most 256 characters; host IDs MUST be
canonical UUIDs. Equal local IDs from distinct hosts MUST remain distinct.
- Browser/store/dedupe digests MUST SHA-256 UTF-8 fields prefixed by four-byte
big-endian byte lengths. Delimiter concatenation is forbidden.
- Installation descriptors MAY expose only installation ID, public-key fingerprint,
protocol versions, and capabilities. Peer descriptors MAY expose only host ID,
display label, state, selected protocol version, and capabilities. Tokens, keys,
URLs, paths, sockets, transcripts, and credentials MUST NOT be descriptor fields.

## Closed protocol

- The initial protocol version is `fleet/1`. Capabilities are `catalog.read`,
`session.read`, `chat.control`, `prompt.respond`, `pane.read`, `terminal.attach`,
`terminal.input`, `session.spawn`, `session.terminate`, and `completion.event`.
- Peer states are `connecting`, `syncing`, `online`, `degraded`, `offline`,
`revoked`, and `incompatible`. Reconnect MUST start with a full snapshot; a gap or
epoch change MUST enter `syncing`, suspend writes, and require resnapshot.
- Every frame MUST carry protocol version and positive connection generation.
Requests MUST carry a unique request ID, allowlisted operation, exact target, and
JSON body. Responses MUST bind the request and target and state `success` or
`failure` plus side effect `none`, `applied`, or `possible`. Events MUST carry an
event ID, allowlisted event, source host, and JSON body. Unknown fields and
discriminants MUST fail closed.
- Operations are limited to catalog snapshot; session read/history/search; chat
send/abort; prompt/approval read/respond; pane capture/attach/input/resize/
interrupt/escape/terminate; process termination; and session spawn/termination.
Events are limited to catalog snapshot/delta, host state, chat delta,
prompt/approval change, pane output, and completion ready.
- Errors are closed values for malformed frames, protocol/capability/identifier
failures, unknown operation/event/error, host availability, uncertain command
outcome, stale/altered/cache-full requests, deadline/frame bounds, and denial.

## Side-effect safety

Mutations MUST be at-most-once within one accepted connection generation. A peer
MUST retain at most 4,096 canonical mutation results and reject new mutations once
full. Mutations MUST NOT replay after disconnect; uncertainty is
`HOST_COMMAND_OUTCOME_UNKNOWN` until reconciliation and fresh user intent. A read
MAY retry once only after a newly authenticated, synchronized connection.
Loading
Loading