@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/docs/AUDIT.md
CHANGED
|
@@ -10,6 +10,15 @@ whose evidence cannot be told apart from its assumptions is worse than no audit:
|
|
|
10
10
|
|
|
11
11
|
Re-run the checks in §5 rather than trusting this file; it records a moment, not a guarantee.
|
|
12
12
|
|
|
13
|
+
> **SUPERSEDED — kept as the historical record.** This audit covers **v0.4.1 … v0.5.1**, and it is
|
|
14
|
+
> superseded by **v0.6.0 (2026-10-11)**. It is not being rewritten: what it recorded was true of the
|
|
15
|
+
> build it measured, and a reader who wants to know what that build did should still read it. Two
|
|
16
|
+
> figures in it were **already stale when it was written**, and are called out here rather than
|
|
17
|
+
> silently corrected below: §1's npm row says `@0.5.1, latest`, and §2 says `v0.5.0 installed` — the
|
|
18
|
+
> audit describes the session that built v0.4.1 … v0.5.1, and both rows name a version that had
|
|
19
|
+
> already been superseded by the time a reader could act on them. For the current state, use
|
|
20
|
+
> `CHANGELOG.md` and `README.md`.
|
|
21
|
+
|
|
13
22
|
## 1. Designed features: implemented?
|
|
14
23
|
|
|
15
24
|
| Designed feature | State | Evidence |
|
|
@@ -29,7 +38,7 @@ Re-run the checks in §5 rather than trusting this file; it records a moment, no
|
|
|
29
38
|
| Delivery-ladder documentation + acceptance script | ✅ | **[M]** `deploy/networking/acceptance.mjs` ran against the live fleet; baseline recorded (all three machines on rung 5 for a relay-dispatched task) |
|
|
30
39
|
| npm publication with Trusted Publishing | ✅ | **[M]** `@twinsearth/w2m-dsh-plugin@0.5.1`, latest, with a SLSA provenance attestation |
|
|
31
40
|
| dsh-plugin.org listing | ✅ | **[M]** listed and marked *Verified*; the page's own snapshot is stale (submission issue #141 asks for a re-scan) |
|
|
32
|
-
| TUN/VPN mode | ❌ **deliberately not built** | Decision and reasoning in `deploy/networking/LESSONS.md`: a tunnel is hub-and-spoke, so it would make "the data path is peer-to-peer" false for exactly the traffic a tunnel exists to carry |
|
|
41
|
+
| TUN/VPN mode | ❌ **deliberately not built** | Decision and reasoning in `deploy/networking/LESSONS.md`: a tunnel is hub-and-spoke, so it would make "the data path is peer-to-peer" false for exactly the traffic a tunnel exists to carry. That reasoning applies to adding a **TUN** adapter. It does not describe rung 4, which **is** deployed: a self-hosted WireGuard hub whose tunnelled traffic is named explicitly rather than passed off as server-free. See `deploy/networking/README.md`. |
|
|
33
42
|
|
|
34
43
|
**Also not built, and known:** the ladder is not yet ordered *data* with per-rung budgets; the
|
|
35
44
|
plugin has no `p2pPort` of its own (only the agent CLI needs one, since a dispatcher only dials out).
|
package/docs/DEPLOY.md
CHANGED
|
@@ -141,14 +141,27 @@ real one):
|
|
|
141
141
|
[w2m-rabbit] trust_proxy=false (X-Forwarded-* ignored)
|
|
142
142
|
[w2m-rabbit] pair_rate_limit=5/min/IP
|
|
143
143
|
[w2m-rabbit] persistence=on dir=/var/lib/w2m revived_devices=0 revived_tasks=0
|
|
144
|
-
... plus
|
|
145
|
-
<state>/operator-token.txt with mode 0600
|
|
144
|
+
... plus the OPERATOR TOKEN **on the start that generated it**, and otherwise
|
|
145
|
+
only where it is stored: <state>/operator-token.txt with mode 0600.
|
|
146
|
+
Later starts print "operator token (restored) ... NOT printed" instead,
|
|
147
|
+
because this banner lands in journald / a container log / CI output and
|
|
148
|
+
that token authorises dispatching to the whole fleet. Print it anyway with
|
|
149
|
+
--print-operator-token (or W2M_PRINT_OPERATOR_TOKEN=1).
|
|
146
150
|
```
|
|
147
151
|
|
|
152
|
+
On that one generating start the value appears **twice, deliberately**: once in
|
|
153
|
+
the `[w2m-rabbit]` line above (stderr — the log copy) and once in the CLI's
|
|
154
|
+
`OPERATOR TOKEN` box (stdout — the handoff a human reads). Two sinks on the start
|
|
155
|
+
that minted the token is not the defect this behaviour fixes; printing it on
|
|
156
|
+
*every* start, restored ones included, was. Every other start prints it in
|
|
157
|
+
neither place unless the opt-in above asks for it.
|
|
158
|
+
|
|
148
159
|
With `--json`, all of the above goes to **stderr** and stdout carries exactly
|
|
149
160
|
one JSON line; the operator token is reported only as
|
|
150
161
|
`"operatorTokenPresent": true`. Read the token from
|
|
151
|
-
`<state>/operator-token.txt`, never from stdout
|
|
162
|
+
`<state>/operator-token.txt`, never from stdout — the one exception is the start
|
|
163
|
+
that generated it, which prints it once so a first-time operator is not left
|
|
164
|
+
without it.
|
|
152
165
|
|
|
153
166
|
- the **pairing code** (`PAIR-XXXXXXXX`) — hand this to each machine, once. It
|
|
154
167
|
**rotates after every successful pairing**, and the new one is printed, so you
|
package/docs/P2P-CRYPTO.md
CHANGED
|
@@ -30,7 +30,9 @@ that is actually valuable here and needs no privileges: **its cryptography and i
|
|
|
30
30
|
* A passive observer anywhere on the path between two machines: task offers, results and file
|
|
31
31
|
fingerprints on the direct UDP channel become unreadable.
|
|
32
32
|
* An off-path attacker who knows a peer's address: without the peer's static private key a forged
|
|
33
|
-
`HELLO` cannot complete the handshake.
|
|
33
|
+
`HELLO` cannot complete the handshake, and a forged `REFUSE` cannot be sealed with `k_ref` either.
|
|
34
|
+
What such an attacker *can* still do is send an unsealed refusal if they can also spoof the punched
|
|
35
|
+
peer's source address and port — see the does-not-protect list below, which is where that belongs.
|
|
34
36
|
* Replay and reordering of data frames: counters plus a replay window, not hope.
|
|
35
37
|
* A *curious relay operator* reading task data **that travels over the direct channel**. Note the
|
|
36
38
|
relay still sees the control plane — see below.
|
|
@@ -42,6 +44,17 @@ that is actually valuable here and needs no privileges: **its cryptography and i
|
|
|
42
44
|
when, and the aggregate verdict. Metadata is not hidden by this work, and pretending otherwise would
|
|
43
45
|
be worse than saying so.
|
|
44
46
|
* Traffic analysis, or an attacker who can drop packets (availability is the relay's job).
|
|
47
|
+
* **An active attacker who can inject a datagram at a punch address.** `REFUSE` is what tells a
|
|
48
|
+
dialler "the handshake is impossible, here is why", and the unknown-peer form of it (`NO_PEER_KEY`,
|
|
49
|
+
`NO_KEY`) is sent precisely when the sender holds no key for the dialler — so it can carry no `k0`
|
|
50
|
+
proof, and an attacker who can put one such datagram on the wire *from the punched peer's own
|
|
51
|
+
address and port* can force the recorded outcome: a plaintext channel with `P2P_CRYPTO_NO_KEY`, or —
|
|
52
|
+
with a terminal refusal — the relay fallback. The v0.6.0 guard (`REFUSE_GUARD` in `src/agent/p2p.mjs`)
|
|
53
|
+
narrows this to exactly that attacker: a refusal that names another session, arrives from any other
|
|
54
|
+
address, or carries a `k_ref` seal that does not verify changes nothing, and a sealed refusal cannot
|
|
55
|
+
be forged without the peer's static private key. A peer that *can* seal its refusal is therefore not
|
|
56
|
+
downgradable at all; a peer running an older build cannot seal one, and its plaintext fallback has to
|
|
57
|
+
stay reachable.
|
|
45
58
|
* Any traffic that falls back to the relay, **unless** the relay leg is separately protected. Stage 1
|
|
46
59
|
leaves the relay leg as it is and says so in the ledger (`encrypted: false` for that machine).
|
|
47
60
|
|
|
@@ -96,7 +109,28 @@ session key k = HKDF( ECDH(e_i, e_r) ‖ ECDH(s_i, e_r) ‖ ECDH(e_
|
|
|
96
109
|
* A replayed `HELLO` cannot produce a session key, because the responder's fresh `e_r` never existed
|
|
97
110
|
at the time the original was captured.
|
|
98
111
|
* A machine that does not know the initiator's static key cannot verify `mac` and refuses, naming the
|
|
99
|
-
reason
|
|
112
|
+
reason. It does **not** silently downgrade to plaintext, and that sentence is narrower than it reads
|
|
113
|
+
— see the guard below and the residual in §2. What it means precisely is that a *failed handshake*
|
|
114
|
+
never falls back into the clear by itself: a plaintext channel is only handed back when the peer
|
|
115
|
+
named `NO_KEY`/`NO_PEER_KEY` **and** that refusal either came with proof or came from the address
|
|
116
|
+
that answered the punch. The v0.5.9 review broke the earlier, absolute wording with one 13-byte
|
|
117
|
+
datagram from a third socket, so the absolute wording is gone.
|
|
118
|
+
|
|
119
|
+
**Refusals, and what makes one evidence rather than a claim.** A `REFUSE` may carry a trailing 48-byte
|
|
120
|
+
MAC, sealed with `k_ref = HKDF(ECDH(e_i, s_r))` — the initiator's ephemeral (public on the wire) and
|
|
121
|
+
the responder's static key (which the responder always holds). Both ends derive it, neither needs the
|
|
122
|
+
other's *static* public key on file, and nobody else can. `NO_PEER_KEY`, `UNKNOWN_PEER`, `BAD_MAC` and
|
|
123
|
+
`REPLAY` are sealed when the sender can; a `VERSION` mismatch is not, because the peer that needs it
|
|
124
|
+
speaks a body layout this build cannot vouch for. A dialler drops a refusal that does not name the
|
|
125
|
+
agreed session, or that arrives from an address the punch never dialled, and it drops a **sealed**
|
|
126
|
+
refusal that would choose a plaintext channel while its seal does not verify. A plaintext-choosing
|
|
127
|
+
refusal with *no* seal is honoured only when it comes from the endpoint that acknowledged the punch —
|
|
128
|
+
that is the form an older build sends, and the reason it is still reachable. A terminal refusal
|
|
129
|
+
(`BAD_MAC`, `REPLAY`, a version mismatch) is honoured from a dialled address whether or not it is
|
|
130
|
+
sealed, and the outcome records which it was (`refusal_authenticated`: `true` proof, `null` no seal,
|
|
131
|
+
`false` a seal that did not verify) so a claim is never mistaken for proof. A dropped refusal is
|
|
132
|
+
recorded (`crypto.ignored_refusals`) and never fails the punch, because a datagram an attacker can
|
|
133
|
+
forge must not be able to deny service either.
|
|
100
134
|
|
|
101
135
|
**Transport.** ChaCha20-Poly1305 per frame; nonce = four zero bytes followed by a 64-bit
|
|
102
136
|
little-endian counter, per direction (WireGuard's layout). A 64-message sliding replay window. Rekey
|
|
@@ -110,7 +144,12 @@ untouched.
|
|
|
110
144
|
**Fallback, and it is not optional.** If the handshake fails — an older peer, an unknown key, a
|
|
111
145
|
tampered packet — the task proceeds **exactly as today**, over the relay, and the ledger records
|
|
112
146
|
`p2p.encrypted: false` together with the reason. A fleet must never lose a machine because a security
|
|
113
|
-
upgrade could not complete; it must only *say* that it did not.
|
|
147
|
+
upgrade could not complete; it must only *say* that it did not. The one outcome that is not a failure
|
|
148
|
+
is the peer saying it cannot take part at all (`NO_KEY`/`NO_PEER_KEY`): that is a plaintext channel,
|
|
149
|
+
recorded with its reason and with whether the refusal was sealed, and it is the documented outcome
|
|
150
|
+
rather than a downgrade. What §4 does **not** promise is that no datagram can produce it: an
|
|
151
|
+
unsealed refusal from the punched peer's own address can, which is the §2 residual and the reason the
|
|
152
|
+
guard exists.
|
|
114
153
|
|
|
115
154
|
**Compatibility.** Additive: an older peer ignores the unknown offer fields (that property is already
|
|
116
155
|
pinned by tests) and its frames are plaintext, which the newer peer accepts while recording
|
|
@@ -128,7 +167,11 @@ window carry over unchanged.
|
|
|
128
167
|
|
|
129
168
|
Unchanged from `deploy/networking/`: the hub makes machines reachable regardless of NAT, and it does
|
|
130
169
|
**not** make them peers — traffic between two clients is forwarded by the hub. It stays a fallback for
|
|
131
|
-
reachability,
|
|
170
|
+
reachability. When it carries a task, the ledger's `transport` may describe the socket path — rung 4
|
|
171
|
+
really is a socket between the two machines — but the tunnel **must be named explicitly** wherever it
|
|
172
|
+
carried the traffic (the `p2p` block's `opened_by` / `offer_path`, or an equivalent field), and
|
|
173
|
+
**"direct" must never be read as "the bytes avoided a server"**: here it means "a socket between the two
|
|
174
|
+
machines, forwarded by the hub". Naming the tunnel is the requirement, not falsifying the socket.
|
|
132
175
|
|
|
133
176
|
## 7. How any of this is believed
|
|
134
177
|
|
|
@@ -223,3 +266,61 @@ decision nobody has needed yet.
|
|
|
223
266
|
rediscovered.
|
|
224
267
|
* A v0.4.x/v0.5.x peer is unaffected: it gets the plain channel it always had, and our side records
|
|
225
268
|
`encrypted: false` with a reason. Never silently downgraded, never dropped from the fleet.
|
|
269
|
+
|
|
270
|
+
### Added by the v0.5.9 review (MEDIUM-4)
|
|
271
|
+
|
|
272
|
+
The review reproduced a downgrade with one datagram: a third socket on loopback sending a 13-byte
|
|
273
|
+
`HELLO_ACK` whose body is `REFUSE(NO_PEER_KEY)`. Without it the punch refused with
|
|
274
|
+
`P2P_CRYPTO_HANDSHAKE_TIMEOUT` and returned no channel; with it the same punch returned `ok: true` with
|
|
275
|
+
a plaintext channel and recorded `P2P_CRYPTO_NO_KEY` — a live channel where the text of §4 and the
|
|
276
|
+
`cryptoOutcome` comment said none could exist, and a ledger entry blaming the peer. The fix is
|
|
277
|
+
`REFUSE_GUARD` (`src/agent/p2p.mjs`) plus the `k_ref` seal (`src/agent/crypto.mjs`), and it is stated
|
|
278
|
+
here rather than folded into §4 because §4 is the contract as it was agreed:
|
|
279
|
+
|
|
280
|
+
* a refusal must name the session the punch agreed, and arrive from an address that punch dialled;
|
|
281
|
+
* a **sealed** refusal that would choose a plaintext channel and whose `k_ref` seal does not verify is
|
|
282
|
+
dropped — an honest peer either seals correctly or sends no seal at all;
|
|
283
|
+
* an *unsealed* refusal that would choose plaintext must come from the endpoint that acknowledged the
|
|
284
|
+
punch — which is how an older peer, one that cannot seal, keeps its fallback;
|
|
285
|
+
* a *terminal* refusal is honoured from a dialled address sealed or not, and `refusal_authenticated`
|
|
286
|
+
records whether its seal was proof;
|
|
287
|
+
* a dropped refusal is recorded with a `reason_code` (`P2P_CRYPTO_REFUSE_SESSION_MISMATCH`,
|
|
288
|
+
`P2P_CRYPTO_REFUSE_UNPUNCHED_SOURCE`, `P2P_CRYPTO_REFUSE_BAD_MAC`,
|
|
289
|
+
`P2P_CRYPTO_REFUSE_UNCORROBORATED`) and never fails the punch, so forging one is not a cheap denial
|
|
290
|
+
of service;
|
|
291
|
+
* the outcome now says whether a refusal was proof or a claim (`refusal_authenticated`), because a
|
|
292
|
+
ledger that cannot tell those apart is the part of the defect that outlives the packet.
|
|
293
|
+
|
|
294
|
+
**Residual, and it is the honest version of §2's new bullet**: an attacker who can send an unsealed
|
|
295
|
+
refusal *from the punched peer's own address and port* can still force the recorded plaintext outcome,
|
|
296
|
+
or the relay fallback with a terminal refusal, against a peer old enough not to seal. Against a peer
|
|
297
|
+
that seals, it cannot. What no longer works at all is a datagram from anywhere else, one naming
|
|
298
|
+
another session, or a sealed downgrade that does not verify.
|
|
299
|
+
|
|
300
|
+
**Why a sealed downgrade that does not verify is dropped, and what that costs.** Sealing is not optional
|
|
301
|
+
for a peer that can do it — `k_ref` needs only the ephemeral it already received and the static key it
|
|
302
|
+
already holds — so a *plaintext-choosing* refusal that arrives sealed and fails verification is not a
|
|
303
|
+
peer whose records disagree with ours; it is a datagram somebody assembled. Honouring it as a claim
|
|
304
|
+
would let anyone who can spoof the punched address force plaintext, which is the defect this section
|
|
305
|
+
exists to close. The cost is stated rather than hidden: when **our** stored key for the peer is stale,
|
|
306
|
+
the peer seals its honest refusal with a `k_ref` we cannot reproduce, and what happens next depends on
|
|
307
|
+
which refusal it is. If it is a *downgrade* — the peer also holds no key for us, so it answers
|
|
308
|
+
`NO_PEER_KEY` — the seal fails and the refusal is dropped, so that misconfigured pair falls back to the
|
|
309
|
+
**relay** instead of a plaintext direct channel. If it is *terminal* — the peer does hold a good key for
|
|
310
|
+
us, so the mismatch shows up as `BAD_MAC` — the refusal is still honoured and still named, with
|
|
311
|
+
`refusal_authenticated: false` recording that its seal proved nothing. Either way the task completes,
|
|
312
|
+
the ledger records a fact that is true (`P2P_CRYPTO_HANDSHAKE_TIMEOUT`, or the peer's own named
|
|
313
|
+
refusal) rather than a plaintext channel an attacker could have chosen, and the staleness is the
|
|
314
|
+
operator's to fix. `refusal_authenticated` is what keeps the rest honest, and it is deliberately
|
|
315
|
+
three-valued on the outcome: `true` (a `k_ref` seal verified — proof), `null` (no seal at all: the form
|
|
316
|
+
an older build sends, which proves nothing), `false` (a seal was present and did not verify — the
|
|
317
|
+
stale-key `BAD_MAC` just described, which fails the punch honestly and is simply never counted as
|
|
318
|
+
proof). A reader of the ledger can therefore never mistake "somebody said the peer refused" for "the
|
|
319
|
+
peer proved it refused".
|
|
320
|
+
|
|
321
|
+
One collision was found and closed while adding the seal: `decodeRefuse` accepted one length and
|
|
322
|
+
validated nothing else, so a genuine `HELLO_ACK` could be read as a refusal — `body[6]` (the first byte
|
|
323
|
+
of a random ephemeral) as the code and `body[7]` as an id length — roughly once in 256 handshakes,
|
|
324
|
+
which would have refused an honest punch. The parse now checks the version byte, both legal lengths and
|
|
325
|
+
that the machine id holds no control bytes; a test bends a real acknowledgement's length byte into the
|
|
326
|
+
collision and asserts it is still not a refusal.
|
package/docs/PIPELINE-DESIGN.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# Pipeline mode: the design question that has to be answered first
|
|
2
2
|
|
|
3
|
-
Status:
|
|
4
|
-
`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
> **Status: resolved and implemented — keep reading for the reasoning, not for a to-do list.**
|
|
4
|
+
> `pipeline` shipped in **v0.3.4** as **Option C** below (`replicate` applied to a sequence): every
|
|
5
|
+
> machine runs the whole `stages` chain in order, stopping at the first stage that fails, with no
|
|
6
|
+
> cross-machine data flow. `src/relay/state.mjs`'s `createTask` accepts `mode: 'pipeline'`, requires a
|
|
7
|
+
> non-empty `stages` array of at most `MAX_PIPELINE_STAGES` (16) entries, and refuses `index_total > 1`.
|
|
8
|
+
> `CHANGELOG.md`'s `[0.3.4]` entry is the record, and `test/pipeline-e2e.test.mjs` drives a real relay
|
|
9
|
+
> and a real agent over it.
|
|
10
|
+
>
|
|
11
|
+
> **This header used to say "unresolved, and `pipeline` is deliberately not implemented", and that had
|
|
12
|
+
> been false since v0.3.4.** The sections below are left as they were written — the analysis is the
|
|
13
|
+
> reason the answer is Option C and not Option B, and editing the argument to match its conclusion would
|
|
14
|
+
> destroy the only thing this file is good for. Two things in it are now historical rather than
|
|
15
|
+
> current: the recommendation was **Option A**, and Option A is **not** what shipped; and "what
|
|
16
|
+
> implementing Option A requires" describes a plan, not the implementation.
|
|
9
17
|
|
|
10
18
|
## What is already settled
|
|
11
19
|
|
|
@@ -71,6 +79,12 @@ it is `replicate` applied to a sequence.
|
|
|
71
79
|
|
|
72
80
|
**Option A**, with Option C as a cheap follow-on if useful.
|
|
73
81
|
|
|
82
|
+
> **What shipped was Option C, not Option A.** v0.3.4 implemented the chain as `replicate` applied to a
|
|
83
|
+
> sequence: no `executor_machine_id`, every machine runs every stage. Option A — "one machine runs the
|
|
84
|
+
> whole chain, composed with `broadcast`" — remains **unbuilt**. The recommendation below is kept as
|
|
85
|
+
> written, because the reasons it gives are still the reasons Option B was refused; the divergence
|
|
86
|
+
> between this recommendation and the shipped behaviour is recorded rather than smoothed over.
|
|
87
|
+
|
|
74
88
|
Reasons, in order of weight:
|
|
75
89
|
|
|
76
90
|
1. Option B is the only one that needs cross-machine data flow, and it is the only one whose
|
|
@@ -82,14 +96,25 @@ Reasons, in order of weight:
|
|
|
82
96
|
|
|
83
97
|
## What implementing Option A requires
|
|
84
98
|
|
|
99
|
+
**None of this is a to-do list: Option C was implemented instead (v0.3.4), and it met four of these
|
|
100
|
+
five points on the way.** It is kept because it is still the checklist for Option A if that is ever
|
|
101
|
+
built, and because it records which parts of the system a chain touches.
|
|
102
|
+
|
|
85
103
|
- `state.mjs`: accept `mode: 'pipeline'` with `stages: [{command_argv, cwd_rel, ...}]`; keep
|
|
86
104
|
`executor_machine_id` meaningful; reject `index_total > 1` for the same reason `broadcast` does.
|
|
105
|
+
— **Done in v0.3.4 for Option C.** One difference: `executor_machine_id` is *not* used by a
|
|
106
|
+
pipeline, so a chain cannot be pinned to one machine this way.
|
|
87
107
|
- `agent.mjs`: execute stages in order, stop on the first non-`ok` stage, and report per-stage
|
|
88
108
|
outcomes. This is the substantial part — the envelope currently describes exactly one command.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- `
|
|
109
|
+
— **Done in v0.3.4.** The per-stage record travels in the envelope, and `command_hash` binds
|
|
110
|
+
**every** stage rather than stage 0 alone.
|
|
111
|
+
- `report.mjs`: a chain failure must be distinguishable from a chain that never started. — **Done.**
|
|
112
|
+
- `tools.mjs`: `w2m_run` gains `stages`. — **Done**, and `stages` is refused rather than ignored
|
|
113
|
+
under any other mode.
|
|
114
|
+
- `PROTOCOL`: the new field, and a compatibility note. — **Done**; the mode enum is in §5.1 and the
|
|
115
|
+
chain semantics are in §4.1.
|
|
92
116
|
|
|
93
117
|
**Not verified, and it cannot be verified here:** whether Option A is what was actually wanted. That
|
|
94
118
|
is a product question, not a technical one, and guessing it is how a fleet ends up with a chained
|
|
95
|
-
mode nobody asked for.
|
|
119
|
+
mode nobody asked for. As shipped, the question is answered for Option C by the fact that it exists
|
|
120
|
+
and is tested; whether Option A is *also* wanted is still open.
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -550,9 +550,28 @@ Two consequences that are easy to walk into:
|
|
|
550
550
|
[["C:\\Program Files\\nodejs\\node.exe", "-e"]]
|
|
551
551
|
```
|
|
552
552
|
|
|
553
|
-
(This is why the pipeline end-to-end suite
|
|
554
|
-
a path without spaces there —
|
|
555
|
-
|
|
553
|
+
(This is why the pipeline end-to-end suite used to fail locally and pass on CI
|
|
554
|
+
runners — Node lives at a path without spaces there — until the entry in that
|
|
555
|
+
suite was written as an array entry, the form above.)
|
|
556
|
+
|
|
557
|
+
Three more refusals look like this section's symptom, and each has its own code
|
|
558
|
+
because each has its own remedy:
|
|
559
|
+
|
|
560
|
+
* `COMMAND_REPARSING_ARGUMENT` — the entry matched, but a **trailing** argument
|
|
561
|
+
would make the tool load code the entry never named: `node --test
|
|
562
|
+
--test-reporter=file:///tmp/evil.mjs`, `--import`, `--require`, `-e`, `python -c`,
|
|
563
|
+
`bash -c`, `git --exec-path`, `make -f`, `tar --to-command`, … Name the argument
|
|
564
|
+
in the entry if this machine is meant to run it (`["node --test
|
|
565
|
+
--test-reporter=tap"]`); an entry that merely names the tool is not read as
|
|
566
|
+
permission to eval.
|
|
567
|
+
* `COMMAND_EXECUTABLE_PATH_MISMATCH` — `argv[0]` is a path, and it is not the
|
|
568
|
+
file the entry's executable resolves to. `C:\Program Files\nodejs\node.exe` is
|
|
569
|
+
admitted for the entry `node` only when that is what `node` resolves to; a copy
|
|
570
|
+
of another program at `E:\Temp\evil\node.exe` is refused, because a copy under an
|
|
571
|
+
allowed name is a different program.
|
|
572
|
+
* `COMMAND_EXECUTABLE_RELATIVE_PATH` — `argv[0]` is a relative path
|
|
573
|
+
(`./node.exe`). The working directory comes from the offer, so the path names a
|
|
574
|
+
different file in every directory; use the bare name or an absolute path.
|
|
556
575
|
|
|
557
576
|
To see what the machine actually received, ask the relay for the last task and
|
|
558
577
|
read the machine's `refusal_reason`, or run the agent in the foreground: the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -101,11 +101,11 @@
|
|
|
101
101
|
"scripts": {
|
|
102
102
|
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs",
|
|
103
103
|
"test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
|
|
104
|
-
"test:relay": "node --test test/relay.test.mjs",
|
|
105
|
-
"test:agent": "node --test test/agent.test.mjs",
|
|
106
|
-
"test:tools": "node --test test/tools.test.mjs",
|
|
104
|
+
"test:relay": "node --test --test-force-exit test/relay.test.mjs",
|
|
105
|
+
"test:agent": "node --test --test-force-exit test/agent.test.mjs",
|
|
106
|
+
"test:tools": "node --test --test-force-exit test/tools.test.mjs",
|
|
107
107
|
"test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
|
|
108
|
-
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs",
|
|
108
|
+
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs",
|
|
109
109
|
"pack": "node scripts/pack.mjs",
|
|
110
110
|
"lint": "eslint .",
|
|
111
111
|
"lint:fix": "eslint . --fix",
|