@twinsearth/w2m-dsh-plugin 0.5.9 → 0.6.2
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/CHANGELOG.md +379 -3
- package/PROTOCOL-v0.4.0.md +11 -3
- package/PROTOCOL.md +69 -15
- package/README.md +197 -27
- package/SECURITY.md +69 -19
- package/TROUBLESHOOTING.md +129 -53
- package/bin/w2m-localside.mjs +5 -0
- package/bin/w2m-rabbit.mjs +32 -5
- package/deploy/networking/LESSONS.md +18 -1
- package/deploy/networking/README.md +32 -7
- package/deploy/windows/README.md +20 -1
- package/deploy/windows/install-service.ps1 +98 -19
- package/deploy/windows/verify-supervisor.mjs +794 -0
- package/deploy/windows/w2m-agent-supervisor.mjs +867 -40
- package/docs/AUDIT.md +10 -1
- package/docs/DEPLOY.md +16 -3
- package/docs/P2P-CRYPTO.md +105 -4
- package/docs/PIPELINE-DESIGN.md +35 -10
- package/docs/TROUBLESHOOTING.md +22 -3
- package/package.json +5 -5
- package/src/agent/agent.mjs +242 -23
- package/src/agent/allowed-commands.mjs +480 -18
- package/src/agent/crypto.mjs +252 -40
- package/src/agent/p2p-node.mjs +136 -7
- package/src/agent/p2p.mjs +342 -37
- package/src/agent/resolve.mjs +68 -3
- package/src/plugin/fleet-target.mjs +160 -15
- package/src/plugin/tools.mjs +27 -29
- package/src/relay/persistence.mjs +72 -2
- package/src/relay/server.mjs +84 -11
- package/src/relay/state.mjs +9 -2
package/README.md
CHANGED
|
@@ -65,8 +65,13 @@ when the network will not allow it.
|
|
|
65
65
|
Both machines announce the address a STUN server sees them as, the dispatching
|
|
66
66
|
machine punches to the other's candidate, and the task offer travels over the
|
|
67
67
|
punched channel instead of the relay. The relay holds its own copy of that offer
|
|
68
|
-
back for
|
|
69
|
-
punch does not land — so a failed punch costs latency,
|
|
68
|
+
back for **3000 ms** (`DEFAULT_P2P_OFFER_GRACE_MS`) while the punch is attempted,
|
|
69
|
+
and offers it anyway if the punch does not land — so a failed punch costs latency,
|
|
70
|
+
not the task. The constant was 1200 ms first and that was **too tight**: measured on
|
|
71
|
+
a 1-vCPU shared host, two agent tests saw the relay's copy overtake the direct one,
|
|
72
|
+
which under `p2pMode: direct` means refusing an offer that was about to arrive.
|
|
73
|
+
3000 ms covers a slow host with room to spare, and the cost is bounded — one delayed
|
|
74
|
+
copy per task, once.
|
|
70
75
|
|
|
71
76
|
The result *notice* returns the same way, with one stated limit: the frame carries
|
|
72
77
|
the task and machine identity, not the result payload. `result_path` is one of the
|
|
@@ -95,27 +100,52 @@ socket — `endpoint-dependent`, punchable: false. A default that hard-required
|
|
|
95
100
|
direct path would break every deployment on such a network the moment it
|
|
96
101
|
upgraded. See [PROTOCOL-v0.4.0.md](PROTOCOL-v0.4.0.md) §1.
|
|
97
102
|
|
|
98
|
-
###
|
|
103
|
+
### Four coordination modes
|
|
104
|
+
|
|
105
|
+
Four — and the third and fourth are the ones people fail to find, which is why they are named here.
|
|
99
106
|
|
|
100
107
|
| Mode | What it does | When to use |
|
|
101
108
|
|---|---|---|
|
|
102
109
|
| `replicate` | every machine runs the **same** command; results compared side by side | "does this change pass on both platforms?" |
|
|
103
110
|
| `split` | work divided by `index` (`explicit` or `modulo` sharding) | one large test suite, several machines |
|
|
111
|
+
| `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" |
|
|
112
|
+
| `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" |
|
|
113
|
+
|
|
114
|
+
Two properties of the last two are worth stating because they are what stop them from being footguns:
|
|
104
115
|
|
|
105
|
-
|
|
116
|
+
* **Observers are never counted as agreeing.** An `observing` lease is excluded from every aggregation
|
|
117
|
+
step, so a machine that did not run the command cannot be reported as having produced the same result.
|
|
118
|
+
* **A pipeline has no cross-machine data flow.** It is `replicate` applied to a sequence: stage 2 on a
|
|
119
|
+
machine consumes that machine's stage 1, never a peer's. The alternative — fan stage 1 out, converge
|
|
120
|
+
the outputs into stage 2 — has no well-defined input on a fleet, and two machines finishing in a
|
|
121
|
+
different order would make one `task_id` describe two different executions. The reasoning is in
|
|
122
|
+
[docs/PIPELINE-DESIGN.md](docs/PIPELINE-DESIGN.md), which also records that the chain's `command_hash`
|
|
123
|
+
binds **every** stage, not just the first.
|
|
106
124
|
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
### Nine aggregation states
|
|
126
|
+
|
|
127
|
+
Nine, not four — because "the environments differ" and "the results differ" are different findings, and
|
|
128
|
+
collapsing them turns noise into bug reports. Three more were added in v0.3.0, each replacing a case that
|
|
129
|
+
used to be reported as something less true.
|
|
109
130
|
|
|
110
131
|
| State | Meaning |
|
|
111
132
|
|---|---|
|
|
112
133
|
| `consistent` | every machine agreed on every comparable field |
|
|
113
134
|
| `divergent` | machines disagreed — the report names the field and each machine's value |
|
|
114
135
|
| `divergent-platform` | only toolchain/platform differ, and the difference is confined to stdout text — expected |
|
|
136
|
+
| `degraded` | every machine reported; some succeeded and some failed. Distinct from `partial`, which also means "still in flight" |
|
|
115
137
|
| `failed` | every machine failed |
|
|
116
|
-
| `partial` | some succeeded, some failed |
|
|
138
|
+
| `partial` | some succeeded, some failed, and machines are still out |
|
|
139
|
+
| `timeout` | the deadline passed and the machines that never answered are not coming back |
|
|
140
|
+
| `cancelled` | the operator stopped it — not a defect, so it must not be reported as `failed` |
|
|
117
141
|
| `unverifiable` | an **anchor** did not match, so comparison is refused |
|
|
118
142
|
|
|
143
|
+
Two more values are **not verdicts**, because no terminal answer exists yet or none was ever possible:
|
|
144
|
+
`pending` (before the first result lands) and `refused` (every machine was refused at the capability
|
|
145
|
+
gate). `pending` is the only non-terminal one; `refused` reads like "still deciding" but every machine
|
|
146
|
+
has already answered, so waiting cannot change it. All nine verdicts are declared in
|
|
147
|
+
`src/relay/report.mjs`'s `AGGREGATE_STATUSES`.
|
|
148
|
+
|
|
119
149
|
## Install
|
|
120
150
|
|
|
121
151
|
> **This package can run three ways, and all three start the same binaries.**
|
|
@@ -181,6 +211,28 @@ runs unless it matches a prefix:
|
|
|
181
211
|
> split on whitespace — so `node -e console.log` does **not** allow
|
|
182
212
|
> `node -e console.log(1)`, and a path containing a space has to be written as an
|
|
183
213
|
> array entry. See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) §10.
|
|
214
|
+
>
|
|
215
|
+
> **What an entry actually grants — read this before trusting a list.** An entry
|
|
216
|
+
> names a tool and the arguments up to a point; whatever follows is still that
|
|
217
|
+
> tool's own behaviour, so `node --test` allows `node --test <any file>`, and the
|
|
218
|
+
> test runner loads and runs whatever file it is handed. **A prefix entry is not a
|
|
219
|
+
> sandbox**: it says which program may run, not what that program will do. Two
|
|
220
|
+
> narrower rules sit on top of the prefix match:
|
|
221
|
+
>
|
|
222
|
+
> * **Code-loading trailing arguments are refused** (`COMMAND_REPARSING_ARGUMENT`):
|
|
223
|
+
> `--test-reporter`, `--import`, `--require`, `-r`, `--experimental-loader`,
|
|
224
|
+
> `-e`/`--eval`, `-p`/`--print`, `python -c`/`-m`, `bash -c`, `git -c`/`--exec-path`,
|
|
225
|
+
> `make -f`, `tar --to-command`, and their equivalents in the interpreters in
|
|
226
|
+
> common use. If a machine is meant to run one of them, name it **in the entry**
|
|
227
|
+
> (`["node --test --test-reporter=tap"]`) — the entry is the explicit grant, and
|
|
228
|
+
> the refusal says so.
|
|
229
|
+
> * **The executable is compared by identity, not by spelling.** A bare name is
|
|
230
|
+
> resolved through PATH as before; an absolute path is admitted only when it
|
|
231
|
+
> resolves to the same file the entry's executable does (otherwise
|
|
232
|
+
> `COMMAND_EXECUTABLE_PATH_MISMATCH`), and a relative path is refused outright
|
|
233
|
+
> (`COMMAND_EXECUTABLE_RELATIVE_PATH`) because the working directory comes from
|
|
234
|
+
> the offer. A copy of some other program at `E:\Temp\evil\node.exe` is not
|
|
235
|
+
> `node`, however it is named.
|
|
184
236
|
|
|
185
237
|
> ⚠️ **v0.0.1 note:** Localside is verified as a standalone process. Running it
|
|
186
238
|
> *in-process* from the plugin (`autoStartAgent: true`) is implemented but has
|
|
@@ -198,7 +250,7 @@ dsh plugin --profile <profile-name> add @twinsearth/w2m-dsh-plugin
|
|
|
198
250
|
|
|
199
251
|
```bash
|
|
200
252
|
dsh plugin --profile <profile-name> add \
|
|
201
|
-
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.
|
|
253
|
+
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
|
|
202
254
|
```
|
|
203
255
|
|
|
204
256
|
**From the repository** — the form a marketplace's one-click installer uses (it clones
|
|
@@ -231,6 +283,23 @@ Then give it the relay URL in that profile's `cordis.patch.yml` — or leave
|
|
|
231
283
|
p2pMode: auto
|
|
232
284
|
```
|
|
233
285
|
|
|
286
|
+
> **Do not add a `name:` line to that row.** In a patch, `name` is not a rename — it is an
|
|
287
|
+
> *assertion* about the entry being patched, and the Loader skips the whole patch when it does not
|
|
288
|
+
> match. The row is already inserted by the bundle with the package name, so an override that
|
|
289
|
+
> asserts something else is discarded, and the plugin mounts with the bundle's **empty** defaults:
|
|
290
|
+
>
|
|
291
|
+
> ```
|
|
292
|
+
> patch: name mismatch for "w2m" (expected "@twinsearth/w2m-dsh-plugin",
|
|
293
|
+
> got "@twinsearth/w2m-dsh-plugin/tools"), skipping
|
|
294
|
+
> ```
|
|
295
|
+
>
|
|
296
|
+
> That is a silent failure by construction — the install looks right, eight tools appear, and
|
|
297
|
+
> `w2m_status` reports `paired: false` and a null `rabbitUrl` because none of your values were ever
|
|
298
|
+
> applied. It is not hypothetical: this row's expected name moved from the `/tools` subpath to the
|
|
299
|
+
> package root in v0.5.4 (see `cordis.patch.yml`'s header for why), and every override that still
|
|
300
|
+
> asserted the old subpath stopped being applied on upgrade. `id` already identifies the row, so the
|
|
301
|
+
> safest `name:` is no `name:` at all.
|
|
302
|
+
|
|
234
303
|
Restart DSH. You should see eight tools: `w2m_devices`, `w2m_run`, `w2m_wait`,
|
|
235
304
|
`w2m_report`, `w2m_status`, `w2m_update`, `w2m_history`, `w2m_stats`.
|
|
236
305
|
|
|
@@ -342,10 +411,14 @@ before 0.1.2.
|
|
|
342
411
|
| `operator_token` | **only you** | **sending work** (`POST /v1/task`) |
|
|
343
412
|
|
|
344
413
|
A machine that can be told what to do should not automatically be able to tell the
|
|
345
|
-
others. The relay writes the operator token to `<state>/operator-token.txt`
|
|
346
|
-
first start and prints it once
|
|
347
|
-
|
|
348
|
-
|
|
414
|
+
others. The relay writes the operator token to `<state>/operator-token.txt` (mode
|
|
415
|
+
0600) on first start and prints it once — on that first start only, because a
|
|
416
|
+
relay's stdout ends up in journald, a container log or a CI transcript and this
|
|
417
|
+
token authorises dispatching to the whole fleet. Every later start names the file
|
|
418
|
+
instead of the value; `--print-operator-token` (or `W2M_PRINT_OPERATOR_TOKEN=1`)
|
|
419
|
+
prints it anyway for an operator who needs it there. The plugin takes it as
|
|
420
|
+
`operatorToken`. Starting the relay with `--operator-token ''` removes the
|
|
421
|
+
requirement — it warns loudly, and that is only appropriate for a trusted LAN.
|
|
349
422
|
|
|
350
423
|
### Surviving a restart
|
|
351
424
|
|
|
@@ -367,8 +440,8 @@ Turn it on in the profile patch:
|
|
|
367
440
|
```yaml
|
|
368
441
|
# ~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
369
442
|
- insert:
|
|
370
|
-
- id: w2m
|
|
371
|
-
name: '@twinsearth/w2m-dsh-plugin
|
|
443
|
+
- id: w2m
|
|
444
|
+
name: '@twinsearth/w2m-dsh-plugin'
|
|
372
445
|
config:
|
|
373
446
|
rabbitUrl: 'http://100.x.y.z:8787'
|
|
374
447
|
autoUpdate: true # default false
|
|
@@ -378,6 +451,16 @@ Turn it on in the profile patch:
|
|
|
378
451
|
# updateRepo: TwinsEarth/dsh-windows2macos # default
|
|
379
452
|
```
|
|
380
453
|
|
|
454
|
+
> **The id is `w2m` and the name is the bare package — both matter, and neither is cosmetic.**
|
|
455
|
+
> `w2m` is the id the packaged `cordis.patch.yml` inserts, so a second row under a different id
|
|
456
|
+
> is a second plugin instance rather than a configuration of the first. `@twinsearth/w2m-dsh-plugin`
|
|
457
|
+
> must be the **bare** package name: a subpath such as
|
|
458
|
+
> `@twinsearth/w2m-dsh-plugin/tools` resolves to no `package.json`, so `dsh.client` is never read
|
|
459
|
+
> and **the client half is silently disabled** — the composer control never registers, and nothing
|
|
460
|
+
> on the host side reports a problem. `./tools` remains a supported mount point; it is simply not
|
|
461
|
+
> the one this bundle uses, and using it costs you the browser half. See
|
|
462
|
+
> [TROUBLESHOOTING.md](TROUBLESHOOTING.md) §2.
|
|
463
|
+
|
|
381
464
|
Ask it what it is doing from any session with `w2m_update` (`action: "status"` to
|
|
382
465
|
inspect, `action: "check"` to run one cycle now).
|
|
383
466
|
|
|
@@ -431,8 +514,11 @@ Read this before exposing the relay to anything.
|
|
|
431
514
|
- **Commands are never shell strings.** argv arrays are passed to
|
|
432
515
|
`spawn(cmd, args, { shell: false })`, so nothing a caller types can be
|
|
433
516
|
reinterpreted as shell syntax.
|
|
434
|
-
- **Default-deny whitelist.** A machine runs nothing that
|
|
435
|
-
`--allowed-commands
|
|
517
|
+
- **Default-deny whitelist, with its limit stated.** A machine runs nothing that
|
|
518
|
+
does not match `--allowed-commands`, and the matching rules are in §2 above.
|
|
519
|
+
The list controls *which program* may run; a prefix entry grants what that
|
|
520
|
+
program can be told to do with the arguments it is given (`node --test` runs
|
|
521
|
+
the test files it is handed), so it is a control, not a sandbox.
|
|
436
522
|
- **Read-only by default.** `replicate` tasks run with `write: false`; there is
|
|
437
523
|
no code path that writes to your project in that mode.
|
|
438
524
|
- **TLS is your choice, but a deliberate one.** Shape A gets it from WireGuard;
|
|
@@ -496,19 +582,103 @@ a product.
|
|
|
496
582
|
|
|
497
583
|
| Suite | Tests | What it covers |
|
|
498
584
|
|---|---|---|
|
|
499
|
-
| relay |
|
|
500
|
-
| agent |
|
|
501
|
-
|
|
|
502
|
-
|
|
|
585
|
+
| 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 |
|
|
586
|
+
| 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 |
|
|
587
|
+
| tools | 89 | eight tools registered, schemas, typed errors, polling, operator-token enforcement (no request is sent without it), sub-path endpoints, and the credential canaries |
|
|
588
|
+
| 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 |
|
|
589
|
+
| claims | 19 | the document claims themselves: the version, the bundle patch, the tool list, every documented tarball URL, and the CHANGELOG's shape |
|
|
590
|
+
| fleet-target | 37 | the composer's target selection, and the `executor_machine_id` narrowing that overrides it |
|
|
591
|
+
| client-bundle | 25 | the browser half builds, exports, and resolves against `PLATFORM_MODULES` |
|
|
503
592
|
| 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 |
|
|
504
593
|
| 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 |
|
|
505
594
|
| 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) |
|
|
506
595
|
| 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 |
|
|
507
|
-
| update-wiring |
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
596
|
+
| update-wiring | 34 | configuration validation that names the setting, and one `ctx.effect` owning the timer whose disposer stops it |
|
|
597
|
+
| signing | 33 | request signing: mismatch, expiry, replay and required-signature refusals |
|
|
598
|
+
| stress | 9 | sustained load on the relay's frame handling |
|
|
599
|
+
| shared-config | 16 | the fleet-wide config file, including its refusal of secret-looking keys |
|
|
600
|
+
| metrics / metrics-route | 16 + 5 | the Prometheus exposition and its route, with no credential ever rendered |
|
|
601
|
+
| tool-cards | 8 | the tool descriptions the model actually reads |
|
|
602
|
+
| p2p-stun | 18 | the STUN responder and reflexive-address discovery |
|
|
603
|
+
| p2p-transport | 28 | the reliable ordered channel over UDP |
|
|
604
|
+
| p2p-signaling | 17 | peer candidates: announce, fetch, expiry |
|
|
605
|
+
| p2p-node | 38 | the node that binds the punch socket, announces, punches and dials |
|
|
606
|
+
| p2p-transport-fields | 28 | `transport` / `p2p.offer_path` / `p2p.result_path` / `p2p.opened_by` on every result |
|
|
607
|
+
| p2p-plugin | 32 | the plugin half of the direct path, including the pushes and their named failures |
|
|
608
|
+
| p2p-crypto | 33 | stage 1 of the data-plane encryption: the handshake, the refusal codes, the replay window and the epoch ratchet |
|
|
609
|
+
| stun-server | 27 | the standalone `w2m-stun` binary |
|
|
610
|
+
| loader-entry | 10 | the entry DSH actually mounts, packed and installed — the gate v0.5.4 needed |
|
|
611
|
+
| mdns-wire / mdns-discover | 38 + 45 | the DNS wire codec, and LAN discovery |
|
|
612
|
+
| 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 |
|
|
613
|
+
| signing-e2e | 8 | a signed task end to end |
|
|
614
|
+
| outbound-only | 7 | a running agent binds no TCP listener, and `relay` mode binds no UDP socket at all |
|
|
615
|
+
| recovery | 4 | restart and lost-offer recovery |
|
|
616
|
+
| 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 |
|
|
617
|
+
| p2p-agent | 21 | the agent half of the direct path, including the reverse dial for results |
|
|
618
|
+
|
|
619
|
+
**The numbers above are from v0.5.9 on `win32 x64`, Node `v24.18.1`** — the floor this package
|
|
620
|
+
declares is `>=20.19.0`, and Node 24 is what DSH ships on this host. **36 suites on disk, 1,139 tests:**
|
|
621
|
+
1,131 pass, **2 fail**, 6 skip. The two failures are `test/pipeline-e2e.test.mjs`'s two cases on this
|
|
622
|
+
Windows host; **everything else is green.** Three suites are environment-sensitive and their counts
|
|
623
|
+
move with it: `claims` is 19 tests here and **22 when `dist/` holds a packed tarball**, which is when
|
|
624
|
+
its two tarball-vs-`package.json` checks run instead of skipping — so a checkout with `dist/` measures
|
|
625
|
+
**1,142 tests / 1,134 pass / 4 skip**, and that is the number to expect from a release tree.
|
|
626
|
+
|
|
627
|
+
**Which script runs which suite matters, because they are not the same set:**
|
|
628
|
+
|
|
629
|
+
| Command | Suites | Result on this host (v0.5.9) |
|
|
630
|
+
|---|---|---|
|
|
631
|
+
| `npm test` — the unit suite | 29 | 1,051 tests, **all pass**, 6 skip |
|
|
632
|
+
| `npm run test:all` — units + end-to-end | 35 | 1,106 tests, 1,098 pass, **2 fail** (`pipeline-e2e`), 6 skip |
|
|
633
|
+
| `pwsh scripts/verify.ps1` — the acceptance run | 29 | 1,050 tests, 1,048 pass, **2 fail** (`pipeline-e2e`), 6 skip |
|
|
634
|
+
|
|
635
|
+
> **`test:all` and `verify.ps1` are not supersets of each other.** At v0.5.9 `test:all` was still
|
|
636
|
+
> missing `test/p2p-crypto.test.mjs` — 33 tests covering the data-plane encryption, absent from the
|
|
637
|
+
> suite a release runs, and `CHANGELOG.md`'s v0.5.9 entry says so instead of claiming otherwise.
|
|
638
|
+
> **v0.6.0 applied the one-line fix that entry asked its owner for**, so the file is now in `npm test`
|
|
639
|
+
> and in `test:all`; `verify.ps1`'s own suite list still does not name it.
|
|
640
|
+
|
|
641
|
+
`%DSH_HOME%\dsh-runtimes\` is absent on this host, which is why `verify.ps1` falls back to the
|
|
642
|
+
`node` on `PATH`; the resolver's own bundled-runtime case is exercised against a throwaway home in
|
|
643
|
+
`test/agent.test.mjs`, and “Not verified” below says what that does and does not prove.
|
|
644
|
+
|
|
645
|
+
> **`pipeline-e2e` fails 2/2 at the v0.5.9 tag on this host, and the cause is the test, not the
|
|
646
|
+
> product.** Both cases die with `ENOENT … order.txt` / `stop.txt`: the witness files the stages are
|
|
647
|
+
> supposed to append to were never created, because **no stage ever ran**. In that revision the suite
|
|
648
|
+
> passes its agent `--allowed-commands` as the *string* entry `process.execPath`
|
|
649
|
+
> (`pipeline-e2e.test.mjs`, `allowedCommands = [NODE]`). A string entry is split on whitespace **by
|
|
650
|
+
> design**, and on a machine whose Node lives under `C:\Program Files\`, that entry becomes the two
|
|
651
|
+
> tokens `C:\Program` and `Files\nodejs\node.exe`, which match nothing — so the agent refused the
|
|
652
|
+
> chain with `COMMAND_NOT_ALLOWED` before its first stage. It is therefore **not a Node-24 artifact**:
|
|
653
|
+
> it is a test defect that appears wherever the interpreter's path contains a space, and it is
|
|
654
|
+
> consistent with this suite passing on CI runners, whose Node is unpacked to a space-free path.
|
|
655
|
+
> **The working tree already repairs it** (array-form entry naming `-e`, plus a third case asserting
|
|
656
|
+
> that a stage outside the allow-list refuses the whole chain and runs nothing), and measured on this
|
|
657
|
+
> host that repair takes the suite to **3/3 pass**. The failure is recorded here because v0.5.9 is
|
|
658
|
+
> what a reader installs, and the honest label for the tag is **failing on Windows under
|
|
659
|
+
> `C:\Program Files`** rather than "passes".
|
|
660
|
+
|
|
661
|
+
**Skips are per platform, not a single number, and the difference is one test.** On this Windows host
|
|
662
|
+
(Node 24.18.1, `dist/` present, network tests not enabled) there are **4**:
|
|
663
|
+
|
|
664
|
+
| Suite | Skipped case | Why |
|
|
665
|
+
|---|---|---|
|
|
666
|
+
| agent | `writes 0600 where the platform honours POSIX bits` | NTFS carries no POSIX mode bits. **Skips on Windows; runs on macOS/Linux.** |
|
|
667
|
+
| 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.** |
|
|
668
|
+
| claims | `every documented tarball exists as a release asset` | network check off unless `W2M_NETWORK_TESTS=1` |
|
|
669
|
+
| update-source | `fetches, parses and verifies the published release end to end` | same gate |
|
|
670
|
+
|
|
671
|
+
Two further cases are environment-conditional rather than platform-conditional, and skip only when
|
|
672
|
+
their input is missing: `claims`'s two tarball-vs-`package.json` checks run when `dist/` holds a
|
|
673
|
+
packed tarball and skip when it does not, and `agent`'s `git anchors` suite skips when `git` is not on
|
|
674
|
+
`PATH`. On this machine `git` is present, so that one does not fire.
|
|
675
|
+
|
|
676
|
+
The consequence, stated rather than generalised: **macOS and Linux run the two agent cases Windows
|
|
677
|
+
cannot, and skip `reports the windows filesystem honestly`, which is Windows-only**, so their skip
|
|
678
|
+
count is set by the same network gate plus whichever platform case applies — **it is not this host's
|
|
679
|
+
number, and calling it one is exactly the generalisation this section used to make.** No macOS or
|
|
680
|
+
Linux run was performed for this table; only the CI matrix measures those, and by this project's own
|
|
681
|
+
labelling rule the figure above is `[M]` for Windows and `[?]` for the other two.
|
|
512
682
|
|
|
513
683
|
The one test that calls GitHub for real is **opt-in** (`W2M_NETWORK_TESTS=1`).
|
|
514
684
|
GitHub allows 60 unauthenticated API requests per hour *per IP* and hosted CI
|
|
@@ -618,7 +788,7 @@ DSH 目前**不能**跨机器协作,而且不是疏忽:
|
|
|
618
788
|
| **中继 = 会合点 + 账本 + 回落** | 中继仍然收下每一条结果:它是账本,账本不完整,比较就不可信 |
|
|
619
789
|
| **默认只读**(`replicate` 且 `write: false`) | 一次性消除重复提交、覆盖写、推送竞争这三类最常见的事故 |
|
|
620
790
|
| **三锚校验** | 实测:脏工作区上 `git checkout --detach <sha>` **退出码 0 却保留本地改动** —— 只看 commit SHA 会把"不是同一份代码"误判成"结果分歧" |
|
|
621
|
-
|
|
|
791
|
+
| **九态聚合** | "环境不同"与"结果不同"是两件事,合并记账会把噪声当 bug;另有 `pending` / `refused` 两个非判定值不计入结论 |
|
|
622
792
|
| **能力不匹配 → `refused`** | 拒绝执行,而不是静默降级 |
|
|
623
793
|
| **长租约 + 进度心跳** | 固定超时会让跑 40 分钟的机器被判死并重投 → **两台同时跑、同时产生副作用** |
|
|
624
794
|
| **零第三方运行时依赖** | 只用 `node:` 内置模块;下行 SSE + 上行 POST,不用 WebSocket |
|
|
@@ -640,7 +810,7 @@ node bin/w2m-localside.mjs --rabbit http://<中继地址>:8787 --pair PAIR-XXXXX
|
|
|
640
810
|
|
|
641
811
|
# 3) 装 DSH 插件
|
|
642
812
|
dsh plugin --profile <profile-name> add \
|
|
643
|
-
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.
|
|
813
|
+
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
|
|
644
814
|
```
|
|
645
815
|
|
|
646
816
|
然后在 profile 的 `cordis.patch.yml` 里给插件 `rabbitUrl`(留空即用共享服务器)与 `operatorToken`,重启 DSH,即可直接用自然语言指挥:
|
package/SECURITY.md
CHANGED
|
@@ -52,16 +52,23 @@ real service and other people's machines are paired to it.
|
|
|
52
52
|
|
|
53
53
|
## Supported versions
|
|
54
54
|
|
|
55
|
-
**The supported line is v0.5.
|
|
56
|
-
says otherwise. Security fixes land on the newest installable version; older installable
|
|
57
|
-
not back-patched unless a report makes that necessary, and if it does, this table will
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| v0.5.
|
|
63
|
-
| v0.5.
|
|
64
|
-
|
|
|
55
|
+
**The supported line is v0.5.9, the newest published version.** A version is installable unless the
|
|
56
|
+
table says otherwise. Security fixes land on the newest installable version; older installable
|
|
57
|
+
versions are not back-patched unless a report makes that necessary, and if it does, this table will
|
|
58
|
+
say so.
|
|
59
|
+
|
|
60
|
+
| Version | Installable | Note |
|
|
61
|
+
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
62
|
+
| **v0.5.9** | **yes** | The supported line, and the current release. The client half is present and activates. |
|
|
63
|
+
| v0.5.6 – v0.5.8 | **yes** | Installable, and each one works, but each is superseded by v0.5.9. Prefer the newest. |
|
|
64
|
+
| v0.5.5 | **no — withdrawn** | Fails to activate during DSH's web boot, which makes the whole application refuse to start, and takes all eight tools with it. |
|
|
65
|
+
| v0.5.4 | **no — withdrawn** | Same loader failure. `CHANGELOG.md` marks it `DO NOT INSTALL`. |
|
|
66
|
+
| v0.5.3 and earlier | **yes — outdated** | Installable, and v0.5.3 is the last host-only release. Nothing before v0.5.4 has the client half, so no composer control. |
|
|
67
|
+
|
|
68
|
+
**v0.5.4 and v0.5.5 are withdrawn because they prevent the application from starting.** That is not a
|
|
69
|
+
degraded plugin or a missing feature: the loader entry fails, so the whole host entry is skipped and
|
|
70
|
+
DSH's web boot refuses, taking all eight tools with it. The failure and its cause are written up in
|
|
71
|
+
§3 of `TROUBLESHOOTING.md`.
|
|
65
72
|
|
|
66
73
|
The withdrawals are not a security judgement: those versions are not installable, so they are not
|
|
67
74
|
supported. `CHANGELOG.md` is the authority — an entry marked `DO NOT INSTALL` is not supported
|
|
@@ -71,11 +78,11 @@ whatever this table says, and the reverse is also true.
|
|
|
71
78
|
|
|
72
79
|
```bash
|
|
73
80
|
# The newest installable version from the registry
|
|
74
|
-
dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.
|
|
81
|
+
dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.9
|
|
75
82
|
|
|
76
83
|
# Or the same bytes from the release, verified against SHA256SUMS from the same release
|
|
77
84
|
dsh plugin --profile <profile> add \
|
|
78
|
-
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.
|
|
85
|
+
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
|
|
79
86
|
```
|
|
80
87
|
|
|
81
88
|
A version published minutes ago may be invisible to pnpm's `minimumReleaseAge` gate. That is the
|
|
@@ -98,6 +105,14 @@ The split is the point: a machine that can receive work does not thereby gain th
|
|
|
98
105
|
the others. If a machine's device token is stolen, the damage is confined to that machine's own
|
|
99
106
|
execution surface; it cannot be used to drive the fleet.
|
|
100
107
|
|
|
108
|
+
**It does, however, read the fleet's task metadata.** `GET /v1/tasks/{task_id}`, `POST /v1/heartbeat`
|
|
109
|
+
and the result/notice routes accept a **device** token, and a task's aggregate is keyed by `task_id`,
|
|
110
|
+
not by which machine is asking. So any paired machine's device token can read the aggregate of **any**
|
|
111
|
+
task on **every** machine — statuses, per-machine outcomes, anchors, comparable fields, counts. What it
|
|
112
|
+
cannot read that way is the captured output itself: the aggregate carries hashes and byte counts, not
|
|
113
|
+
`stdout`/`stderr` text. Treat the device token as a **fleet-wide metadata read credential**, not as a
|
|
114
|
+
credential scoped to its own machine.
|
|
115
|
+
|
|
101
116
|
The relay writes the operator token once, on first start, and prints it once on stderr. It is
|
|
102
117
|
deliberately absent from `--json` output — a token in a log is a leak. Treat it as a credential: it is
|
|
103
118
|
never needed in a bug report.
|
|
@@ -106,8 +121,22 @@ never needed in a bug report.
|
|
|
106
121
|
|
|
107
122
|
- **Run commands you allow, in the project directory it was pointed at.** The allow-list is
|
|
108
123
|
**default-deny** and matches the whole `argv`, token by token: an entry `node --test` permits
|
|
109
|
-
`node --test x.test.mjs` and refuses `node --testify x`.
|
|
110
|
-
|
|
124
|
+
`node --test x.test.mjs` and refuses `node --testify x`. That permission is **as wide as the tool**:
|
|
125
|
+
the test runner loads and runs the file it is handed, so `node --test` is a grant of code execution
|
|
126
|
+
from any file the machine can read — a prefix entry is not a sandbox. Two narrower rules apply on
|
|
127
|
+
top of the prefix match: trailing arguments that make the tool load code the entry never named
|
|
128
|
+
(`--test-reporter`, `--import`, `-e`, `python -c`, …) are refused with `COMMAND_REPARSING_ARGUMENT`,
|
|
129
|
+
and `argv[0]` is admitted as a path only when it is the file the entry's executable resolves to
|
|
130
|
+
(`COMMAND_EXECUTABLE_PATH_MISMATCH`; relative paths are refused outright with
|
|
131
|
+
`COMMAND_EXECUTABLE_RELATIVE_PATH`). Neither turns a prefix entry into a per-argument policy — if
|
|
132
|
+
you need that, name the arguments in the entry. **An empty allow-list means two opposite
|
|
133
|
+
things on the two sides, so set both.** The plugin's list (`allowedCommands`) is not a gate at all
|
|
134
|
+
when it is empty: with nothing configured the plugin declines to refuse, deliberately, and says so —
|
|
135
|
+
`checkAllowedCommand` returns "may proceed" rather than duplicating a policy it was not given. The
|
|
136
|
+
agent's list (`--allowed-commands`) is default-deny with nothing configured: an empty list refuses
|
|
137
|
+
**every** offer, and the refusal names the reason. So a fleet with no list on either side runs
|
|
138
|
+
nothing, and a fleet with the plugin's list empty but an agent list set runs exactly what the agent
|
|
139
|
+
allows — which is the only one of the two that is a control.
|
|
111
140
|
- **Read the project** it was started with, and compute git anchors and fingerprints over it.
|
|
112
141
|
- **Report results**, including command output, to the relay — where the operator who dispatched the
|
|
113
142
|
work can read them.
|
|
@@ -116,12 +145,16 @@ never needed in a bug report.
|
|
|
116
145
|
### What a paired machine cannot do
|
|
117
146
|
|
|
118
147
|
- **Run anything outside its allow-list.** A refused command is reported as `refused` with
|
|
119
|
-
`COMMAND_NOT_ALLOWED
|
|
148
|
+
`COMMAND_NOT_ALLOWED` — or, for the narrower rules above, `COMMAND_REPARSING_ARGUMENT`,
|
|
149
|
+
`COMMAND_EXECUTABLE_PATH_MISMATCH` or `COMMAND_EXECUTABLE_RELATIVE_PATH` — not executed.
|
|
120
150
|
- **Command other machines.** That requires the operator token, which the agent is not given.
|
|
121
|
-
- **Read another machine's
|
|
122
|
-
being
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
- **Read another machine's captured output.** This is the claim people most reasonably doubt, so it is
|
|
152
|
+
worth being exact rather than broad — see the read-scope paragraph above, which is the other half of
|
|
153
|
+
it. The operator token authorises _dispatch_, and no step of a dispatch needs a machine's `stdout`.
|
|
154
|
+
The relay's stored envelope does carry `stdout_head`/`stdout_tail`, but the aggregate that
|
|
155
|
+
`GET /v1/tasks/{task_id}` and the report render expose only `stdout_sha256`, `stdout_bytes` and the
|
|
156
|
+
comparable fields, so a device token alone cannot fetch that text. **Fleet-wide metadata, no captured
|
|
157
|
+
output** — if you need stronger than that, it is not there today.
|
|
125
158
|
- **Ask for your interactive confirmation per command.** There is no approval prompt in the loop. The
|
|
126
159
|
allow-list is the gate, which is why it is default-deny and why you should make it as narrow as the
|
|
127
160
|
work allows.
|
|
@@ -131,6 +164,23 @@ never needed in a bug report.
|
|
|
131
164
|
and Windows has no `sshd` by default. If you find anything in this project writing to an
|
|
132
165
|
`authorized_keys` file, that is a security report — please use the private channel above.
|
|
133
166
|
|
|
167
|
+
### The guarantees nobody has tested yet
|
|
168
|
+
|
|
169
|
+
The consent model above is the claim this project asks you to trust. Four of its statements are, as of
|
|
170
|
+
v0.5.9, **not backed by any test** — and the difference between "enforced", "true by absence" and "true
|
|
171
|
+
because we looked" matters, so each is labelled here the way an audit would label it rather than the way
|
|
172
|
+
a policy document would.
|
|
173
|
+
|
|
174
|
+
| Statement | Why it holds today | What it would take to be checkable |
|
|
175
|
+
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
176
|
+
| **The relay holds no model credentials and no working copy.** | **Enforced by the code's shape, not by a test or a guard.** The relay is a process whose entire filesystem access is its own state: `src/relay/persistence.mjs` reads and writes `devices.json`, `ledger.jsonl` and `operator-token.txt`, and `src/relay/server.mjs` reads the TLS cert and key. It never reads a project directory, so there is no code path a model credential could arrive through. It _does_ hold your task metadata, results and the operator token — that is the ledger's job, and it is stated above. | A test that the packed relay, started in a temp home with a project directory containing a `.credentials.yaml`, opens no file under that directory. `test/claims.test.mjs` is the natural home. |
|
|
177
|
+
| **The plugin never reads `.credentials.yaml`.** | **True by absence.** There is **no code path that references it anywhere in this repository** — no `readFile`, no `join`, nothing but the one comment in `src/plugin/tools.mjs` that promises not to. That is the strongest evidence available and it is **weaker than a test**: it says nobody wrote it yet, not that nothing can. | A source-level assertion over the shipped files: no `src/**` or `bin/**` file mentions a credentials path. Cheap, and it would catch the day someone adds one. |
|
|
178
|
+
| **Nothing writes `authorized_keys`, and no SSH server is opened.** | **True by absence**, same as above, and the same caveat: no `authorized_keys`, `ssh-keygen` or equivalent string exists in any `.mjs` in this repository. The design reason is real (every machine makes only outbound connections, and Windows ships no `sshd`), but the design reason is not the check. | Same as above: a source-level scan asserted by a test, plus the existing `test/outbound-only.test.mjs`, which already shows the machine side opens no listening TCP port. |
|
|
179
|
+
| **There is no approval prompt in the loop.** | **True by design, and nameable.** The plugin contains no permission-gating code at all — the word does not appear in it outside a comment listing what the tool dispatcher does _not_ implement — so a command that reaches the agent runs, and the allow-list is the only gate. Note that this is deliberately **not** the DSH permission prompt: if the dispatching session's own policy asks you to approve a call, that prompt is the harness's and is unaffected. | Hard to assert negatively; a claims check that the plugin registers no `approval`/`permission` request surface is the nearest honest form. |
|
|
180
|
+
|
|
181
|
+
None of the four becomes a guarantee by being written down. If you need one of them to be load-bearing
|
|
182
|
+
for a workload, treat it as **unverified** until a test in `test/claims.test.mjs` carries it.
|
|
183
|
+
|
|
134
184
|
### What it touches on your machine
|
|
135
185
|
|
|
136
186
|
Declared here and machine-readably in `package.json`'s `disclosure`, because installing a plugin runs
|