@twinsearth/w2m-dsh-plugin 0.30.0 → 0.32.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,96 @@ 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.32.0] — 2026-10-15
8
+
9
+ **Widening real-machine verification: eight suites tried, none passed, and the cause is a hypothesis.**
10
+
11
+ ### What was measured
12
+
13
+ The eight suites dispatched to MacMini all returned **non-zero there while passing here**. The packed code
14
+ that reported this was at times ambiguous, so the run was repeated with a simpler encoding — **one bit per
15
+ suite, exit 0 or not** — and that is unambiguous: **the packed result was `0`, meaning no suite passed.**
16
+
17
+ ### What was NOT measured, and is therefore not claimed
18
+
19
+ The likely cause is a **missing `node_modules` on that machine** — the suites import from `src/`, and some of
20
+ those modules reach for a dependency. **That was not established.** It is a hypothesis, and this release
21
+ records it as one. The acceptance document now says so in a row of its own rather than in a footnote.
22
+
23
+ ### Five dispatches spent on a channel that carries eight bits
24
+
25
+ The attempts, in order, and what each one actually answered:
26
+
27
+ | Attempt | Question asked | What came back |
28
+ |---|---|---|
29
+ | 1 | four suites, two bits each (status + reason) | 8 bits, but the tail truncated — **unreadable** |
30
+ | 2 | eight suites, three bits each | 24 bits packed into 8 — **arithmetically impossible, and it returned 0** |
31
+ | 3 | re-read the saved outputs | the parse matched nothing, because it guessed Node's summary format |
32
+ | 4 | eight suites, one bit each | **`0` — no suite passed. Unambiguous.** |
33
+ | 5 | print the environment facts | ran to completion, **and stdout is not visible through a dispatch** |
34
+
35
+ **Every one of those was the same mistake**: asking a channel more than it carries, then reading the
36
+ truncation as an answer. `exit code` is one byte, and the correct instinct — already written into
37
+ `machine-test-packet.mjs` and ignored here — is to ask **one bit per suite and nothing else**.
38
+
39
+ `--inline` was added to that script so its loop can be delivered as a `node -e` one-liner, because a real
40
+ machine's allow-list **admits `node -e` and refuses `node scripts/…`**. The generated command is 486 characters
41
+ and self-checks locally. **Its stdout is still invisible through a dispatch**, which the docstring now says
42
+ plainly: the inline form is useful when the packed code is enough, and **when it is not, the answer is one
43
+ command run by a person rather than a cleverer encoding.**
44
+
45
+ ### What is unchanged
46
+
47
+ **Two of the three decisions the user made are not in this release.** The authority-level *selector* — the
48
+ request that the level be **chosen rather than typed** — is not built, and the permission gate remains
49
+ **off by default**, which §3.11 of the acceptance draft still records as incompatible with claiming 分权限.
50
+
51
+ **No new tests.** The suites that exist all still pass; what changed is a measurement and a docstring.
52
+
53
+ ## [0.31.0] — 2026-10-15
54
+
55
+ **The approval gate got a caller, and a draft acceptance definition for 1.0.0.**
56
+
57
+ ### The defect this round started by finding, in the release that had just criticised it
58
+
59
+ `approval-gate.mjs` shipped in 0.30.0 with a full suite and **no caller** — which is exactly the shape of the
60
+ two defects 0.30.0 had just finished writing into `ROADMAP.md`. **Building a mechanism and wiring it are
61
+ separate acts**, and a mechanism with no caller cannot be told from a finished feature by reading either the
62
+ tests or the source.
63
+
64
+ It is now on the raise path, and the wiring is asserted rather than described: a real dispatch raises once,
65
+ the queue holds one request, a second raise is **refused by the front door before a request exists**, and the
66
+ queue still holds exactly one. Two more tests pin the asymmetry — reading the queue five times past any raise
67
+ budget, and settling the request — because a gate that also metered *those* would pass a flooding test and
68
+ quietly break the queue.
69
+
70
+ `approvalRaiseLimitPerMinute` is configurable so a host can set it and so a test can reach the refusal without
71
+ raising thirty requests first. Unset, the gate's own default governs.
72
+
73
+ ### `docs/1.0.0-ACCEPTANCE.md` — a draft for objection
74
+
75
+ The goal names **分模块 / 分层级 / 分权限**, and a version number means nothing without a definition that can
76
+ fail. The draft lists 21 criteria across the three, each with **where it is checked and whether that check is
77
+ actually running**, under one rule: **a criterion nothing measures is not a criterion.**
78
+
79
+ Its two honest admissions are the point of writing it:
80
+
81
+ * **§1.5 — reachability is NOT checked.** Three mechanisms shipped fully tested and completely uncalled in
82
+ this span. The `claims` suite catches *documentation* drift and **cannot** catch this, because the document
83
+ and the code can agree while neither is reachable.
84
+ * **§3.11 — the permission gate is OFF by default.** An unset `actorAuthorityLevel` means `enforced: false`
85
+ and no change, so **a default install dispatches exactly as it did before the permission model existed.**
86
+ Treating unset as `0` would break every machine that never opted in, so the current behaviour is defensible
87
+ — but **"off unless configured" cannot be part of a 1.0.0 that claims 分权限.** The draft puts the choice to
88
+ a reader rather than resolving it: **require a level and refuse to start without one, or ship with the gate
89
+ off and say so in the README.** Silence is the one option that is not defensible.
90
+
91
+ It also records what is **not** claimed: no scale statement (3 usable machines of 5), no restart durability for
92
+ the queue, no MacMini verification beyond the four suites measured in 0.28.0, and `P2P_PUNCH_FAILED` still
93
+ UNVERIFIED.
94
+
95
+ **2 new tests, 0 failures.**
96
+
7
97
  ## [0.30.0] — 2026-10-15
8
98
 
9
99
  **The same ruler on the other two entries: v4.9.0 had the same defect, v4.10.0 did not.**
@@ -0,0 +1,110 @@
1
+ # What 1.0.0 would have to prove — a draft for objection
2
+
3
+ **Status: DRAFT, and deliberately incomplete.** Nothing here is an announcement that 1.0.0 is close. This file
4
+ exists because the goal names three properties — **分模块 / 分层级 / 分权限** — and a version number means
5
+ nothing without a definition that can fail. It is written to be argued with, and every row says **where the
6
+ claim is checked and whether that check is actually running**.
7
+
8
+ The rule applied throughout: **a criterion that nothing measures is not a criterion.** Where a check exists
9
+ but has no caller, that is stated in its own column rather than hidden behind a tick.
10
+
11
+ ---
12
+
13
+ ## 1. 分模块 — modules with a declared identity
14
+
15
+ | # | Criterion | Checked by | Enforced? |
16
+ |---|---|---|---|
17
+ | 1.1 | Every source module belongs to exactly one declared layer | `scripts/layer-check.mjs` | **yes** — `clean: every cross-layer edge is declared` |
18
+ | 1.2 | Every dependency edge is declared, and every declared edge is used | `scripts/layer-check.mjs` | **yes** |
19
+ | 1.3 | Every suite on disk is named by a script | pre-flight `test wiring`, `claims` | **yes** — reported, not silently ignored |
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** |
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.
28
+
29
+ ---
30
+
31
+ ## 2. 分层级 — layers, with a direction
32
+
33
+ | # | Criterion | Checked by | Enforced? |
34
+ |---|---|---|---|
35
+ | 2.1 | Ranked layer model, declared as data | `scripts/layer-model.mjs` | **yes** |
36
+ | 2.2 | An allow-list of edges, not a denylist | `layer-model.mjs` | **yes** — a new edge is refused and forces a decision |
37
+ | 2.3 | No upward edge | `layer-check.mjs` | **yes** — it refused `agent → relay` in 0.30.0 |
38
+ | 2.4 | The layer a module sits in matches what it uses | `layer-check.mjs` | **yes** — it is how `SlidingWindowRateLimiter` was found mis-layered |
39
+
40
+ **`2.3` and `2.4` have each caught a real defect in this span**, which is the only evidence that a check is
41
+ worth its place. `2.3` fired when the approval gate reached for a relay module; the fix was to move the shared
42
+ limiter down to `util`, **not** to add the edge.
43
+
44
+ ---
45
+
46
+ ## 3. 分权限 — declared capabilities, and something that consults them
47
+
48
+ | # | Criterion | Checked by | Enforced? |
49
+ |---|---|---|---|
50
+ | 3.1 | Capabilities declared with a cost and a destructiveness flag | `authority.mjs` (`CAPABILITIES`) | **yes** |
51
+ | 3.2 | Every declared capability is reachable by some band | `auditCapabilities()`, asserted | **yes** |
52
+ | 3.3 | A decision is reached from a real dispatch's own facts | `gateDispatch`, `capabilityForDispatch` | **yes** |
53
+ | 3.4 | The dispatch path consults it | `tools.mjs`, asserted end to end | **yes** — since 0.23.0 |
54
+ | 3.5 | An undeclared capability is refused, not granted or escalated | `permission.test.mjs` | **yes** |
55
+ | 3.6 | Escalation is exactly one band | asserted across the whole table | **yes** |
56
+ | 3.7 | Nothing destructive auto-settles, at any level | `mayAutoSettle`, asserted | **yes** |
57
+ | 3.8 | An escalated dispatch reaches a queue an approver can answer | `w2m_approvals`, asserted end to end | **yes** |
58
+ | 3.9 | The queue's front door meters **raising only** | `approval-gate.mjs`, wired in 0.31.0 | **yes** |
59
+ | 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** |
61
+
62
+ ### `3.11` is the load-bearing gap, and it is a decision rather than an oversight
63
+
64
+ `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.
78
+
79
+ ---
80
+
81
+ ## 4. What is explicitly NOT claimed
82
+
83
+ Recorded here so that absence is a decision rather than an omission:
84
+
85
+ | Not claimed | Why |
86
+ |---|---|
87
+ | **Reachability of every module** | nothing measures it (§1.5). Three uncalled mechanisms were shipped in this span. |
88
+ | **The gate is active** | `actorAuthorityLevel` is unset by default (§3.11). |
89
+ | **Field authority reaches the memory layer** | `field-authority.mjs` has no production caller; `w2m_memory` does not use it. |
90
+ | **The queue survives a restart** | it is per-plugin-lifetime by design, and is **not** an audit log. |
91
+ | **Any scale claim** | this fleet is 3 usable machines of 5 paired. `ROADMAP.md` refuses to plan v4.11.0+ for exactly this reason, and `fleet-scale.mjs` refuses to extrapolate from five. |
92
+ | **MacMini verification of everything** | four suites were verified on that machine in 0.28.0; the other 69 were not. **Measured in 0.32.0: the eight suites tried there did not pass, and the cause is not yet established.** |
93
+ | **The eight suites tried on MacMini in 0.32.0** | every one of them returned non-zero there while passing here. The likely cause is a missing `node_modules` on that machine, but **that was not measured**, so it is a hypothesis and not a finding. |
94
+ | **`P2P_PUNCH_FAILED`** | cause UNVERIFIED; three hypotheses measured and excluded. |
95
+
96
+ ---
97
+
98
+ ## 5. What would make this file finished
99
+
100
+ 1. **A reachability check** — every exported function has a non-test caller, or is declared as
101
+ interface-only with a reason. Without it, §1.5 stays open and the class of defect that produced three
102
+ dead mechanisms stays invisible.
103
+ 2. **A decision on §3.11**, whichever way it goes, reflected in `README.md` and in the pre-flight.
104
+ 3. **Every row above promoted to a check that runs**, so this document becomes a description of the
105
+ pre-flight rather than a parallel list that can drift from it. **A document that restates what the
106
+ pre-flight does is a second source of truth, and this project has already paid for one of those.**
107
+
108
+ This draft deliberately stops short of those three, because each is a decision about what 1.0.0 must be
109
+ rather than a defect to fix, and the person who owns it has not answered the question this session asked in
110
+ round 12: **write the acceptance definition, or keep advancing the roadmap?**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.30.0",
3
+ "version": "0.32.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",
@@ -129,6 +129,8 @@ import { gateDispatch, approvalRequestFor, raiseApproval } from '../agent/permis
129
129
  // v0.24.0: the queue an escalation is raised into. Imported from `src/`, never from `scripts/` -- a
130
130
  // `src/` module reaching above itself breaks the published package (see CHANGELOG 0.23.0).
131
131
  import { createApprovalIndex } from '../agent/approval-index.mjs';
132
+ // v0.31.0: the approval queue's front door. Written in 0.30.0 and given a caller here.
133
+ import { createApprovalGate } from '../agent/approval-gate.mjs';
132
134
  import { AUTHORITY_MAX, isAuthorityLevel } from '../agent/authority.mjs';
133
135
  // v0.6.0: the device keypair and the offer's key distribution (`docs/P2P-CRYPTO.md` §4). Imported
134
136
  // rather than reimplemented: the dispatcher and the executor have to derive the *same* keys, and two
@@ -193,7 +195,7 @@ const MAX_STAGES = 16;
193
195
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
194
196
  * overrides it at runtime for tests and packagers.
195
197
  */
196
- const PLUGIN_VERSION = '0.30.0';
198
+ const PLUGIN_VERSION = '0.32.0';
197
199
 
198
200
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
199
201
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -3246,6 +3248,28 @@ export async function apply(ctx, config = {}) {
3246
3248
  return approvals;
3247
3249
  };
3248
3250
 
3251
+ /**
3252
+ * v0.31.0: the rate limiter that is the approval queue's front door.
3253
+ *
3254
+ * ## Why this took a round to arrive
3255
+ *
3256
+ * `approval-gate.mjs` was written in 0.30.0 with a full suite and **no caller** — which is precisely the
3257
+ * defect that same release had just finished documenting in `ROADMAP.md` twice. Building the mechanism and
3258
+ * wiring it are separate acts, and a mechanism with no caller is the thing a reader cannot tell from a
3259
+ * finished feature.
3260
+ *
3261
+ * It guards **raising only**. Reading the queue and settling a request are deliberately unmetered: limiting
3262
+ * the view would limit knowing what is waiting, and limiting the answer would punish the supervisor
3263
+ * clearing the backlog. The threat is queue flooding by a runaway retry loop, and that is a threat against
3264
+ * the raise path.
3265
+ */
3266
+ const approvalGate = createApprovalGate(
3267
+ // Configurable so a host can tighten or loosen it, and so a test can reach the refusal without raising
3268
+ // thirty requests first. Omitted entirely when unset, which leaves the gate's own default in charge --
3269
+ // passing `undefined` through would be the same thing with more ways to be wrong.
3270
+ Number.isInteger(config.approvalRaiseLimitPerMinute) ? { limit: config.approvalRaiseLimitPerMinute } : {},
3271
+ );
3272
+
3249
3273
  // v0.6.1: the two browser routes behind the composer's target control. Additive and optional: a
3250
3274
  // host without `connection.fetch` still gets every tool, and `w2m_status` reports the absence - // which is the honest place for it, since a host that has no Connection at all is simply not a web
3251
3275
  // composition and does not need a warning on every headless run.
@@ -3778,6 +3802,24 @@ export async function apply(ctx, config = {}) {
3778
3802
  machineId: null,
3779
3803
  requestedBy: cfg.machineName,
3780
3804
  });
3805
+ // v0.31.0: the raise path passes the front door BEFORE a request exists. A flood of raises is
3806
+ // what the limiter is for, so a refused raise must not have already created the request it was
3807
+ // refused for.
3808
+ const permission = approvalGate.mayRaise({ id: cfg.machineName, level: cfg.actorAuthorityLevel });
3809
+ if (!permission.allowed) {
3810
+ throw new W2MError(
3811
+ 'W2M_PERMISSION_APPROVAL_RATE_LIMITED',
3812
+ 'W2M_PERMISSION_APPROVAL_RATE_LIMITED: ' + permission.detail,
3813
+ {
3814
+ hint: 'wait for the window to pass and dispatch again; the queue was not cleared and existing requests are still answerable',
3815
+ detail: {
3816
+ reason_code: permission.reason_code,
3817
+ retry_after_seconds: permission.retry_after_seconds,
3818
+ budget: approvalGate.budgetFor({ id: cfg.machineName, level: cfg.actorAuthorityLevel }),
3819
+ },
3820
+ },
3821
+ );
3822
+ }
3781
3823
  const approvalId = randomUUID();
3782
3824
  const raised = raiseApproval(approvalQueue(), request, { approvalId, toolName: 'w2m_run' });
3783
3825
  if (!raised.ok) {