@twinsearth/w2m-dsh-plugin 0.5.5 → 0.5.7

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
@@ -54,6 +54,98 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
54
54
  * Still unverified, and not claimed: how the control *renders*. No browser was opened. What is proven is
55
55
  the entire host side — the entry activates, the eight tools register, the boot-graph row is composed
56
56
  from the shipped bundle, the route is registered and attached — plus the seat shape in a VM test.
57
+ ## [0.5.7] — 2026-10-11
58
+
59
+ ### Fixed
60
+
61
+ * **v0.5.6 shipped without its own documents.** `TROUBLESHOOTING.md` and `SECURITY.md` were written,
62
+ reviewed and committed, and then **not packed**, because `files` names the documents individually
63
+ rather than by pattern. Anyone installing v0.5.6 got the code and none of the guidance. They are in the
64
+ package now, and `test/claims.test.mjs` is the place this class of defect belongs.
65
+ * **v0.5.6 shipped without its own gates wired in.** `test/claims.test.mjs`, `test/loader-entry.test.mjs`
66
+ and the two mDNS suites existed and passed, but were listed in none of `test`, `test:all` or `verify` —
67
+ a test that does not run protects nothing, which is the exact defect this release's own study
68
+ identified in someone else's repository. All four are wired in.
69
+ * A typographic character (a smart quote) in `test/mdns-discover.test.mjs`, caught by the repository's
70
+ own mojibake scan.
71
+
72
+ ### Notes
73
+
74
+ * Fix-forward, not a rewrite: v0.5.6 is published and stays published, because the alternative is
75
+ rewriting history someone may already have. Both defects are mine, and both are the class this project
76
+ keeps naming — a claim that nothing checks.
77
+ * No runtime change: the code in v0.5.7 is the code in v0.5.6.
78
+ ## [0.5.6] — 2026-10-11
79
+
80
+ ### Fixed
81
+
82
+ * **The client entry could not activate, and that made the whole application refuse to start.**
83
+ v0.5.4 and v0.5.5 were withdrawn for it. The real cause, taken from the page itself rather than
84
+ guessed:
85
+
86
+ ```
87
+ w2m-client-probe: apply threw for @twinsearth/w2m-dsh-plugin: cannot get property "locale" without inject
88
+ Error: web boot: 1 entry did not activate
89
+ ```
90
+
91
+ Cordis throws for any `ctx.<name>` a fiber reads without declaring it, and the client half declared
92
+ `inject = []`. **The first throw was `locale`, not `slots`** — so declaring the seat alone, or guarding
93
+ the locale alone, would each still have died. Now `inject = ['slots']` (a declared name is a *required*
94
+ dependency, which is why `locale` is deliberately not among them), the optional carrier is read
95
+ inject-free with `ctx.get('locale')` while its dictionary registration rides a child fiber, a context
96
+ with no usable seat becomes a **no-op with a diagnostic** instead of taking the page down, and `apply`
97
+ prints the inner message to the error-level renderer console — the channel the crash log reads — and
98
+ then rethrows, because a swallow would boot a healthy-looking page with no control.
99
+ * **A failed entry was invisible unless you read fiber state.** `cordis-plugin-loader` no longer
100
+ propagates a failed mount to `await ctx.loader.await()`, and the loader driver inspected only the one
101
+ entry it knew about — so a failure landing on a **child** fiber (our silently-refused route) went
102
+ unseen. The driver now sweeps the whole registry and reports `failedFibers`.
103
+
104
+ ### Added
105
+
106
+ * **A gate that would have caught all of it**: `test/loader-entry.test.mjs` packs the tree, installs the
107
+ tarball with the installation's own `dsh plugin add` into a throwaway profile under a throwaway
108
+ `DSH_HOME`, boots it with the real `runProfile`, activates the client entries through the real module
109
+ system, and — when a browser is present — boots the profile's own page and asserts our control renders.
110
+ Proven in both directions: **10/10 on this tree, 2 failures against the preserved pre-fix tarball, 9
111
+ against the v0.5.4 one**, and with a deliberately injected route defect it names the **child** fiber
112
+ while "entry active" and "eight tools" still pass. It reports one stage per line, so a failure names
113
+ its cause.
114
+ * **`test/claims.test.mjs`** — 22 checks that our own published claims match the artifact, with the
115
+ registered tool set *measured* by importing the entry. It caught a stale claim in our README on its
116
+ **first run** (`README.md` said the installer asserts 5 tools; it asserts 8), and its author's own
117
+ mutation proof caught two false greens in its first draft — a scan that extracted zero tool names and
118
+ a lookahead that invented one out of a metric.
119
+ * **`TROUBLESHOOTING.md`** — 17 symptom-indexed sections that name the culprit when it is not us (pnpm's
120
+ `minimumReleaseAge`, carrier-grade NAT, the OS firewall, an installer that rewrites the profile patch),
121
+ ending with a data-location table and a reason-code table.
122
+ * **`SECURITY.md`** — a private reporting channel and **no public-issue route**, an honest support table,
123
+ and the consent model written out: what a paired machine may and may not do, and that we never write to
124
+ another machine's `authorized_keys`.
125
+ * **`CONTRIBUTING.md`** — one page of falsifiable conventions, including **"a test that would pass
126
+ against a broken artifact is not evidence"** and **"run mutation proofs in a copy, never in the shared
127
+ tree"** (both learned the hard way).
128
+ * **Opt-in LAN discovery** (`src/agent/mdns-wire.mjs`, `src/agent/mdns.mjs`, `src/plugin/mdns-discover.mjs`):
129
+ a bounded, hand-written DNS/mDNS codec with parse hardening (a pointer may only name a *prior* offset, so
130
+ termination is a property of the format; a 128-hop budget reported separately from a loop; label, name,
131
+ record and packet caps; every read bounds-checked) and a discovery source exposed through
132
+ `w2m_devices`' new `include_discovered` (default `false`). **Discovery is a hint, never an
133
+ authorisation**: a discovered device is refused as a dispatch target unless the relay already lists it
134
+ and it is online, and the positive control is tested so the rule cannot pass by refusing everything.
135
+
136
+ ### Notes
137
+
138
+ * **v0.5.4 and v0.5.5 remain DO NOT INSTALL.** This release also exists so that `latest` on the registry,
139
+ which still pointed at v0.5.5, stops handing out a version that prevents the application from starting.
140
+ * **Upgrading from v0.5.4 needs one edit outside this package**: the profile's `cordis.patch.yml` must
141
+ name the package (`@twinsearth/w2m-dsh-plugin`), not the `/tools` subpath. With the corrected bundle row
142
+ a mismatch makes the loader skip the whole patch and silently revert every configured value to its
143
+ default. The claims gate now checks this.
144
+ * **LAN discovery is unverified on a real network.** Every socket in its suite is loopback and no packet
145
+ left the host, so it says nothing about whether a switch forwards `224.0.0.251` or how a real
146
+ Bonjour/Avahi peer answers. There is **no third-party interoperability test**, no probing or conflict
147
+ resolution, IPv4 only, and the responder is **built and tested but not started** by the plugin —
148
+ loading a plugin should not silently multicast on the host's network.
57
149
  ## [0.5.4] — 2026-10-11
58
150
 
59
151
  > **DO NOT INSTALL 0.5.4.** It fails to load in the Harness: the loader entry
@@ -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/README.md CHANGED
@@ -535,7 +535,7 @@ the plugin:
535
535
 
536
536
  ```bash
537
537
  node scripts/check-entrypoints.mjs # imports lib/tools.js, the exact specifier cordis.patch.yml mounts
538
- node scripts/verify-installed.mjs <path-to-installed-package> # asserts the installed copy registers 5 tools
538
+ node scripts/verify-installed.mjs <path-to-installed-package> # asserts the installed copy registers 8 tools
539
539
  ```
540
540
 
541
541
  **Not verified:**
@@ -588,6 +588,8 @@ Issues and pull requests are welcome. If you change observable behaviour, update
588
588
  `PROTOCOL.md` in the same commit — that file is the contract three components
589
589
  agree on.
590
590
 
591
+ When something does not work, start from the symptom: [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
592
+
591
593
  ## License
592
594
 
593
595
  [MIT](LICENSE)
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.