@twinsearth/w2m-dsh-plugin 0.23.0 → 0.24.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 +50 -0
- package/package.json +1 -1
- package/src/agent/permission.mjs +76 -0
- package/src/plugin/tools.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,56 @@ 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.24.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**The escalation reaches the approval index — and the bridge between them is where the defect would have been.**
|
|
10
|
+
|
|
11
|
+
Two shapes that look alike and are not:
|
|
12
|
+
|
|
13
|
+
| `approvalRequestFor()` produces | `index.register()` wants |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `subject_level` (flat) | `subject: { id, level }` (nested) |
|
|
16
|
+
| `capability` | `action` |
|
|
17
|
+
| `task_id`, `requested_by` | `sessionKey`, `subject.id` |
|
|
18
|
+
|
|
19
|
+
A caller hand-mapping these at the call site would get it right once and wrong later, and **the failure would
|
|
20
|
+
be a request registered against the wrong subject — the one field the whole decision rests on.** So
|
|
21
|
+
`raiseApproval()` owns the mapping and lives beside the thing it maps from.
|
|
22
|
+
|
|
23
|
+
### It does not smooth anything over
|
|
24
|
+
|
|
25
|
+
`register()` refuses a subject level that is not an integer 0..100, and the adapter **passes the index's own
|
|
26
|
+
`reason_code` and `detail` through unchanged**. A bridge that substituted a default level to make registration
|
|
27
|
+
succeed would be **inventing the single fact the approver's decision rests on, and would look like success
|
|
28
|
+
while doing it.** Tested directly: a request whose level is `'captain'` is refused, and the queue stays empty.
|
|
29
|
+
|
|
30
|
+
The `approvalId` is **required from the caller, not generated here**, because an id invented at this layer
|
|
31
|
+
could not be correlated with whoever is waiting on the decision.
|
|
32
|
+
|
|
33
|
+
### The end-to-end property is asserted, not described
|
|
34
|
+
|
|
35
|
+
A real `createApprovalIndex()` is constructed in the suite and an escalation is raised into it: the request
|
|
36
|
+
appears in `index.all()`, `subjectLevel` survives the mapping as `49`, and after an approver at level 50
|
|
37
|
+
settles it, **the subject level is still 49** — settling must not rewrite it.
|
|
38
|
+
|
|
39
|
+
### Two of my own errors, both caught by the suite rather than by reading
|
|
40
|
+
|
|
41
|
+
* I asserted `INDEX_NO_SUBJECT_LEVEL`. The value is `APPROVAL_NO_SUBJECT_LEVEL` — **every code in
|
|
42
|
+
`INDEX_REFUSAL` carries the `APPROVAL_` prefix.** The implementation was right and had passed the index's
|
|
43
|
+
own code through faithfully, which is exactly what the test was there to check. The assertion now reads the
|
|
44
|
+
value **out of the table** so it cannot drift again.
|
|
45
|
+
* The generator script's escape handling produced a stray `$` inside a template literal in a *failure message*,
|
|
46
|
+
which threw at runtime rather than at parse time. Caught by running the suite, not by `node --check`.
|
|
47
|
+
|
|
48
|
+
### Still not wired to a live queue
|
|
49
|
+
|
|
50
|
+
`createApprovalIndex` has **no caller in the plugin** — it was built in 0.10.0 and is exported from
|
|
51
|
+
`approval-index.mjs` alone. This release makes the gate and the index meet **on a real object**, with an
|
|
52
|
+
end-to-end test, but no dispatch path yet constructs a queue. So `W2M_PERMISSION_NEEDS_APPROVAL` still
|
|
53
|
+
refuses rather than queuing, and it still says so.
|
|
54
|
+
|
|
55
|
+
**6 new tests, 0 failures.**
|
|
56
|
+
|
|
7
57
|
## [0.23.0] — 2026-10-15
|
|
8
58
|
|
|
9
59
|
**The gate is on the dispatch path. "分权限" now executes, and a machine that has not opted in behaves exactly as before.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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/agent/permission.mjs
CHANGED
|
@@ -255,6 +255,82 @@ export function approvalRequestFor(decision, context = {}) {
|
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Turn an approval request into the entry the approval index accepts, and register it.
|
|
260
|
+
*
|
|
261
|
+
* ## Why this adapter exists rather than a direct call
|
|
262
|
+
*
|
|
263
|
+
* The two shapes are close and **not identical**, which is precisely where a defect would live:
|
|
264
|
+
*
|
|
265
|
+
* | `approvalRequestFor` produces | `index.register` wants |
|
|
266
|
+
* |---|---|
|
|
267
|
+
* | `subject_level` (flat) | `subject: { id, level }` (nested) |
|
|
268
|
+
* | `capability` | `action` |
|
|
269
|
+
* | `task_id` / `requested_by` | `sessionKey` / `subject.id` |
|
|
270
|
+
*
|
|
271
|
+
* A caller hand-mapping these at the call site would get it right once and wrong later, and the failure would
|
|
272
|
+
* be a request registered against the wrong subject — the one field the whole decision rests on. So the
|
|
273
|
+
* mapping lives here, beside the thing it maps from.
|
|
274
|
+
*
|
|
275
|
+
* ## What it refuses, and why it does not smooth anything over
|
|
276
|
+
*
|
|
277
|
+
* `register` refuses a subject level that is not an integer 0..100, and this adapter **passes the index's own
|
|
278
|
+
* `reason_code` and `detail` through unchanged**. A bridge that substituted a default level to make
|
|
279
|
+
* registration succeed would be inventing the single fact the approver's decision rests on, and would look
|
|
280
|
+
* like success while doing it.
|
|
281
|
+
*
|
|
282
|
+
* @param {{register: (entry: object) => {ok: boolean, reason_code?: string, detail?: string}}} index
|
|
283
|
+
* @param {object} request from {@link approvalRequestFor}
|
|
284
|
+
* @param {{approvalId: string, sessionKey?: string|null, toolName?: string|null,
|
|
285
|
+
* deadlineAt?: number|null, now?: number}} context
|
|
286
|
+
* @returns {{ok: boolean, approval_id: string|null, reason_code: string|null, detail: string}}
|
|
287
|
+
*/
|
|
288
|
+
export function raiseApproval(index, request, context = {}) {
|
|
289
|
+
if (index === null || typeof index !== 'object' || typeof index.register !== 'function') {
|
|
290
|
+
return { ok: false, approval_id: null, reason_code: 'APPROVAL_NO_INDEX', detail: 'no approval index was supplied, so nothing was registered' };
|
|
291
|
+
}
|
|
292
|
+
if (request === null || typeof request !== 'object') {
|
|
293
|
+
return { ok: false, approval_id: null, reason_code: 'APPROVAL_NO_REQUEST', detail: 'no approval request was supplied, so nothing was registered' };
|
|
294
|
+
}
|
|
295
|
+
const approvalId = context.approvalId;
|
|
296
|
+
if (typeof approvalId !== 'string' || approvalId === '') {
|
|
297
|
+
// Required from the caller rather than generated here: an id invented at this layer could not be
|
|
298
|
+
// correlated with whoever is waiting for the decision.
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
approval_id: null,
|
|
302
|
+
reason_code: 'APPROVAL_NO_ID',
|
|
303
|
+
detail: 'raiseApproval needs a caller-supplied approvalId, so the request can be correlated with whoever waits on it',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = index.register({
|
|
308
|
+
approvalId,
|
|
309
|
+
sessionKey: context.sessionKey ?? null,
|
|
310
|
+
subject: { id: request.requested_by ?? null, level: request.subject_level },
|
|
311
|
+
action: request.capability,
|
|
312
|
+
toolName: context.toolName ?? null,
|
|
313
|
+
reason: request.detail ?? null,
|
|
314
|
+
...(Number.isFinite(context.now) ? { openedAt: context.now } : {}),
|
|
315
|
+
deadlineAt: Number.isFinite(context.deadlineAt) ? context.deadlineAt : null,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (result?.ok !== true) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
approval_id: null,
|
|
322
|
+
reason_code: result?.reason_code ?? 'APPROVAL_REFUSED',
|
|
323
|
+
detail: result?.detail ?? 'the approval index refused the request without saying why',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
approval_id: approvalId,
|
|
329
|
+
reason_code: null,
|
|
330
|
+
detail: `registered ${approvalId} for ${request.capability} at subject level ${request.subject_level}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
258
334
|
/**
|
|
259
335
|
* Whether a decision may be settled automatically.
|
|
260
336
|
*
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -190,7 +190,7 @@ const MAX_STAGES = 16;
|
|
|
190
190
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
191
191
|
* overrides it at runtime for tests and packagers.
|
|
192
192
|
*/
|
|
193
|
-
const PLUGIN_VERSION = '0.
|
|
193
|
+
const PLUGIN_VERSION = '0.24.0';
|
|
194
194
|
|
|
195
195
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
196
196
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|