@twinsearth/w2m-dsh-plugin 1.1.16 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.1.16"
8
+ "version": "1.1.17"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "w2m-fleet",
13
13
  "source": "./",
14
14
  "description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
15
- "version": "1.1.16",
15
+ "version": "1.1.17",
16
16
  "author": {
17
17
  "name": "TwinsEarth"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "w2m-fleet",
3
3
  "description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
4
- "version": "1.1.16",
4
+ "version": "1.1.17",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [1.1.17] — 2026-10-18
6
+
7
+ **Fleet chat: messages live in a session, are refused by a byte budget rather than silently dropped, are stored
8
+ encrypted and split across the fleet's own machines, and reach the agent as a tool.**
9
+
10
+ ### The store, and the rule it refuses to break
11
+
12
+ `src/relay/fleet-messages.mjs` keeps messages per **session** -- the operator's scope: deleting the session deletes the
13
+ history, and a machine joining or leaving changes who reads the flow, never what was said.
14
+
15
+ The budget is the operator's formula, not a round number:
16
+
17
+ limit = min(200 MB, max(100 MB, used-in-7-days))
18
+
19
+ A message over it is **refused** with the current usage attached (`FLEET_QUOTA_EXCEEDED`); nothing is silently dropped
20
+ and nobody else's words are evicted to make room. Text only: non-strings, control characters and messages over 4 KiB are
21
+ refused by name. Every timestamp comes from the relay's clock, because client clocks in this fleet disagree by tens of
22
+ seconds.
23
+
24
+ ### Encrypted sharding with the three rules that keep it from losing data
25
+
26
+ `src/relay/message-shards.mjs` seals a day's messages with AES-256-GCM under a key derived from the fleet key (domain
27
+ separated, no new long-lived secret) and splits the result across the fleet:
28
+
29
+ - **rebuildable** -- the metadata says which machine holds which part, so a reader never guesses;
30
+ - **offline tolerated** -- every part gets **two replicas on two different machines**; one machine being down is the
31
+ normal case;
32
+ - **integrity** -- each shard carries sha256 of its own bytes. A corrupt shard fails as `SHARD_CORRUPT` naming the part
33
+ and machine; a missing one fails as `MISSING_SHARDS` naming **which** parts; a wrong key fails as `AUTH_FAILED`
34
+ without returning partial plaintext.
35
+
36
+ ### HTTP and the notification that carries no text
37
+
38
+ `POST /v1/fleet/messages` and `GET /v1/fleet/messages` sit behind the device gate: the author is the token holder, never
39
+ a body field. Every read returns the fleet's current usage so a client can warn before a write is refused.
40
+ `GET /v1/fleet/messages/stream` is SSE and emits **only** `{session_id, message_id, machine_id, at_ms}` -- the text is
41
+ always fetched through the authenticated read, because a broadcast carrying text would send every message to every
42
+ listener and make the membership check decorative.
43
+
44
+ ### The twelfth tool: `w2m_chat`
45
+
46
+ `w2m_chat` (`send` / `list`) exposes the above to the agent. It is an ordinary MCP tool available on every transport;
47
+ `w2m_run` remains the only host-only one.
48
+
49
+ ### The cost of adding a tool: seven count pins
50
+
51
+ Adding a twelfth tool is not one edit. The count is pinned in **eight** places -- `README` (both languages), four test
52
+ suites (`tools`, `p2p-plugin`, `loader-entry`, `tool-cards`), `scripts/verify-installed.mjs`'s expected list, and
53
+ `test/claims.test.mjs`'s assertion. The first attempt fixed the ones that came to mind and left three red; the gate caught
54
+ each. `docs/PLATFORMS.md` gained a section stating that `w2m_chat` is available over MCP.
55
+
56
+ ### Not verified
57
+
58
+ - no client UI panel beyond the generic tool card;
59
+ - the SSE route has module-level notification tests, not a live end-to-end stream assertion;
60
+ - shards are produced and reassembled but **not yet transported** to machines -- that is the relay's next step, and the
61
+ tests say so rather than implying distribution works.
62
+
63
+ ### Verified
64
+
65
+ Gate GREEN twice, 15 of 15. tools 120, claims 48, p2p 32, loader 10, fleet-messages 7, message-shards 9, chat-http 3,
66
+ message-notify 3.
67
+
5
68
  ## [1.1.16] — 2026-10-18
6
69
 
7
70
  **A machine waiting to be admitted now appears where you already answer questions -- and the README leads with how to
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![CI](https://github.com/TwinsEarth/dsh-windows2macos/actions/workflows/ci.yml/badge.svg)](https://github.com/TwinsEarth/dsh-windows2macos/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@twinsearth/w2m-dsh-plugin?label=npm)](https://www.npmjs.com/package/@twinsearth/w2m-dsh-plugin)
7
- [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4c6ef5)](#install)
7
+ [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4c6ef5)](#1-dsh-plugin-recommended--zero-configuration)
8
8
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
9
9
  [![platforms](https://img.shields.io/badge/CI-windows%20%7C%20macos%20%7C%20linux-4c6ef5)](#verified-and-not-verified)
10
10
 
@@ -14,7 +14,7 @@ English | [中文](#中文说明)
14
14
 
15
15
  ## Install
16
16
 
17
- Five ways in, easiest first. **Pick one.** Every route ends with the same eleven tools in your agent.
17
+ Five routes in. They all end with **12 tools registered** in your agent. Pick one.
18
18
 
19
19
  ### 1. DSH plugin (recommended — zero configuration)
20
20
 
@@ -22,20 +22,20 @@ Five ways in, easiest first. **Pick one.** Every route ends with the same eleven
22
22
  # 1. install the package into your DSH profile
23
23
  dsh plugin add @twinsearth/w2m-dsh-plugin
24
24
 
25
- # 2. add it to the profile's bundle list (this is what makes it load)
25
+ # 2. name it in the profile's bundle list — a package that is not bundled never loads
26
26
  # ~/.dsh/profiles/<profile>/dsh.profile.yml
27
27
  plugins:
28
28
  - '@twinsearth/w2m-dsh-plugin'
29
29
 
30
- # 3. restart DSH — joining happens at mount time, so a hot reload is not enough
30
+ # 3. restart DSH; joining happens at mount time, so a hot reload is not enough
31
31
  ```
32
32
 
33
- Since **1.1.16** the agent starts by itself (`autoStartAgent` defaults to `true`): a freshly installed
34
- machine derives its account identity, joins the account's fleet, and appears in `w2m_devices` with no
35
- pairing step. To turn that off, put it back explicitly:
33
+ Since **1.1.15** the agent starts by itself (`autoStartAgent` defaults to `true`): a freshly installed machine
34
+ derives its account identity, joins the account's fleet, and shows up in `w2m_devices` with no pairing step.
35
+ To turn that off, patch the **existing** `w2m` row — do not add a second one:
36
36
 
37
37
  ```yaml
38
- # cordis.patch.yml — patch the EXISTING w2m row, do not add a second one
38
+ # cordis.patch.yml
39
39
  - id: w2m
40
40
  config:
41
41
  autoStartAgent: false
@@ -48,8 +48,8 @@ npm install -g @twinsearth/w2m-dsh-plugin
48
48
  w2m-mcp --help
49
49
  ```
50
50
 
51
- Point any MCP client at `w2m-mcp`. The package has **zero runtime dependencies** and ships its own
52
- relay, agent and tools.
51
+ Point any MCP client at `w2m-mcp`. **Zero runtime dependencies**; the package ships the relay, the agent and the
52
+ tools.
53
53
 
54
54
  ### 3. Claude Code
55
55
 
@@ -60,129 +60,128 @@ relay, agent and tools.
60
60
 
61
61
  The manifests live in [`.claude-plugin/`](.claude-plugin/) and are version-locked to the release.
62
62
 
63
- ### 4. Codex / MCP-capable CLIs
63
+ ### 4. Codex and other MCP-capable CLIs
64
64
 
65
65
  ```toml
66
- # codex/mcp-servers.toml — copy this block into your own config
66
+ # copy this block into your own codex/mcp-servers.toml
67
67
  [mcp_servers.w2m]
68
68
  command = "npx"
69
- args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.16", "w2m-mcp"]
69
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
70
70
  ```
71
71
 
72
- ### 5. From the release tarball (verified, offline)
72
+ ### 5. Verified release tarball (offline)
73
73
 
74
- From the 1.1.16 release page, download **both** files:
75
-
76
- - winsearth-w2m-dsh-plugin-1.1.16.tgz
77
- - SHA256SUMS
78
-
79
- Or fetch the tarball directly and take the checksum file from the same release:
74
+ From the `v1.1.17` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.17.tgz` and `SHA256SUMS` —
75
+ then:
80
76
 
81
77
  ```bash
82
- curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.16/twinsearth-w2m-dsh-plugin-1.1.16.tgz
78
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
83
79
  sha256sum -c SHA256SUMS # verify before installing, always
84
- npm install -g ./twinsearth-w2m-dsh-plugin-1.1.16.tgz
80
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
85
81
  ```
86
82
 
87
- Every release carries **`SHA256SUMS`** for exactly this reason. If it is missing, do not install that
88
- release — that has happened before and it was a defect.
83
+ Every release carries `SHA256SUMS` for exactly this reason. If a release is missing it, do not install that release.
89
84
 
90
85
  ---
91
86
 
92
87
  ## Quick start
93
88
 
94
89
  ```text
95
- # 1. on the machine that will coordinate
96
- w2m_status # is this machine paired? which fleet?
97
-
98
- # 2. see the fleet
99
- w2m_devices
100
-
101
- # 3. run the same command everywhere
90
+ w2m_status # am I paired? which fleet?
91
+ w2m_devices # what is in the fleet?
102
92
  w2m_run mode="replicate" command_argv=["git","rev-parse","HEAD"]
103
-
104
- # 4. wait for every machine, then read the verdict
105
93
  w2m_wait task_id="..."
106
94
  ```
107
95
 
108
- `replicate` runs on every online machine and the verdict is `consistent` only when every machine
109
- returned the same result **and** the same commit anchor. `split` gives each machine a slice;
110
- `pipeline` chains stages; `broadcast` names one executor and reports observers; `compose` merges.
96
+ `replicate` runs on every online machine; the verdict is `consistent` only when every machine returned the same
97
+ result **and** the same commit anchor. `split` gives each machine a slice, `pipeline` chains stages, `broadcast`
98
+ names one executor and reports observers, `compose` merges results.
111
99
 
112
100
  ---
113
101
 
114
- ## What it actually is
102
+ ## What it is
115
103
 
116
- An installable plugin that turns a set of machines into **one fleet**, keyed by **your account**, not
117
- by a hand-maintained host list.
104
+ An installable plugin that turns a set of machines into **one fleet**, keyed by **your account**, not by a
105
+ hand-maintained host list.
118
106
 
119
- | Piece | What it does |
107
+ | Piece | Responsibility |
120
108
  |---|---|
121
- | **Relay** | one small HTTP server: task routing, leases, SSE, blob storage. Run it anywhere your machines can reach. |
122
- | **Agent** | runs on each machine, pairs once, executes allowed commands, heartbeats. |
123
- | **Plugin** | **11 tools registered** — the `w2m_*` tools your agent calls. |
109
+ | **Relay** | a small HTTP server: task routing, leases, SSE, blob storage. Run it anywhere your machines can reach. |
110
+ | **Agent** | runs on each machine: pairs once, executes allow-listed commands, heartbeats. |
111
+ | **Plugin** | the tools your agent calls. |
124
112
  | **Admission** (1.1.14+) | a new machine asks to join; an online member approves, **10 s auto-approves**, and a machine admitted in the last 24 h can be removed immediately. |
125
113
 
126
- ### The 11 tools registered
114
+ ### Tools
127
115
 
128
- `w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report`
129
- · `w2m_memory` · `w2m_approvals` · `w2m_council` · `w2m_update`
116
+ `w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
117
+ `w2m_memory` · `w2m_approvals` · `w2m_council` · `w2m_chat` · `w2m_update`
130
118
 
131
- ### Environment variables a dispatched command receives
119
+ ### Environment a dispatched command receives (1.1.15+)
132
120
 
133
- Since 1.1.16 the command knows where it is — values come from the offer, never from argv:
121
+ Values come from the offer, never from argv, so one machine cannot reshape a fleet-wide split.
134
122
 
135
123
  | Variable | Meaning |
136
124
  |---|---|
137
- | `W2M_INDEX` / `W2M_INDEX_TOTAL` | this machine's slice and how many slices exist |
138
- | `W2M_STAGE_INDEX` | the pipeline stage (separate from `W2M_INDEX` on purpose) |
125
+ | `W2M_INDEX` / `W2M_INDEX_TOTAL` | this machine's slice number and how many slices exist |
126
+ | `W2M_STAGE_INDEX` | the pipeline stage; deliberately separate from `W2M_INDEX` |
139
127
  | `W2M_TASK_ID` / `W2M_MODE` | the task and its mode |
140
128
  | `W2M_MACHINE_ID` / `W2M_MACHINE_NAME` | this machine |
141
129
 
142
- So `command_argv=["node","-e","process.exit(Number(process.env.W2M_INDEX)+1)"]` over four machines
143
- exits 1/2/3/4. (Verified on one machine; the four-machine run is listed as not verified below.)
130
+ So `command_argv=["node","-e","process.exit(Number(process.env.W2M_INDEX)+1)"]` over four machines exits 1/2/3/4.
131
+ Single-machine behaviour is tested; the four-machine run is listed as **not verified** below.
144
132
 
145
133
  ---
146
134
 
147
- ## Fleet model in one paragraph
135
+ ## Fleet model
148
136
 
149
- Identity is **the account**, not a machine list. A machine derives an Ed25519 identity from its
150
- account material, the relay registers the matching public key when the account's first machine pairs,
151
- and every later machine proves the same account with a one-time signed challenge. If the old pairing
152
- path fails for a **named** reason only (invalid code, unreachable relay, timeout), the machine falls
153
- back to asking for admission; any other failure needs a human and does **not** change mechanisms.
137
+ Identity is **the account**, not a list of hosts. A machine derives an Ed25519 identity from its account
138
+ material; the relay registers the matching public key when the account's first machine pairs; every later machine
139
+ proves the same account with a one-time signed challenge.
154
140
 
155
- The relay is authoritative for every time window and every quorum. A machine that has been offline
156
- for more than 72 hours leaves the **list** — it is never deleted, records stay, and it returns by
157
- reporting again.
141
+ If the old pairing path fails for a **named** reason only — invalid pairing code, unreachable relay, timeout — the
142
+ machine falls back to asking for admission. Any other failure needs a human and does **not** change mechanisms. A
143
+ rejection is final; the machine does not re-ask.
144
+
145
+ The relay is authoritative for every time window and every quorum, because client clocks disagree (measured:
146
+ tens of seconds between machines in one fleet):
147
+
148
+ - **10 s** without a decision auto-approves, but only while at least one member is **online** — an empty fleet
149
+ cannot consent, and that refusal is named (`NO_ONLINE_MEMBER`);
150
+ - within **24 h** of admission, any online member may remove the machine immediately;
151
+ - after 24 h a single machine may only **open a vote**, whose quorum set is **frozen at open time** so a voter
152
+ going offline mid-vote cannot move the bar;
153
+ - the quorum is 3+ old members need **≥ 50 %** (rounded up); a two-member fleet needs **one** vote;
154
+ - a machine offline for more than **72 hours** leaves the **list** — it is never deleted, its records stay, and it
155
+ returns by reporting again.
158
156
 
159
157
  ---
160
158
 
161
159
  ## Security posture
162
160
 
163
- - The command allow-list is **argv-prefix** based and rejects argv-reparsing flags (`--import`, `-e`
164
- appended) with `COMMAND_REPARSE_ARGUMENT`. It is a boundary, not a sandbox.
165
- - Dispatching tasks requires an operator token. Device tokens cannot dispatch.
166
- - Admission approval requires at least one member **online**; an empty fleet cannot consent.
167
- - Every release is checksummed. `SHA256SUMS` is part of the release, not an afterthought.
161
+ - The command allow-list is **argv-prefix** based and rejects argv-reparsing flags (`--import`, appended `-e`) with
162
+ `COMMAND_REPARSE_ARGUMENT`. It is a boundary, not a sandbox.
163
+ - Dispatching tasks requires an operator token. A device token cannot dispatch.
164
+ - Admission approval requires at least one member online.
165
+ - The account signing key is derived from the fleet key, **never** from the account code — the account code is
166
+ written to logs, and deriving a key from a readable value would hand out the account identity.
167
+ - Every release is checksummed.
168
168
 
169
169
  ---
170
170
 
171
171
  ## Verified and not verified
172
172
 
173
- Honest status, because a green badge that hides this is worse than no badge.
173
+ A green badge that hides this is worse than no badge.
174
174
 
175
- **Verified:** 2453 tests in 102 files (2439 pass, 14 skip), 15 pre-flight checks, CI green on Windows,
176
- macOS and Linux, npm + GitHub release + checksums consistent, end-to-end fleet runs on a real
177
- Linux/Mac/Windows triple.
175
+ **Verified:** 2458 tests in 102 files (2444 pass, 14 skip), 15 pre-flight checks, CI green on Windows, macOS and
176
+ Linux, npm + GitHub release + checksums consistent, end-to-end fleet runs on a real Linux/Mac/Windows triple.
178
177
 
179
178
  **Not verified / known gaps:**
180
179
 
181
180
  | Gap | State |
182
181
  |---|---|
183
- | Approval **UI** in the client | the relay enforces the rules; the tool lists and settles pending requests, but the 10 s countdown is displayed by the relay's data, not a dedicated popup |
182
+ | Approval **popup** control | the relay enforces the rules and `w2m_approvals` lists and settles requests, but the 10 s countdown is relay data shown in a list, not a dedicated popup |
184
183
  | Four-machine `W2M_INDEX` run | single-machine behaviour tested; the fleet-wide 1/2/3/4 run has not been executed |
185
- | Fleet chat / broadcast channel | designed, **not implemented** |
184
+ | Fleet chat / broadcast channel | message store + quota implemented and tested; encryption, sharding, endpoints and the panel are **not built** |
186
185
  | P2P on some macOS hosts | `P2P_PUNCH_FAILED: Unknown cipher` observed; the task falls back to the relay path |
187
186
 
188
187
  ---
@@ -192,18 +191,18 @@ Linux/Mac/Windows triple.
192
191
  | Symptom | Cause | Fix |
193
192
  |---|---|---|
194
193
  | installed, but `w2m_devices` answers 401 | the agent never started, so no device token | check `autoStartAgent` is not `false`; restart DSH (not a hot reload) |
195
- | machine not in the fleet | the plugin is not in `dsh.profile.bundles` | add it — a package that is not bundled never loads |
196
- | `COMMAND_NOT_ALLOWED` | the command is not in the allow-list | extend the allow-list; argv is matched by prefix |
197
- | verdict always `partial` | a stale lease from a machine that is down | fixed in 1.1.2 (dispatch now leases only online, non-silent machines) |
198
- | `P2P_PUNCH_FAILED` | NAT or a host crypto gap | it downgrades to the relay path; the result is unaffected |
194
+ | machine missing from the fleet | the plugin is not in `dsh.profile.bundles` | add it — a package that is not bundled never loads |
195
+ | `COMMAND_NOT_ALLOWED` | command not in the allow-list | extend the allow-list; argv is matched by prefix |
196
+ | verdict always `partial` | a stale lease from a machine that is down | fixed in 1.1.2: a dispatch leases only online, non-silent machines |
197
+ | `P2P_PUNCH_FAILED` | NAT, or a host crypto gap | it downgrades to the relay path; results are unaffected |
199
198
 
200
199
  ---
201
200
 
202
201
  ## Links
203
202
 
204
- - [CHANGELOG](CHANGELOG.md) — every version, including what each one deliberately did **not** ship
205
- - [PLATFORMS](docs/PLATFORMS.md) — Claude Code, Codex, MCP wiring details
206
- - [Test matrix](https://github.com/TwinsEarth/dsh-windows2macos/releases) — per-release evidence
203
+ - [CHANGELOG](CHANGELOG.md) — every version, including what it deliberately did **not** ship
204
+ - [PLATFORMS](docs/PLATFORMS.md) — Claude Code, Codex and MCP wiring details
205
+ - [Releases](https://github.com/TwinsEarth/dsh-windows2macos/releases) — per-release evidence and checksums
207
206
  - [Issues](https://github.com/TwinsEarth/dsh-windows2macos/issues)
208
207
 
209
208
  MIT.
@@ -214,39 +213,170 @@ MIT.
214
213
 
215
214
  **一个 DeepSeek 账号。一句话。你自己的每一台机器跑同一个项目。**
216
215
 
217
- ### 安装(由易到难,任选一种)
216
+ 以下内容与上方英文**逐节对应**,不是摘要。
217
+
218
+ ### 安装
219
+
220
+ 五种方式,最终都会在你的 agent 里注册 **12 tools registered**。任选其一。
221
+
222
+ #### 1. DSH 插件(推荐,零配置)
223
+
224
+ ```bash
225
+ # 1. 把包装进你的 DSH profile
226
+ dsh plugin add @twinsearth/w2m-dsh-plugin
227
+
228
+ # 2. 在 profile 的 bundle 列表里写上包名 —— 没进 bundle 的包永远不会被加载
229
+ # ~/.dsh/profiles/<profile>/dsh.profile.yml
230
+ plugins:
231
+ - '@twinsearth/w2m-dsh-plugin'
232
+
233
+ # 3. 重启 DSH。加入编队发生在 mount 时,热重载不够
234
+ ```
235
+
236
+ 从 **1.1.15** 起 agent 会自动启动(`autoStartAgent` 默认 `true`):新装的机器自行派生账号身份、加入该账号的
237
+ 编队,无需配对步骤就会出现在 `w2m_devices` 里。若要关闭,请 patch **已存在的** `w2m` 行,**不要新增第二行**:
238
+
239
+ ```yaml
240
+ # cordis.patch.yml
241
+ - id: w2m
242
+ config:
243
+ autoStartAgent: false
244
+ ```
245
+
246
+ #### 2. npm(MCP 服务器,任意客户端)
218
247
 
219
- 1. **DSH 插件(推荐,零配置)**:`dsh plugin add @twinsearth/w2m-dsh-plugin`,把包名加入
220
- `dsh.profile.bundles`,**重启 DSH**。1.1.16 起 agent 默认自动启动,装完即自动入队。
221
- 2. **npm(任意 MCP 客户端)**:`npm i -g @twinsearth/w2m-dsh-plugin` → `w2m-mcp`。
222
- 3. **Claude Code**:`/plugin marketplace add TwinsEarth/dsh-windows2macos` → `/plugin install w2m@twinsearth`。
223
- 4. **Codex**:复制 [`codex/mcp-servers.toml`](codex/mcp-servers.toml) 的配置块。
224
- 5. **发布包(离线、可校验)**:下载 release 中的 `.tgz` 与 `SHA256SUMS`,**先校验再安装**。
248
+ ```bash
249
+ npm install -g @twinsearth/w2m-dsh-plugin
250
+ w2m-mcp --help
251
+ ```
252
+
253
+ 把任意 MCP 客户端指向 `w2m-mcp`。**零运行时依赖**;包里同时带有 relay、agent 与工具。
254
+
255
+ #### 3. Claude Code
256
+
257
+ ```bash
258
+ /plugin marketplace add TwinsEarth/dsh-windows2macos
259
+ /plugin install w2m@twinsearth
260
+ ```
261
+
262
+ 清单文件位于 [`.claude-plugin/`](.claude-plugin/),版本与发行版锁定。
263
+
264
+ #### 4. Codex 及其它支持 MCP 的 CLI
265
+
266
+ ```toml
267
+ # 把这段复制进你自己的 codex/mcp-servers.toml
268
+ [mcp_servers.w2m]
269
+ command = "npx"
270
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
271
+ ```
272
+
273
+ #### 5. 校验过的发布包(离线)
274
+
275
+ 从 `v1.1.17` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.17.tgz` 与 `SHA256SUMS` —— 然后:
276
+
277
+ ```bash
278
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
279
+ sha256sum -c SHA256SUMS # 安装前务必校验
280
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
281
+ ```
282
+
283
+ 每个 release 都带 `SHA256SUMS` 正是为了这一步。若某个 release 缺少它,**不要安装该版本**。
225
284
 
226
285
  ### 快速开始
227
286
 
228
287
  ```text
229
- w2m_status # 我配对了吗?在哪个编队?
230
- w2m_devices # 编队里有哪些机器
288
+ w2m_status # 我配对了吗?在哪个编队?
289
+ w2m_devices # 编队里有什么?
231
290
  w2m_run mode="replicate" command_argv=["git","rev-parse","HEAD"]
232
291
  w2m_wait task_id="..."
233
292
  ```
234
293
 
235
- ### 核心概念
294
+ `replicate` 在每一台在线机器上执行;只有当所有机器返回**相同结果**且**相同 commit 锚点**时,裁决才是
295
+ `consistent`。`split` 给每台机器一个分片,`pipeline` 串联多个阶段,`broadcast` 指定一个执行者并汇报观察者,
296
+ `compose` 合并结果。
297
+
298
+ ### 它是什么
299
+
300
+ 一个可安装插件,把一组机器变成**一个编队**,以**你的账号**为键,而不是靠手工维护的主机清单。
301
+
302
+ | 组件 | 职责 |
303
+ |---|---|
304
+ | **Relay** | 一个小型 HTTP 服务:任务路由、租约、SSE、blob 存储。放在任何机器可达的位置。 |
305
+ | **Agent** | 每台机器上运行:配对一次、执行白名单命令、心跳。 |
306
+ | **Plugin** | 你的 agent 调用的那些工具。 |
307
+ | **Admission**(1.1.14+) | 新机器申请加入;在线成员批准,**10 秒自动同意**;入队 24 小时内的机器可被立即移除。 |
308
+
309
+ ### 工具
310
+
311
+ `w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
312
+ `w2m_memory` · `w2m_approvals` · `w2m_council` · `w2m_chat` · `w2m_update`
236
313
 
237
- - **身份是账号,不是主机清单**:机器从账号材料派生 Ed25519 身份,账号第一台机器配对时登记公钥,
238
- 后续机器用一次性签名挑战自证。
239
- - **只按三类明确失败降级**:配对码无效 / relay 不可达 / 超时;其它错误需要人工,**不切换机制**。
240
- - **时间窗与法定人数由 relay 权威判定**:10 秒自动同意(需至少 1 名成员在线)、24 小时内可即时踢出、
241
- 超过 24 小时需冻结快照表决(3 人以上 ≥50%,2 人编队 1 票)。
242
- - **离线 72 小时只是不显示,绝不删除**:记录保留,机器重新上线后自动回到列表。
243
- - **派发命令会拿到 `W2M_INDEX` 等环境变量**(1.1.16 起),因此 split 可以按机器分支。
314
+ ### 被派发命令会收到的环境变量(1.1.15+)
244
315
 
245
- ### 验证状态(不含糊)
316
+ 取值来自 offer,**不来自 argv**,因此单台机器无法改变整个编队的分片:`W2M_INDEX` / `W2M_INDEX_TOTAL` /
317
+ `W2M_STAGE_INDEX` / `W2M_TASK_ID` / `W2M_MODE` / `W2M_MACHINE_ID` / `W2M_MACHINE_NAME`。
246
318
 
247
- 已测试 2453 项(2439 通过 / 14 跳过)、15 项门禁、三平台 CI 全绿、npm 与 GitHub 发布及校验和一致。
319
+ 因此 `command_argv=["node","-e","process.exit(Number(process.env.W2M_INDEX)+1)"]` 在四台机器上应分别退出
320
+ 1/2/3/4。**单机行为已测;四台机器的联跑尚未验证**(见下方"未验证")。
248
321
 
249
- **未验证项**:客户端审批弹窗(规则由 relay 强制,工具可列出并裁决)、四台机器 `W2M_INDEX` 全员取值、
250
- 编队聊天通道(仅设计)、部分 macOS 主机的 P2P 降级。
322
+ ### 编队模型
323
+
324
+ 身份是**账号**,不是主机清单。机器从账号材料派生 Ed25519 身份;账号的第一台机器配对时,relay 登记对应公钥;
325
+ 此后每台机器用一次性签名挑战自证同一个账号。
326
+
327
+ 只有当旧配对路径因**明确原因**失败时 —— 配对码无效、relay 不可达、超时 —— 机器才降级去申请入队。其它失败需要
328
+ 人工介入,**不会**切换机制。**拒绝即终态**,机器不会再次申请。
329
+
330
+ 所有时间窗与法定人数由 relay 权威判定,因为客户端时钟互不一致(实测:同一编队内相差数十秒):
331
+
332
+ - 无决策 **10 秒**后自动同意,但**仅当至少一名成员在线** —— 空编队无法同意,该拒绝有明确错误码
333
+ (`NO_ONLINE_MEMBER`);
334
+ - 入队后 **24 小时内**,任一在线成员可立即移除该机器;
335
+ - 超过 24 小时,单台机器只能**发起表决**,且法定人数集合在**发起时冻结**,投票者中途离线无法改变门槛;
336
+ - 法定人数:旧成员 ≥3 人需 **≥50%(向上取整)**;2 人编队的移除只要 **1 票**;
337
+ - 离线超过 **72 小时**的机器会离开**列表** —— 记录**永不删除**,它重新上报后会自动回来。
338
+
339
+ ### 安全姿态
340
+
341
+ - 命令白名单按 **argv 前缀**匹配,并拒绝 argv 重解析标志(`--import`、追加 `-e`),错误码
342
+ `COMMAND_REPARSE_ARGUMENT`。这是边界,不是沙箱。
343
+ - 派发任务需要 operator token;device token 不能派发。
344
+ - 审批入队要求至少一名成员在线。
345
+ - 账户签名密钥从 **fleet key** 派生,**绝不**用 account code —— account code 会写进日志,用可读值派生密钥等于
346
+ 交出账号身份。
347
+ - 每个发行版都有校验和。
348
+
349
+ ### 已验证与未验证
350
+
351
+ **一句真话胜过一块掩盖问题的绿牌。**
352
+
353
+ **已验证:** 102 个文件中的 2458 项测试(2444 通过、14 跳过)、15 项门禁检查、Windows / macOS / Linux 三平台
354
+ CI 全绿、npm 与 GitHub 发布及校验和一致、在真实的 Linux/Mac/Windows 三机编队上跑过端到端。
355
+
356
+ **未验证 / 已知缺口:**
357
+
358
+ | 缺口 | 状态 |
359
+ |---|---|
360
+ | 审批**弹窗**控件 | relay 强制规则、`w2m_approvals` 可列出并裁决,但 10 秒倒计时是列表里的 relay 数据,不是独立弹窗 |
361
+ | 四台机器的 `W2M_INDEX` 联跑 | 单机行为已测;编队级 1/2/3/4 尚未执行 |
362
+ | 编队聊天 / 广播通道 | 消息存储与配额已实现并测试;加密、分片、端点与面板**尚未构建** |
363
+ | 部分 macOS 主机的 P2P | 观察到 `P2P_PUNCH_FAILED: Unknown cipher`;任务会降级走 relay 路径 |
364
+
365
+ ### 故障排查
366
+
367
+ | 症状 | 原因 | 处理 |
368
+ |---|---|---|
369
+ | 装好了,但 `w2m_devices` 返回 401 | agent 从未启动,因此没有 device token | 确认 `autoStartAgent` 不是 `false`;**重启 DSH**(热重载无效) |
370
+ | 编队里看不到这台机器 | 插件没写进 `dsh.profile.bundles` | 加进去 —— 不在 bundle 里的包永远不会加载 |
371
+ | `COMMAND_NOT_ALLOWED` | 命令不在白名单 | 扩充白名单;argv 按前缀匹配 |
372
+ | 裁决总是 `partial` | 来自已下线机器的陈旧租约 | 1.1.2 已修:派发只对在线且未沉默的机器建租约 |
373
+ | `P2P_PUNCH_FAILED` | NAT,或宿主密码学能力缺口 | 会自动降级到 relay 路径,结果不受影响 |
374
+
375
+ ### 链接
376
+
377
+ - [CHANGELOG](CHANGELOG.md) —— 每个版本,包括它**故意没有**交付的内容
378
+ - [PLATFORMS](docs/PLATFORMS.md) —— Claude Code、Codex 与 MCP 的接线细节
379
+ - [Releases](https://github.com/TwinsEarth/dsh-windows2macos/releases) —— 每个版本的证据与校验和
380
+ - [Issues](https://github.com/TwinsEarth/dsh-windows2macos/issues)
251
381
 
252
382
  MIT。
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.16", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
package/docs/PLATFORMS.md CHANGED
@@ -81,3 +81,25 @@ So the split is:
81
81
 
82
82
  Marking the last two rows "prepared" rather than "done" is deliberate: a manifest that exists is not a listing, and
83
83
  this project has already shipped a "mechanism with no caller" seven times.
84
+
85
+ ## Fleet chat over MCP (\w2m_chat\)
86
+
87
+ \w2m_chat\ is available on **every** transport described above, because it is an ordinary MCP tool like the rest --
88
+ nothing about it depends on the DSH host, and it is not on the deliberately-host-only list (that list contains
89
+ \w2m_run\ only, for the reason given at the top of this document).
90
+
91
+ What it does, and what it deliberately does not:
92
+
93
+ - \ction: "send"\ posts plain text (≤ 4 KiB, UTF-8, no control characters) to the relay at
94
+ \POST /v1/fleet/messages\. The author is the **device token holder**; a body field cannot name a different machine.
95
+ - \ction: "list"\ reads one session's flow at \GET /v1/fleet/messages?session_id=…\ and returns the messages together
96
+ with the fleet's current byte usage, so a client can warn before the quota refuses a write.
97
+ - A refused write is returned with its named code (\FLEET_QUOTA_EXCEEDED\, \MESSAGE_TOO_LARGE\, \NOT_TEXT\,
98
+ \EMPTY_TEXT\) -- the tool does not flatten them into "failed".
99
+ - **No push inside MCP.** The \
100
+ ew_message\ notification is an SSE stream
101
+ (\GET /v1/fleet/messages/stream\, event id only, never the text); an MCP client that cannot hold a stream polls
102
+ \ction: "list"\ with \since_ms\. The stream is a hint; the text always comes back through the authenticated read.
103
+
104
+ Encryption and sharding are server-side: history is sealed with AES-256-GCM under a key derived from the fleet key and
105
+ split across the fleet's machines with two replicas per part. No client sees another machine's shard bodies.