@twinsearth/w2m-dsh-plugin 0.30.0 → 0.31.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 +44 -0
- package/docs/1.0.0-ACCEPTANCE.md +109 -0
- package/package.json +1 -1
- package/src/plugin/tools.mjs +43 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ 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.31.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**The approval gate got a caller, and a draft acceptance definition for 1.0.0.**
|
|
10
|
+
|
|
11
|
+
### The defect this round started by finding, in the release that had just criticised it
|
|
12
|
+
|
|
13
|
+
`approval-gate.mjs` shipped in 0.30.0 with a full suite and **no caller** — which is exactly the shape of the
|
|
14
|
+
two defects 0.30.0 had just finished writing into `ROADMAP.md`. **Building a mechanism and wiring it are
|
|
15
|
+
separate acts**, and a mechanism with no caller cannot be told from a finished feature by reading either the
|
|
16
|
+
tests or the source.
|
|
17
|
+
|
|
18
|
+
It is now on the raise path, and the wiring is asserted rather than described: a real dispatch raises once,
|
|
19
|
+
the queue holds one request, a second raise is **refused by the front door before a request exists**, and the
|
|
20
|
+
queue still holds exactly one. Two more tests pin the asymmetry — reading the queue five times past any raise
|
|
21
|
+
budget, and settling the request — because a gate that also metered *those* would pass a flooding test and
|
|
22
|
+
quietly break the queue.
|
|
23
|
+
|
|
24
|
+
`approvalRaiseLimitPerMinute` is configurable so a host can set it and so a test can reach the refusal without
|
|
25
|
+
raising thirty requests first. Unset, the gate's own default governs.
|
|
26
|
+
|
|
27
|
+
### `docs/1.0.0-ACCEPTANCE.md` — a draft for objection
|
|
28
|
+
|
|
29
|
+
The goal names **分模块 / 分层级 / 分权限**, and a version number means nothing without a definition that can
|
|
30
|
+
fail. The draft lists 21 criteria across the three, each with **where it is checked and whether that check is
|
|
31
|
+
actually running**, under one rule: **a criterion nothing measures is not a criterion.**
|
|
32
|
+
|
|
33
|
+
Its two honest admissions are the point of writing it:
|
|
34
|
+
|
|
35
|
+
* **§1.5 — reachability is NOT checked.** Three mechanisms shipped fully tested and completely uncalled in
|
|
36
|
+
this span. The `claims` suite catches *documentation* drift and **cannot** catch this, because the document
|
|
37
|
+
and the code can agree while neither is reachable.
|
|
38
|
+
* **§3.11 — the permission gate is OFF by default.** An unset `actorAuthorityLevel` means `enforced: false`
|
|
39
|
+
and no change, so **a default install dispatches exactly as it did before the permission model existed.**
|
|
40
|
+
Treating unset as `0` would break every machine that never opted in, so the current behaviour is defensible
|
|
41
|
+
— but **"off unless configured" cannot be part of a 1.0.0 that claims 分权限.** The draft puts the choice to
|
|
42
|
+
a reader rather than resolving it: **require a level and refuse to start without one, or ship with the gate
|
|
43
|
+
off and say so in the README.** Silence is the one option that is not defensible.
|
|
44
|
+
|
|
45
|
+
It also records what is **not** claimed: no scale statement (3 usable machines of 5), no restart durability for
|
|
46
|
+
the queue, no MacMini verification beyond the four suites measured in 0.28.0, and `P2P_PUNCH_FAILED` still
|
|
47
|
+
UNVERIFIED.
|
|
48
|
+
|
|
49
|
+
**2 new tests, 0 failures.**
|
|
50
|
+
|
|
7
51
|
## [0.30.0] — 2026-10-15
|
|
8
52
|
|
|
9
53
|
**The same ruler on the other two entries: v4.9.0 had the same defect, v4.10.0 did not.**
|
|
@@ -0,0 +1,109 @@
|
|
|
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. |
|
|
93
|
+
| **`P2P_PUNCH_FAILED`** | cause UNVERIFIED; three hypotheses measured and excluded. |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 5. What would make this file finished
|
|
98
|
+
|
|
99
|
+
1. **A reachability check** — every exported function has a non-test caller, or is declared as
|
|
100
|
+
interface-only with a reason. Without it, §1.5 stays open and the class of defect that produced three
|
|
101
|
+
dead mechanisms stays invisible.
|
|
102
|
+
2. **A decision on §3.11**, whichever way it goes, reflected in `README.md` and in the pre-flight.
|
|
103
|
+
3. **Every row above promoted to a check that runs**, so this document becomes a description of the
|
|
104
|
+
pre-flight rather than a parallel list that can drift from it. **A document that restates what the
|
|
105
|
+
pre-flight does is a second source of truth, and this project has already paid for one of those.**
|
|
106
|
+
|
|
107
|
+
This draft deliberately stops short of those three, because each is a decision about what 1.0.0 must be
|
|
108
|
+
rather than a defect to fix, and the person who owns it has not answered the question this session asked in
|
|
109
|
+
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.
|
|
3
|
+
"version": "0.31.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",
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -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.
|
|
198
|
+
const PLUGIN_VERSION = '0.31.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) {
|