@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +63 -0
- package/README.md +230 -100
- package/codex/mcp-servers.toml +1 -1
- package/docs/PLATFORMS.md +22 -0
- package/package.json +8 -4
- package/plugin.json +1 -1
- package/src/plugin/tools.mjs +46 -2
- package/src/plugin/tools.mjs.bak-c7 +7029 -0
- package/src/relay/fleet-messages.mjs +145 -0
- package/src/relay/message-shards.mjs +124 -0
- package/src/relay/server.mjs +71 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
[](https://github.com/TwinsEarth/dsh-windows2macos/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@twinsearth/w2m-dsh-plugin)
|
|
7
|
-
[](#
|
|
7
|
+
[](#1-dsh-plugin-recommended--zero-configuration)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](#verified-and-not-verified)
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ English | [中文](#中文说明)
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
Five
|
|
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.
|
|
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
|
|
30
|
+
# 3. restart DSH; joining happens at mount time, so a hot reload is not enough
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Since **1.1.
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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`.
|
|
52
|
-
|
|
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
|
|
63
|
+
### 4. Codex and other MCP-capable CLIs
|
|
64
64
|
|
|
65
65
|
```toml
|
|
66
|
-
#
|
|
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.
|
|
69
|
+
args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
### 5.
|
|
72
|
+
### 5. Verified release tarball (offline)
|
|
73
73
|
|
|
74
|
-
From the
|
|
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.
|
|
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.
|
|
80
|
+
npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
|
|
85
81
|
```
|
|
86
82
|
|
|
87
|
-
Every release carries
|
|
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
|
-
#
|
|
96
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
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 |
|
|
107
|
+
| Piece | Responsibility |
|
|
120
108
|
|---|---|
|
|
121
|
-
| **Relay** |
|
|
122
|
-
| **Agent** | runs on each machine
|
|
123
|
-
| **Plugin** |
|
|
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
|
-
###
|
|
114
|
+
### Tools
|
|
127
115
|
|
|
128
|
-
`w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report`
|
|
129
|
-
|
|
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
|
|
119
|
+
### Environment a dispatched command receives (1.1.15+)
|
|
132
120
|
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
135
|
+
## Fleet model
|
|
148
136
|
|
|
149
|
-
Identity is **the account**, not a
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
165
|
-
- Dispatching tasks requires an operator token.
|
|
166
|
-
- Admission approval requires at least one member
|
|
167
|
-
-
|
|
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
|
-
|
|
173
|
+
A green badge that hides this is worse than no badge.
|
|
174
174
|
|
|
175
|
-
**Verified:**
|
|
176
|
-
|
|
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 **
|
|
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 |
|
|
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
|
|
196
|
-
| `COMMAND_NOT_ALLOWED` |
|
|
197
|
-
| verdict always `partial` | a stale lease from a machine that is down | fixed in 1.1.2
|
|
198
|
-
| `P2P_PUNCH_FAILED` | NAT or a host crypto gap | it downgrades to the relay path;
|
|
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
|
|
205
|
-
- [PLATFORMS](docs/PLATFORMS.md) — Claude Code, Codex
|
|
206
|
-
- [
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
+
因此 `command_argv=["node","-e","process.exit(Number(process.env.W2M_INDEX)+1)"]` 在四台机器上应分别退出
|
|
320
|
+
1/2/3/4。**单机行为已测;四台机器的联跑尚未验证**(见下方"未验证")。
|
|
248
321
|
|
|
249
|
-
|
|
250
|
-
|
|
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。
|
package/codex/mcp-servers.toml
CHANGED
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.
|