@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 +92 -45
- package/CONTRIBUTING.md +123 -0
- package/SECURITY.md +162 -0
- package/TROUBLESHOOTING.md +787 -0
- package/package.json +8 -5
- package/src/agent/mdns-wire.mjs +54 -54
- package/src/agent/mdns.mjs +31 -31
- package/src/plugin/tools.mjs +1 -1
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.
|
|
27
|
+
## [0.5.7] — 2026-10-11
|
|
8
28
|
|
|
9
29
|
### Fixed
|
|
10
30
|
|
|
11
|
-
* **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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.
|