@twinsearth/w2m-dsh-plugin 0.60.0 → 1.0.2

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
@@ -1,6 +1,235 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project are documented here. The format follows
3
+ All notable changes to this project are documented here.
4
+
5
+ ## [1.0.2] — 2026-10-16
6
+
7
+ **One missing line made the whole plugin fail to mount, and it had been missing since v0.41.0.**
8
+
9
+ ### The defect
10
+
11
+ JsonSchemaError: unsupported JSON schema:
12
+ parameters.accept_block.additionalProperties must be explicitly true or false at authorError
13
+ at parameterSchemaSpecToJsonSchema (dsh-tools/lib/index.js:803)
14
+ at defineTool (dsh-tools/lib/index.js:848)
15
+ at Object.apply (src/plugin/tools.mjs:5025)
16
+
17
+ `w2m_memory`'s `accept_block` was declared as `type: 'object'` **without `additionalProperties`**, and DSH's
18
+ compiler requires it to be explicit. **The throw happens while the tool is being defined, so the entire fiber fails
19
+ to mount** — the client shows the package as `异常`, **all eleven tools disappear**, and the message is visible
20
+ only if something gets far enough to print it.
21
+
22
+ **That single parameter is the whole failure.** Checked across the entire tool surface: **one `type: 'object'`
23
+ parameter exists in this file, and it was the one.**
24
+
25
+ ### Verified against the real compiler, not the shim
26
+
27
+ `verify-installed.mjs` runs against an offline shim that checks shape only, **which is why it reported
28
+ `PASS: 11 tools registered` for every broken version**. The real `@deepseek-ai/dsh-tools` was extracted from
29
+ `app.asar` — 10,913 entries, preserving the `node_modules` layout so bare specifiers resolve — and the same call
30
+ was made against it both ways:
31
+
32
+ WITHOUT additionalProperties -> THREW JsonSchemaError: ... must be explicitly true or false
33
+ WITH additionalProperties -> ACCEPTED
34
+
35
+ **Same error, same code path as the client reported.** That is the first measurement in this session taken from the
36
+ compiler that actually runs.
37
+
38
+ ### `true`, not `false`
39
+
40
+ `verifyBlock()` and `acceptWithAuthority()` already validate the block's structure and its hash, **so the schema
41
+ is not the guard here**. A peer that adds a field outside the seven would be refused **for a reason that has nothing
42
+ to do with trust**, and refusing a legitimate block is the worse of the two errors.
43
+
44
+ ### Why this took so long to find, stated plainly
45
+
46
+ **The error was invisible until the installer was used.** When the package was installed with `pnpm` and then
47
+ removed, the client failed *earlier*, with `cannot resolve profile bundle`, which names a resolution problem and
48
+ not a schema one. **Installing through the plugin manager took it far enough to reach `defineTool` and print the
49
+ real cause.**
50
+
51
+ **So the user's instinct was right in the way that mattered**: not that the official path behaves differently, but
52
+ that it is the path which gets far enough to report the truth. Four earlier rounds of diagnosis — a fabricated
53
+ incompatibility with the host, a mount-row duplicate, a release-notes defect, a runtime-discovery defect — were all
54
+ real, and **none of them was this**.
55
+
56
+ ### The method that worked
57
+
58
+ I extracted the host's own compiler from `app.asar` and called it directly. **Every previous attempt had tested
59
+ against a shim, a clone, or an inference**, and each of those agreed with the broken declaration. **The only
60
+ measurement that disagreed was the one taken from the code that throws.**
61
+
62
+ ## [1.0.1] — 2026-10-16
63
+
64
+ **The first stable release. Three defects fixed, two of them mine and one of them published 56 times.**
65
+
66
+ ### The command allow-list could not admit the interpreter DSH itself runs on
67
+
68
+ Twelve end-to-end tests failed with `COMMAND_EXECUTABLE_PATH_MISMATCH`, and **the refusal was correct** —
69
+ the identity check was reading the wrong machine.
70
+
71
+ bundled node : E:\DS\resources\runtime\primary-runtime\dependencies\node\bin\node.exe
72
+ PATH : E:\Unity3D\Tuanjie Cowork\cli\bin\win32-x64\node.exe <- a different program
73
+ PATH contains the bundled directory : False
74
+
75
+ The tests dispatch `[process.execPath, '-e', …]` against an allow-list entry of the bare name `node`. **DSH runs
76
+ on the bundled interpreter and does not put its directory on PATH**, so the bare name resolved to an unrelated
77
+ `node.exe` shipped by another application, and an absolute path to the bundled copy was refused as "a different
78
+ file".
79
+
80
+ **Fixed by answering the right question.** `resolveCommand` still keeps PATH ahead of the bundled tree — a test
81
+ guards that precedence, and it is about *which file a name means*. The new `bundledExecutable()` answers a
82
+ different question: *is this path the bundled copy of that tool?* **The bundled copy of a tool is that tool**, so
83
+ identity holds, and **nothing is weakened**: it only ever returns files under `<deps>`, so
84
+ `E:\Temp\evil\node.exe` is still refused.
85
+
86
+ **The runtime is now discovered rather than assumed.** `dshDependenciesDir()` tried exactly one path,
87
+ `$DSH_HOME/dsh-runtimes/<runtime>/dependencies`, which stopped existing when the application moved its runtime
88
+ inside the bundle. It now tries the explicit override, then that documented layout, then **the directory implied
89
+ by `process.execPath`** — a bundled interpreter lives at `<deps>/node/bin/node`, so its own path states where
90
+ `<deps>` is. **That cannot go stale when the installation moves again.**
91
+
92
+ ### 56 of 66 release scripts published v0.1.3's commit message under an unrelated title
93
+
94
+ Found because a test plan written against v0.60.0 reported its release notes as "highly duplicated" with several
95
+ earlier releases. **The scripts state the right intention and then read the wrong file:**
96
+
97
+ /** The release notes reuse the commit message */
98
+ const NOTES = fs.readFileSync('…\\commit-0103b.txt', 'utf8'); // v0.1.3's message
99
+ name: 'v0.60.0 — approval index, fleet sync, service health, tiered storage', // hardcoded
100
+
101
+ `scripts/github-release.mjs` **derives both fields from the repository's own record** — the title from the commit
102
+ subject, the body from the CHANGELOG entry — **so neither can be copied by accident.** It refuses to publish a
103
+ version with no CHANGELOG entry, and reads the release back after writing. **The five most-read releases
104
+ (v0.60.0, v0.59.0, v0.58.0, v0.57.1, v0.57.0) are corrected**, each verified by reading it back.
105
+
106
+ ### My mount-row advice broke the client twice, and §3.2 withdraws it
107
+
108
+ **An installed plugin mounts itself**, from its own bundle patch — so adding a row to `cordis.patch.yml` inserts
109
+ a **second** entry with the same `id`, and the client stops opening. Measured: **0 rows before install, exactly 1
110
+ after, 2 with the manual row, 1 again once removed.**
111
+
112
+ ### Also in this release
113
+
114
+ **Two client failures had one cause, and it was this repository's own advice: do not add a mount row.**
115
+
116
+ ### The plugin mounts itself
117
+
118
+ `package.json` declares `dsh.bundle.patch`, so **the host composes an installed package from the patch shipped
119
+ inside the package**. Nothing needs to be written into the profile — and writing it anyway inserts a **second**
120
+ entry with the same `id`, which stops the client from opening.
121
+
122
+ **Measured on a throwaway profile**, because the desktop profile cannot be previewed (`rejectElectronProfile`)
123
+ and that is exactly where the failures happened:
124
+
125
+ | State | `id: w2m` rows |
126
+ |---|---|
127
+ | profile created, plugin not installed | 0 |
128
+ | **plugin installed, nothing else done** | **1** |
129
+ | a correctly written `insert:` row added as well | **2** ← the failure |
130
+ | that row removed | **1** |
131
+
132
+ **The symptom is worth stating precisely, because it is not a crash:** the client does not open, there is no
133
+ error message, and deleting `cordis.patch.yml` restores it immediately. **That is what pointed at configuration
134
+ rather than code.**
135
+
136
+ ### §3.2, and why the old advice is withdrawn
137
+
138
+ The section now leads with the rule — **install, and change nothing else** — then covers the three ways an edit
139
+ goes wrong:
140
+
141
+ * **`insert:` is an append.** Appending a row whose `id` already exists is the failure. To change configuration,
142
+ address the row the bundle patch created, as a non-insert patch.
143
+ * **`Add-Content` onto this file is always wrong.** It is a YAML list; a fresh profile ships `[]`, and a second
144
+ document after it is a parse error. **The measured exception message is in the section**, because "failed to
145
+ parse overlay" alone sends people looking at the wrong thing.
146
+ * **The desktop profile has no safe preview.** `--dump-config` composes and exits without starting anything, and
147
+ it is the right tool — but the CLI refuses the desktop profile, so for that one the only protection is writing
148
+ the row correctly. **Which in practice means not writing one.**
149
+
150
+ ### What I got wrong, since the section exists because of it
151
+
152
+ I added the row to a live profile without a way to test it that did not involve the user's client, and the
153
+ backup went with the profile when it was reset. **The cost was two client failures and a profile reset.**
154
+
155
+ **I also formed a plausible hypothesis and disproved it before writing any of this**: I suspected
156
+ `Add-Content -Encoding utf8` had written a mid-file BOM. Measured on PowerShell 5.1 — a 45-byte file with the
157
+ BOM only at offset 0. **Not the cause, and recorded so it is not repeated.**
158
+
159
+ **The experiment that settled it took one command and started nothing**: `dsh --profile <probe> --dump-config`,
160
+ which prints the composed tree and exits. **It would have caught this before the first restart had I run it
161
+ first.**
162
+
163
+ **No code change and no test count change** — a documentation release. It is versioned rather than left
164
+ unreleased because the incorrect procedure was published in `TROUBLESHOOTING.md`, and a user following it would
165
+ break their client.
166
+
167
+ **56 of 66 release scripts published the wrong text, and this is the fix that makes it impossible to repeat.**
168
+
169
+ ### The defect, found while checking someone else's test plan
170
+
171
+ A test plan written against v0.60.0 reported that its GitHub release notes were *"highly duplicated"* with
172
+ v0.59.0, v0.58.0 and several earlier releases, and concluded they were probably template-generated. **That
173
+ conclusion was right and the cause was mine:**
174
+
175
+ // publish-0600-github.mjs
176
+ /** The release notes reuse the commit message: it already states the cause, the fix and the evidence. */
177
+ const NOTES = fs.readFileSync('E:\\GPT\\scripts\\commit-0103b.txt', 'utf8'); // <- v0.1.3's message
178
+ name: 'v0.60.0 — approval index, fleet sync, service health, tiered storage', // <- hardcoded
179
+
180
+ **The script states the right intention in its own comment and then reads the wrong file.** Measured: **56 of 66
181
+ `publish-*-github.mjs` scripts read that same stale file, and 57 carry that same title** -- four roadmap
182
+ keywords that describe none of those releases.
183
+
184
+ ### What I got wrong twice while investigating
185
+
186
+ **First** I said the `health` module did not exist and that the test plan was testing a fiction. **It exists:**
187
+ `src/agent/health.mjs`, whose own header records the macOS mistake and its fix
188
+ (`darwin: ['/var/log', '/Library/Logs', '/tmp']`). **It was in the code and missing from my CHANGELOG, and I
189
+ read the CHANGELOG as if it were the record.** The test plan was more accurate than my denial of it.
190
+
191
+ **Second**, having been wrong, the honest correction is that the plan is right about `health` and wrong about
192
+ three counts, which are now measured rather than quoted: **89 suites (not 82), 2352 tests (not 2228), 15
193
+ pre-flight checks (not 12), and six aggregation verdicts (not nine)** -- `degraded`, `timeout` and
194
+ `cancelled` do not exist, so the three rows testing them would report false failures.
195
+
196
+ ### The fix is a module, not another corrected copy
197
+
198
+ `scripts/github-release.mjs` **derives both fields from the repository's own record**: the title from the commit
199
+ subject, the body from the CHANGELOG entry for that version. **Neither is written by hand, so neither can be
200
+ copied by accident** -- which was the actual failure mode, since each release was a copy of the previous script
201
+ and the two constants that must change were the two easiest to forget.
202
+
203
+ **It refuses to publish when the CHANGELOG has no entry for the version.** A release whose body cannot be derived
204
+ is a release that would otherwise be published under the previous one's text, which is precisely what happened 56
205
+ times. **It also reads back after writing and compares**, because `PATCH` returning 200 is not evidence that the
206
+ field changed.
207
+
208
+ Verified: a version with no CHANGELOG entry is refused (exit 1); a version with one yields the commit subject as
209
+ its title and a 3024-character body (exit 0, dry run); the exported helpers split CHANGELOG sections correctly,
210
+ return null for an unknown version, and import without side effects.
211
+
212
+ **The token comes from `GITHUB_TOKEN` rather than being embedded**, which all 66 older scripts do.
213
+
214
+ ### The five most-read releases are corrected
215
+
216
+ Titles and bodies rewritten from their own CHANGELOG entries, **each verified by reading the release back**
217
+ rather than by trusting the response:
218
+
219
+ | Release | Title now |
220
+ |---|---|
221
+ | v0.60.0 | `docs(troubleshooting) + feat(script): the lock rule, and an out-of-band session bound` |
222
+ | v0.59.0 | `fix(bin): every published bin can be imported without killing the importing process` |
223
+ | v0.58.0 | `feat(bridge): report what arrived instead of guessing what an image looks like` |
224
+ | v0.57.1 | `docs(bridge): correct a comment that implied a sequence the file does not have` |
225
+ | v0.57.0 | `feat(vision): an image path becomes text evidence, and the credential never touches argv` |
226
+
227
+ Each was 2044 characters -- one identical body -- before the repair.
228
+
229
+ **The remaining 51 are left as they are**, which is a decision rather than an oversight: they are old enough that
230
+ nobody reads them, and the module means the next release is correct without anyone remembering to check.
231
+
232
+ format follows
4
233
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
234
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
235
 
@@ -311,6 +311,109 @@ instance. **Killing that process is the smaller, safer step, and it is the one t
311
311
  **What this cost, stated plainly:** the deleted directory carried session history and profile
312
312
  configuration that no longer exists. **`Stop-Process` would have cost nothing.**
313
313
 
314
+ ### 3.2 Never add a mount row: an installed plugin mounts itself
315
+
316
+ **This section exists because two client failures were caused by following advice that used to be in this
317
+ repository.** A mount row in `cordis.patch.yml` is **not necessary, and adding one breaks the client.**
318
+
319
+ **The plugin mounts itself when the package is installed.** The host composes an installed package from **the
320
+ plugin's own bundle patch** — `package.json`'s `dsh.bundle.patch`, which points at the `cordis.patch.yml` shipped
321
+ *inside this package*. Nothing has to be written into the profile.
322
+
323
+ **Confirmed by experiment.** On a throwaway profile created from the shipped `web` template, `--dump-config`
324
+ reports **exactly one** `w2m` row with the plugin installed and **nothing** added to the profile:
325
+
326
+ ```
327
+ # == @twinsearth/w2m-dsh-plugin
328
+ - id: w2m
329
+ name: '@twinsearth/w2m-dsh-plugin'
330
+ config:
331
+ rabbitUrl: ''
332
+ autoStartAgent: false
333
+ ```
334
+
335
+ **Adding a row inserts a second entry with the same `id`:**
336
+
337
+ ```
338
+ 1260: # == @twinsearth/w2m-dsh-plugin <- automatic
339
+ 1261: - id: w2m
340
+
341
+ 1271: # == ...\profiles\<name>\cordis.patch.yml <- the manual one
342
+ 1272: - id: w2m <- SAME id, twice
343
+ ```
344
+
345
+ **Two rows with one `id` is what made the client stop responding.** The symptom is not a crash and not an
346
+ error message — **the client does not open at all.**
347
+
348
+ #### The rule
349
+
350
+ ```bash
351
+ # Install. That is the whole procedure.
352
+ dsh plugin --profile <name> add @twinsearth/w2m-dsh-plugin@<version>
353
+ ```
354
+
355
+ **Do not edit `cordis.patch.yml` for this plugin.** If you have already added a row, delete that row — **and
356
+ delete the row, not the `~/.dsh` directory.**
357
+
358
+ #### To change configuration, target the existing row
359
+
360
+ **Do not insert.** Address the row that the bundle patch already created:
361
+
362
+ ```yaml
363
+ - id: w2m # a non-insert patch: replaces fields on the existing entry
364
+ config:
365
+ autoStartAgent: true
366
+ ```
367
+
368
+ **An `insert:` is an append.** Appending a row whose `id` already exists is the failure described above.
369
+
370
+ #### `Add-Content` onto this file is always wrong
371
+
372
+ It is a YAML **list**, so the file's root is a sequence:
373
+
374
+ ```
375
+ Error: dsh: failed to parse overlay ...\cordis.patch.yml:
376
+ YAMLException: end of the stream or a document separator is expected (6:1)
377
+
378
+ 4 | []
379
+ 6 | - insert:
380
+ -----^
381
+ ```
382
+
383
+ `[]` is a complete document; a `- insert:` list after it is a second one. **Rewrite the file with a list root;
384
+ never append to it.** A freshly created profile ships `[]`, so this bites on the first edit.
385
+
386
+ #### Verify before you restart, when you can
387
+
388
+ `--dump-config` **composes the configuration and exits without starting an application**, which makes it the
389
+ safe way to check an edit:
390
+
391
+ ```bash
392
+ dsh --profile <name> --dump-config # then search the output for this package's name
393
+ ```
394
+
395
+ **The desktop profile is the one case where this is unavailable.** The CLI refuses it
396
+ (`rejectElectronProfile`): Desktop manages that profile exclusively. **So for the desktop profile there is no
397
+ safe preview — the only protection is writing the row correctly, which in practice means not writing one at all,
398
+ because the plugin mounts itself.**
399
+
400
+ If you must test a row, **create a separate profile and try it there first**:
401
+
402
+ ```bash
403
+ dsh probe --from-default-profile web --dump-config
404
+ dsh plugin --profile probe add @twinsearth/w2m-dsh-plugin@<version>
405
+ dsh --profile probe --dump-config # count the rows naming this package: expect exactly one
406
+ ```
407
+
408
+ **The measured sequence, so the expectation is on record:**
409
+
410
+ | State | `id: w2m` rows |
411
+ |---|---|
412
+ | profile created, plugin not installed | 0 |
413
+ | **plugin installed, nothing else done** | **1** |
414
+ | a correct `insert:` row added as well | **2** ← the failure |
415
+ | that row removed | **1** |
416
+
314
417
  ---
315
418
 
316
419
  ## 4. A machine is listed but never answers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.60.0",
3
+ "version": "1.0.2",
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/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "twinsearth-w2m-dsh",
4
- "version": "0.60.0",
4
+ "version": "1.0.2",
5
5
  "description": "Cross-platform fleet execution between Windows and macOS: one DeepSeek Harness account, a Rabbit relay, and signed device identity. Ships four Agent Skills describing fleet dispatch, field-level memory acceptance, session references, and resource scheduling.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -58,7 +58,7 @@
58
58
 
59
59
  import { basename, extname } from 'node:path';
60
60
 
61
- import { hasPathSeparator, isRelativePath, resolveCommand, sameFile } from './resolve.mjs';
61
+ import { bundledExecutable, hasPathSeparator, isRelativePath, resolveCommand, sameFile } from './resolve.mjs';
62
62
 
63
63
  /**
64
64
  * Parse `--allowed-commands`.
@@ -474,6 +474,24 @@ function executableIdentity(argv0, prefix0, options) {
474
474
  return { kind: 'no-match', detail: null };
475
475
  }
476
476
  if (sameFile(resolvedArgv, resolvedPrefix)) return { kind: 'match', detail: null };
477
+
478
+ /**
479
+ * Second identity path, and the one that matters on a machine where the bundled tree is not on PATH.
480
+ *
481
+ * A bare entry names a tool; **the bundled copy of that tool is that tool**, whether or not PATH happens to
482
+ * point at it. This is what admits `process.execPath` for the entry `node` without weakening anything: it
483
+ * **only ever** returns a file under `<deps>`, so `E:\Temp\evil\node.exe` is still refused, and it does not
484
+ * touch the search order, so `keeps PATH ahead of the bundled runtime` still holds.
485
+ *
486
+ * It also fixes a real case rather than a test-only one: **DSH runs on the bundled interpreter and does not
487
+ * put its directory on PATH**, so an allow-list entry of `node` could not be satisfied by the very interpreter
488
+ * the host executes.
489
+ */
490
+ if (!prefixIsPath && argvIsPath) {
491
+ const bundled = bundledExecutable(prefix0, options.env ?? process.env);
492
+ if (bundled !== null && sameFile(argv0, bundled)) return { kind: 'match', detail: null };
493
+ }
494
+
477
495
  return {
478
496
  kind: 'path-mismatch',
479
497
  detail:
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { accessSync, constants, realpathSync, statSync } from 'node:fs';
20
20
  import { homedir } from 'node:os';
21
- import { delimiter, isAbsolute, join } from 'node:path';
21
+ import { basename, delimiter, dirname, isAbsolute, join } from 'node:path';
22
22
 
23
23
  /** The DSH runtime directory layout we fall back to (task-8, measured). */
24
24
  export const DSH_RUNTIME_RELATIVE = join('dsh-runtimes', 'dsh-primary-runtime', 'dependencies');
@@ -34,22 +34,72 @@ export function dshHome(env = process.env) {
34
34
  return explicit ?? join(homedir(), '.dsh');
35
35
  }
36
36
 
37
+ /**
38
+ * The dependency directory implied by the interpreter that is running this code, or `null`.
39
+ *
40
+ * ## Why this exists, and why it is the first fallback
41
+ *
42
+ * `DSH_HOME/dsh-runtimes/<runtime>/dependencies` was the only layout this module knew. **The application moved
43
+ * and that assumption stopped holding**: the runtime now ships inside the bundle at
44
+ * `E:\DS\resources\runtime\primary-runtime\dependencies`, so `$DSH_HOME` contains no `dsh-runtimes/` at all, and
45
+ * every lookup that fell back to it found nothing.
46
+ *
47
+ * **The measurement that located it:** twelve end-to-end tests refused with `COMMAND_EXECUTABLE_PATH_MISMATCH`.
48
+ * They dispatch `[process.execPath, '-e', ...]` against an allow-list entry of the bare name `node`, so the
49
+ * identity check resolves `node` and compares the two files. `process.execPath` is the **bundled** interpreter --
50
+ * `...\dependencies\node\bin\node.exe` -- while the bare name resolved to whatever PATH held. Two different
51
+ * files, so the refusal was correct and the lookup was wrong.
52
+ *
53
+ * **So the fix is discovery rather than another hardcoded path**: a bundled interpreter lives at
54
+ * `<deps>/node/bin/node`, which means its own path *states* where `<deps>` is. That holds wherever the
55
+ * application is installed, and it cannot go stale when the installation moves again.
56
+ *
57
+ * @returns {string|null}
58
+ */
59
+ function depsDirFromExecutable() {
60
+ const exe = process.execPath;
61
+ if (typeof exe !== 'string' || exe === '') return null;
62
+ const binDir = dirname(exe);
63
+ // `<deps>/node/bin/node[.exe]` is the shipped shape on every platform.
64
+ if (basename(binDir) !== 'bin' || basename(dirname(binDir)) !== 'node') return null;
65
+ const deps = dirname(dirname(binDir));
66
+ try {
67
+ return statSync(deps).isDirectory() ? deps : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
37
73
  /**
38
74
  * Absolute path of the bundled DSH dependency directory, or `null` when absent.
39
75
  *
40
- * `DSH_RUNTIME_DEPS` overrides the guess, which keeps tests hermetic.
76
+ * Candidates are tried in order and the **first that exists** wins:
77
+ *
78
+ * 1. `DSH_RUNTIME_DEPS`, the explicit override that keeps tests hermetic;
79
+ * 2. `$DSH_HOME/dsh-runtimes/<runtime>/dependencies`, the shipped layout this module documented;
80
+ * 3. the directory implied by `process.execPath`, which is how the bundled layout is found now that the
81
+ * application carries its runtime inside the bundle instead of under the home directory.
82
+ *
83
+ * Returning the first *existing* candidate rather than a single computed path is the part that matters: a
84
+ * stale first guess must not shadow a real second one.
41
85
  *
42
86
  * @param {Record<string,string|undefined>} [env]
43
87
  * @returns {string|null}
44
88
  */
45
89
  export function dshDependenciesDir(env = process.env) {
46
90
  const override = env.DSH_RUNTIME_DEPS && env.DSH_RUNTIME_DEPS.trim() !== '';
47
- const dir = override ? env.DSH_RUNTIME_DEPS : join(dshHome(env), DSH_RUNTIME_RELATIVE);
48
- try {
49
- return statSync(dir).isDirectory() ? dir : null;
50
- } catch {
51
- return null;
91
+ const candidates = [
92
+ override ? env.DSH_RUNTIME_DEPS : join(dshHome(env), DSH_RUNTIME_RELATIVE),
93
+ depsDirFromExecutable(),
94
+ ].filter((dir) => typeof dir === 'string' && dir !== '');
95
+ for (const dir of candidates) {
96
+ try {
97
+ if (statSync(dir).isDirectory()) return dir;
98
+ } catch {
99
+ // Try the next candidate; an absent guess is not an error.
100
+ }
52
101
  }
102
+ return null;
53
103
  }
54
104
 
55
105
  /**
@@ -123,6 +173,53 @@ export function bundledBinDirs(env = process.env) {
123
173
  });
124
174
  }
125
175
 
176
+ /**
177
+ * The file a bare tool name would name *inside DSH's bundled runtime*, or `null`.
178
+ *
179
+ * ## Why the identity check needs this and the search order must not
180
+ *
181
+ * `resolveCommand` keeps PATH ahead of the bundled tree, and a test guards that precedence
182
+ * (`keeps PATH ahead of the bundled runtime`). **The two are not in conflict**: PATH decides which file a
183
+ * *name* means, while this answers a different question -- *is this path the bundled copy of that tool?*
184
+ *
185
+ * **The measurement that required it:** twelve end-to-end tests refused with
186
+ * `COMMAND_EXECUTABLE_PATH_MISMATCH`. They dispatch `[process.execPath, '-e', ...]` against an allow-list entry
187
+ * of the bare name `node`. `process.execPath` is the bundled interpreter
188
+ * (`<deps>\node\bin\node.exe`), but that directory is **not on this machine's PATH**, so the bare name resolved
189
+ * to an unrelated `node.exe` shipped by another application and the two files differed.
190
+ *
191
+ * **The refusal was correct and the identity was wrong.** An absolute path that *is* the bundled copy of the
192
+ * tool the entry names is the tool the entry names -- which is exactly the rule the header states, and the
193
+ * reason `E:\Temp\evil\node.exe` must still be refused: that is a different directory, and this only ever
194
+ * returns files under `<deps>`.
195
+ *
196
+ * @param {string} name Bare command name, e.g. `node`.
197
+ * @param {Record<string,string|undefined>} [env]
198
+ * @returns {string|null} Absolute path when the bundled copy exists.
199
+ */
200
+ export function bundledExecutable(name, env = process.env) {
201
+ if (typeof name !== 'string' || name === '' || hasPathSeparator(name)) return null;
202
+ const depsDir = dshDependenciesDir(env);
203
+ if (depsDir === null) return null;
204
+ const isWin = process.platform === 'win32';
205
+ const exts = isWin ? ['', ...WIN_EXTENSIONS] : [''];
206
+ const bases = [
207
+ join(depsDir, name, 'bin'),
208
+ join(depsDir, name),
209
+ ];
210
+ for (const base of bases) {
211
+ for (const ext of exts) {
212
+ const candidate = join(base, name + ext);
213
+ try {
214
+ if (statSync(candidate).isFile()) return candidate;
215
+ } catch {
216
+ /* keep looking */
217
+ }
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+
126
223
  /**
127
224
  * Resolve `argv[0]` the way the capability probe resolves a tool.
128
225
  *
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
208
208
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
209
209
  * overrides it at runtime for tests and packagers.
210
210
  */
211
- const PLUGIN_VERSION = '0.60.0';
211
+ const PLUGIN_VERSION = '1.0.2';
212
212
 
213
213
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
214
214
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -5067,6 +5067,33 @@ export async function apply(ctx, config = {}) {
5067
5067
  },
5068
5068
  accept_block: {
5069
5069
  type: 'object',
5070
+ /**
5071
+ * `true`, and it has to be stated.
5072
+ *
5073
+ * ## The defect this closes, and why it took so long to see
5074
+ *
5075
+ * DSH's `parameterSchemaSpecToJsonSchema` **requires an explicit `additionalProperties` on every
5076
+ * `type: 'object'` parameter** and throws otherwise:
5077
+ *
5078
+ * JsonSchemaError: unsupported JSON schema:
5079
+ * parameters.accept_block.additionalProperties must be explicitly true or false at authorError
5080
+ * at parameterSchemaSpecToJsonSchema (dsh-tools/lib/index.js:803)
5081
+ * at defineTool (dsh-tools/lib/index.js:848)
5082
+ * at Object.apply (src/plugin/tools.mjs:5025)
5083
+ *
5084
+ * **The throw happens while the tool is being defined, so the whole fiber fails to mount** -- the client
5085
+ * reports the package as `异常`, none of the eleven tools appear, and **the message is only visible if
5086
+ * something gets far enough to print it.** This parameter has had that shape since v0.41.0, so the
5087
+ * failure dates from then.
5088
+ *
5089
+ * **`true` rather than `false`:** `verifyBlock()` and `acceptWithAuthority()` already validate the block's
5090
+ * structure and hash, so the schema is not the guard here, and a peer that adds a field outside the seven
5091
+ * would be **refused for a reason that has nothing to do with trust**. Refusing a legitimate block is the
5092
+ * worse error of the two.
5093
+ *
5094
+ * Only one parameter in this file had the shape -- checked across the whole tool surface, not just here.
5095
+ */
5096
+ additionalProperties: true,
5070
5097
  description:
5071
5098
  'v0.41.0. Accept a CAT7 cognitive block received from another machine, per field, DRIVEN BY AN ' +
5072
5099
  'AUTHORITY LEVEL (actor_authority below) rather than accepted whole. A field the level cannot ' +