@twinsearth/w2m-dsh-plugin 0.33.0 → 0.35.0

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
@@ -4,6 +4,107 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.35.0] — 2026-10-15
8
+
9
+ **§1.5 measured at last: the reachability report, and the honest reason it is not a gate.**
10
+
11
+ `docs/1.0.0-ACCEPTANCE.md` §1.5 recorded that **nothing** measured whether an export is reachable. Three
12
+ mechanisms shipped in this span fully tested and completely uncalled — `field-authority.mjs`,
13
+ `approval-gate.mjs`, `createApprovalIndex` — and **a tested mechanism with no caller is indistinguishable from
14
+ a finished feature** by reading either the tests or the source.
15
+
16
+ `scripts/reachability-check.mjs` measures it.
17
+
18
+ ### The first version was unusable, and the false positive that proved it
19
+
20
+ It asked *"which files mention this name"* and reported **378 findings**, almost all false. The clearest:
21
+ `deepEqual` from `src/relay/state.mjs`, which appeared to be named by **69 test files** — every one of them
22
+ calling `assert.deepEqual`. Whole-word matching cannot tell an imported binding from a coincidental name.
23
+
24
+ **An import is a statement about two things at once — a module and a name — so matching on the name alone
25
+ throws away half of what the source says.** The check now reads both and matches on the pair:
26
+
27
+ ```
28
+ importedNames(text) -> module specifier -> imported names
29
+ resolveSpecifier() -> the file that specifier names, the way Node resolves it
30
+ ```
31
+
32
+ ### Why it exits 0, which is the part worth defending
33
+
34
+ Measured against this repository it names **465 exports**, and it **cannot tell three kinds apart**:
35
+
36
+ 1. **the defect** — built, tested, never wired;
37
+ 2. **a deliberate surface** — the plugin's entry points and re-exports;
38
+ 3. **composition** — a capability reached only through another module's use.
39
+
40
+ Only the first is a finding, and no static name-and-import check separates them. **A check that fails on 465
41
+ things nobody will fix is a check that gets disabled within a week — and a disabled check is worse than none,
42
+ because it is cited as coverage.** So it prints, exits 0, and closes with *"READ THIS AS A LIST TO TRIAGE, NOT
43
+ A LIST OF BUGS."*
44
+
45
+ **§1.5 is therefore measured, not closed.** A defect of this class is now *visible*; nothing fails when one
46
+ appears. The tests build synthetic trees where ground truth is known, because asserting on the real 465 would
47
+ pin noise.
48
+
49
+ ### Three of my own errors, and one of them is the lesson of this whole span
50
+
51
+ * My re-export exclusion looked for ` from ` **inside** the captured braces, so it never saw it —
52
+ `export { X } from './y.mjs'` was counted as a definition. **The test failed because the code was wrong.**
53
+ * I expected `b` in an export list where the source says `b as bee`. The implementation was right.
54
+ * **I "fixed" that by sorting my own guessed array.** Sorting a wrong list does not make it measured — and
55
+ that is the same mistake, in miniature, that this project has now paid for six times: **predicting what the
56
+ output is instead of printing it.** The fix was to print it.
57
+
58
+ **16 new tests, 0 failures.**
59
+
60
+ ## [0.34.0] — 2026-10-15
61
+
62
+ **`requireAuthorityLevel`: the policy that makes 分权限 real, and why it is a flag rather than a default.**
63
+
64
+ `docs/1.0.0-ACCEPTANCE.md` §3.11 recorded the gap plainly — the gate is **off unless configured**, so a
65
+ default install dispatches exactly as it did before the permission model existed, and **"off unless
66
+ configured" cannot be part of a 1.0.0 that claims 分权限.**
67
+
68
+ `requireAuthorityLevel: true` (or `W2M_REQUIRE_AUTHORITY_LEVEL`) closes it for a machine that asks. Measured,
69
+ the whole truth table:
70
+
71
+ | `requireAuthorityLevel` | level | result |
72
+ |---|---|---|
73
+ | off | unset | **starts**, gate unenforced |
74
+ | off | chosen | starts, gate enforced |
75
+ | **on** | **unset** | **refuses to start** — `AUTHORITY_CHOICE_NOT_CHOSEN` |
76
+ | **on** | **unusable** | **refuses to start** — `AUTHORITY_CHOICE_UNKNOWN` |
77
+ | on | chosen | starts, gate enforced |
78
+
79
+ A refusal names the four choices, so it is fixable from the message alone, and a machine that opted in says
80
+ so in `w2m_status` — a reader can tell "asked and answered" from "never asked".
81
+
82
+ ### Why opt-in, which is a deliberate limit rather than a hedge
83
+
84
+ Refusing unconditionally would be the honest reading of *"a permission system that is off is untested in
85
+ production"* — and it would also **stop every machine running an earlier version the moment it upgraded**,
86
+ because none of them has a level. That trade is one silent failure for a loud outage, and **an outage is not
87
+ a security property.**
88
+
89
+ **So the gap is narrowed, not closed, and the document says so** rather than letting the flag's existence
90
+ imply otherwise. A fleet that never sets it is exactly as unenforced as before.
91
+
92
+ ### `W2M_REQUIRE_AUTHORITY_LEVEL=0` means off
93
+
94
+ Asserted, because a truthiness test would read the string `"0"` as set-and-therefore-on and **refuse to start
95
+ a machine whose operator explicitly wrote zero.**
96
+
97
+ ### My own errors, both of them syntax rather than logic
98
+
99
+ * I wrote `await` inside a **non-async arrow** passed to `assert.rejects`, which does not parse. `node --check`
100
+ caught it immediately; the fix is to compute the state directory first, because making the arrow async would
101
+ change what it does.
102
+ * I asserted refusal codes against the **message text**; the codes live on `error.reason_code`, which is the
103
+ field a consumer branches on. **The code was correct and the assertion was looking in the wrong place** —
104
+ now it pins the field.
105
+
106
+ **6 new tests, 0 failures.**
107
+
7
108
  ## [0.33.0] — 2026-10-15
8
109
 
9
110
  **The authority level is chosen by name. Nobody types a number.**
@@ -18,13 +18,36 @@ but has no caller, that is stated in its own column rather than hidden behind a
18
18
  | 1.2 | Every dependency edge is declared, and every declared edge is used | `scripts/layer-check.mjs` | **yes** |
19
19
  | 1.3 | Every suite on disk is named by a script | pre-flight `test wiring`, `claims` | **yes** — reported, not silently ignored |
20
20
  | 1.4 | Every document's claim about the artifact matches the artifact | `test/claims.test.mjs` | **yes** — 48 claims, and it has caught real drift |
21
- | 1.5 | No module is unreachable — every declared thing has a caller | *nothing* | **NO — see §4** |
21
+ | 1.5 | No module is unreachable — every declared thing has a caller | `scripts/reachability-check.mjs` | **MEASURED, not enforced — see below** |
22
22
 
23
- `1.5` is the row that matters most and is the one **not** satisfied. This span shipped three mechanisms that
24
- were, for a round each, fully tested and completely uncalled: `field-authority.mjs`, `approval-gate.mjs`, and
25
- before them `createApprovalIndex`. **A tested mechanism with no caller is indistinguishable from a finished
26
- feature by reading either the tests or the source.** `1.4` catches *documentation* drift and cannot catch this,
27
- because the document and the code can agree while neither is reachable.
23
+ `1.5` is the row that mattered most and the one that went unsatisfied longest. This span shipped three
24
+ mechanisms that were, for a round each, fully tested and completely uncalled: `field-authority.mjs`,
25
+ `approval-gate.mjs`, and before them `createApprovalIndex`. **A tested mechanism with no caller is
26
+ indistinguishable from a finished feature by reading either the tests or the source.** `1.4` catches
27
+ *documentation* drift and cannot catch this, because the document and the code can agree while neither is
28
+ reachable.
29
+
30
+ **0.35.0 measures it.** `scripts/reachability-check.mjs` matches **imports** — a module and a name together —
31
+ and reports exports that only test files import.
32
+
33
+ ### Why it is a report and not a gate, which is the honest part
34
+
35
+ Measured against this repository it names **465 exports**, and it **cannot tell three kinds apart**:
36
+
37
+ 1. **the defect** — built, tested, never wired;
38
+ 2. **a deliberate surface** — the plugin's entry points and re-exports;
39
+ 3. **composition** — a capability reached only through another module's use.
40
+
41
+ Only the first is a finding. **A check that fails on 465 things nobody will fix is a check that gets
42
+ disabled within a week, and a disabled check is worse than none because it is cited as coverage.** So it exits
43
+ **0** and prints, and its own output says *"READ THIS AS A LIST TO TRIAGE, NOT A LIST OF BUGS."*
44
+
45
+ The tests therefore build **synthetic trees where ground truth is known** and pin the one distinction it *is*
46
+ reliable for — including the false positive that broke the first version, `deepEqual` from
47
+ `src/relay/state.mjs` appearing to be named by **69 test files** that were all calling `assert.deepEqual`.
48
+
49
+ **So `1.5` is measured, not closed.** A defect of this class is now *visible*; it is not *prevented*, and
50
+ nothing in the pre-flight fails when one appears.
28
51
 
29
52
  ---
30
53
 
@@ -57,24 +80,35 @@ limiter down to `util`, **not** to add the edge.
57
80
  | 3.8 | An escalated dispatch reaches a queue an approver can answer | `w2m_approvals`, asserted end to end | **yes** |
58
81
  | 3.9 | The queue's front door meters **raising only** | `approval-gate.mjs`, wired in 0.31.0 | **yes** |
59
82
  | 3.10 | Field-level memory acceptance is driven by the same model | `field-authority.mjs` | **mechanism yes, caller NO — see §4** |
60
- | 3.11 | **The gate is ON by default** | *nothing — it is off* | **NO — see below** |
83
+ | 3.11 | **The gate is ON by default** | `requireAuthorityLevel`, asserted | **NARROWED — see below** |
84
+ | 3.12 | A level is chosen by name, never typed | `authority-choice.mjs`, asserted | **yes** |
85
+ | 3.13 | A misconfigured level is reported, not silently ignored | `w2m_status.actor_authority` | **yes** |
61
86
 
62
- ### `3.11` is the load-bearing gap, and it is a decision rather than an oversight
87
+ ### `3.11` — narrowed rather than closed, and the distinction is stated
63
88
 
64
89
  `actorAuthorityLevel` is **unset by default**, and an unset level means the gate reports `enforced: false` and
65
- changes nothing. So today, **a default install dispatches exactly as it did before the permission model
66
- existed.** The reasoning is in `permission.mjs` and it is sound as far as it goes: treating an unset level as
67
- `0` would refuse every writing dispatch on every machine that never opted in, breaking a working fleet to
68
- enforce a policy nobody asked for.
69
-
70
- **But "off unless configured" cannot be part of a 1.0.0 that claims 分权限**, because a permission system that
71
- is off is a permission system that is untested in production. The question this file puts to a reader is
72
- therefore concrete:
73
-
74
- > **Should 1.0.0 require `actorAuthorityLevel` to be set, refusing to start without one — or should it ship
75
- > with the gate off and say so in the README?**
76
-
77
- Both are defensible. Silence is not, and neither is claiming 分权限 while it is off.
90
+ changes nothing. So a default install dispatches exactly as it did before the permission model existed.
91
+
92
+ **0.34.0 adds `requireAuthorityLevel`** (or `W2M_REQUIRE_AUTHORITY_LEVEL`), which closes it for a machine that
93
+ asks for it to be closed. Measured, the whole table:
94
+
95
+ | `requireAuthorityLevel` | level | result |
96
+ |---|---|---|
97
+ | off | unset | **starts**, gate unenforced |
98
+ | off | chosen | starts, gate enforced |
99
+ | **on** | **unset** | **refuses to start** — `AUTHORITY_CHOICE_NOT_CHOSEN` |
100
+ | **on** | **unusable** | **refuses to start** — `AUTHORITY_CHOICE_UNKNOWN` |
101
+ | on | chosen | starts, gate enforced |
102
+
103
+ **Why it is opt-in, which is a deliberate limit rather than a hedge.** Refusing unconditionally would be the
104
+ honest reading of *"a permission system that is off is untested in production"* — and it would also **stop
105
+ every machine running an earlier version the moment it upgraded**, because none of them has a level. That
106
+ trades one silent failure for a loud outage, and **an outage is not a security property.**
107
+
108
+ **So the gap is narrowed, not closed.** A fleet that does not set this flag is exactly as unenforced as
109
+ before, and this document says so rather than letting the flag's existence imply otherwise. **The remaining
110
+ question for 1.0.0 is whether shipping a flag is enough, or whether the default must flip** — and that is a
111
+ decision about who this release is for, not a defect to fix.
78
112
 
79
113
  ---
80
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -113,13 +113,13 @@
113
113
  }
114
114
  },
115
115
  "scripts": {
116
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
116
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
117
117
  "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
118
118
  "test:relay": "node --test --test-force-exit test/relay.test.mjs",
119
119
  "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
120
120
  "test:tools": "node --test --test-force-exit test/tools.test.mjs",
121
121
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
122
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
122
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
123
123
  "pack": "node scripts/pack.mjs",
124
124
  "verify:join": "node scripts/verify-join.mjs",
125
125
  "lint": "eslint .",
@@ -149,7 +149,9 @@
149
149
  "test:packet": "node --test --test-force-exit test/machine-test-packet.test.mjs",
150
150
  "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
151
151
  "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
152
- "test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs"
152
+ "test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs",
153
+ "test:reachability": "node --test --test-force-exit test/reachability-check.test.mjs",
154
+ "reachability": "node scripts/reachability-check.mjs"
153
155
  },
154
156
  "keywords": [
155
157
  "dsh-bundle",
@@ -198,7 +198,7 @@ const MAX_STAGES = 16;
198
198
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
199
199
  * overrides it at runtime for tests and packagers.
200
200
  */
201
- const PLUGIN_VERSION = '0.33.0';
201
+ const PLUGIN_VERSION = '0.35.0';
202
202
 
203
203
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
204
204
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -720,6 +720,40 @@ function readConfig(config = {}) {
720
720
  ? process.env.W2M_ACTOR_AUTHORITY.trim()
721
721
  : undefined;
722
722
  const resolved = resolveConfiguredLevel(fromEnv !== undefined ? fromEnv : config.actorAuthorityLevel);
723
+
724
+ /**
725
+ * v0.34.0: the policy that makes 分权限 real, and why it is a policy rather than a default.
726
+ *
727
+ * `docs/1.0.0-ACCEPTANCE.md` §3.11 records the gap plainly: the gate is **off unless configured**, so a
728
+ * default install dispatches exactly as it did before the permission model existed, and **"off unless
729
+ * configured" cannot be part of a 1.0.0 that claims 分权限.**
730
+ *
731
+ * `requireAuthorityLevel: true` closes it: with the policy on, a machine that has not chosen a level
732
+ * **refuses to start** rather than dispatching unenforced. `W2M_REQUIRE_AUTHORITY_LEVEL` supplies the
733
+ * same policy from the environment, so a fleet can require it without editing every profile.
734
+ *
735
+ * ## Why it is opt-in, which is a deliberate limit
736
+ *
737
+ * Refusing unconditionally would be the honest reading of "a permission system that is off is
738
+ * untested" — and it would also **stop every machine running an earlier version the moment it
739
+ * upgraded**, since none of them has a level. That trades one silent failure for a loud outage, and an
740
+ * outage is not a security property.
741
+ *
742
+ * So the policy is per-machine and explicit. **The gap is narrowed, not closed**: a fleet that does not
743
+ * turn this on is exactly as unenforced as before, and this comment says so rather than letting the
744
+ * existence of the flag imply otherwise.
745
+ */
746
+ const required = config.requireAuthorityLevel === true
747
+ || (typeof process.env.W2M_REQUIRE_AUTHORITY_LEVEL === 'string' && process.env.W2M_REQUIRE_AUTHORITY_LEVEL.trim() !== '' && process.env.W2M_REQUIRE_AUTHORITY_LEVEL.trim() !== '0');
748
+ if (required && !resolved.ok) {
749
+ throw configError(
750
+ 'actorAuthorityLevel',
751
+ `is required on this machine (requireAuthorityLevel is set) and ${resolved.ok ? '' : resolved.detail}`,
752
+ `choose one of ${AUTHORITY_CHOICES.map((c) => c.value).join(', ')} as \`actorAuthorityLevel\`, or as \`W2M_ACTOR_AUTHORITY\`; to dispatch without a level, unset requireAuthorityLevel`,
753
+ resolved.code ?? 'W2M_AUTHORITY_LEVEL_REQUIRED',
754
+ );
755
+ }
756
+
723
757
  return {
724
758
  actorAuthorityLevel: resolved.ok ? resolved.level : null,
725
759
  actorAuthorityChoice: {
@@ -728,6 +762,7 @@ function readConfig(config = {}) {
728
762
  source: resolved.source,
729
763
  reason_code: resolved.code,
730
764
  detail: resolved.detail,
765
+ required,
731
766
  },
732
767
  };
733
768
  })(),