@twinsearth/w2m-dsh-plugin 0.5.6 → 0.5.8

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 CHANGED
@@ -1,59 +1,51 @@
1
+ ## [0.5.8] — 2026-10-11
2
+
3
+ ### Fixed
4
+
5
+ * **The CHANGELOG was out of order, and the claims gate caught it.** My bump scripts anchored the new
6
+ entry on the wrong heading, so v0.5.5 ended up *above* v0.5.6 and v0.5.7 and "the newest entry" stopped
7
+ meaning anything — which is exactly the assertion `test/claims.test.mjs` makes. Entries are ordered
8
+ newest-first again, and the anchor is the top of the file rather than a version I have to remember.
9
+ * **Mojibake in shipped code.** `src/agent/mdns-wire.mjs` is packed, and its comments carried an em dash
10
+ and a section sign that had been decoded as the wrong encoding and re-saved, so a reader would have
11
+ seen `鈥` and `搂` where the RFC references belong. The same damage was in `test/mdns-discover.test.mjs`.
12
+ All five mDNS files are ASCII now, and the repository's own mojibake scan is clean.
13
+
14
+ ### Notes
15
+
16
+ * Fix-forward again, and for the same reason: v0.5.6 and v0.5.7 are published and stay published.
17
+ * Three releases in an hour, each fixing a defect the previous one introduced and a gate caught. That is
18
+ the gates working, but it is also a release process with no pre-flight of its own — which is the next
19
+ thing this project should fix rather than the next thing it should be proud of.
20
+ * No runtime change: the code in v0.5.8 is the code in v0.5.6.
1
21
  # Changelog
2
22
 
3
23
  All notable changes to this project are documented here. The format follows
4
24
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
25
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
26
 
7
- ## [0.5.5] — 2026-10-11
27
+ ## [0.5.7] — 2026-10-11
8
28
 
9
29
  ### Fixed
10
30
 
11
- * **The v0.5.4 loader failure, with the cause the loader actually reported** rather than the hypothesis
12
- that stood in for it:
13
-
14
- ```
15
- w2m (@twinsearth/w2m-dsh-plugin/tools): Error: cannot get property "connection" without inject
16
- at registerTargetRoutes (src/plugin/fleet-target.mjs:483:25)
17
- ```
18
-
19
- cordis throws on reading a service property the fiber never declared, so the entry died and took all
20
- eight tools with it. **Every existing test was blind to this**, because they drive the plugin with a
21
- plain-object context where an undeclared read merely yields `undefined` — the fake was more forgiving
22
- than the runtime. Fixed with `ctx.inject(['connection'], …)`, which is also the only shape that can
23
- work: in the real composition the service is genuinely absent while `apply()` runs, and siblings are
24
- mounted concurrently.
25
- * **The client half was never composed either, for a second and independent reason:** the bundle row
26
- named the subpath `@twinsearth/w2m-dsh-plugin/tools` instead of the package, and a client package is
27
- identified by its Loader row specifier — a subpath resolves to no `package.json`, so `dsh.client` was
28
- never read and no boot-graph row was built. The row now names the package.
29
- * **A silent route rejection:** the control fetched `/w2m/target`, but fetch routes are validated
30
- against `/api/`, and the refusal failed only a child fiber, so nothing surfaced. Now
31
- `/api/w2m/target`, mirrored in the shipped bundle.
32
-
33
- ### Added
34
-
35
- * **`test/loader-entry.test.mjs`** — the test that would have caught v0.5.4: it packs the tree, installs
36
- the tarball with the installation's own `dsh plugin add` into a throwaway profile under a throwaway
37
- `DSH_HOME`, and boots it with the real `runProfile` under the Harness runtime, asserting that the
38
- entry activates, all eight tools are registered, the boot-graph row is composed from the shipped
39
- bundle, and no failed entry names the package. **Proved in both directions**: 6/6 pass on the fixed
40
- tree and 6/6 fail against the preserved v0.5.4 tarball, failing on precisely the loader error quoted
41
- above. It skips loudly, naming the probes it tried, when no installation is found.
42
- * Cheap always-on guards in `test/client-bundle.test.mjs` that need no Harness: the platform field, the
43
- client export, the row must be the bare package name, and the bundle's route must equal the host's.
31
+ * **v0.5.6 shipped without its own documents.** `TROUBLESHOOTING.md` and `SECURITY.md` were written,
32
+ reviewed and committed, and then **not packed**, because `files` names the documents individually
33
+ rather than by pattern. Anyone installing v0.5.6 got the code and none of the guidance. They are in the
34
+ package now, and `test/claims.test.mjs` is the place this class of defect belongs.
35
+ * **v0.5.6 shipped without its own gates wired in.** `test/claims.test.mjs`, `test/loader-entry.test.mjs`
36
+ and the two mDNS suites existed and passed, but were listed in none of `test`, `test:all` or `verify` —
37
+ a test that does not run protects nothing, which is the exact defect this release's own study
38
+ identified in someone else's repository. All four are wired in.
39
+ * A typographic character (a smart quote) in `test/mdns-discover.test.mjs`, caught by the repository's
40
+ own mojibake scan.
44
41
 
45
42
  ### Notes
46
43
 
47
- * **An upgrade from v0.5.4 requires one edit outside this package:** `cordis.patch.yml` in the profile
48
- must name the package (`@twinsearth/w2m-dsh-plugin`), not the `/tools` subpath. With the corrected
49
- bundle row, a mismatch makes the loader skip the whole patch — silently reverting `rabbitUrl`,
50
- `operatorToken`, `stateDir`, `projectDir`, `machineName`, `p2pMode` and `allowedCommands` to their
51
- shipped defaults. Measured both ways with `--dump-config`.
52
- * **0.5.4 remains DO NOT INSTALL.** This release exists so that a rebuilt artifact is distinguishable
53
- from the broken one by version alone.
54
- * Still unverified, and not claimed: how the control *renders*. No browser was opened. What is proven is
55
- the entire host side — the entry activates, the eight tools register, the boot-graph row is composed
56
- from the shipped bundle, the route is registered and attached — plus the seat shape in a VM test.
44
+ * Fix-forward, not a rewrite: v0.5.6 is published and stays published, because the alternative is
45
+ rewriting history someone may already have. Both defects are mine, and both are the class this project
46
+ keeps naming — a claim that nothing checks.
47
+ * No runtime change: the code in v0.5.7 is the code in v0.5.6.
48
+
57
49
  ## [0.5.6] — 2026-10-11
58
50
 
59
51
  ### Fixed
@@ -125,6 +117,58 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
125
117
  Bonjour/Avahi peer answers. There is **no third-party interoperability test**, no probing or conflict
126
118
  resolution, IPv4 only, and the responder is **built and tested but not started** by the plugin —
127
119
  loading a plugin should not silently multicast on the host's network.
120
+
121
+ ## [0.5.5] — 2026-10-11
122
+
123
+ ### Fixed
124
+
125
+ * **The v0.5.4 loader failure, with the cause the loader actually reported** rather than the hypothesis
126
+ that stood in for it:
127
+
128
+ ```
129
+ w2m (@twinsearth/w2m-dsh-plugin/tools): Error: cannot get property "connection" without inject
130
+ at registerTargetRoutes (src/plugin/fleet-target.mjs:483:25)
131
+ ```
132
+
133
+ cordis throws on reading a service property the fiber never declared, so the entry died and took all
134
+ eight tools with it. **Every existing test was blind to this**, because they drive the plugin with a
135
+ plain-object context where an undeclared read merely yields `undefined` — the fake was more forgiving
136
+ than the runtime. Fixed with `ctx.inject(['connection'], …)`, which is also the only shape that can
137
+ work: in the real composition the service is genuinely absent while `apply()` runs, and siblings are
138
+ mounted concurrently.
139
+ * **The client half was never composed either, for a second and independent reason:** the bundle row
140
+ named the subpath `@twinsearth/w2m-dsh-plugin/tools` instead of the package, and a client package is
141
+ identified by its Loader row specifier — a subpath resolves to no `package.json`, so `dsh.client` was
142
+ never read and no boot-graph row was built. The row now names the package.
143
+ * **A silent route rejection:** the control fetched `/w2m/target`, but fetch routes are validated
144
+ against `/api/`, and the refusal failed only a child fiber, so nothing surfaced. Now
145
+ `/api/w2m/target`, mirrored in the shipped bundle.
146
+
147
+ ### Added
148
+
149
+ * **`test/loader-entry.test.mjs`** — the test that would have caught v0.5.4: it packs the tree, installs
150
+ the tarball with the installation's own `dsh plugin add` into a throwaway profile under a throwaway
151
+ `DSH_HOME`, and boots it with the real `runProfile` under the Harness runtime, asserting that the
152
+ entry activates, all eight tools are registered, the boot-graph row is composed from the shipped
153
+ bundle, and no failed entry names the package. **Proved in both directions**: 6/6 pass on the fixed
154
+ tree and 6/6 fail against the preserved v0.5.4 tarball, failing on precisely the loader error quoted
155
+ above. It skips loudly, naming the probes it tried, when no installation is found.
156
+ * Cheap always-on guards in `test/client-bundle.test.mjs` that need no Harness: the platform field, the
157
+ client export, the row must be the bare package name, and the bundle's route must equal the host's.
158
+
159
+ ### Notes
160
+
161
+ * **An upgrade from v0.5.4 requires one edit outside this package:** `cordis.patch.yml` in the profile
162
+ must name the package (`@twinsearth/w2m-dsh-plugin`), not the `/tools` subpath. With the corrected
163
+ bundle row, a mismatch makes the loader skip the whole patch — silently reverting `rabbitUrl`,
164
+ `operatorToken`, `stateDir`, `projectDir`, `machineName`, `p2pMode` and `allowedCommands` to their
165
+ shipped defaults. Measured both ways with `--dump-config`.
166
+ * **0.5.4 remains DO NOT INSTALL.** This release exists so that a rebuilt artifact is distinguishable
167
+ from the broken one by version alone.
168
+ * Still unverified, and not claimed: how the control *renders*. No browser was opened. What is proven is
169
+ the entire host side — the entry activates, the eight tools register, the boot-graph row is composed
170
+ from the shipped bundle, the route is registered and attached — plus the seat shape in a VM test.
171
+
128
172
  ## [0.5.4] — 2026-10-11
129
173
 
130
174
  > **DO NOT INSTALL 0.5.4.** It fails to load in the Harness: the loader entry
@@ -165,6 +209,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
165
209
  serves the plugin from the profile's `node_modules`; the new revision must be installed *and the
166
210
  Harness restarted* before the control appears. The control is therefore in the artifact and not in the
167
211
  running UI, and this entry says so rather than implying otherwise.
212
+
168
213
  ## [0.5.3] — 2026-10-11
169
214
 
170
215
  ### Added
@@ -194,6 +239,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
194
239
  * Every socket in the new suite is loopback and crosses no NAT; that is stated in the suite header and
195
240
  is not evidence that a real translation can be crossed.
196
241
  * No protocol version bump: `envelope_version` stays `1.0` and a v0.4.x/v0.5.x peer is unaffected.
242
+
197
243
  ## [0.5.2] — 2026-10-10
198
244
 
199
245
  ### Fixed
@@ -227,6 +273,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
227
273
  * This release exists because `deploy/` and `docs/` are inside the published package: after v0.5.1 the
228
274
  tag and the tarball had drifted, and a release is the only honest way to close that.
229
275
  * No runtime change to the eight tools.
276
+
230
277
  ## [0.5.1] — 2026-10-10
231
278
 
232
279
  ### Fixed
@@ -261,6 +308,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
261
308
  supervisor leaves the agent alive with its punch socket still bound (`0.0.0.0:41235`).
262
309
  * Also included: the test fix for `test/outbound-only.test.mjs`, whose port was being read from the
263
310
  timestamp rather than from the field that states it (commit `57dbc5f`).
311
+
264
312
  ## [0.5.0] — 2026-10-10
265
313
 
266
314
  The release that stops treating "the relay carried it" as the only answer for a machine behind a
@@ -740,7 +788,6 @@ written down as not measured.
740
788
  this entry claim it is. The WAN measurement is one hop with one NAT, and it is
741
789
  labelled as one hop with one NAT.
742
790
 
743
-
744
791
  ## [0.3.9] — 2026-10-09
745
792
 
746
793
  P2P hole punching, so the payload path no longer has to cross the relay.
@@ -0,0 +1,123 @@
1
+ # Contributing
2
+
3
+ One page. Everything here is a rule you can check, not a preference — if a line in this document
4
+ cannot be verified by reading the diff, it does not belong here.
5
+
6
+ ## Setup
7
+
8
+ ```bash
9
+ git clone https://github.com/TwinsEarth/dsh-windows2macos
10
+ cd dsh-windows2macos
11
+ npm install # dev tools only: eslint, prettier, globals
12
+ node --version # must satisfy package.json engines: >=20.19.0
13
+ ```
14
+
15
+ There are **no runtime dependencies** — `dependencies` in `package.json` is `{}` and stays that way.
16
+ Adding one is a design change, not a convenience, and it will be refused. `@deepseek-ai/dsh-tools` is
17
+ an optional peer: the plugin imports it when DSH provides it and falls back to a small offline shim
18
+ when it does not, so the suites run with no Harness installed.
19
+
20
+ ## The commands
21
+
22
+ ```bash
23
+ # The unit suites (what scripts.test runs), and everything including e2e
24
+ node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs
25
+ npm test
26
+ npm run test:all
27
+
28
+ # One suite while you work
29
+ node --test --test-force-exit test/claims.test.mjs
30
+
31
+ # Lint: must be 0 errors, and 0 warnings
32
+ node node_modules/eslint/bin/eslint.js .
33
+
34
+ # Entry points import: lib/tools.js and lib/index.js, the specifiers a profile mounts
35
+ node scripts/check-entrypoints.mjs
36
+
37
+ # The installed copy registers its tools (point it at a profile's node_modules copy)
38
+ node scripts/verify-installed.mjs <path-to-installed-package>
39
+ ```
40
+
41
+ `scripts/check-entrypoints.mjs` proves the entry points **import**. It does not prove the loader
42
+ **composes** them — those are different failures, and the second one shipped once (v0.5.4: the entry
43
+ failed inside the real Loader and every tool disappeared while `verify-installed.mjs` passed). When
44
+ your change touches mounting, exports, `cordis.patch.yml` or the client half, run
45
+ `test/loader-entry.test.mjs` too: it installs the packed tarball with `dsh plugin add` into a
46
+ throwaway profile under a throwaway `DSH_HOME` and boots it with the real `runProfile`.
47
+
48
+ ## Conventions
49
+
50
+ Each line is checkable against a diff.
51
+
52
+ 1. **Zero runtime dependencies.** `dependencies === {}` in `package.json`. Node built-ins only.
53
+ 2. **Argv execution, never a shell string.** A command is an array of arguments passed to
54
+ `spawn`/`execFile` with no shell. No `exec`, no `shell: true`, no string interpolation into a
55
+ command line. This is what makes the allow-list meaningful: a shell would re-parse the argv the
56
+ allow-list just approved.
57
+ 3. **One named constant per network bound.** Every timeout, port, retry count, size limit and
58
+ interval is a named `const` in one place, exported if a test needs it. A literal `5000` in a
59
+ request call is a review comment.
60
+ 4. **A failure carries a reason code, not a boolean.** `{ ok: false, code: 'W2M_UNREACHABLE', ... }`
61
+ or a thrown error with `code` — never `false`, never `null`, never a bare string. `undefined` may
62
+ mean "absent"; it may not mean "failed".
63
+ 5. **Commit format: `<type>: <one line>`.** Types: `feat`, `fix`, `docs`, `test`, `chore`, `perf`,
64
+ `refactor`, `build`, `ci`. One line, imperative, no trailing period; put the argument for the
65
+ change in the body, not in the subject.
66
+ 6. **Docs changes are code changes.** If a claim in `README.md`, `PROTOCOL.md` or a deploy guide is
67
+ now false, fix it in the same PR. `test/claims.test.mjs` gates the machine-checkable subset.
68
+ 7. **A test that would pass against a broken artifact is not evidence.** See below.
69
+ 8. **Say what you verified, and how.** In the PR: what was run live, what was covered by a test, and
70
+ what is not verified at all. "Not verified" is an acceptable answer; an implied verification is
71
+ not.
72
+
73
+ ## Rule 7, in full: prefer the real runtime to a stand-in
74
+
75
+ This project learned it the hard way. The v0.5.4 loader failure was invisible to **every** existing
76
+ test, because those tests drove the plugin with a plain-object context where reading an undeclared
77
+ service property merely yields `undefined`. The real Loader throws instead. The fake was more
78
+ forgiving than the runtime, so the suite was green while the shipped artifact broke the application.
79
+
80
+ Therefore:
81
+
82
+ - **Drive the real thing when the real thing is reachable.** The entry points, the packed tarball,
83
+ the real Loader, a real pnpm install into a throwaway profile, the real `fetch` against a local
84
+ server. `test/loader-entry.test.mjs` exists precisely to pay this cost once.
85
+ - **When a stand-in is unavoidable, prove it in both directions.** A fix is not demonstrated by a
86
+ green test alone. Take the broken artifact, keep it, and show the new test **fails** against it —
87
+ then show it passes against the fix. `test/loader-entry.test.mjs` does this: 6/6 pass on the fixed
88
+ tree and 6/6 fail against the preserved v0.5.4 tarball, on the exact loader error.
89
+ - **Make the fake stricter than the runtime, never more forgiving.** If your stub silently accepts
90
+ something the runtime throws on, the test is measuring your stub.
91
+ - **A skip must be loud and must name what it could not reach.** A test that quietly returns when a
92
+ probe is missing is worse than no test: it reports confidence it does not have. Say which probe you
93
+ tried and how to enable it.
94
+ - **Fix a claim's drift at its source.** If a document, a count or a version is wrong, correct it
95
+ rather than exempting it in the test that caught it. An exemption list is where a gate goes to die.
96
+
97
+ ## Pull requests
98
+
99
+ 1. **Branch** off `main`; do not commit to `main`.
100
+ 2. **Keep it one idea.** A rename plus a behaviour change plus a doc rewrite is three PRs.
101
+ 3. **Run before you push**, and paste the real output into the PR:
102
+
103
+ ```bash
104
+ npm test
105
+ node node_modules/eslint/bin/eslint.js .
106
+ node scripts/check-entrypoints.mjs
107
+ ```
108
+
109
+ 4. **Describe verification, not effort.** The PR body answers: what changed, what proves it, what is
110
+ not proven, and which commands you ran with their result counts.
111
+ 5. **Add a CHANGELOG entry** under the version you are working on, in the existing style. A behaviour
112
+ change with no CHANGELOG line will be sent back.
113
+ 6. **Reviewers check the conventions list above**, line by line, against the diff. Expect to be asked
114
+ "which constant?", "which code?", "what did you run?".
115
+ 7. **A version that must not be installed gets marked.** If a release is broken after publication,
116
+ add `DO NOT INSTALL` to its CHANGELOG entry, in its own section, and say why. `test/claims.test.mjs`
117
+ reads that marker: it refuses to let the current version be one the CHANGELOG tells users not to
118
+ install.
119
+
120
+ ## Security
121
+
122
+ Do not open a public issue for a vulnerability. See [SECURITY.md](SECURITY.md) for the private
123
+ channel and what to include.
package/SECURITY.md ADDED
@@ -0,0 +1,162 @@
1
+ # Security policy
2
+
3
+ This document covers two things: how to report a vulnerability **privately**, and the consent model —
4
+ what a machine that joins your fleet can and cannot do. The second is the claim this project asks you
5
+ to trust, so it is stated as behaviour you can check, not as a promise.
6
+
7
+ ---
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ **Do not open a public issue for a vulnerability.** A public issue is readable by everyone the moment
12
+ it is filed, including anyone who has not updated yet, and it turns a fix into a race. This project
13
+ deliberately does **not** offer a public-issue route for security reports.
14
+
15
+ Use GitHub's private vulnerability reporting — the form creates a private advisory only the
16
+ maintainers can see:
17
+
18
+ ```
19
+ https://github.com/TwinsEarth/dsh-windows2macos/security/advisories/new
20
+ ```
21
+
22
+ If that page is unavailable, open a **non-sensitive** issue that says only _"I have a security report
23
+ and need a private channel"_, with no details, no reproducer and no affected version. A maintainer
24
+ will open the private thread. Do not put the substance in the issue.
25
+
26
+ ### What to include
27
+
28
+ The report is only actionable if it starts from a shape we can reproduce. Please include:
29
+
30
+ 1. **What you can do that you should not be able to** — one sentence, then the steps.
31
+ 2. **Affected version**, exactly as installed
32
+ (`node -p "require('./node_modules/@twinsearth/w2m-dsh-plugin/package.json').version"`), and which
33
+ of the two halves is involved: the DSH plugin, the relay (`w2m-rabbit`), the per-machine agent
34
+ (`w2m-localside`), or the STUN responder.
35
+ 3. **The trust boundary you crossed.** This project's boundaries are: the operator token vs. the
36
+ device token; the per-machine allow-list; and the direct UDP path vs. the relay. Saying which one
37
+ moved is most of the analysis.
38
+ 4. **A reproducer**, or the exact commands and their output. If it needs two machines, say what the
39
+ second one was doing.
40
+ 5. **Your assessment of impact**, and whether the issue is public already.
41
+
42
+ ### What we will do
43
+
44
+ - Acknowledge the report in the private thread.
45
+ - Say whether we can reproduce it, and if not, say what we would need.
46
+ - Tell you when a fixed version is published, and credit you unless you ask us not to.
47
+
48
+ No bug bounty is offered. Do not test against infrastructure you do not own: the shared relay is a
49
+ real service and other people's machines are paired to it.
50
+
51
+ ---
52
+
53
+ ## Supported versions
54
+
55
+ **The supported line is v0.5.3 and everything before it.** A version is installable unless the table
56
+ says otherwise. Security fixes land on the newest installable version; older installable versions are
57
+ not back-patched unless a report makes that necessary, and if it does, this table will say so.
58
+
59
+ | Version | Installable | Note |
60
+ | ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
61
+ | **v0.5.6 and later** | not yet released | In progress as this document is written. Check `CHANGELOG.md` for the newest entry that is not marked withdrawn. |
62
+ | 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. |
63
+ | v0.5.4 | **no — withdrawn** | Same loader failure. `CHANGELOG.md` marks it `DO NOT INSTALL`. |
64
+ | **v0.5.3 and earlier** | **yes** | The supported line. The client half was added in v0.5.4; v0.5.3 is a host-only plugin that loads. |
65
+
66
+ The withdrawals are not a security judgement: those versions are not installable, so they are not
67
+ supported. `CHANGELOG.md` is the authority — an entry marked `DO NOT INSTALL` is not supported
68
+ whatever this table says, and the reverse is also true.
69
+
70
+ ### Installing a supported version
71
+
72
+ ```bash
73
+ # The newest installable version from the registry
74
+ dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.3
75
+
76
+ # Or the same bytes from the release, verified against SHA256SUMS from the same release
77
+ dsh plugin --profile <profile> add \
78
+ https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.3/twinsearth-w2m-dsh-plugin-0.5.3.tgz
79
+ ```
80
+
81
+ A version published minutes ago may be invisible to pnpm's `minimumReleaseAge` gate. That is the
82
+ gate working, not a supply-chain problem — see `TROUBLESHOOTING.md` §11.
83
+
84
+ ---
85
+
86
+ ## The consent model
87
+
88
+ Pairing a machine is a decision. This is exactly what it grants, and what it does not.
89
+
90
+ ### Two credentials, on purpose
91
+
92
+ | Credential | Held by | Can | Cannot |
93
+ | ------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
94
+ | **Operator token** | the dispatcher: your DSH profile (`operatorToken` config or `W2M_OPERATOR_TOKEN`) | authorise a _dispatch_ — send work to machines | read another machine's results, or act as an agent. No request that dispatches is sent without it |
95
+ | **Device token** | each machine's agent, in its own state directory (§16 of `TROUBLESHOOTING.md`) | take work offered to **that** machine and report that machine's results | dispatch work to any machine, including itself |
96
+
97
+ The split is the point: a machine that can receive work does not thereby gain the ability to command
98
+ the others. If a machine's device token is stolen, the damage is confined to that machine's own
99
+ execution surface; it cannot be used to drive the fleet.
100
+
101
+ The relay writes the operator token once, on first start, and prints it once on stderr. It is
102
+ deliberately absent from `--json` output — a token in a log is a leak. Treat it as a credential: it is
103
+ never needed in a bug report.
104
+
105
+ ### What a paired machine can do
106
+
107
+ - **Run commands you allow, in the project directory it was pointed at.** The allow-list is
108
+ **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`. An empty list is not "nothing runs" — it
110
+ means the plugin-side gate is not applied, so rely on the agent-side `--allowed-commands`.
111
+ - **Read the project** it was started with, and compute git anchors and fingerprints over it.
112
+ - **Report results**, including command output, to the relay — where the operator who dispatched the
113
+ work can read them.
114
+ - **Reach its peers' punch port over UDP**, if the punch succeeds.
115
+
116
+ ### What a paired machine cannot do
117
+
118
+ - **Run anything outside its allow-list.** A refused command is reported as `refused` with
119
+ `COMMAND_NOT_ALLOWED`, not executed.
120
+ - **Command other machines.** That requires the operator token, which the agent is not given.
121
+ - **Read another machine's results.** This is the claim people most reasonably doubt, and it is worth
122
+ being precise: the operator token authorises _dispatch_. A machine's results are visible to whoever
123
+ holds the operator token for that relay — the ledger is the relay's, not the agent's — so an agent
124
+ holding only its device token cannot enumerate or fetch another machine's output.
125
+ - **Ask for your interactive confirmation per command.** There is no approval prompt in the loop. The
126
+ allow-list is the gate, which is why it is default-deny and why you should make it as narrow as the
127
+ work allows.
128
+ - **Write to `~/.ssh/authorized_keys` — on any machine, ever.** Neither the plugin, the relay nor the
129
+ agent installs an SSH key, opens an SSH server, or adds a key file anywhere. The design reason is
130
+ that cross-machine execution here does not need SSH: every machine makes only outbound connections,
131
+ and Windows has no `sshd` by default. If you find anything in this project writing to an
132
+ `authorized_keys` file, that is a security report — please use the private channel above.
133
+
134
+ ### What it touches on your machine
135
+
136
+ Declared here and machine-readably in `package.json`'s `disclosure`, because installing a plugin runs
137
+ its code with your permissions:
138
+
139
+ | Surface | What it is |
140
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | **Filesystem** | reads and writes inside the project directory you name (anchors, fingerprints), plus its own state directory: the spool, `agent-state.json`, `device.json` and `device.key` |
142
+ | **Network** | the configured relay (default: the shared server), the STUN servers (one is the shared server, three are public), and UDP to peer machines |
143
+ | **Inbound** | peers can reach this machine's punch port. It is **ephemeral unless you pin it** with `--p2p-port`; pinning is what makes a narrow firewall rule possible, and an ephemeral port means a broader rule is needed |
144
+ | **Credentials** | the operator token in profile config or `W2M_OPERATOR_TOKEN`; the device token and key under the state directory |
145
+ | **Jurisdiction** | data crosses between wherever your machines are and wherever the relay is; the reference deployment is CN ↔ JP |
146
+
147
+ ### What this project does not claim
148
+
149
+ - **The relay sees your task metadata and results.** It is the ledger. If that is not acceptable for
150
+ a workload, run your own relay — `deploy/` covers shared-server, nginx, Caddy, Docker, WireGuard and
151
+ systemd shapes — or do not use this plugin for that workload.
152
+ - **The direct path is a security boundary.** A successful UDP hole punch means a peer can send you
153
+ packets on that socket. The data plane's encryption status is a separate, in-progress subject and is
154
+ specified in `docs/P2P-CRYPTO.md`; `w2m_status` reports what is actually in force rather than what is
155
+ intended.
156
+ - **`chmod 0600` means the same thing everywhere.** On NTFS it is best-effort: the file mode does not
157
+ restrict other users of that Windows machine. The key file's protection on Windows is the account's
158
+ own filesystem ACLs, not the mode this project sets.
159
+ - **Anything is verified by the attacker's absence.** The plugin's own test suites are the evidence we
160
+ have, and they are listed in `README.md` with what is and is not covered. Two things in particular
161
+ are **not** verified: a real Windows↔macOS pair over the network, and one account driving two
162
+ machines' model sessions.