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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +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) and [Fleet RFC revision 4](docs/FLEET-FEDERATION-RFC.md#hub-managed-ssh-forwarding): one hub plus at most nine full peers, owner-only enrollment, Tailscale HTTPS/WSS by default, and only literal loopback `ws://` behind an owner-managed or explicitly requested hub-managed SSH forward. Do not describe a relay, downgrade, automatic failover, fleet updater, cloud sync, remote desktop/IDE, arbitrary commands, or zero-configuration reachability.
- 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) and [Fleet RFC revision 6](docs/FLEET-FEDERATION-RFC.md#optional-ssh-bootstrap-and-candidate-suggestions): one hub plus at most nine full peers, owner-only enrollment, Tailscale HTTPS/WSS by default, and only literal loopback `ws://` behind an owner-managed or explicitly requested hub-managed SSH forward. Optional SSH bootstrap requires owner opt-in and a missing installation; it never updates or repairs an existing peer. 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
38 changes: 38 additions & 0 deletions docs/FLEET-FEDERATION-RFC.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Revision 4 (2026-09-03): permits hub-managed SSH local forwards for enrollment,
Revision 5 (2026-09-04): adds bounded full-tool-output reads under the existing
`session.read` operation/capability. Other operations and descriptor fields are unchanged.

Revision 6 (2026-09-05): permits explicit, owner-requested bootstrap of a missing
remote ChatMux installation during SSH enrollment and bounded, owner-only
Tailscale candidate suggestions. These are hub-local setup surfaces, not new fleet
wire operations or remote administration capabilities.

### Full tool output

- The owner-only host-qualified tool-result endpoint MUST resolve the addressed
Expand Down Expand Up @@ -76,6 +81,39 @@ Revision 5 (2026-09-04): adds bounded full-tool-output reads under the existing
- Host keys use trust-on-first-use recorded in a hub-owned `known_hosts` file dedicated to fleet tunnels. A changed host key MUST fail the tunnel. Silent acceptance is forbidden.
- Removing the peer tears the tunnel down and best-effort removes the installed public key from the remote `authorized_keys`.

### Optional SSH bootstrap and candidate suggestions

- SSH reachability and the remote account MUST already be configured by the owner.
The hub MAY bootstrap a missing ChatMux installation only when that enrollment
request explicitly sets `installCli: true`. The UI MUST default this option off
and disclose installation of a user service before submission.
- Bootstrap MUST use the canonical published installer and archive for the hub's
exact stable version, over HTTPS, and request loopback port 3001 explicitly.
It MUST NOT silently select another port or accept a browser-supplied version,
installer URL, path, command, environment, or argument list. The canonical
installer retains its platform, checksum, root-ownership, and service checks.
- A working remote CLI MUST be reused. A broken existing wrapper or managed root,
including a symlink, MUST require manual recovery rather than bootstrap. Only a
missing installation on Linux x86_64 is eligible. Recheck that absence immediately
before installation and atomically claim the absent managed root with mode 0700;
a competing claim MUST fail without running the installer. Bootstrap MUST NOT
become an updater or repair operation.
- Authentication material MUST be removed after SSH authentication, before a long
installation starts. The dedicated reconnect key MUST retain its command
restriction. Installation has a 15-minute bound and MUST NOT retry automatically.
A timeout or disconnect can leave a partial or completed installation: report a
closed error and require owner inspection. Enrollment failure or peer removal
MUST NOT uninstall ChatMux, remove its data, or terminate its tmux work.
- `GET /api/fleet/ssh-candidates` MAY return up to 128 sanitized, deduplicated
Tailscale peer suggestions and an editable suggested SSH username to an
authenticated owner only, with `Cache-Control: no-store`. It MUST reuse the
bounded read-only local Tailscale status probe and degrade to manual SSH entry
when unavailable. Candidates MUST NOT include keys, raw status, diagnostics,
paths, or credentials, and MUST NOT become public fleet descriptors.
- A candidate's hostname, IP, online flag, or OS hint grants no authority and proves
neither SSH reachability nor CPU compatibility. Enrollment MUST retain host-key
checking, owner intent, installation-key pinning, and the normal peer checks.

## Identity, descriptors, and keys

- Session and project references MUST be `{ hostId, localId }`. Pane references MUST
Expand Down
7 changes: 5 additions & 2 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,11 @@ Use the actual HTTPS host and port printed by `chatmux status`; do not assume
`8443`. Both PCs must be able to reach that Tailscale Serve address. ChatMux
never downgrades WSS to plaintext and does not provide a relay.

If direct WSS is unavailable, choose **Easy SSH setup** on the hub to connect to an
already-installed remote ChatMux backend at `127.0.0.1:3001`. Enter the reachable
If direct WSS is unavailable, choose **Easy SSH setup** on the hub to connect to a
remote ChatMux backend at `127.0.0.1:3001`. For a new Linux x86_64 PC, explicitly
select **Install ChatMux if missing** (off by default) to install the hub's exact
published version and user service on port 3001. Existing or broken installations
are never reinstalled or updated through this option. SSH access must already work. Enter the reachable
`user@host[:ssh-port]` and SSH password, review the dedicated-key disclosure, then
select **Add with SSH**. The hub obtains the pairing token and manages the forward;
the password is never saved. Changed host keys fail closed. See
Expand Down
22 changes: 17 additions & 5 deletions docs/REMOTE-ACCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,21 +307,33 @@ The only supported plaintext transport is a literal-loopback SSH local forward.
Owners can either request a hub-managed forward through **Easy SSH setup** or
create one manually.

For **Easy SSH setup**, first install and start ChatMux on the remote PC and ensure
its SSH account is reachable from the hub. The shipped easy setup forwards to the
remote backend at `127.0.0.1:3001`; use the manual path below for a different backend
port. On the hub, open **Settings → Hosts → Add a PC**, choose **Easy SSH setup**,
For **Easy SSH setup**, ensure the remote SSH account is reachable from the hub.
Use an existing ChatMux backend at `127.0.0.1:3001`, or explicitly select **Install
ChatMux if missing** for a new Linux x86_64 installation. This option is off by
default. It installs the hub's exact published version as a user service on port
3001; the canonical installer checks platform support and archive checksums. A
busy port fails rather than selecting another port. Existing or broken installs
require their own recovery/update path. Use the manual path below for a different
backend port. On the hub, open **Settings → Hosts → Add a PC**, choose **Easy SSH setup**,
and enter `user@host[:ssh-port]` and the SSH password. Review the key-installation
disclosure, then select **Add with SSH**. The hub installs a dedicated Ed25519
public key, obtains the peer's single-use pairing token over SSH, creates the local
forward, and enrolls the peer. There is no separate token-copy step in this mode.

If Tailscale is available on the hub, an optional PC selector suggests addresses
from its peer list. Check the suggested username and target before submission;
these hints do not verify SSH reachability or installation compatibility.

Installation can take up to 15 minutes. A failed or interrupted attempt may leave
ChatMux installed or partially installed; inspect that PC before retrying. Neither
enrollment cleanup nor peer removal uninstalls ChatMux or removes its data.

The password is used for authentication and is never saved. The hub stores the
dedicated private key and known-hosts data privately and uses the key to
re-establish the tunnel. First contact records the host key; a changed host key
fails closed and requires owner investigation. Removing the peer stops its tunnel
and attempts to remove the installed public key from the remote account. These
operations follow [Fleet RFC revision 4](FLEET-FEDERATION-RFC.md#hub-managed-ssh-forwarding).
operations follow [Fleet RFC revision 6](FLEET-FEDERATION-RFC.md#optional-ssh-bootstrap-and-candidate-suggestions).

For a manual forward, run this on the **hub PC** before enrollment, then use the
peer token from §8.1:
Expand Down
8 changes: 6 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ ChatMux가 제공하는 핵심 가치는 다음과 같다.
- Live Activity, Apple Watch 같은 네이티브 알림 표면
- provider가 제공하는 CLI, 인증, sandbox, 모델 실행 기능의 재구현
- zero-config 원격 도달성, ChatMux가 관리하는 Tailscale 또는 범용 SSH 설정·키
(Fleet RFC revision 4의 owner 요청 기반 전용 SSH 터널·키는 지원)
(Fleet RFC revision 6의 owner 요청 기반 전용 SSH 터널·키와 선택적 최초 설치는 지원)

Electron 셸, 데스크톱 패키징, 전용 알림 채널과 원격 target 확장 코드는
2026-07-23 제거를 확정했다. 웹/PWA 외의 전달 표면은 유지하지 않는다.
Expand Down Expand Up @@ -170,7 +170,10 @@ mosh, et.rs)도 모두 Tailscale 또는 포트 개방으로 수렴한다.
- [x] owner가 수동 생성하거나 hub에 명시적으로 생성을 요청한 SSH local forward에 한해 literal
`ws://127.0.0.1` 또는 `ws://[::1]` 허용
- [x] **Easy SSH setup**으로 전용 키 설치·터널 관리·페어링 자동 수행
(원격 ChatMux 설치와 SSH 도달성은 사전 준비, 비밀번호는 미보관)
(SSH 도달성은 사전 준비, 비밀번호는 미보관)
- [x] owner가 명시적으로 선택한 경우에만 Linux x86_64 원격 PC에 허브와 같은
릴리스 최초 설치. 기존 설치 복구·업데이트는 수행하지 않음
- [x] owner-only Tailscale PC 후보 목록으로 SSH 주소 입력 보조
- [x] host-qualified catalog/session/chat/verified terminal/completion routing
- [x] `offline`·`syncing`·`revoked`·`incompatible` fail-closed 상태와 명시적 reconnect
- [x] local-first revoke, peer 직접 UI 복구, installation-key loss revoke/re-pair
Expand Down Expand Up @@ -213,3 +216,4 @@ Fleet는 remote desktop/IDE, cloud sync, relay, 자동 failover, fleet updater
| 2026-08-27 | one hub + nine full peers fleet를 현재 기능으로 기록. Tailscale WSS 기본, literal loopback SSH 예외, owner-only enrollment, direct-peer recovery와 hub-first update를 계약으로 확정 |
| 2026-08-29 | P1·P2·P4를 현재 기능으로 기록. P3은 terminal fallback만 현재 기능이고 custom agent 감지, 여러 tmux 서버 discovery, 진단 화면은 열어 둔다 |
| 2026-09-03 | Fleet RFC revision 4와 #101에서 owner 요청 기반 SSH 간편 등록·전용 터널 관리를 추가. 기존 수동 loopback forward 경로도 유지 |
| 2026-09-05 | Fleet RFC revision 6: owner의 명시적 선택에 따른 원격 최초 설치와 Tailscale PC 후보 목록을 추가. 기존 설치 복구·업데이트는 제외 |
11 changes: 9 additions & 2 deletions docs/SELF-HOST.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Source and release semantics remain distinct. A source installation continues it
configured moving `origin/main` plus `deploy.sh` flow and never claims an exact
release target. The first updater-capable release still needs the manual bootstrap
below; later compatible releases can be updated from the owner’s mobile UI.
Easy SSH setup may also run the canonical installer for an explicitly requested
first installation of a missing peer (see the fleet section below).
Terminal/SSH use of `install.sh` is a manual bootstrap or recovery fallback, not
the ordinary mobile update experience.

Expand Down Expand Up @@ -173,8 +175,13 @@ Use Tailscale Serve HTTPS/WSS by default. The saved peer URL is the peer's actua
Serve host and port with `/fleet-ws`, for example
`wss://peer.example.ts.net:8443/fleet-ws`. Plain `ws://` is restricted to a
literal-loopback SSH local forward, created manually or through the hub owner's
**Easy SSH setup** action. Easy setup requires an already-installed remote ChatMux
backend on `127.0.0.1:3001` and a reachable SSH account. It installs a dedicated key,
**Easy SSH setup** action. Easy setup requires a reachable SSH account and uses
`127.0.0.1:3001`. The owner may explicitly enable **Install ChatMux if missing**
(off by default) for a new Linux x86_64 installation of the hub's exact published
version. Existing or broken installations require manual recovery; this is not a
fleet update path. A timeout may leave a partial installation that the owner must
inspect. Enrollment cleanup never uninstalls ChatMux or removes its data.
Easy setup installs a dedicated key,
obtains the pairing token, and manages the tunnel without saving the password.
Changed host keys fail closed; investigate the peer identity before reconnecting.
See [the SSH setup contract](REMOTE-ACCESS.md#82-default-transport-tailscale-httpswss).
Expand Down
24 changes: 24 additions & 0 deletions docs/evidence/ssh-bootstrap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SSH setup UI verification

These images show the production enrollment components in a local fixture with
simulated responses. No real SSH connection, remote installation, or credential
was used. The mobile cases use Chromium viewport emulation; this is not physical
phone or release-grade CUA evidence.

The matrix covers English desktop (1280×900), English mobile (390×844), and Korean
mobile (320×844): candidate pre-fill, default-off installation, explicit opt-in,
cleared passwords, success, missing CLI, unsupported platform, installation
failure, network failure, and horizontal overflow.

With the documented CUA Python/Playwright environment available, reproduce from
the repository root:

```sh
npm run client -- --host 127.0.0.1 --port 4341 --strictPort
# In another terminal:
python3 scripts/cua/ssh-bootstrap-ui.py --base-url http://127.0.0.1:4341
```

![English desktop fixture](desktop.png)

![Korean mobile fixture](mobile-ko.png)
Binary file added docs/evidence/ssh-bootstrap/desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/ssh-bootstrap/mobile-ko.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions docs/evidence/ssh-bootstrap/results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"scope": "simulated SSH UI; no remote operations",
"cases": [
{
"case": "desktop",
"language": "en",
"viewport": [
1280,
900
],
"scenarios": 5,
"result": "passed"
},
{
"case": "mobile-en",
"language": "en",
"viewport": [
390,
844
],
"scenarios": 5,
"result": "passed"
},
{
"case": "mobile-ko",
"language": "ko",
"viewport": [
320,
844
],
"scenarios": 5,
"result": "passed"
}
]
}
27 changes: 27 additions & 0 deletions scripts/cua/ssh-bootstrap-fixture.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ChatMux SSH setup review</title><style>html,body{height:auto!important;min-height:100%;overflow:auto!important}</style></head>
<body class="bg-background text-foreground"><main style="max-width:760px;margin:24px auto;padding:16px"><h1 style="font-size:24px;font-weight:700">ChatMux SSH setup</h1><p style="font-size:12px;margin:8px 0 20px">Local UI fixture · simulated responses · no SSH connections or installations</p><label>Test scenario <select id="scenario"><option value="success">Success</option><option value="missing">Missing CLI</option><option value="unsupported">Unsupported platform</option><option value="failed">Installation failed</option><option value="network">Network error</option></select></label><div id="app" style="margin-top:20px"></div><output id="fixture-result" style="display:block;margin-top:20px;font-size:12px;overflow-wrap:anywhere"></output></main>
<script type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import i18next from 'i18next';
import { I18nextProvider } from 'react-i18next';
import '/src/index.css';
import en from '/src/i18n/locales/en/settings.json';
import ko from '/src/i18n/locales/ko/settings.json';
import { fleetApi, FleetSettingsRequestError } from '/src/components/settings/fleet/fleetApi.ts';
import { FleetEnrollmentForm } from '/src/components/settings/view/tabs/fleet/FleetEnrollmentForm.tsx';
const i18n=i18next.createInstance();
await i18n.init({lng:new URL(location.href).searchParams.get('lang') || 'en',resources:{en:{settings:en},ko:{settings:ko}},ns:['settings'],defaultNS:'settings',interpolation:{escapeValue:false}});
fleetApi.sshCandidates=async()=>({available:true,defaultUser:'demo',candidates:[{hostName:'lab-linux',address:'100.64.0.9',os:'linux',online:true,supported:true},{hostName:'test-mac',address:'100.64.0.8',os:'macOS',online:true,supported:false}]});
const onSshEnroll=async(input)=>{
document.querySelector('#fixture-result').textContent=JSON.stringify({target:input.sshTarget,installCli:input.installCli,passwordProvided:!!input.password});
const scenario=document.querySelector('#scenario').value;
if(scenario==='network')throw new TypeError('simulated network failure');
if(scenario==='missing')throw new FleetSettingsRequestError('REMOTE_CLI_MISSING',409);
if(scenario==='unsupported')throw new FleetSettingsRequestError('REMOTE_PLATFORM_UNSUPPORTED',409,{os:'Darwin',arch:'arm64'});
if(scenario==='failed')throw new FleetSettingsRequestError('REMOTE_INSTALL_FAILED',502);
return {peerId:'fixture-peer',port:8022};
};
createRoot(document.querySelector('#app')).render(React.createElement(I18nextProvider,{i18n},React.createElement(FleetEnrollmentForm,{pending:false,onEnroll:async()=>{},onSshEnroll})));
</script></body></html>
Loading
Loading