@twinsearth/w2m-dsh-plugin 1.1.16 → 1.1.18

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.18"
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.18",
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.18",
5
5
  "author": {
6
6
  "name": "TwinsEarth"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,114 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [1.1.18] — 2026-10-18
6
+
7
+ **The developer declares this a STABLE release.** It ships the three-channel updater and the release plumbing that makes
8
+ the beta channel real.
9
+
10
+ ### Three channels, resolved from the two releases that exist
11
+
12
+ `w2m_update action:"status" channels:true` reports:
13
+
14
+ | Channel | Source | Rule |
15
+ |---|---|---|
16
+ | `stable` | GitHub's `/releases/latest` | never a prerelease |
17
+ | `beta` | the newest **prerelease**, fetched from `/releases` and filtered | exists only when one is published |
18
+ | `latest` | whichever of the two is newer by SemVer | equal versions resolve to stable |
19
+
20
+ Every decision comes from the existing `decideUpdate`, so the channel logic cannot drift from the install rule; this
21
+ module only chooses which two releases to ask about. A missing beta is reported as `available: null` rather than
22
+ substituted with stable — "there is no beta right now" is a fact the UI has to be able to show as a disabled button.
23
+
24
+ **The channel report is opt-in.** A plain `status` remains local and offline; making every status call reach GitHub would
25
+ slow the tool and break it behind a proxy. Asserted by a test that fails if any fetch happens.
26
+
27
+ ### Installing a channel, with the checksum enforced twice
28
+
29
+ `w2m_update action:"apply" channel:"stable"|"beta"|"latest"`: the release's `SHA256SUMS` is downloaded, the hash for the
30
+ tarball is required (no hash, no install), `downloadVerified` removes its temp file on any mismatch, and `installUpdate`
31
+ receives `expectedSha256` as well and keeps its rollback path. A mismatch is not retried: it means the asset and its
32
+ checksums disagree, which is never a transient condition.
33
+
34
+ ### The beta channel is a GitHub prerelease, and never npm
35
+
36
+ `.github/workflows/beta.yml`: a bump is a beta by default; a beta becomes a **prerelease** (so neither GitHub nor npm
37
+ resolves it as latest) gated by the `beta-access` environment, which requires a reviewer. There is **no `npm publish`
38
+ line in that file** — a channel cannot leak into the default install if the publication cannot happen at all. npm
39
+ receives a version only when the developer declares it stable, as this release does.
40
+
41
+ `.github/workflows/packages.yml` mirrors a released stable version to GitHub Packages with the registry overridden **on
42
+ the command line**, because `publishConfig.registry` would silently redirect the operator-channel publish too.
43
+
44
+ ### Verified
45
+
46
+ Gate GREEN twice, 15/15. 2504 tests / 2490 pass / 0 fail / 14 skip. tools 122, update-channels 4, update-prerelease 2.
47
+ The first GitHub Packages publication succeeded; the package page itself is confirmed by the operator, since the PAT has
48
+ no `read:packages` scope to read it back.
49
+
50
+ ## [1.1.17] — 2026-10-18
51
+
52
+ **Fleet chat: messages live in a session, are refused by a byte budget rather than silently dropped, are stored
53
+ encrypted and split across the fleet's own machines, and reach the agent as a tool.**
54
+
55
+ ### The store, and the rule it refuses to break
56
+
57
+ `src/relay/fleet-messages.mjs` keeps messages per **session** -- the operator's scope: deleting the session deletes the
58
+ history, and a machine joining or leaving changes who reads the flow, never what was said.
59
+
60
+ The budget is the operator's formula, not a round number:
61
+
62
+ limit = min(200 MB, max(100 MB, used-in-7-days))
63
+
64
+ A message over it is **refused** with the current usage attached (`FLEET_QUOTA_EXCEEDED`); nothing is silently dropped
65
+ and nobody else's words are evicted to make room. Text only: non-strings, control characters and messages over 4 KiB are
66
+ refused by name. Every timestamp comes from the relay's clock, because client clocks in this fleet disagree by tens of
67
+ seconds.
68
+
69
+ ### Encrypted sharding with the three rules that keep it from losing data
70
+
71
+ `src/relay/message-shards.mjs` seals a day's messages with AES-256-GCM under a key derived from the fleet key (domain
72
+ separated, no new long-lived secret) and splits the result across the fleet:
73
+
74
+ - **rebuildable** -- the metadata says which machine holds which part, so a reader never guesses;
75
+ - **offline tolerated** -- every part gets **two replicas on two different machines**; one machine being down is the
76
+ normal case;
77
+ - **integrity** -- each shard carries sha256 of its own bytes. A corrupt shard fails as `SHARD_CORRUPT` naming the part
78
+ and machine; a missing one fails as `MISSING_SHARDS` naming **which** parts; a wrong key fails as `AUTH_FAILED`
79
+ without returning partial plaintext.
80
+
81
+ ### HTTP and the notification that carries no text
82
+
83
+ `POST /v1/fleet/messages` and `GET /v1/fleet/messages` sit behind the device gate: the author is the token holder, never
84
+ a body field. Every read returns the fleet's current usage so a client can warn before a write is refused.
85
+ `GET /v1/fleet/messages/stream` is SSE and emits **only** `{session_id, message_id, machine_id, at_ms}` -- the text is
86
+ always fetched through the authenticated read, because a broadcast carrying text would send every message to every
87
+ listener and make the membership check decorative.
88
+
89
+ ### The twelfth tool: `w2m_chat`
90
+
91
+ `w2m_chat` (`send` / `list`) exposes the above to the agent. It is an ordinary MCP tool available on every transport;
92
+ `w2m_run` remains the only host-only one.
93
+
94
+ ### The cost of adding a tool: seven count pins
95
+
96
+ Adding a twelfth tool is not one edit. The count is pinned in **eight** places -- `README` (both languages), four test
97
+ suites (`tools`, `p2p-plugin`, `loader-entry`, `tool-cards`), `scripts/verify-installed.mjs`'s expected list, and
98
+ `test/claims.test.mjs`'s assertion. The first attempt fixed the ones that came to mind and left three red; the gate caught
99
+ each. `docs/PLATFORMS.md` gained a section stating that `w2m_chat` is available over MCP.
100
+
101
+ ### Not verified
102
+
103
+ - no client UI panel beyond the generic tool card;
104
+ - the SSE route has module-level notification tests, not a live end-to-end stream assertion;
105
+ - shards are produced and reassembled but **not yet transported** to machines -- that is the relay's next step, and the
106
+ tests say so rather than implying distribution works.
107
+
108
+ ### Verified
109
+
110
+ Gate GREEN twice, 15 of 15. tools 120, claims 48, p2p 32, loader 10, fleet-messages 7, message-shards 9, chat-http 3,
111
+ message-notify 3.
112
+
5
113
  ## [1.1.16] — 2026-10-18
6
114
 
7
115
  **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.18", "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.18` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.18.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.18/twinsearth-w2m-dsh-plugin-1.1.18.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.18.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
136
+
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.
148
140
 
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.
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.
154
144
 
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.
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,39 @@ 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 |
198
+
199
+ ---
200
+
201
+ ## Release channels
202
+
203
+ Three channels, and only one of them is what \
204
+ pm install\ gives you.
199
205
 
206
+ | Channel | Where it comes from | Who should use it |
207
+ |---|---|---|
208
+ | **stable** | npm \latest\ — a version the developer declared stable | everyone; this is the default install |
209
+ | **beta** | a GitHub **prerelease** (tarball + \SHA256SUMS\), **never on npm** | developers granted \eta-access\ who accept that it can fail |
210
+ | **latest build** | the newest of the two, whatever its channel | a developer who wants the newest code and knows the above |
211
+
212
+ A bump is a **beta by default**. npm receives a version only when the developer says, in so many words, that this one is a
213
+ stable release: a beta is published as a prerelease (so neither GitHub nor npm ever resolves it as \latest\), it is
214
+ downloadable only after the \eta-access\ environment approves the run, and installing it requires reading
215
+ [SECURITY.md](SECURITY.md) and accepting the risk.
216
+
217
+ There is no \
218
+ pm publish\ line in the beta workflow. That is deliberate — a channel cannot leak to the default install if
219
+ the publication cannot happen at all.
200
220
  ---
201
221
 
202
222
  ## Links
203
223
 
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
224
+ - [CHANGELOG](CHANGELOG.md) — every version, including what it deliberately did **not** ship
225
+ - [PLATFORMS](docs/PLATFORMS.md) — Claude Code, Codex and MCP wiring details
226
+ - [Releases](https://github.com/TwinsEarth/dsh-windows2macos/releases) — per-release evidence and checksums
207
227
  - [Issues](https://github.com/TwinsEarth/dsh-windows2macos/issues)
208
228
 
209
229
  MIT.
@@ -214,39 +234,187 @@ MIT.
214
234
 
215
235
  **一个 DeepSeek 账号。一句话。你自己的每一台机器跑同一个项目。**
216
236
 
217
- ### 安装(由易到难,任选一种)
237
+ 以下内容与上方英文**逐节对应**,不是摘要。
238
+
239
+ ### 安装
240
+
241
+ 五种方式,最终都会在你的 agent 里注册 **12 tools registered**。任选其一。
242
+
243
+ #### 1. DSH 插件(推荐,零配置)
244
+
245
+ ```bash
246
+ # 1. 把包装进你的 DSH profile
247
+ dsh plugin add @twinsearth/w2m-dsh-plugin
248
+
249
+ # 2. 在 profile 的 bundle 列表里写上包名 —— 没进 bundle 的包永远不会被加载
250
+ # ~/.dsh/profiles/<profile>/dsh.profile.yml
251
+ plugins:
252
+ - '@twinsearth/w2m-dsh-plugin'
218
253
 
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`,**先校验再安装**。
254
+ # 3. 重启 DSH。加入编队发生在 mount 时,热重载不够
255
+ ```
256
+
257
+ 从 **1.1.15** 起 agent 会自动启动(`autoStartAgent` 默认 `true`):新装的机器自行派生账号身份、加入该账号的
258
+ 编队,无需配对步骤就会出现在 `w2m_devices` 里。若要关闭,请 patch **已存在的** `w2m` 行,**不要新增第二行**:
259
+
260
+ ```yaml
261
+ # cordis.patch.yml
262
+ - id: w2m
263
+ config:
264
+ autoStartAgent: false
265
+ ```
266
+
267
+ #### 2. npm(MCP 服务器,任意客户端)
268
+
269
+ ```bash
270
+ npm install -g @twinsearth/w2m-dsh-plugin
271
+ w2m-mcp --help
272
+ ```
273
+
274
+ 把任意 MCP 客户端指向 `w2m-mcp`。**零运行时依赖**;包里同时带有 relay、agent 与工具。
275
+
276
+ #### 3. Claude Code
277
+
278
+ ```bash
279
+ /plugin marketplace add TwinsEarth/dsh-windows2macos
280
+ /plugin install w2m@twinsearth
281
+ ```
282
+
283
+ 清单文件位于 [`.claude-plugin/`](.claude-plugin/),版本与发行版锁定。
284
+
285
+ #### 4. Codex 及其它支持 MCP 的 CLI
286
+
287
+ ```toml
288
+ # 把这段复制进你自己的 codex/mcp-servers.toml
289
+ [mcp_servers.w2m]
290
+ command = "npx"
291
+ args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.18", "w2m-mcp"]
292
+ ```
293
+
294
+ #### 5. 校验过的发布包(离线)
295
+
296
+ 从 `v1.1.18` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.18.tgz` 与 `SHA256SUMS` —— 然后:
297
+
298
+ ```bash
299
+ curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.18/twinsearth-w2m-dsh-plugin-1.1.18.tgz
300
+ sha256sum -c SHA256SUMS # 安装前务必校验
301
+ npm install -g ./twinsearth-w2m-dsh-plugin-1.1.18.tgz
302
+ ```
303
+
304
+ 每个 release 都带 `SHA256SUMS` 正是为了这一步。若某个 release 缺少它,**不要安装该版本**。
225
305
 
226
306
  ### 快速开始
227
307
 
228
308
  ```text
229
- w2m_status # 我配对了吗?在哪个编队?
230
- w2m_devices # 编队里有哪些机器
309
+ w2m_status # 我配对了吗?在哪个编队?
310
+ w2m_devices # 编队里有什么?
231
311
  w2m_run mode="replicate" command_argv=["git","rev-parse","HEAD"]
232
312
  w2m_wait task_id="..."
233
313
  ```
234
314
 
235
- ### 核心概念
315
+ `replicate` 在每一台在线机器上执行;只有当所有机器返回**相同结果**且**相同 commit 锚点**时,裁决才是
316
+ `consistent`。`split` 给每台机器一个分片,`pipeline` 串联多个阶段,`broadcast` 指定一个执行者并汇报观察者,
317
+ `compose` 合并结果。
318
+
319
+ ### 它是什么
320
+
321
+ 一个可安装插件,把一组机器变成**一个编队**,以**你的账号**为键,而不是靠手工维护的主机清单。
322
+
323
+ | 组件 | 职责 |
324
+ |---|---|
325
+ | **Relay** | 一个小型 HTTP 服务:任务路由、租约、SSE、blob 存储。放在任何机器可达的位置。 |
326
+ | **Agent** | 每台机器上运行:配对一次、执行白名单命令、心跳。 |
327
+ | **Plugin** | 你的 agent 调用的那些工具。 |
328
+ | **Admission**(1.1.14+) | 新机器申请加入;在线成员批准,**10 秒自动同意**;入队 24 小时内的机器可被立即移除。 |
329
+
330
+ ### 工具
331
+
332
+ `w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
333
+ `w2m_memory` · `w2m_approvals` · `w2m_council` · `w2m_chat` · `w2m_update`
334
+
335
+ ### 被派发命令会收到的环境变量(1.1.15+)
336
+
337
+ 取值来自 offer,**不来自 argv**,因此单台机器无法改变整个编队的分片:`W2M_INDEX` / `W2M_INDEX_TOTAL` /
338
+ `W2M_STAGE_INDEX` / `W2M_TASK_ID` / `W2M_MODE` / `W2M_MACHINE_ID` / `W2M_MACHINE_NAME`。
236
339
 
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 可以按机器分支。
340
+ 因此 `command_argv=["node","-e","process.exit(Number(process.env.W2M_INDEX)+1)"]` 在四台机器上应分别退出
341
+ 1/2/3/4。**单机行为已测;四台机器的联跑尚未验证**(见下方"未验证")。
244
342
 
245
- ### 验证状态(不含糊)
343
+ ### 编队模型
246
344
 
247
- 已测试 2453 项(2439 通过 / 14 跳过)、15 项门禁、三平台 CI 全绿、npm 与 GitHub 发布及校验和一致。
345
+ 身份是**账号**,不是主机清单。机器从账号材料派生 Ed25519 身份;账号的第一台机器配对时,relay 登记对应公钥;
346
+ 此后每台机器用一次性签名挑战自证同一个账号。
248
347
 
249
- **未验证项**:客户端审批弹窗(规则由 relay 强制,工具可列出并裁决)、四台机器 `W2M_INDEX` 全员取值、
250
- 编队聊天通道(仅设计)、部分 macOS 主机的 P2P 降级。
348
+ 只有当旧配对路径因**明确原因**失败时 —— 配对码无效、relay 不可达、超时 —— 机器才降级去申请入队。其它失败需要
349
+ 人工介入,**不会**切换机制。**拒绝即终态**,机器不会再次申请。
350
+
351
+ 所有时间窗与法定人数由 relay 权威判定,因为客户端时钟互不一致(实测:同一编队内相差数十秒):
352
+
353
+ - 无决策 **10 秒**后自动同意,但**仅当至少一名成员在线** —— 空编队无法同意,该拒绝有明确错误码
354
+ (`NO_ONLINE_MEMBER`);
355
+ - 入队后 **24 小时内**,任一在线成员可立即移除该机器;
356
+ - 超过 24 小时,单台机器只能**发起表决**,且法定人数集合在**发起时冻结**,投票者中途离线无法改变门槛;
357
+ - 法定人数:旧成员 ≥3 人需 **≥50%(向上取整)**;2 人编队的移除只要 **1 票**;
358
+ - 离线超过 **72 小时**的机器会离开**列表** —— 记录**永不删除**,它重新上报后会自动回来。
359
+
360
+ ### 安全姿态
361
+
362
+ - 命令白名单按 **argv 前缀**匹配,并拒绝 argv 重解析标志(`--import`、追加 `-e`),错误码
363
+ `COMMAND_REPARSE_ARGUMENT`。这是边界,不是沙箱。
364
+ - 派发任务需要 operator token;device token 不能派发。
365
+ - 审批入队要求至少一名成员在线。
366
+ - 账户签名密钥从 **fleet key** 派生,**绝不**用 account code —— account code 会写进日志,用可读值派生密钥等于
367
+ 交出账号身份。
368
+ - 每个发行版都有校验和。
369
+
370
+ ### 已验证与未验证
371
+
372
+ **一句真话胜过一块掩盖问题的绿牌。**
373
+
374
+ **已验证:** 102 个文件中的 2458 项测试(2444 通过、14 跳过)、15 项门禁检查、Windows / macOS / Linux 三平台
375
+ CI 全绿、npm 与 GitHub 发布及校验和一致、在真实的 Linux/Mac/Windows 三机编队上跑过端到端。
376
+
377
+ **未验证 / 已知缺口:**
378
+
379
+ | 缺口 | 状态 |
380
+ |---|---|
381
+ | 审批**弹窗**控件 | relay 强制规则、`w2m_approvals` 可列出并裁决,但 10 秒倒计时是列表里的 relay 数据,不是独立弹窗 |
382
+ | 四台机器的 `W2M_INDEX` 联跑 | 单机行为已测;编队级 1/2/3/4 尚未执行 |
383
+ | 编队聊天 / 广播通道 | 消息存储与配额已实现并测试;加密、分片、端点与面板**尚未构建** |
384
+ | 部分 macOS 主机的 P2P | 观察到 `P2P_PUNCH_FAILED: Unknown cipher`;任务会降级走 relay 路径 |
385
+
386
+ ### 故障排查
387
+
388
+ | 症状 | 原因 | 处理 |
389
+ |---|---|---|
390
+ | 装好了,但 `w2m_devices` 返回 401 | agent 从未启动,因此没有 device token | 确认 `autoStartAgent` 不是 `false`;**重启 DSH**(热重载无效) |
391
+ | 编队里看不到这台机器 | 插件没写进 `dsh.profile.bundles` | 加进去 —— 不在 bundle 里的包永远不会加载 |
392
+ | `COMMAND_NOT_ALLOWED` | 命令不在白名单 | 扩充白名单;argv 按前缀匹配 |
393
+ | 裁决总是 `partial` | 来自已下线机器的陈旧租约 | 1.1.2 已修:派发只对在线且未沉默的机器建租约 |
394
+ | `P2P_PUNCH_FAILED` | NAT,或宿主密码学能力缺口 | 会自动降级到 relay 路径,结果不受影响 |
395
+
396
+ ### 发布通道(中文)
397
+
398
+ 三个通道,其中只有一个是你 \
399
+ pm install\ 默认拿到的。
400
+
401
+ | 通道 | 来源 | 适合谁 |
402
+ |---|---|---|
403
+ | **stable** | npm \latest\,开发者明确声明为稳定版 | 所有人;默认安装就是它 |
404
+ | **beta** | GitHub **prerelease**(tarball + \SHA256SUMS\),**绝不进 npm** | 被授予 \eta-access\、且接受它可能失败的开发者 |
405
+ | **最新构建** | 两者中版本较新的那个,不论通道 | 想要最新代码、并理解上述含义的开发者 |
406
+
407
+ **默认的 bump 是 beta**。只有开发者明确说明"这次是稳定版",npm 才会收到该版本:beta 以 prerelease 形式发布(因此
408
+ GitHub 与 npm 都不会把它解析成 \latest\),只有在 \eta-access\ 环境审批通过后才能下载,安装前需阅读
409
+ [SECURITY.md](SECURITY.md) 并自担风险。
410
+
411
+ beta 工作流里**没有 \
412
+ pm publish\ 这一行**,这是刻意的 —— 只要发布动作不可能发生,这个通道就不可能泄漏到默认安装。
413
+ ### 链接
414
+
415
+ - [CHANGELOG](CHANGELOG.md) —— 每个版本,包括它**故意没有**交付的内容
416
+ - [PLATFORMS](docs/PLATFORMS.md) —— Claude Code、Codex 与 MCP 的接线细节
417
+ - [Releases](https://github.com/TwinsEarth/dsh-windows2macos/releases) —— 每个版本的证据与校验和
418
+ - [Issues](https://github.com/TwinsEarth/dsh-windows2macos/issues)
251
419
 
252
420
  MIT。