@twinsearth/w2m-dsh-plugin 1.1.15 → 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 +112 -0
- package/README.md +252 -870
- 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 +119 -5
- 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
package/README.md
CHANGED
|
@@ -2,999 +2,381 @@
|
|
|
2
2
|
|
|
3
3
|
**One DeepSeek account. One instruction. Every machine you own runs the same project.**
|
|
4
4
|
|
|
5
|
-
English | [中文](#中文说明)
|
|
6
|
-
|
|
7
5
|
[](https://github.com/TwinsEarth/dsh-windows2macos/actions/workflows/ci.yml)
|
|
8
6
|
[](https://www.npmjs.com/package/@twinsearth/w2m-dsh-plugin)
|
|
9
|
-
[](#install)
|
|
11
|
-
[](#design-notes)
|
|
7
|
+
[](#1-dsh-plugin-recommended--zero-configuration)
|
|
12
8
|
[](LICENSE)
|
|
13
9
|
[](#verified-and-not-verified)
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Progress — where this actually is
|
|
18
|
-
|
|
19
|
-
**Current: `0.45.0`.** 66 versions since `0.8.0`. The number that matters more than the version:
|
|
20
|
-
**11 tools, 82 suites, 2228 tests, 12 pre-flight checks, 0 failures.**
|
|
21
|
-
|
|
22
|
-
### The three properties this project set out to have
|
|
23
|
-
|
|
24
|
-
| Property | State | What it means concretely |
|
|
25
|
-
|---|---|---|
|
|
26
|
-
| **分模块** (modular) | **enforced** | every file belongs to exactly one declared layer; `layer-check` fails on an undeclared edge |
|
|
27
|
-
| **分层级** (layered) | **enforced** | six ranked layers and an **allow-list** of edges — not a rule that can be interpreted, a list that can be read |
|
|
28
|
-
| **分权限** (tiered authority) | **built, opt-in** | four bands, escalation moves **exactly one band**, nothing destructive auto-settles. **Off by default** — see the gap below |
|
|
29
|
-
|
|
30
|
-
### Roadmap
|
|
31
|
-
|
|
32
|
-
| Version | Deliverable | State |
|
|
33
|
-
|---|---|---|
|
|
34
|
-
| **v4.8.0** | field-level acceptance, MMP P1/P2, lineage | **delivered** — `w2m_memory` accepts a received CAT7 block **field by field at the actor's level**; a field that costs more than the actor has is **omitted, not nulled**; a block naming an ancestor the caller cannot produce is **refused, never re-rooted** |
|
|
35
|
-
| **v4.9.0** | approval queue | **partial** — the queue, the escalation bridge and the raise-limiter are wired and reachable by an operator through `w2m_approvals`. **The "front door" named in the roadmap was never built**: `approval-index.mjs` imports `authority.mjs` and nothing else |
|
|
36
|
-
| **v4.10.0** | committee governance | **delivered** — seven phases, weighted votes, **a tie is refused rather than broken**, and a sealed decision is **retrievable with its dissent and its reasons** after the council that produced it is gone |
|
|
37
|
-
|
|
38
|
-
### What the reachability work found, because it is the honest part
|
|
39
|
-
|
|
40
|
-
Three mechanisms were shipped in this span **fully tested and reachable by nothing** — `field-authority`,
|
|
41
|
-
`approval-gate`, and `council`. Two were **described as delivered** while nothing ran them. A single `grep`
|
|
42
|
-
disproved one of them.
|
|
43
|
-
|
|
44
|
-
There is now a **gate**: a module nothing reaches makes the pre-flight **red** unless it appears in
|
|
45
|
-
`scripts/accepted-orphans.mjs` **with a reason and a statement of what would remove it**. An acceptance that
|
|
46
|
-
outlives its cause also fails. **Seven remain**:
|
|
47
|
-
|
|
48
|
-
| Module | Kind | What would remove it |
|
|
49
|
-
|---|---|---|
|
|
50
|
-
| `health.mjs` | **defect** | the agent calling it on a schedule |
|
|
51
|
-
| `log-bounds.mjs` | **defect** | the same — it is the answer to a **measured 40× log growth** |
|
|
52
|
-
| `storage.mjs` | **defect** | a tool writing through its undelatable tiers |
|
|
53
|
-
| `fleet-scale.mjs`, `fleet-state.mjs`, `exit-channel.mjs`, `test-report.mjs` | library | a caller, when one is wanted |
|
|
54
|
-
|
|
55
|
-
**The defect class is named as a defect rather than folded in with the libraries**, because the two are
|
|
56
|
-
indistinguishable from the import graph alone and only a written reason can separate them.
|
|
57
|
-
|
|
58
|
-
### The gap that matters most right now
|
|
59
|
-
|
|
60
|
-
**The plugin this machine runs is 30+ versions behind this source tree.** Every source-level conclusion about
|
|
61
|
-
the running runtime is void until it is upgraded — which is one command, and it is in
|
|
62
|
-
[Install](#3-the-dsh-plugin).
|
|
63
|
-
|
|
64
|
-
### Measured on real machines
|
|
65
|
-
|
|
66
|
-
| Fact | Evidence |
|
|
67
|
-
|---|---|
|
|
68
|
-
| a **seven-phase council** runs on macOS | `exit 0` on MacMini: phases 7, dissent retained, recount reproduced |
|
|
69
|
-
| **field-level acceptance** holds on macOS | `exit 0` on MacMini: level 0 cannot reach `evidence`, level 70 can, tampering caught, a missing ancestor refused rather than re-rooted |
|
|
70
|
-
| the fleet is **3 usable machines of 5 paired** | and `fleet-scale.mjs` **refuses to extrapolate** from five |
|
|
71
|
-
| **`P2P_PUNCH_FAILED`** | **UNVERIFIED.** Three hypotheses measured and excluded (BLAKE2s-256, the wider primitive set, version skew). **The plugin that raises it runs 0.9.5** — upgrading is the next experiment |
|
|
72
|
-
|
|
73
|
-
### Not claimed
|
|
74
|
-
|
|
75
|
-
**This is not 1.0.0.** `docs/1.0.0-ACCEPTANCE.md` sorts all 30 criteria into **ENFORCED / MEASURED /
|
|
76
|
-
UNVERIFIED** by a rule rather than an opinion, names **seven claims this work had to retract**, and states
|
|
77
|
-
plainly that four of its own closing items are undone. It is bound to the pre-flight by a test, so a criterion
|
|
78
|
-
cannot quietly become unenforced while the file keeps claiming otherwise.
|
|
11
|
+
English | [中文](#中文说明)
|
|
79
12
|
|
|
80
13
|
---
|
|
81
14
|
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
You have a Windows desktop and a Mac laptop. You are working on one project. You
|
|
85
|
-
want to say *"run the test suite"* once, and have **both** machines run it — then
|
|
86
|
-
see whether they agree.
|
|
15
|
+
## Install
|
|
87
16
|
|
|
88
|
-
|
|
17
|
+
Five routes in. They all end with **12 tools registered** in your agent. Pick one.
|
|
89
18
|
|
|
90
|
-
|
|
91
|
-
- its transport is **loopback only** (the web server accepts `127.0.0.1`/`0.0.0.0`
|
|
92
|
-
and the CLI refuses `--host 0.0.0.0`, because it ships no TLS or origin policy);
|
|
93
|
-
- the official agent-team feature is explicit that it does **not** support
|
|
94
|
-
teammates with separate working directories or **several processes
|
|
95
|
-
coordinating over one team**.
|
|
19
|
+
### 1. DSH plugin (recommended — zero configuration)
|
|
96
20
|
|
|
97
|
-
|
|
21
|
+
```bash
|
|
22
|
+
# 1. install the package into your DSH profile
|
|
23
|
+
dsh plugin add @twinsearth/w2m-dsh-plugin
|
|
98
24
|
|
|
99
|
-
|
|
25
|
+
# 2. name it in the profile's bundle list — a package that is not bundled never loads
|
|
26
|
+
# ~/.dsh/profiles/<profile>/dsh.profile.yml
|
|
27
|
+
plugins:
|
|
28
|
+
- '@twinsearth/w2m-dsh-plugin'
|
|
100
29
|
|
|
101
|
-
|
|
102
|
-
you, on any machine, talking to DSH
|
|
103
|
-
│
|
|
104
|
-
▼
|
|
105
|
-
┌──────────────────────────────────┐ ┌──────────────────────────┐
|
|
106
|
-
│ Windows · DSH session │ │ macOS · DSH session │
|
|
107
|
-
│ w2m plugin (11 tools) │ │ w2m plugin (11 tools) │
|
|
108
|
-
│ │ │ │ │ │
|
|
109
|
-
│ Localside agent │ │ Localside agent │
|
|
110
|
-
│ · three anchors │ │ · three anchors │
|
|
111
|
-
│ · local spool │ │ · local spool │
|
|
112
|
-
│ · executes argv │ │ · executes argv │
|
|
113
|
-
└──────────┬───────────────────────┘ └──────────┬───────────────┘
|
|
114
|
-
│ outbound POST + SSE │
|
|
115
|
-
▼ ▼
|
|
116
|
-
┌──────────────────────────────────────────────────────────┐
|
|
117
|
-
│ Rabbit relay: rendezvous, ledger, STUN responder │
|
|
118
|
-
└──────────────────────────────────────────────────────────┘
|
|
30
|
+
# 3. restart DSH; joining happens at mount time, so a hot reload is not enough
|
|
119
31
|
```
|
|
120
32
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
ledger; the payload path is a UDP hole punch between the machines themselves — on
|
|
125
|
-
by default in v0.4.0 — and it falls back to the relay loudly, naming the reason,
|
|
126
|
-
when the network will not allow it.
|
|
127
|
-
|
|
128
|
-
### The direct path (v0.4.0, default)
|
|
129
|
-
|
|
130
|
-
Both machines announce the address a STUN server sees them as, the dispatching
|
|
131
|
-
machine punches to the other's candidate, and the task offer travels over the
|
|
132
|
-
punched channel instead of the relay. The relay holds its own copy of that offer
|
|
133
|
-
back for **3000 ms** (`DEFAULT_P2P_OFFER_GRACE_MS`) while the punch is attempted,
|
|
134
|
-
and offers it anyway if the punch does not land — so a failed punch costs latency,
|
|
135
|
-
not the task. The constant was 1200 ms first and that was **too tight**: measured on
|
|
136
|
-
a 1-vCPU shared host, two agent tests saw the relay's copy overtake the direct one,
|
|
137
|
-
which under `p2pMode: direct` means refusing an offer that was about to arrive.
|
|
138
|
-
3000 ms covers a slow host with room to spare, and the cost is bounded — one delayed
|
|
139
|
-
copy per task, once.
|
|
140
|
-
|
|
141
|
-
The result *notice* returns the same way, with one stated limit: the frame carries
|
|
142
|
-
the task and machine identity, not the result payload. `result_path` is one of the
|
|
143
|
-
envelope's own fields and the envelope's hash covers it, so the acknowledgement has
|
|
144
|
-
to be known before the envelope is built. The direct path therefore saves the
|
|
145
|
-
dispatcher the wait for the relay's copy, not the bytes — the payload still reaches
|
|
146
|
-
everyone through the ledger, which is the only writer allowed to hold it.
|
|
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:
|
|
147
36
|
|
|
148
37
|
```yaml
|
|
38
|
+
# cordis.patch.yml
|
|
149
39
|
- id: w2m
|
|
150
40
|
config:
|
|
151
|
-
|
|
152
|
-
# stunServers: ['202.182.123.154:3478'] # default: the shared server, then three public ones
|
|
41
|
+
autoStartAgent: false
|
|
153
42
|
```
|
|
154
43
|
|
|
155
|
-
|
|
156
|
-
refuses the relay copy rather than falling back, so a punch failure becomes a
|
|
157
|
-
visible failure; `relay` is byte-for-byte v0.3.9 — no UDP socket is bound at all.
|
|
158
|
-
Every result carries `transport` plus a `p2p` object saying which path the offer
|
|
159
|
-
took and which path the result took, and `w2m_wait` shows it per machine.
|
|
160
|
-
|
|
161
|
-
**Why `auto` and not `direct`.** Punching cannot succeed behind a symmetric NAT,
|
|
162
|
-
and that is measured rather than assumed. On the reference Windows client network
|
|
163
|
-
(China Mobile), three STUN servers reported three different mapped ports for one
|
|
164
|
-
socket — `endpoint-dependent`, punchable: false. A default that hard-required the
|
|
165
|
-
direct path would break every deployment on such a network the moment it
|
|
166
|
-
upgraded. See [PROTOCOL-v0.4.0.md](PROTOCOL-v0.4.0.md) §1.
|
|
44
|
+
### 2. npm (MCP server, any client)
|
|
167
45
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g @twinsearth/w2m-dsh-plugin
|
|
48
|
+
w2m-mcp --help
|
|
49
|
+
```
|
|
171
50
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
| `replicate` | every machine runs the **same** command; results compared side by side | "does this change pass on both platforms?" |
|
|
175
|
-
| `split` | work divided by `index` (`explicit` or `modulo` sharding) | one large test suite, several machines |
|
|
176
|
-
| `broadcast` | **exactly one** machine runs it, chosen with `executor_machine_id` (or the composer's selected target); every other machine holds an `observing` lease and is told the outcome | "run this migration on the database host, and tell everyone what happened" |
|
|
177
|
-
| `pipeline` | **every** machine runs the whole `stages` chain, in order, stopping at the first stage that fails | "check out, build, then test — as one task with one verdict" |
|
|
51
|
+
Point any MCP client at `w2m-mcp`. **Zero runtime dependencies**; the package ships the relay, the agent and the
|
|
52
|
+
tools.
|
|
178
53
|
|
|
179
|
-
|
|
54
|
+
### 3. Claude Code
|
|
180
55
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
the outputs into stage 2 — has no well-defined input on a fleet, and two machines finishing in a
|
|
186
|
-
different order would make one `task_id` describe two different executions. The reasoning is in
|
|
187
|
-
[docs/PIPELINE-DESIGN.md](docs/PIPELINE-DESIGN.md), which also records that the chain's `command_hash`
|
|
188
|
-
binds **every** stage, not just the first.
|
|
56
|
+
```bash
|
|
57
|
+
/plugin marketplace add TwinsEarth/dsh-windows2macos
|
|
58
|
+
/plugin install w2m@twinsearth
|
|
59
|
+
```
|
|
189
60
|
|
|
190
|
-
|
|
61
|
+
The manifests live in [`.claude-plugin/`](.claude-plugin/) and are version-locked to the release.
|
|
191
62
|
|
|
192
|
-
|
|
193
|
-
collapsing them turns noise into bug reports. Three more were added in v0.3.0, each replacing a case that
|
|
194
|
-
used to be reported as something less true.
|
|
63
|
+
### 4. Codex and other MCP-capable CLIs
|
|
195
64
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
| `failed` | every machine failed |
|
|
203
|
-
| `partial` | some succeeded, some failed, and machines are still out |
|
|
204
|
-
| `timeout` | the deadline passed and the machines that never answered are not coming back |
|
|
205
|
-
| `cancelled` | the operator stopped it — not a defect, so it must not be reported as `failed` |
|
|
206
|
-
| `unverifiable` | an **anchor** did not match, so comparison is refused |
|
|
207
|
-
|
|
208
|
-
Two more values are **not verdicts**, because no terminal answer exists yet or none was ever possible:
|
|
209
|
-
`pending` (before the first result lands) and `refused` (every machine was refused at the capability
|
|
210
|
-
gate). `pending` is the only non-terminal one; `refused` reads like "still deciding" but every machine
|
|
211
|
-
has already answered, so waiting cannot change it. All nine verdicts are declared in
|
|
212
|
-
`src/relay/report.mjs`'s `AGGREGATE_STATUSES`.
|
|
65
|
+
```toml
|
|
66
|
+
# copy this block into your own codex/mcp-servers.toml
|
|
67
|
+
[mcp_servers.w2m]
|
|
68
|
+
command = "npx"
|
|
69
|
+
args = ["-y", "@twinsearth/w2m-dsh-plugin@1.1.17", "w2m-mcp"]
|
|
70
|
+
```
|
|
213
71
|
|
|
214
|
-
|
|
72
|
+
### 5. Verified release tarball (offline)
|
|
215
73
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
> ```bash
|
|
219
|
-
> node bin/w2m-rabbit.mjs ... # from a clone (what the examples below use)
|
|
220
|
-
> npx @twinsearth/w2m-dsh-plugin ... # if it is on a registry
|
|
221
|
-
> w2m-rabbit ... # once installed, via its bin entry
|
|
222
|
-
> ```
|
|
223
|
-
>
|
|
224
|
-
> The examples use the clone form because that is what is verified in this
|
|
225
|
-
> repository. Nothing needs to be installed to use the relay or the agent: they
|
|
226
|
-
> are plain Node scripts with **no runtime dependencies**.
|
|
227
|
-
|
|
228
|
-
### 1. The relay + STUN (once, on any always-on machine)
|
|
229
|
-
|
|
230
|
-
> **v0.4.0 defaults to a shared server, so this step is optional.** With no
|
|
231
|
-
> `rabbitUrl` configured, the plugin uses `http://202.182.123.154:8787` — a
|
|
232
|
-
> rendezvous, ledger and STUN responder that is already running. Point it at your
|
|
233
|
-
> own host when you want your own; nothing else changes.
|
|
74
|
+
From the `v1.1.17` release page download **both** files — `twinsearth-w2m-dsh-plugin-1.1.17.tgz` and `SHA256SUMS` —
|
|
75
|
+
then:
|
|
234
76
|
|
|
235
77
|
```bash
|
|
236
|
-
|
|
237
|
-
|
|
78
|
+
curl -LO https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v1.1.17/twinsearth-w2m-dsh-plugin-1.1.17.tgz
|
|
79
|
+
sha256sum -c SHA256SUMS # verify before installing, always
|
|
80
|
+
npm install -g ./twinsearth-w2m-dsh-plugin-1.1.17.tgz
|
|
238
81
|
```
|
|
239
82
|
|
|
240
|
-
`
|
|
241
|
-
immediately prints a **new** code — so you can pair the second machine without
|
|
242
|
-
restarting anything. (For scripted onboarding, start it with
|
|
243
|
-
`pairingCodeReusable: true` and one code stays valid for its whole TTL.)
|
|
83
|
+
Every release carries `SHA256SUMS` for exactly this reason. If a release is missing it, do not install that release.
|
|
244
84
|
|
|
245
|
-
|
|
246
|
-
peer has to aim at. Machines fall back to three public STUN servers if it is not
|
|
247
|
-
there, so leaving it out degrades the punch rather than breaking it.
|
|
248
|
-
|
|
249
|
-
Deploying both on a fresh host, with the proxy and firewall rules, is one command:
|
|
250
|
-
`deploy/shared-server/install.sh` — see
|
|
251
|
-
[deploy/shared-server/README.md](deploy/shared-server/README.md), including the
|
|
252
|
-
part about TLS that this shape does not have by default.
|
|
85
|
+
---
|
|
253
86
|
|
|
254
|
-
|
|
87
|
+
## Quick start
|
|
255
88
|
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
--name win-desktop \
|
|
262
|
-
--state ~/.dsh/xclient/localside-win \
|
|
263
|
-
--p2p-mode auto
|
|
264
|
-
# --p2p-port 41234 # v0.4.1: pin the punch socket, so an inbound firewall
|
|
265
|
-
# # rule names one port and survives a restart
|
|
89
|
+
```text
|
|
90
|
+
w2m_status # am I paired? which fleet?
|
|
91
|
+
w2m_devices # what is in the fleet?
|
|
92
|
+
w2m_run mode="replicate" command_argv=["git","rev-parse","HEAD"]
|
|
93
|
+
w2m_wait task_id="..."
|
|
266
94
|
```
|
|
267
95
|
|
|
268
|
-
|
|
269
|
-
|
|
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.
|
|
270
99
|
|
|
271
|
-
|
|
272
|
-
--allowed-commands '["node --test","git status --porcelain"]'
|
|
273
|
-
```
|
|
100
|
+
---
|
|
274
101
|
|
|
275
|
-
|
|
276
|
-
> split on whitespace — so `node -e console.log` does **not** allow
|
|
277
|
-
> `node -e console.log(1)`, and a path containing a space has to be written as an
|
|
278
|
-
> array entry. See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) §10.
|
|
279
|
-
>
|
|
280
|
-
> **What an entry actually grants — read this before trusting a list.** An entry
|
|
281
|
-
> names a tool and the arguments up to a point; whatever follows is still that
|
|
282
|
-
> tool's own behaviour, so `node --test` allows `node --test <any file>`, and the
|
|
283
|
-
> test runner loads and runs whatever file it is handed. **A prefix entry is not a
|
|
284
|
-
> sandbox**: it says which program may run, not what that program will do. Two
|
|
285
|
-
> narrower rules sit on top of the prefix match:
|
|
286
|
-
>
|
|
287
|
-
> * **Code-loading trailing arguments are refused** (`COMMAND_REPARSING_ARGUMENT`):
|
|
288
|
-
> `--test-reporter`, `--import`, `--require`, `-r`, `--experimental-loader`,
|
|
289
|
-
> `-e`/`--eval`, `-p`/`--print`, `python -c`/`-m`, `bash -c`, `git -c`/`--exec-path`,
|
|
290
|
-
> `make -f`, `tar --to-command`, and their equivalents in the interpreters in
|
|
291
|
-
> common use. If a machine is meant to run one of them, name it **in the entry**
|
|
292
|
-
> (`["node --test --test-reporter=tap"]`) — the entry is the explicit grant, and
|
|
293
|
-
> the refusal says so.
|
|
294
|
-
> * **The executable is compared by identity, not by spelling.** A bare name is
|
|
295
|
-
> resolved through PATH as before; an absolute path is admitted only when it
|
|
296
|
-
> resolves to the same file the entry's executable does (otherwise
|
|
297
|
-
> `COMMAND_EXECUTABLE_PATH_MISMATCH`), and a relative path is refused outright
|
|
298
|
-
> (`COMMAND_EXECUTABLE_RELATIVE_PATH`) because the working directory comes from
|
|
299
|
-
> the offer. A copy of some other program at `E:\Temp\evil\node.exe` is not
|
|
300
|
-
> `node`, however it is named.
|
|
301
|
-
|
|
302
|
-
> ⚠️ **v0.0.1 note:** Localside is verified as a standalone process. Running it
|
|
303
|
-
> *in-process* from the plugin (`autoStartAgent: true`) is implemented but has
|
|
304
|
-
> not been exercised end-to-end in this release — start it as its own process.
|
|
305
|
-
|
|
306
|
-
### 3. The DSH plugin
|
|
307
|
-
|
|
308
|
-
**From npm** — the shortest form, and the one the marketplaces install:
|
|
102
|
+
## What it is
|
|
309
103
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
```
|
|
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.
|
|
313
106
|
|
|
314
|
-
|
|
107
|
+
| Piece | Responsibility |
|
|
108
|
+
|---|---|
|
|
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. |
|
|
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. |
|
|
315
113
|
|
|
316
|
-
|
|
317
|
-
dsh plugin --profile <profile-name> add \
|
|
318
|
-
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.41.0/twinsearth-w2m-dsh-plugin-0.41.0.tgz
|
|
319
|
-
```
|
|
114
|
+
### Tools
|
|
320
115
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
line. **This repository's own profile did exactly that and fell 30+ versions behind** —
|
|
324
|
-
so if you use it, re-check the number against the badge at the top of this file.
|
|
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`
|
|
325
118
|
|
|
326
|
-
|
|
327
|
-
but not `npm`. `pnpm add @twinsearth/w2m-dsh-plugin@0.41.0 --save-exact` installs the
|
|
328
|
-
identical tarball from the identical registry.
|
|
119
|
+
### Environment a dispatched command receives (1.1.15+)
|
|
329
120
|
|
|
330
|
-
|
|
331
|
-
the repo, builds nothing, and registers `cordis.patch.yml`):
|
|
121
|
+
Values come from the offer, never from argv, so one machine cannot reshape a fleet-wide split.
|
|
332
122
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
123
|
+
| Variable | Meaning |
|
|
124
|
+
|---|---|
|
|
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` |
|
|
127
|
+
| `W2M_TASK_ID` / `W2M_MODE` | the task and its mode |
|
|
128
|
+
| `W2M_MACHINE_ID` / `W2M_MACHINE_NAME` | this machine |
|
|
336
129
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
that exact file rather than repacking — so "which artifact did you install?" has one
|
|
340
|
-
answer, not three.
|
|
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.
|
|
341
132
|
|
|
342
|
-
|
|
343
|
-
> `node_modules` **and** a row in the profile's `cordis.patch.yml`. A bundle that is
|
|
344
|
-
> listed in `package.json` but never composed is the failure this project hit once
|
|
345
|
-
> already — see `cordis.patch.yml`'s header for what that looked like.
|
|
133
|
+
---
|
|
346
134
|
|
|
347
|
-
|
|
348
|
-
install are two different things:
|
|
135
|
+
## Fleet model
|
|
349
136
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
```
|
|
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.
|
|
354
140
|
|
|
355
|
-
|
|
356
|
-
|
|
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.
|
|
357
144
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
config:
|
|
361
|
-
rabbitUrl: '' # empty => the shared server's default
|
|
362
|
-
operatorToken: '' # required to dispatch; the relay prints it once
|
|
363
|
-
stateDir: /absolute/path/to/$DSH_HOME/xclient # `!!js process.env.DSH_HOME` can evaluate to undefined
|
|
364
|
-
machineName: win-desktop
|
|
365
|
-
p2pMode: auto
|
|
366
|
-
```
|
|
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):
|
|
367
147
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
> That is a silent failure by construction — the install looks right, eleven tools appear, and
|
|
379
|
-
> `w2m_status` reports `paired: false` and a null `rabbitUrl` because none of your values were ever
|
|
380
|
-
> applied. It is not hypothetical: this row's expected name moved from the `/tools` subpath to the
|
|
381
|
-
> package root in v0.5.4 (see `cordis.patch.yml`'s header for why), and every override that still
|
|
382
|
-
> asserted the old subpath stopped being applied on upgrade. `id` already identifies the row, so the
|
|
383
|
-
> safest `name:` is no `name:` at all.
|
|
384
|
-
|
|
385
|
-
Restart DSH. You should see eleven tools: `w2m_devices`, `w2m_run`, `w2m_wait`,
|
|
386
|
-
`w2m_report`, `w2m_status`, `w2m_update`, `w2m_history`, `w2m_stats`, `w2m_memory`,
|
|
387
|
-
`w2m_approvals`, `w2m_council`.
|
|
388
|
-
|
|
389
|
-
> `dsh` may not be on your `PATH`. On a packaged install it lives under
|
|
390
|
-
> `resources/runtime/cli/bin/`. If `dsh` is not found, call it by path.
|
|
391
|
-
|
|
392
|
-
### What this plugin touches, before you install it
|
|
393
|
-
|
|
394
|
-
Stated here and machine-readably in `package.json`'s `disclosure`, because installing a
|
|
395
|
-
plugin runs its code with your permissions:
|
|
396
|
-
|
|
397
|
-
| | |
|
|
398
|
-
|---|---|
|
|
399
|
-
| **Cloud** | yes — a relay is required. The default is the shared server (`202.182.123.154:8787`, STUN on `:3478`); point `rabbitUrl` at your own host, or run the relay yourself with `deploy/shared-server/install.sh` |
|
|
400
|
-
| **Offline** | no. The relay is the ledger; peer-to-peer only moves the task offer, and the result still reaches the ledger |
|
|
401
|
-
| **Credentials** | the operator token (profile config or `W2M_OPERATOR_TOKEN`) authorises *dispatched work*; the device token lives in `$DSH_HOME/xclient/device.json` (mode 0600 where the platform honours it) and authorises *taking* work |
|
|
402
|
-
| **Filesystem** | reads and writes inside your project directory (anchors, fingerprints), plus its own spool and state under `--state` |
|
|
403
|
-
| **Network** | the configured relay, the STUN servers, and UDP to the peer machines. Peers can reach this machine's punch port, which is ephemeral unless you pin it with `--p2p-port` |
|
|
404
|
-
| **Jurisdiction** | data crosses between wherever your machines are and wherever the relay is; the reference deployment is CN ↔ JP |
|
|
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.
|
|
156
|
+
|
|
157
|
+
---
|
|
405
158
|
|
|
406
|
-
##
|
|
159
|
+
## Security posture
|
|
407
160
|
|
|
408
|
-
|
|
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.
|
|
409
168
|
|
|
410
|
-
|
|
411
|
-
> tell me whether the output matches.
|
|
169
|
+
---
|
|
412
170
|
|
|
413
|
-
|
|
414
|
-
you the comparison.
|
|
171
|
+
## Verified and not verified
|
|
415
172
|
|
|
416
|
-
|
|
173
|
+
A green badge that hides this is worse than no badge.
|
|
417
174
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
matches the base while the working tree does not. Compare on SHA alone and you
|
|
421
|
-
will report a logic difference that is really a "these are not the same files"
|
|
422
|
-
difference.
|
|
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.
|
|
423
177
|
|
|
424
|
-
|
|
178
|
+
**Not verified / known gaps:**
|
|
425
179
|
|
|
426
|
-
|
|
|
180
|
+
| Gap | State |
|
|
427
181
|
|---|---|
|
|
428
|
-
| `
|
|
429
|
-
| `
|
|
430
|
-
|
|
|
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 |
|
|
183
|
+
| Four-machine `W2M_INDEX` run | single-machine behaviour tested; the fleet-wide 1/2/3/4 run has not been executed |
|
|
184
|
+
| Fleet chat / broadcast channel | message store + quota implemented and tested; encryption, sharding, endpoints and the panel are **not built** |
|
|
185
|
+
| P2P on some macOS hosts | `P2P_PUNCH_FAILED: Unknown cipher` observed; the task falls back to the relay path |
|
|
431
186
|
|
|
432
|
-
|
|
433
|
-
(`git read-tree` → `git add -A` → `git write-tree`) that never touches your
|
|
434
|
-
working tree. Each call gets its **own** temporary index — sharing one
|
|
435
|
-
`GIT_INDEX_FILE` across processes makes them fail with `idx.lock: File exists`.
|
|
187
|
+
---
|
|
436
188
|
|
|
437
|
-
|
|
438
|
-
an empty string in both the "clean" and "only untracked files" cases, so the two
|
|
439
|
-
are indistinguishable.
|
|
189
|
+
## Troubleshooting
|
|
440
190
|
|
|
441
|
-
|
|
442
|
-
|
|
191
|
+
| Symptom | Cause | Fix |
|
|
192
|
+
|---|---|---|
|
|
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) |
|
|
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 |
|
|
443
198
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Links
|
|
447
202
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
206
|
+
- [Issues](https://github.com/TwinsEarth/dsh-windows2macos/issues)
|
|
451
207
|
|
|
452
|
-
|
|
208
|
+
MIT.
|
|
453
209
|
|
|
454
|
-
|
|
455
|
-
> host: endpoints were built with `new URL('/v1/stream', rabbitUrl)`, which
|
|
456
|
-
> discards a path prefix, so anything mounted under `/w2m` answered 404 to
|
|
457
|
-
> everything. Sub-path deployment now works, and sending work requires its own
|
|
458
|
-
> credential. See [CHANGELOG](CHANGELOG.md#012--2026-10-07).
|
|
210
|
+
---
|
|
459
211
|
|
|
460
|
-
|
|
461
|
-
both sides can reach. Four shapes are supported and documented end to end in
|
|
462
|
-
[docs/DEPLOY.md](docs/DEPLOY.md):
|
|
212
|
+
## 中文说明
|
|
463
213
|
|
|
464
|
-
|
|
465
|
-
|---|---|---|
|
|
466
|
-
| **The shared server** (v0.4.0 default) | nothing — plain `http://` on a public IP | `http://202.182.123.154:8787` |
|
|
467
|
-
| **Your own VPS** | the relay (`--tls-cert`/`--tls-key`) or a proxy | `https://w2m.example.com` |
|
|
468
|
-
| **Tailscale / WireGuard** | the network — WireGuard encrypts | `http://100.x.y.z:8787` |
|
|
469
|
-
| **Tunnel** (Cloudflare Tunnel, ngrok) | the tunnel service | `https://<sub>.example.com` or with a sub-path |
|
|
470
|
-
|
|
471
|
-
> **The shared server has no TLS, and that is stated rather than implied.** There is
|
|
472
|
-
> no hostname to issue a certificate for. Pairing codes, tokens and results cross
|
|
473
|
-
> the wire in clear text; the direct path keeps the payload off the relay, and the
|
|
474
|
-
> machine-side allow-list is the load-bearing control. With a domain, add TLS and
|
|
475
|
-
> put `https://` in `rabbitUrl` — nothing else changes:
|
|
476
|
-
> [deploy/shared-server/README.md](deploy/shared-server/README.md).
|
|
477
|
-
|
|
478
|
-
> **Plain `http://` over a Tailscale address is not a mistake.** WireGuard already
|
|
479
|
-
> provides end-to-end encryption and authenticates both peers; adding TLS on top
|
|
480
|
-
> would add certificate plumbing without adding a property you do not already
|
|
481
|
-
> have.
|
|
482
|
-
|
|
483
|
-
If your proxy or tunnel mounts the relay under a prefix — `https://host/w2m/` —
|
|
484
|
-
then start it with `--base-path /w2m` and put the same prefix in `rabbitUrl`.
|
|
485
|
-
Sub-path deployment works because URLs are joined by concatenation; see
|
|
486
|
-
[CHANGELOG](CHANGELOG.md#012--2026-10-07) for the bug that made this impossible
|
|
487
|
-
before 0.1.2.
|
|
488
|
-
|
|
489
|
-
### Two credentials, on purpose
|
|
490
|
-
|
|
491
|
-
| Credential | Held by | Authorises |
|
|
492
|
-
|---|---|---|
|
|
493
|
-
| `device_token` | each machine | taking work, reporting results |
|
|
494
|
-
| `operator_token` | **only you** | **sending work** (`POST /v1/task`) |
|
|
214
|
+
**一个 DeepSeek 账号。一句话。你自己的每一台机器跑同一个项目。**
|
|
495
215
|
|
|
496
|
-
|
|
497
|
-
others. The relay writes the operator token to `<state>/operator-token.txt` (mode
|
|
498
|
-
0600) on first start and prints it once — on that first start only, because a
|
|
499
|
-
relay's stdout ends up in journald, a container log or a CI transcript and this
|
|
500
|
-
token authorises dispatching to the whole fleet. Every later start names the file
|
|
501
|
-
instead of the value; `--print-operator-token` (or `W2M_PRINT_OPERATOR_TOKEN=1`)
|
|
502
|
-
prints it anyway for an operator who needs it there. The plugin takes it as
|
|
503
|
-
`operatorToken`. Starting the relay with `--operator-token ''` removes the
|
|
504
|
-
requirement — it warns loudly, and that is only appropriate for a trusted LAN.
|
|
216
|
+
以下内容与上方英文**逐节对应**,不是摘要。
|
|
505
217
|
|
|
506
|
-
###
|
|
218
|
+
### 安装
|
|
507
219
|
|
|
508
|
-
|
|
509
|
-
machines and its task history, which matters once the relay lives on a VPS you
|
|
510
|
-
will eventually reboot. A corrupt ledger line is skipped and reported; a corrupt
|
|
511
|
-
device file starts empty **and says so** rather than silently forgetting every
|
|
512
|
-
machine. `--no-persist` restores in-memory behaviour.
|
|
220
|
+
五种方式,最终都会在你的 agent 里注册 **12 tools registered**。任选其一。
|
|
513
221
|
|
|
514
|
-
|
|
222
|
+
#### 1. DSH 插件(推荐,零配置)
|
|
515
223
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
> plugin is opt-in.
|
|
224
|
+
```bash
|
|
225
|
+
# 1. 把包装进你的 DSH profile
|
|
226
|
+
dsh plugin add @twinsearth/w2m-dsh-plugin
|
|
520
227
|
|
|
521
|
-
|
|
228
|
+
# 2. 在 profile 的 bundle 列表里写上包名 —— 没进 bundle 的包永远不会被加载
|
|
229
|
+
# ~/.dsh/profiles/<profile>/dsh.profile.yml
|
|
230
|
+
plugins:
|
|
231
|
+
- '@twinsearth/w2m-dsh-plugin'
|
|
522
232
|
|
|
523
|
-
|
|
524
|
-
# ~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
525
|
-
- insert:
|
|
526
|
-
- id: w2m
|
|
527
|
-
name: '@twinsearth/w2m-dsh-plugin'
|
|
528
|
-
config:
|
|
529
|
-
rabbitUrl: 'http://100.x.y.z:8787'
|
|
530
|
-
autoUpdate: true # default false
|
|
531
|
-
# autoUpdateTimes: ['00:00:00', '03:00:00', '05:00:00'] # Beijing wall clock
|
|
532
|
-
# autoUpdateTimeZone: Asia/Shanghai # default
|
|
533
|
-
# autoUpdateDryRun: true # verify, install nothing
|
|
534
|
-
# updateRepo: TwinsEarth/dsh-windows2macos # default
|
|
233
|
+
# 3. 重启 DSH。加入编队发生在 mount 时,热重载不够
|
|
535
234
|
```
|
|
536
235
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
> is a second plugin instance rather than a configuration of the first. `@twinsearth/w2m-dsh-plugin`
|
|
540
|
-
> must be the **bare** package name: a subpath such as
|
|
541
|
-
> `@twinsearth/w2m-dsh-plugin/tools` resolves to no `package.json`, so `dsh.client` is never read
|
|
542
|
-
> and **the client half is silently disabled** — the composer control never registers, and nothing
|
|
543
|
-
> on the host side reports a problem. `./tools` remains a supported mount point; it is simply not
|
|
544
|
-
> the one this bundle uses, and using it costs you the browser half. See
|
|
545
|
-
> [TROUBLESHOOTING.md](TROUBLESHOOTING.md) §2.
|
|
546
|
-
|
|
547
|
-
Ask it what it is doing from any session with `w2m_update` (`action: "status"` to
|
|
548
|
-
inspect, `action: "check"` to run one cycle now).
|
|
549
|
-
|
|
550
|
-
**What it does and does not do:**
|
|
551
|
-
|
|
552
|
-
- Compares the newest GitHub release against the version it was built as. It
|
|
553
|
-
installs **only a strictly newer, non-prerelease** version — a downgrade is
|
|
554
|
-
worse than a missed update, and a release candidate outranks the release it
|
|
555
|
-
precedes by SemVer, so the gate is explicit.
|
|
556
|
-
- Downloads the tarball and **verifies the SHA-256 published in that release's
|
|
557
|
-
`SHA256SUMS`** before anything is written. A mismatch installs nothing.
|
|
558
|
-
- Backs up the profile's `package.json` and `pnpm-lock.yaml`, then installs
|
|
559
|
-
through `dsh plugin --profile <p> add <tarball>` (falling back to the runtime's
|
|
560
|
-
own pnpm). If the install fails, the manifest is restored byte-for-byte.
|
|
561
|
-
- **It does not restart DSH.** The new version loads on the next start; the
|
|
562
|
-
running process keeps the code it loaded. `w2m_status` reports
|
|
563
|
-
`restart_required` once an install has happened, and the installed tarball
|
|
564
|
-
lives in `<profile>/.w2m-update/` so the dependency does not dangle.
|
|
565
|
-
- **It does not hot-swap the running plugin.** Nothing may rewrite the module a
|
|
566
|
-
live Cordis container already loaded, so claiming otherwise would be a lie in
|
|
567
|
-
the status output.
|
|
568
|
-
|
|
569
|
-
**Honest limits.**
|
|
570
|
-
|
|
571
|
-
- A missed slot runs once on the next start if it was missed by less than 90
|
|
572
|
-
minutes (a machine asleep across one slot). Wider than that and the check waits
|
|
573
|
-
for the next slot rather than firing at an arbitrary hour.
|
|
574
|
-
- If pnpm fails *after* it has already changed `node_modules`, restoring the
|
|
575
|
-
manifest is not enough to guarantee DSH still starts. The result then carries
|
|
576
|
-
`rollbackComplete: false` and a `reconciliation` command; `w2m_status` surfaces
|
|
577
|
-
it as `reconciliation_needed`. This is the one state that needs a human.
|
|
578
|
-
- A negative result is never reported as success: a GitHub lookup that fails is
|
|
579
|
-
recorded as an **error**, not as "already up to date", because those two look
|
|
580
|
-
identical in a log and only one of them is a problem.
|
|
581
|
-
|
|
582
|
-
## Security
|
|
583
|
-
|
|
584
|
-
Read this before exposing the relay to anything.
|
|
585
|
-
|
|
586
|
-
- **Admission** is a single-use, rotating pairing code, then a per-device bearer
|
|
587
|
-
token stored in `device.json` (mode `0600`). Pairing attempts are rate limited
|
|
588
|
-
(5 per IP per minute by default, successes included).
|
|
589
|
-
- **Sending work needs the operator token**, not a device token.
|
|
590
|
-
- **`--trust-proxy` is off by default and should stay off** unless the relay
|
|
591
|
-
really is behind a proxy you control: it makes the relay believe
|
|
592
|
-
`X-Forwarded-For`, and believing that header without a proxy in front lets any
|
|
593
|
-
client forge its address and walk past the rate limit.
|
|
594
|
-
- **The relay holds no model credentials and no working copy.** It can forge
|
|
595
|
-
tasks, which is exactly why the machine-side whitelist is the load-bearing
|
|
596
|
-
control.
|
|
597
|
-
- **Commands are never shell strings.** argv arrays are passed to
|
|
598
|
-
`spawn(cmd, args, { shell: false })`, so nothing a caller types can be
|
|
599
|
-
reinterpreted as shell syntax.
|
|
600
|
-
- **Default-deny whitelist, with its limit stated.** A machine runs nothing that
|
|
601
|
-
does not match `--allowed-commands`, and the matching rules are in §2 above.
|
|
602
|
-
The list controls *which program* may run; a prefix entry grants what that
|
|
603
|
-
program can be told to do with the arguments it is given (`node --test` runs
|
|
604
|
-
the test files it is handed), so it is a control, not a sandbox.
|
|
605
|
-
- **Read-only by default.** `replicate` tasks run with `write: false`; there is
|
|
606
|
-
no code path that writes to your project in that mode.
|
|
607
|
-
- **TLS is your choice, but a deliberate one.** Shape A gets it from WireGuard;
|
|
608
|
-
shapes B and C get it from the relay's `--tls-cert`/`--tls-key` or from the
|
|
609
|
-
proxy. Running shape B or C over plain `http://` would put the operator token
|
|
610
|
-
and every result on the wire in clear text — do not.
|
|
611
|
-
- The plugin **does not read** `.credentials.yaml`.
|
|
612
|
-
|
|
613
|
-
## Verified, and not verified
|
|
614
|
-
|
|
615
|
-
This project's rule is that claims carry their evidence.
|
|
616
|
-
|
|
617
|
-
> **Current state lives in [Progress](#progress-where-this-actually-is) above.** What follows is the
|
|
618
|
-
> **v0.4.0 record**, kept because it is the measurement the direct path still rests on — not because it
|
|
619
|
-
> describes what ships today. The version is now `0.41.0`.
|
|
620
|
-
|
|
621
|
-
### v0.4.0 — the direct path is the default, and here is what that is worth
|
|
622
|
-
|
|
623
|
-
v0.3.9 shipped the P2P transport and admitted in its own §8 that nothing read
|
|
624
|
-
`p2p.mode`, the announcer did not exist and the task path still went through the
|
|
625
|
-
relay. v0.4.0 is that admission paid off:
|
|
626
|
-
|
|
627
|
-
* **the announcer exists** — a machine binds one UDP socket, asks the STUN servers
|
|
628
|
-
what the world sees, announces its candidates and refreshes them;
|
|
629
|
-
* **the offer can arrive over the punched path**, and the result can return over it;
|
|
630
|
-
* **exactly-once survives both copies arriving** — the relay's offer is still
|
|
631
|
-
emitted, and a second copy of the same `task_id`+`attempt` is never executed;
|
|
632
|
-
* **every result says which path it took** (`transport`, `p2p.offer_path`,
|
|
633
|
-
`p2p.result_path`, and a `路径` column in the report);
|
|
634
|
-
* **the relay is still the ledger** — the direct copy is additional, never a
|
|
635
|
-
replacement, so the comparison cannot become less trustworthy because a network
|
|
636
|
-
was faster.
|
|
637
|
-
|
|
638
|
-
Three things are worth knowing before relying on it:
|
|
639
|
-
|
|
640
|
-
* **Punching cannot succeed against a symmetric NAT**, and this is measured twice
|
|
641
|
-
on two real networks: an iPhone hotspot (v0.3.9) and the reference Windows
|
|
642
|
-
client network, China Mobile (v0.4.0 — three servers, three mapped ports: 36028,
|
|
643
|
-
5855, 26713). Nothing can dial *into* such a network, which is why the default is
|
|
644
|
-
`auto` with a *reported* fallback rather than `direct`.
|
|
645
|
-
* **A punch from a symmetric NAT to a public peer still lands** — measured:
|
|
646
|
-
`HELLO` → `HELLO_ACK` in **331.61 ms** from that same Chinese network to the
|
|
647
|
-
shared server in Tokyo, with the ledger recording `transport: p2p` for the
|
|
648
|
-
Japanese machine. A punch you initiate opens the mapping, and the peer's answer
|
|
649
|
-
to the datagram it just received comes back through it. That asymmetry is why the
|
|
650
|
-
dispatcher dials the executor.
|
|
651
|
-
* **A machine that must accept a punch needs a reachable UDP port.** On a public
|
|
652
|
-
host that is a firewall rule for the agent's punch port (the shared server has
|
|
653
|
-
one); behind a NAT it is the hole the punch opens. **v0.4.1 pins it**: `--p2p-port <n>`
|
|
654
|
-
(or `W2M_P2P_PORT`) makes the rule name one port instead of a range, which v0.4.0 could not
|
|
655
|
-
do — the node always bound an ephemeral one, so the rule had to be re-pointed after every
|
|
656
|
-
restart.
|
|
657
|
-
* **A two-NAT punch is still unverified.** Two sockets on one host share a path
|
|
658
|
-
with no translator between them, so the loopback tests prove the protocol and
|
|
659
|
-
nothing about NAT. What *has* now been measured is one real WAN hop: dispatcher
|
|
660
|
-
on a Chinese residential network, executor on the shared server in Tokyo. That is
|
|
661
|
-
one NAT, and it is labelled as one NAT.
|
|
662
|
-
|
|
663
|
-
**The same suites run on three platforms in CI** — see the badge above, or
|
|
664
|
-
[`.github/workflows/ci.yml`](.github/workflows/ci.yml). The matrix is
|
|
665
|
-
`windows-latest` × node 20/22, `macos-latest` × node 22, `ubuntu-latest` ×
|
|
666
|
-
node 22. That matters here more than in most projects: a Windows-and-macOS
|
|
667
|
-
coordination tool whose macOS half had never been executed would be a claim, not
|
|
668
|
-
a product.
|
|
669
|
-
|
|
670
|
-
| Suite | Tests | What it covers |
|
|
671
|
-
|---|---|---|
|
|
672
|
-
| relay | 142 | pairing, auth, SSE with `ready`-first and `seq` replay, leases with heartbeat renewal and expiry, dedupe, all **nine** aggregation states plus `pending`/`refused`, report generation, operator token, rate limiting, persistence and restart recovery, TLS, and the lost-offer recovery path |
|
|
673
|
-
| agent | 129 | allow-list allow/deny, timeout, output truncation, exit codes, anchors on clean and dirty trees, four concurrent fingerprint computations, spool, a 40-case URL join matrix, cursor lifecycle across relay restarts, and the bundled-runtime resolver |
|
|
674
|
-
| tools | 89 | eleven tools registered, schemas, typed errors, polling, operator-token enforcement (no request is sent without it), sub-path endpoints, and the credential canaries |
|
|
675
|
-
| broadcast | 19 | exactly one executor, the rest `observing`; observers excluded from every aggregation step so a machine that did not run is never counted as agreeing |
|
|
676
|
-
| claims | 19 | the document claims themselves: the version, the bundle patch, the tool list, every documented tarball URL, and the CHANGELOG's shape |
|
|
677
|
-
| fleet-target | 37 | the composer's target selection, and the `executor_machine_id` narrowing that overrides it |
|
|
678
|
-
| client-bundle | 25 | the browser half builds, exports, and resolves against `PLATFORM_MODULES` |
|
|
679
|
-
| schedule | 39 | the daily slots as exact UTC instants, zones that shift by 30 minutes for DST, a full simulated year of consecutive arming, and catch-up collapsing several missed slots into one run |
|
|
680
|
-
| auto-update | 34 | the install/skip decision, no downgrade, prerelease refused, an unverified tarball refused, a failed lookup recorded as an error rather than as "current", and no token ever persisted |
|
|
681
|
-
| update-source | 50 | version ordering, streaming SHA-256 verification, timeouts that really abort, rate-limit reporting, and one real GitHub API call (opt-in — see below) |
|
|
682
|
-
| update-install | 28 | zero writes on a hash mismatch, byte-for-byte restore, atomic staging, dry run, and two real-pnpm integration runs in throwaway profiles |
|
|
683
|
-
| update-wiring | 34 | configuration validation that names the setting, and one `ctx.effect` owning the timer whose disposer stops it |
|
|
684
|
-
| signing | 33 | request signing: mismatch, expiry, replay and required-signature refusals |
|
|
685
|
-
| stress | 9 | sustained load on the relay's frame handling |
|
|
686
|
-
| shared-config | 16 | the fleet-wide config file, including its refusal of secret-looking keys |
|
|
687
|
-
| metrics / metrics-route | 16 + 5 | the Prometheus exposition and its route, with no credential ever rendered |
|
|
688
|
-
| tool-cards | 8 | the tool descriptions the model actually reads |
|
|
689
|
-
| p2p-stun | 18 | the STUN responder and reflexive-address discovery |
|
|
690
|
-
| p2p-transport | 28 | the reliable ordered channel over UDP |
|
|
691
|
-
| p2p-signaling | 17 | peer candidates: announce, fetch, expiry |
|
|
692
|
-
| p2p-node | 38 | the node that binds the punch socket, announces, punches and dials |
|
|
693
|
-
| p2p-transport-fields | 28 | `transport` / `p2p.offer_path` / `p2p.result_path` / `p2p.opened_by` on every result |
|
|
694
|
-
| p2p-plugin | 32 | the plugin half of the direct path, including the pushes and their named failures |
|
|
695
|
-
| p2p-crypto | 33 | stage 1 of the data-plane encryption: the handshake, the refusal codes, the replay window and the epoch ratchet |
|
|
696
|
-
| stun-server | 27 | the standalone `w2m-stun` binary |
|
|
697
|
-
| loader-entry | 10 | the entry DSH actually mounts, packed and installed — the gate v0.5.4 needed |
|
|
698
|
-
| mdns-wire / mdns-discover | 38 + 45 | the DNS wire codec, and LAN discovery |
|
|
699
|
-
| e2e + crossnetwork | 11 + 35 | two Localsides on two checkouts against one relay running real commands: consistent / divergent / failed / refused / unverifiable / deduped / long-lease / split, plus sub-path deployment, the two credential kinds, a SIGKILLed relay restarting with its ledger intact, pairing rate limiting, proxy-header trust boundaries, and the anti-buffering headers |
|
|
700
|
-
| signing-e2e | 8 | a signed task end to end |
|
|
701
|
-
| outbound-only | 7 | a running agent binds no TCP listener, and `relay` mode binds no UDP socket at all |
|
|
702
|
-
| recovery | 4 | restart and lost-offer recovery |
|
|
703
|
-
| pipeline-e2e | 2 | a real relay and a real agent running a chain: the stages append to a witness file, so the ordering evidence is written by the stages themselves; the second case proves the chain stops at the first failure |
|
|
704
|
-
| p2p-agent | 21 | the agent half of the direct path, including the reverse dial for results |
|
|
705
|
-
|
|
706
|
-
**The numbers above are from v0.5.9 on `win32 x64`, Node `v24.18.1`** — the floor this package
|
|
707
|
-
declares is `>=20.19.0`, and Node 24 is what DSH ships on this host. **36 suites on disk, 1,139 tests:**
|
|
708
|
-
1,131 pass, **2 fail**, 6 skip. The two failures are `test/pipeline-e2e.test.mjs`'s two cases on this
|
|
709
|
-
Windows host; **everything else is green.** Three suites are environment-sensitive and their counts
|
|
710
|
-
move with it: `claims` is 19 tests here and **22 when `dist/` holds a packed tarball**, which is when
|
|
711
|
-
its two tarball-vs-`package.json` checks run instead of skipping — so a checkout with `dist/` measures
|
|
712
|
-
**1,142 tests / 1,134 pass / 4 skip**, and that is the number to expect from a release tree.
|
|
713
|
-
|
|
714
|
-
> **These counts are the v0.5.9 measurement and have not been re-taken since.** They are kept, dated
|
|
715
|
-
> and attributed rather than refreshed, because a figure presented as current without having been
|
|
716
|
-
> measured is the failure this project's own rules exist to prevent. Suites added after v0.5.9 —
|
|
717
|
-
> `p2p-crypto`, and from v0.7.0 `chatops-approval` and `chatops-policy` — are therefore **not** in
|
|
718
|
-
> any total on this page. Treat every count below as a v0.5.9 measurement until a release re-runs
|
|
719
|
-
> the suite on its own host and says so.
|
|
720
|
-
|
|
721
|
-
**Which script runs which suite matters, because they are not the same set:**
|
|
722
|
-
|
|
723
|
-
| Command | Suites | Result on this host (v0.5.9) |
|
|
724
|
-
|---|---|---|
|
|
725
|
-
| `npm test` — the unit suite | 29 | 1,051 tests, **all pass**, 6 skip |
|
|
726
|
-
| `npm run test:all` — units + end-to-end | 35 | 1,106 tests, 1,098 pass, **2 fail** (`pipeline-e2e`), 6 skip |
|
|
727
|
-
| `pwsh scripts/verify.ps1` — the acceptance run | 29 | 1,050 tests, 1,048 pass, **2 fail** (`pipeline-e2e`), 6 skip |
|
|
728
|
-
|
|
729
|
-
> **`test:all` and `verify.ps1` are not supersets of each other.** At v0.5.9 `test:all` was still
|
|
730
|
-
> missing `test/p2p-crypto.test.mjs` — 33 tests covering the data-plane encryption, absent from the
|
|
731
|
-
> suite a release runs, and `CHANGELOG.md`'s v0.5.9 entry says so instead of claiming otherwise.
|
|
732
|
-
> **v0.6.0 applied the one-line fix that entry asked its owner for**, so the file is now in `npm test`
|
|
733
|
-
> and in `test:all`; `verify.ps1`'s own suite list still does not name it.
|
|
734
|
-
|
|
735
|
-
`%DSH_HOME%\dsh-runtimes\` is absent on this host, which is why `verify.ps1` falls back to the
|
|
736
|
-
`node` on `PATH`; the resolver's own bundled-runtime case is exercised against a throwaway home in
|
|
737
|
-
`test/agent.test.mjs`, and “Not verified” below says what that does and does not prove.
|
|
738
|
-
|
|
739
|
-
> **`pipeline-e2e` fails 2/2 at the v0.5.9 tag on this host, and the cause is the test, not the
|
|
740
|
-
> product.** Both cases die with `ENOENT … order.txt` / `stop.txt`: the witness files the stages are
|
|
741
|
-
> supposed to append to were never created, because **no stage ever ran**. In that revision the suite
|
|
742
|
-
> passes its agent `--allowed-commands` as the *string* entry `process.execPath`
|
|
743
|
-
> (`pipeline-e2e.test.mjs`, `allowedCommands = [NODE]`). A string entry is split on whitespace **by
|
|
744
|
-
> design**, and on a machine whose Node lives under `C:\Program Files\`, that entry becomes the two
|
|
745
|
-
> tokens `C:\Program` and `Files\nodejs\node.exe`, which match nothing — so the agent refused the
|
|
746
|
-
> chain with `COMMAND_NOT_ALLOWED` before its first stage. It is therefore **not a Node-24 artifact**:
|
|
747
|
-
> it is a test defect that appears wherever the interpreter's path contains a space, and it is
|
|
748
|
-
> consistent with this suite passing on CI runners, whose Node is unpacked to a space-free path.
|
|
749
|
-
> **The working tree already repairs it** (array-form entry naming `-e`, plus a third case asserting
|
|
750
|
-
> that a stage outside the allow-list refuses the whole chain and runs nothing), and measured on this
|
|
751
|
-
> host that repair takes the suite to **3/3 pass**. The failure is recorded here because v0.5.9 is
|
|
752
|
-
> what a reader installs, and the honest label for the tag is **failing on Windows under
|
|
753
|
-
> `C:\Program Files`** rather than "passes".
|
|
754
|
-
|
|
755
|
-
**Skips are per platform, not a single number, and the difference is one test.** On this Windows host
|
|
756
|
-
(Node 24.18.1, `dist/` present, network tests not enabled) there are **4**:
|
|
757
|
-
|
|
758
|
-
| Suite | Skipped case | Why |
|
|
759
|
-
|---|---|---|
|
|
760
|
-
| agent | `writes 0600 where the platform honours POSIX bits` | NTFS carries no POSIX mode bits. **Skips on Windows; runs on macOS/Linux.** |
|
|
761
|
-
| agent | `starts a bundled-only tool from runArgv even when PATH cannot see it` | A POSIX shell script is not a Windows executable. **Skips on Windows; runs on macOS/Linux.** |
|
|
762
|
-
| claims | `every documented tarball exists as a release asset` | network check off unless `W2M_NETWORK_TESTS=1` |
|
|
763
|
-
| update-source | `fetches, parses and verifies the published release end to end` | same gate |
|
|
764
|
-
|
|
765
|
-
Two further cases are environment-conditional rather than platform-conditional, and skip only when
|
|
766
|
-
their input is missing: `claims`'s two tarball-vs-`package.json` checks run when `dist/` holds a
|
|
767
|
-
packed tarball and skip when it does not, and `agent`'s `git anchors` suite skips when `git` is not on
|
|
768
|
-
`PATH`. On this machine `git` is present, so that one does not fire.
|
|
769
|
-
|
|
770
|
-
The consequence, stated rather than generalised: **macOS and Linux run the two agent cases Windows
|
|
771
|
-
cannot, and skip `reports the windows filesystem honestly`, which is Windows-only**, so their skip
|
|
772
|
-
count is set by the same network gate plus whichever platform case applies — **it is not this host's
|
|
773
|
-
number, and calling it one is exactly the generalisation this section used to make.** No macOS or
|
|
774
|
-
Linux run was performed for this table; only the CI matrix measures those, and by this project's own
|
|
775
|
-
labelling rule the figure above is `[M]` for Windows and `[?]` for the other two.
|
|
776
|
-
|
|
777
|
-
The one test that calls GitHub for real is **opt-in** (`W2M_NETWORK_TESTS=1`).
|
|
778
|
-
GitHub allows 60 unauthenticated API requests per hour *per IP* and hosted CI
|
|
779
|
-
runners share IPs, so it failed on `macos-latest` with HTTP 403 `RATE_LIMITED`
|
|
780
|
-
while every other job passed — and a release gate that depends on someone else's
|
|
781
|
-
rate-limit budget eventually blocks a release for a reason unrelated to the code.
|
|
782
|
-
CI still runs it in a separate **non-blocking** job with the workflow token, and
|
|
783
|
-
the same code paths are covered without a network in the blocking matrix.
|
|
784
|
-
|
|
785
|
-
Two behaviours are checked but not covered by a test file, because both concern
|
|
786
|
-
the real binaries rather than the modules:
|
|
787
|
-
|
|
788
|
-
- **CLI smoke test** (`scripts/smoke-cli.ps1`): real `w2m-rabbit` plus two real
|
|
789
|
-
`w2m-localside` processes, ending in a `consistent` verdict and a rendered
|
|
790
|
-
markdown report.
|
|
791
|
-
- **Release-artifact reproducibility**: CI packs the sources twice and fails if
|
|
792
|
-
the two tarballs differ, so a published hash is a statement about the sources
|
|
793
|
-
rather than about when the command ran.
|
|
794
|
-
|
|
795
|
-
Two extra gates exist because "it installed" is not "it loads" — the first
|
|
796
|
-
version of this package shipped a re-export naming a symbol that did not exist,
|
|
797
|
-
and it passed `node --check` and every unit test, failing only when DSH mounted
|
|
798
|
-
the plugin:
|
|
236
|
+
从 **1.1.15** 起 agent 会自动启动(`autoStartAgent` 默认 `true`):新装的机器自行派生账号身份、加入该账号的
|
|
237
|
+
编队,无需配对步骤就会出现在 `w2m_devices` 里。若要关闭,请 patch **已存在的** `w2m` 行,**不要新增第二行**:
|
|
799
238
|
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
|
|
239
|
+
```yaml
|
|
240
|
+
# cordis.patch.yml
|
|
241
|
+
- id: w2m
|
|
242
|
+
config:
|
|
243
|
+
autoStartAgent: false
|
|
803
244
|
```
|
|
804
245
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
- **A real Windows↔macOS pair.** The CI matrix proves each platform runs the
|
|
808
|
-
suites, but the two halves of a matrix job never talk to each other: CI does
|
|
809
|
-
not pair a Windows runner with a macOS runner over the network. The
|
|
810
|
-
machine-to-machine link has only been exercised between two processes on one
|
|
811
|
-
host. Burn-in on your own two machines is still the last step.
|
|
812
|
-
- **A single account driving both machines' model sessions.** This release
|
|
813
|
-
executes commands deterministically; it does not inject prompts into a remote
|
|
814
|
-
DSH session. Whether one account can run two concurrent model sessions at once
|
|
815
|
-
depends on your account's limits and is untested here.
|
|
816
|
-
- **`write: true`.** A write-enabled task is executed, but there is no branch
|
|
817
|
-
merge in 0.0.1.
|
|
818
|
-
|
|
819
|
-
## Design notes
|
|
820
|
-
|
|
821
|
-
- **Zero third-party runtime dependencies.** Only `node:` built-in modules. The
|
|
822
|
-
relay is `node:http`; the downlink is Server-Sent Events and the uplink is
|
|
823
|
-
ordinary POST, so there is no WebSocket handshake to get wrong.
|
|
824
|
-
- **`@deepseek-ai/dsh-tools` is an optional peer dependency**, resolved at
|
|
825
|
-
runtime by DSH. It is deliberately *not* a hard dependency: declaring it would
|
|
826
|
-
pull roughly 287 `@deepseek-ai/*` packages into the install for one import.
|
|
827
|
-
- **Long lease + progress heartbeat**, never a fixed timeout. Measured risk: a
|
|
828
|
-
machine running a 40-minute test would be declared dead and the task re-sent to
|
|
829
|
-
another machine — both machines then running it, both producing side effects.
|
|
830
|
-
Only a *failed renewal* marks a lease dead, and only the relay's clock decides.
|
|
831
|
-
|
|
832
|
-
The full wire contract — every endpoint, field and state transition — is in
|
|
833
|
-
[PROTOCOL.md](PROTOCOL.md). It is frozen: changing it is a breaking change.
|
|
834
|
-
|
|
835
|
-
## Development
|
|
246
|
+
#### 2. npm(MCP 服务器,任意客户端)
|
|
836
247
|
|
|
837
248
|
```bash
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
node --test test/agent.test.mjs
|
|
841
|
-
node --test test/tools.test.mjs
|
|
842
|
-
node --test --test-force-exit test/e2e.test.mjs # see note
|
|
249
|
+
npm install -g @twinsearth/w2m-dsh-plugin
|
|
250
|
+
w2m-mcp --help
|
|
843
251
|
```
|
|
844
252
|
|
|
845
|
-
|
|
846
|
-
> open SSE connection, and a long-lived stream keeps Node's event loop alive
|
|
847
|
-
> after the assertions finish. The suite also needs a git binary on `PATH`.
|
|
848
|
-
|
|
849
|
-
## Contributing
|
|
253
|
+
把任意 MCP 客户端指向 `w2m-mcp`。**零运行时依赖**;包里同时带有 relay、agent 与工具。
|
|
850
254
|
|
|
851
|
-
|
|
852
|
-
`PROTOCOL.md` in the same commit — that file is the contract three components
|
|
853
|
-
agree on.
|
|
255
|
+
#### 3. Claude Code
|
|
854
256
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
[MIT](LICENSE)
|
|
860
|
-
|
|
861
|
-
---
|
|
862
|
-
|
|
863
|
-
## 中文说明
|
|
864
|
-
|
|
865
|
-
**同一个 DeepSeek 账号,一条指令,让在线的 Windows 与 Mac 跑同一个项目。**
|
|
257
|
+
```bash
|
|
258
|
+
/plugin marketplace add TwinsEarth/dsh-windows2macos
|
|
259
|
+
/plugin install w2m@twinsearth
|
|
260
|
+
```
|
|
866
261
|
|
|
867
|
-
|
|
262
|
+
清单文件位于 [`.claude-plugin/`](.claude-plugin/),版本与发行版锁定。
|
|
868
263
|
|
|
869
|
-
|
|
870
|
-
**11 个工具、82 个测试套件、2228 个用例、12 项预检、0 失败。**
|
|
264
|
+
#### 4. Codex 及其它支持 MCP 的 CLI
|
|
871
265
|
|
|
872
|
-
|
|
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
|
+
```
|
|
873
272
|
|
|
874
|
-
|
|
875
|
-
|---|---|---|
|
|
876
|
-
| **分模块** | **已强制** | 每个文件属于且只属于一个已声明的层;出现未声明的依赖边,`layer-check` 直接失败 |
|
|
877
|
-
| **分层级** | **已强制** | 六个带序号的层,以及一张**允许清单**——不是一条可以被解读的规则,而是一张可以被读的清单 |
|
|
878
|
-
| **分权限** | **已建成,需手动开启** | 四个权限带、升级**恰好一级**、破坏性操作**永不自动放行**。**默认关闭**,见下方缺口 |
|
|
273
|
+
#### 5. 校验过的发布包(离线)
|
|
879
274
|
|
|
880
|
-
|
|
275
|
+
从 `v1.1.17` 的 release 页面下载**两个**文件 —— `twinsearth-w2m-dsh-plugin-1.1.17.tgz` 与 `SHA256SUMS` —— 然后:
|
|
881
276
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
+
```
|
|
887
282
|
|
|
888
|
-
|
|
283
|
+
每个 release 都带 `SHA256SUMS` 正是为了这一步。若某个 release 缺少它,**不要安装该版本**。
|
|
889
284
|
|
|
890
|
-
|
|
285
|
+
### 快速开始
|
|
891
286
|
|
|
892
|
-
|
|
287
|
+
```text
|
|
288
|
+
w2m_status # 我配对了吗?在哪个编队?
|
|
289
|
+
w2m_devices # 编队里有什么?
|
|
290
|
+
w2m_run mode="replicate" command_argv=["git","rev-parse","HEAD"]
|
|
291
|
+
w2m_wait task_id="..."
|
|
292
|
+
```
|
|
893
293
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
| `log-bounds.mjs` | **缺陷** | 同上——它是对**实测到的 40 倍日志增长**的答案 |
|
|
898
|
-
| `storage.mjs` | **缺陷** | 有工具真正通过它的「不可删除」层写入 |
|
|
899
|
-
| `fleet-scale.mjs`、`fleet-state.mjs`、`exit-channel.mjs`、`test-report.mjs` | 库 | 需要时给它们调用方 |
|
|
294
|
+
`replicate` 在每一台在线机器上执行;只有当所有机器返回**相同结果**且**相同 commit 锚点**时,裁决才是
|
|
295
|
+
`consistent`。`split` 给每台机器一个分片,`pipeline` 串联多个阶段,`broadcast` 指定一个执行者并汇报观察者,
|
|
296
|
+
`compose` 合并结果。
|
|
900
297
|
|
|
901
|
-
|
|
298
|
+
### 它是什么
|
|
902
299
|
|
|
903
|
-
|
|
300
|
+
一个可安装插件,把一组机器变成**一个编队**,以**你的账号**为键,而不是靠手工维护的主机清单。
|
|
904
301
|
|
|
905
|
-
|
|
302
|
+
| 组件 | 职责 |
|
|
303
|
+
|---|---|
|
|
304
|
+
| **Relay** | 一个小型 HTTP 服务:任务路由、租约、SSE、blob 存储。放在任何机器可达的位置。 |
|
|
305
|
+
| **Agent** | 每台机器上运行:配对一次、执行白名单命令、心跳。 |
|
|
306
|
+
| **Plugin** | 你的 agent 调用的那些工具。 |
|
|
307
|
+
| **Admission**(1.1.14+) | 新机器申请加入;在线成员批准,**10 秒自动同意**;入队 24 小时内的机器可被立即移除。 |
|
|
906
308
|
|
|
907
|
-
|
|
309
|
+
### 工具
|
|
908
310
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
| **七阶段委员会**能在 macOS 上跑通 | MacMini `exit 0`:7 个阶段、异议被保留、`recount` 复现成功 |
|
|
912
|
-
| **字段级接受**在 macOS 上成立 | MacMini `exit 0`:级别 0 拿不到 `evidence`、级别 70 拿得到、篡改被抓、**缺祖先时拒绝而不重新生根** |
|
|
913
|
-
| 编队是**5 台配对中 3 台可用** | 而 `fleet-scale.mjs` 因此**拒绝外推** |
|
|
914
|
-
| **`P2P_PUNCH_FAILED`** | **未验证。** 三个假设已测量并排除(BLAKE2s-256、更广的原语集、版本偏差)。**抛出它的是跑着 0.9.5 的插件**——升级就是下一个实验 |
|
|
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`
|
|
915
313
|
|
|
916
|
-
|
|
314
|
+
### 被派发命令会收到的环境变量(1.1.15+)
|
|
917
315
|
|
|
918
|
-
|
|
919
|
-
|
|
316
|
+
取值来自 offer,**不来自 argv**,因此单台机器无法改变整个编队的分片:`W2M_INDEX` / `W2M_INDEX_TOTAL` /
|
|
317
|
+
`W2M_STAGE_INDEX` / `W2M_TASK_ID` / `W2M_MODE` / `W2M_MACHINE_ID` / `W2M_MACHINE_NAME`。
|
|
920
318
|
|
|
921
|
-
|
|
319
|
+
因此 `command_argv=["node","-e","process.exit(Number(process.env.W2M_INDEX)+1)"]` 在四台机器上应分别退出
|
|
320
|
+
1/2/3/4。**单机行为已测;四台机器的联跑尚未验证**(见下方"未验证")。
|
|
922
321
|
|
|
923
|
-
|
|
322
|
+
### 编队模型
|
|
924
323
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
- 官方 agent-team 明确**不支持** teammates 各自独立的工作目录,也**不支持多个进程协作同一个 team**。
|
|
324
|
+
身份是**账号**,不是主机清单。机器从账号材料派生 Ed25519 身份;账号的第一台机器配对时,relay 登记对应公钥;
|
|
325
|
+
此后每台机器用一次性签名挑战自证同一个账号。
|
|
928
326
|
|
|
929
|
-
|
|
327
|
+
只有当旧配对路径因**明确原因**失败时 —— 配对码无效、relay 不可达、超时 —— 机器才降级去申请入队。其它失败需要
|
|
328
|
+
人工介入,**不会**切换机制。**拒绝即终态**,机器不会再次申请。
|
|
930
329
|
|
|
931
|
-
|
|
330
|
+
所有时间窗与法定人数由 relay 权威判定,因为客户端时钟互不一致(实测:同一编队内相差数十秒):
|
|
932
331
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
| **九态聚合** | "环境不同"与"结果不同"是两件事,合并记账会把噪声当 bug;另有 `pending` / `refused` 两个非判定值不计入结论 |
|
|
940
|
-
| **能力不匹配 → `refused`** | 拒绝执行,而不是静默降级 |
|
|
941
|
-
| **长租约 + 进度心跳** | 固定超时会让跑 40 分钟的机器被判死并重投 → **两台同时跑、同时产生副作用** |
|
|
942
|
-
| **零第三方运行时依赖** | 只用 `node:` 内置模块;下行 SSE + 上行 POST,不用 WebSocket |
|
|
332
|
+
- 无决策 **10 秒**后自动同意,但**仅当至少一名成员在线** —— 空编队无法同意,该拒绝有明确错误码
|
|
333
|
+
(`NO_ONLINE_MEMBER`);
|
|
334
|
+
- 入队后 **24 小时内**,任一在线成员可立即移除该机器;
|
|
335
|
+
- 超过 24 小时,单台机器只能**发起表决**,且法定人数集合在**发起时冻结**,投票者中途离线无法改变门槛;
|
|
336
|
+
- 法定人数:旧成员 ≥3 人需 **≥50%(向上取整)**;2 人编队的移除只要 **1 票**;
|
|
337
|
+
- 离线超过 **72 小时**的机器会离开**列表** —— 记录**永不删除**,它重新上报后会自动回来。
|
|
943
338
|
|
|
944
|
-
###
|
|
339
|
+
### 安全姿态
|
|
945
340
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
# 2) 每台参与机器(在其项目目录所在机器上跑)
|
|
955
|
-
node bin/w2m-localside.mjs --rabbit http://<中继地址>:8787 --pair PAIR-XXXXXXXX \
|
|
956
|
-
--project /path/to/your/project --name win-desktop --p2p-mode auto \
|
|
957
|
-
--allowed-commands '["node --test","git status --porcelain"]'
|
|
958
|
-
|
|
959
|
-
# 3) 装 DSH 插件
|
|
960
|
-
# v0.4.0 起可从 npm 装(比逐个指定 release 的 tgz 更好:不会锁死在某个旧版本上)。
|
|
961
|
-
# 本仓库自己的 profile 就一直锁在一个本地 0.9.5 的 tgz 上,结果落后了 30 多个版本。
|
|
962
|
-
npm install @twinsearth/w2m-dsh-plugin@latest
|
|
963
|
-
# 或者,如果你想钉死一个具体版本:
|
|
964
|
-
# npm install @twinsearth/w2m-dsh-plugin@0.41.0
|
|
965
|
-
#
|
|
966
|
-
# 没有 npm 的托管运行时(DSH 打包版就是这种)用 pnpm 即可,同一个 registry、同一份 tarball:
|
|
967
|
-
# pnpm add @twinsearth/w2m-dsh-plugin@0.41.0 --save-exact
|
|
968
|
-
```
|
|
341
|
+
- 命令白名单按 **argv 前缀**匹配,并拒绝 argv 重解析标志(`--import`、追加 `-e`),错误码
|
|
342
|
+
`COMMAND_REPARSE_ARGUMENT`。这是边界,不是沙箱。
|
|
343
|
+
- 派发任务需要 operator token;device token 不能派发。
|
|
344
|
+
- 审批入队要求至少一名成员在线。
|
|
345
|
+
- 账户签名密钥从 **fleet key** 派生,**绝不**用 account code —— account code 会写进日志,用可读值派生密钥等于
|
|
346
|
+
交出账号身份。
|
|
347
|
+
- 每个发行版都有校验和。
|
|
969
348
|
|
|
970
|
-
|
|
349
|
+
### 已验证与未验证
|
|
971
350
|
|
|
972
|
-
|
|
973
|
-
w2m_status → 应能看到 actor_authority 与 approvals 两个字段(0.9.5 没有)
|
|
974
|
-
工具列表 → 应能看到 w2m_council(旧版只有 8 个工具)
|
|
975
|
-
```
|
|
351
|
+
**一句真话胜过一块掩盖问题的绿牌。**
|
|
976
352
|
|
|
977
|
-
|
|
353
|
+
**已验证:** 102 个文件中的 2458 项测试(2444 通过、14 跳过)、15 项门禁检查、Windows / macOS / Linux 三平台
|
|
354
|
+
CI 全绿、npm 与 GitHub 发布及校验和一致、在真实的 Linux/Mac/Windows 三机编队上跑过端到端。
|
|
978
355
|
|
|
979
|
-
|
|
356
|
+
**未验证 / 已知缺口:**
|
|
980
357
|
|
|
981
|
-
|
|
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 路径 |
|
|
982
364
|
|
|
983
|
-
|
|
365
|
+
### 故障排查
|
|
984
366
|
|
|
985
|
-
|
|
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 路径,结果不受影响 |
|
|
986
374
|
|
|
987
|
-
|
|
988
|
-
- ✅ **跨真网一跳已实测**:派发端在**中国移动的对称 NAT 之后**(三台 STUN 服务器给出三个不同的映射端口:36028 / 5855 / 26713,`endpoint-dependent`,punchable: false),执行端在东京的共享服务器上。**实测打洞成功:`HELLO` → `HELLO_ACK` 331.61 ms**,账本为那台日本机器记录 `transport: p2p`(`result_path: p2p`)。对称 NAT 挡住的是"被拨入",挡不住"主动拨出" —— 这正是派发方负责打洞的原因。
|
|
989
|
-
- ✅ **要接受打洞的机器必须有可达的 UDP 端口**:公网主机上就是一条防火墙规则(共享服务器已加)。**v0.4.1 起可以用 `--p2p-port <n>`(或 `W2M_P2P_PORT`)固定端口**,规则只需写一个端口,不必覆盖区间。
|
|
990
|
-
- ✅ **共享服务器已上线**:中继(`:8787` 与经 nginx 的 `:80`)、STUN 响应器(`:3478/udp`)、systemd 单元、ufw 规则,一条命令可复现:`deploy/shared-server/install.sh`。
|
|
991
|
-
- ✅ **三平台 CI**:同一套用例在 `windows-latest`(node 20/22)、`macos-latest`(node 22)、`ubuntu-latest`(node 22)上跑,见上方徽章与 `.github/workflows/ci.yml`。
|
|
992
|
-
- ⚠️ **两个 NAT 之后的互通仍未实测**:本机环回上的打洞只证明协议正确,不证明能穿透 NAT;目前实测的是**一跳、一个 NAT**(见上)。
|
|
993
|
-
- ⚠️ **真实的 Windows↔macOS 互联未实测**:CI 矩阵只证明**各平台都能跑**这套代码;矩阵里的两台机器**并不会互相通信**。
|
|
994
|
-
- ⚠️ **UDP 直连通道本身不认证**(与 v0.3.9 §6 相同):session id 是 32 位随机数,UDP 源地址可伪造 —— 直连不比中继更可信,威胁模型包含路上攻击者时请开启请求签名。
|
|
995
|
-
- ⚠️ **单账号驱动两台机器的模型会话未实测**:本版本执行的是**确定性命令**,不向远端 DSH 会话注入 prompt。
|
|
996
|
-
- ⚠️ **`write: true` 不合并**:写模式的任务会执行,但没有分支合并。
|
|
375
|
+
### 链接
|
|
997
376
|
|
|
998
|
-
|
|
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)
|
|
999
381
|
|
|
1000
|
-
|
|
382
|
+
MIT。
|