deepseek-coordination 0.1.8 → 0.10.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/LICENSE +21 -21
- package/README.md +349 -1083
- package/README.zh-CN.md +349 -697
- package/cordis.patch.yml +5 -74
- package/lib/client.js +33772 -0
- package/lib/index.d.mts +252 -0
- package/lib/index.mjs +27374 -0
- package/package.json +98 -114
- package/client.js +0 -338
- package/src/host/bin/dsh-os.bat +0 -19
- package/src/host/bin/dsh-os.ps1 +0 -5039
- package/src/host/bin/dsh-os.sh +0 -3385
- package/src/host/bin/dsh-os7.ps1 +0 -5518
- package/src/host/command.txt +0 -136
- package/src/host/docs/command-original.txt +0 -26
- package/src/host/docs/os-spec.md +0 -528
- package/src/host/docs/permission-model.md +0 -206
- package/src/host/spec/charsets.tsv +0 -28
- package/src/host/spec/verbs.tsv +0 -120
- package/src/permission/index.js +0 -535
- package/src/plugin/index.js +0 -226
- package/src/plugin/invoke.js +0 -306
- package/src/plugin/settings.js +0 -349
- package/src/plugin/takeover.js +0 -436
- package/src/plugin/workflow.js +0 -1494
- package/tools/measure-cache.mjs +0 -223
package/README.md
CHANGED
|
@@ -1,1083 +1,349 @@
|
|
|
1
|
-
# deepseek-coordination
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
|
165
|
-
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
`
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
|
324
|
-
|
|
325
|
-
|
|
|
326
|
-
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
the
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
**
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
provider documents the reason and it is worth knowing: it persists a **common prefix detected across
|
|
351
|
-
requests**, so by the second run the repeated identical prefixes had been learned and a mutation still
|
|
352
|
-
matched. Per-variant A/B isolation on a shared account cannot settle the mutation side, so
|
|
353
|
-
`tools/measure-cache.mjs` asserts only the append side, which is stable, and prints the rest as
|
|
354
|
-
observations rather than claims.
|
|
355
|
-
|
|
356
|
-
**The counterfactual rests on independent evidence instead**, and it is strong:
|
|
357
|
-
|
|
358
|
-
* the provider's own rule is that a cache hit requires a request to **fully match a persisted cache prefix
|
|
359
|
-
unit**, and the system prompt sits before the tool catalog and the entire history
|
|
360
|
-
([Context Caching](https://api-docs.deepseek.com/guides/kv_cache/));
|
|
361
|
-
* a ten-variant experiment on the same endpoint found appending a message kept a **98.7%** hit while adding
|
|
362
|
-
one line at the **end** of the system prompt produced **0.0%**
|
|
363
|
-
([prefix-stability chapter](https://github.com/diguike/book-deepseek-harness/blob/main/book/11-prefix-stability.md));
|
|
364
|
-
* the harness itself paid for this lesson: the sandbox policy used to live in the system prompt, and one
|
|
365
|
-
permission switch dropped cache reads from **~14,700 tokens to 256**. The fix was to move the changing
|
|
366
|
-
fact out of the prompt into the message sequence
|
|
367
|
-
([DSH agent note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md)).
|
|
368
|
-
|
|
369
|
-
**The first version of this row rendered the current phase into that section**, so a four-call workflow
|
|
370
|
-
re-wrote the system prompt three times. Whether or not the provider happens to forgive that on a given day,
|
|
371
|
-
the row has no business depending on it — so the fix is structural, not cosmetic:
|
|
372
|
-
|
|
373
|
-
* the section is a constant, so this row's **cache effect is `prefix-stable`**: zero prompt changes per
|
|
374
|
-
workflow, asserted by `tests/workflow.test.mjs` against the registered text at every phase, and again
|
|
375
|
-
after deleting `state.json` outright;
|
|
376
|
-
* the phase and the **one next call** travel in the `workflow_plan` tool's own result instead — appended
|
|
377
|
-
history, measured above at 96–97%;
|
|
378
|
-
* a step taken out of order is refused, and the refusal names the phase to record first, so the model can
|
|
379
|
-
recover from a stale plan without the prompt having told it anything;
|
|
380
|
-
* the tool **description** carries the discovery text. It lives in the tool catalog: inside the cached
|
|
381
|
-
prefix, sent once, and unchanged for the life of the session.
|
|
382
|
-
|
|
383
|
-
> [!IMPORTANT]
|
|
384
|
-
> **The consequence a reader should draw for their own plugins.** Anything that changes per step — the
|
|
385
|
-
> time, the current directory, a branch, a permission, a phase — must not be written into a prompt section.
|
|
386
|
-
> This harness gives that content its own path: register it with `ctx.systemPrompt.context()` and it is
|
|
387
|
-
> delivered as an appended user message, re-projected only when it changes. A section is for text that is
|
|
388
|
-
> stable for the session.
|
|
389
|
-
|
|
390
|
-
**Where the state lives.** `$DSH_HOME/dsh-os/workflows/<session>/`, beside the settings document and for
|
|
391
|
-
the same reason: the package directory is replaced on upgrade, and the workspace belongs to the user.
|
|
392
|
-
`state.json` is the compact record; every other file is the detail. A finished workflow is archived rather
|
|
393
|
-
than overwritten, so the next one starts clean and the last one stays readable.
|
|
394
|
-
|
|
395
|
-
```
|
|
396
|
-
state.json the compact record: phase, intent, decisions, steps, artifact paths
|
|
397
|
-
journal.jsonl append-only: notes and steps, bounded, folded in by a checkpoint
|
|
398
|
-
01-intent.md the prompt verbatim, then the questions and answers (or the assumptions)
|
|
399
|
-
02-plan.md approach and process, alternatives with pros and cons, criteria, the decision
|
|
400
|
-
03-execution.md chosen option, goal, steps, files in scope, constraints
|
|
401
|
-
04-outcome.md what was delivered, and any step left open
|
|
402
|
-
05-checkpoint.md the fold: everything decided so far, written when a compaction is wanted
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
**What is configured.** `workflowRoot` moves the directory; `autoCompact` (default `true`) controls
|
|
406
|
-
whether a checkpoint reports the compaction the engine may perform, and `maxCheckpoints` bounds it. The
|
|
407
|
-
structural half — detail to disk, one line back — is **not** configurable, because it is the reason the
|
|
408
|
-
feature saves anything.
|
|
409
|
-
|
|
410
|
-
> [!WARNING]
|
|
411
|
-
> **This row asks the user questions, so the deployment must be able to answer them.** It injects
|
|
412
|
-
> `userQuestions`; a host without it registers nothing rather than registering a tool that would fail on
|
|
413
|
-
> its second call. Two calls block on a human: `start` when questions are given, and `choose` always.
|
|
414
|
-
|
|
415
|
-
> [!NOTE]
|
|
416
|
-
> **What this row does not do: it does not invoke the engine's compaction service.** `ctx.compaction`
|
|
417
|
-
> exists, and calling it from here would mean constructing a session-shaped context whose contract is not
|
|
418
|
-
> published — a guess dressed up as a feature. What the plugin does instead is deterministic and
|
|
419
|
-
> inspectable: the detail leaves the conversation for a file, and `checkpoint` records the boundary where a
|
|
420
|
-
> compaction is safe. That is why the compression is measurable by `tests/workflow.test.mjs` rather than
|
|
421
|
-
> merely asserted.
|
|
422
|
-
|
|
423
|
-
---
|
|
424
|
-
|
|
425
|
-
## JSON contract
|
|
426
|
-
|
|
427
|
-
Every call writes **exactly one** JSON envelope to stdout, UTF-8 **without** a BOM, terminated by a
|
|
428
|
-
single LF.
|
|
429
|
-
|
|
430
|
-
```jsonc
|
|
431
|
-
// success
|
|
432
|
-
{ "ok": true, "verb": "file.charset", "result": { "charset": "utf-8", "bom": false, "eol": "crlf" },
|
|
433
|
-
"meta": { "durationMs": 3, "platform": "win32", "shell": "pwsh" } }
|
|
434
|
-
|
|
435
|
-
// failure — still exactly one envelope, still on stdout
|
|
436
|
-
{ "ok": false, "verb": "file.charset",
|
|
437
|
-
"error": { "code": "E_NOT_A_FILE", "message": "…", "hint": "…" },
|
|
438
|
-
"meta": { "durationMs": 2, "platform": "win32", "shell": "pwsh" } }
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
* `error.hint` appears only when there really is an actionable suggestion.
|
|
442
|
-
* `--pretty` indents the output; `--quiet` emits only `result` (or only `error`).
|
|
443
|
-
* **stderr carries human-readable diagnostics only and never takes part in parsing.** On Windows,
|
|
444
|
-
setting `DSH_OS_TRACE=1` puts a failure trace on stderr.
|
|
445
|
-
|
|
446
|
-
### `error.code` is authoritative
|
|
447
|
-
|
|
448
|
-
Exit codes are a coarse class, so that a shell can branch cheaply. **Never parse `error.message`.**
|
|
449
|
-
|
|
450
|
-
| Exit code | Class | Representative codes |
|
|
451
|
-
|---|---|---|
|
|
452
|
-
| 0 | success | — |
|
|
453
|
-
| 1 | usage | `E_USAGE` |
|
|
454
|
-
| 2 | type / value | `E_TYPE`, `E_CHARSET_UNKNOWN`, `E_BOUNDARY`, `E_RANGE`, `E_WINDOW_AMBIGUOUS` |
|
|
455
|
-
| 3 | authorization | `E_PERMISSION`, `E_ELEVATION_REQUIRED`, `E_PERMISSION_PROTECTED_PATH`, `E_PERMISSION_DELEGATE_ONLY`, `E_PERMISSION_FILE_UNTRUSTED`, `E_PERMISSION_DENIED_BY_RULE`, `E_PERMISSION_NO_ANSWERER` |
|
|
456
|
-
| 4 | platform unsupported | `E_NOT_SUPPORTED`, `E_NO_DISPLAY`, `E_ACCESSIBILITY_DENIED`, `E_ACCESSIBILITY_UNAVAILABLE` |
|
|
457
|
-
| 5 | target / encoding / IO | `E_NOT_FOUND`, `E_NOT_A_FILE`, `E_EXISTS`, `E_BINARY_CONTENT`, `E_CHARSET_MISMATCH`, `E_TRUNCATED`, `E_PARTIAL_WRITE`, `E_WINDOW_NOT_FOUND`, `E_IO` |
|
|
458
|
-
| 6 | timeout | `E_TIMEOUT` |
|
|
459
|
-
| 7 | internal / aborted | `E_INTERNAL`, `E_ABORTED` |
|
|
460
|
-
|
|
461
|
-
The Windows implementation registers **36** error codes; the POSIX implementation groups them with
|
|
462
|
-
`case`. The two agree on the class of every **named** code, but not on unnamed ones: Windows returns
|
|
463
|
-
7, POSIX returns 5.
|
|
464
|
-
|
|
465
|
-
### Calling convention
|
|
466
|
-
|
|
467
|
-
Values are always separate argv elements and are **never** spliced into a shell string. The two
|
|
468
|
-
spellings below are equivalent, but the same parameter must not be given twice in different forms:
|
|
469
|
-
|
|
470
|
-
```sh
|
|
471
|
-
# positional form
|
|
472
|
-
dsh-os file create "./a.txt" "123" "utf-8-bom"
|
|
473
|
-
|
|
474
|
-
# named form — order is free; the effective order comes from the verb table's parameter list
|
|
475
|
-
dsh-os file create content="123" charset="utf-8-bom" path="./a.txt"
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
`E_USAGE` rejects: an unknown flag, an empty `--`, a value-taking flag with no value, a repeated named
|
|
479
|
-
argument, an unknown name, the same parameter given both positionally and by name, and surplus
|
|
480
|
-
positional arguments. Read operations are never asked for `--allow`; write operations always are.
|
|
481
|
-
|
|
482
|
-
A verb is one to three tokens deep — `selfcheck`, `file charset`, `file verify charset` — and the
|
|
483
|
-
parser consumes the next token only when extending the verb is genuinely known to be possible, so a
|
|
484
|
-
path argument that happens to begin with a word is not swallowed into the verb.
|
|
485
|
-
|
|
486
|
-
### Encoding, BOM and line endings
|
|
487
|
-
|
|
488
|
-
`src/host/spec/charsets.tsv` defines 15 canonical encoding names, each with aliases, a .NET code page,
|
|
489
|
-
an `iconv` name and BOM bytes:
|
|
490
|
-
|
|
491
|
-
`utf-8`, `utf-8-bom`, `utf-16le`, `utf-16be`, `utf-32le`, `utf-32be`, `gbk`, `gb18030`, `big5`,
|
|
492
|
-
`shift_jis`, `euc-jp`, `euc-kr`, `latin1`, `windows-1252`, `ascii`.
|
|
493
|
-
|
|
494
|
-
Detection sniffs the BOM first, then tries **strict** UTF-8, then GBK, then latin1. A read strips the
|
|
495
|
-
BOM and normalises `\r\n` to `\n` for output; **every write restores the file's original BOM presence
|
|
496
|
-
and line-ending convention.** Offsets are byte offsets in the half-open range `[start, end)` and must
|
|
497
|
-
fall on a character boundary, otherwise the call fails with `E_BOUNDARY` rather than cutting a
|
|
498
|
-
character in half.
|
|
499
|
-
|
|
500
|
-
A file is judged binary when a NUL byte appears in the first 8 KiB of the payload, which reports
|
|
501
|
-
`E_BINARY_CONTENT` instead of returning mojibake.
|
|
502
|
-
|
|
503
|
-
---
|
|
504
|
-
|
|
505
|
-
## The verb surface
|
|
506
|
-
|
|
507
|
-
`src/host/spec/verbs.tsv` **declares 86 verbs** and is the single source of truth. Some are declared
|
|
508
|
-
but not implemented; before relying on one, read
|
|
509
|
-
[What is actually available](#what-is-actually-available).
|
|
510
|
-
|
|
511
|
-
| Group | Verbs |
|
|
512
|
-
|---|---|
|
|
513
|
-
| `file` | `create` `charset` `encode` `length` `information` `list` `delete` `move` `copy` `mkdir` `append` `insert` `view` `search` |
|
|
514
|
-
| `file verify` | `charset` `encoding` `completion` |
|
|
515
|
-
| `execute` | `command` `synchronous` `asynchronous` `query` `kill` |
|
|
516
|
-
| `system` | `brand` `cpu` `gpu` `version` `info` |
|
|
517
|
-
| `task` | `list` `query` `show` `kill` `launch` |
|
|
518
|
-
| `service` | `list` `query` `start` `stop` `restart` |
|
|
519
|
-
| `environment` | `list` `get` `add` `set` `delete` |
|
|
520
|
-
| `environment attribute` | `list` `get` `add` `set` `delete` |
|
|
521
|
-
| `permission` | `query` `list` `audit` `grant` `revoke` `acl init` |
|
|
522
|
-
| `window` | `find` `list` `active` `close` `top` `bottom` `move` `resize` `minimize` `maximize` `restore` |
|
|
523
|
-
| `mouse` | `click` `context` `over` `wheel` `down` `up` `cursor` |
|
|
524
|
-
| `keyboard` | `down` `up` `press` `combination` `type` `codes` |
|
|
525
|
-
| `clipboard` | `read` `write` `paste` |
|
|
526
|
-
| `wait` | `window` `ms` |
|
|
527
|
-
| observation | `screenshot` `text` `structure` |
|
|
528
|
-
| self-check | `selfcheck` |
|
|
529
|
-
|
|
530
|
-
A few design points worth knowing before you start:
|
|
531
|
-
|
|
532
|
-
* **`file verify.*` asserts; it does not throw.** `file verify charset` returns `match: false`
|
|
533
|
-
alongside the observed value, so a caller can tell "the file is wrong" from "I asked wrongly".
|
|
534
|
-
`file verify completion` returns `complete: null` when no expectation was given — deliberately not
|
|
535
|
-
`false`.
|
|
536
|
-
* **`file insert`'s `--charset` is a flag, not a positional.** Inserted between `content` and `offset`
|
|
537
|
-
it would **silently displace `offset`**.
|
|
538
|
-
* **`file.delete` goes to the recycle bin on Windows by default.** On POSIX it needs `trash-put`,
|
|
539
|
-
otherwise it reports `E_NOT_SUPPORTED` — it does **not** silently delete for real.
|
|
540
|
-
* **`environment` has two levels.** A variable holds one string; an **attribute** (PATH, PATHEXT, LIB,
|
|
541
|
-
INCLUDE) holds an ordered list. A write returns `visibleToRunningProcesses`, because a `user` or
|
|
542
|
-
`machine` level write is invisible to processes that are already running.
|
|
543
|
-
* **`keyboard type` is Unicode-first.** Non-ASCII text goes through a clipboard paste, because
|
|
544
|
-
`KEYEVENTF_UNICODE` is unreliable when an input method is active.
|
|
545
|
-
|
|
546
|
-
---
|
|
547
|
-
|
|
548
|
-
## Dangerous permissions
|
|
549
|
-
|
|
550
|
-
This chapter is required reading before granting any permission. The layer is deliberately capable,
|
|
551
|
-
and the guarantees it offers are narrower than the feature list suggests.
|
|
552
|
-
|
|
553
|
-
### What a fully authorized agent can do
|
|
554
|
-
|
|
555
|
-
| Capability | Verbs involved | Authorization |
|
|
556
|
-
|---|---|---|
|
|
557
|
-
| **Read the contents of your screen** | `screenshot`, `text`, `structure` | **read-only, never prompts** |
|
|
558
|
-
| **Read and write your clipboard** | `clipboard.read`, `clipboard.write` | `read` never prompts; `write` needs `--allow` |
|
|
559
|
-
| **Synthesise input** — clicks, drags, typing, chords | `mouse.*`, `keyboard.*` | `--allow` |
|
|
560
|
-
| **Manipulate your windows** — move, resize, close, raise | `window.*` | `--allow` |
|
|
561
|
-
| **Start and kill processes** | `task.kill`, `execute.*` | `--allow` |
|
|
562
|
-
| **Run an arbitrary command line** | `execute.synchronous`, `execute.asynchronous` | `--allow` |
|
|
563
|
-
| **Delete, move and overwrite files** | `file.delete`, `file.move`, `file.copy` | `--allow` |
|
|
564
|
-
| **Persistently modify your environment variables** | `environment.set`, `.add`, `.delete`, `attribute.*` | `--allow` |
|
|
565
|
-
| **Control system services** (declared, not implemented) | `service.start`, `.stop`, `.restart` | `--allow` + elevation |
|
|
566
|
-
| **Read process and system inventories** | `task.*`, `system.*` | **read-only, never prompts** |
|
|
567
|
-
|
|
568
|
-
Put plainly: `mouse.*` and `keyboard.*` mean an authorized agent can operate **any** program you can
|
|
569
|
-
operate, with your rights. And because it can read the screen, it can also watch what it has just
|
|
570
|
-
done. Inside the desktop there is no sandbox.
|
|
571
|
-
|
|
572
|
-
### How authorization actually works
|
|
573
|
-
|
|
574
|
-
**Layer B — structural enforcement (always on, cross-platform).**
|
|
575
|
-
|
|
576
|
-
* Every mutating verb requires `--allow`. Without it the call returns `E_PERMISSION` and **nothing
|
|
577
|
-
happens**.
|
|
578
|
-
* **`permission.json` has three write verbs, but they go through the delegate and not through the
|
|
579
|
-
script's own authorization.** The table declares `permission grant`, `permission revoke` and
|
|
580
|
-
`permission acl.init` (all class `W`), while `permission query`, `permission list` and
|
|
581
|
-
`permission audit` are read-only. The flags of `grant` and `revoke` accept only `delegate` (besides
|
|
582
|
-
`allow`), so they can be called **only by the consent delegate**; the script itself cannot create a
|
|
583
|
-
persistent authorization with `--allow`, and reports `E_PERMISSION_DELEGATE_ONLY` when no delegate
|
|
584
|
-
is present.
|
|
585
|
-
* A hard **protected-path boundary** rejects any modification of the files that decide what this layer
|
|
586
|
-
may do: `permission.json` (and `.bak`), `src/host/command.txt`, the **four** `src/host/bin/dsh-os.*`
|
|
587
|
-
entry points (`.ps1`, `dsh-os7.ps1`, `.sh`, `.bat`), `src/host/spec/verbs.tsv` and
|
|
588
|
-
`src/host/spec/charsets.tsv`, plus any same-named temporary staged beside one of them. A violation
|
|
589
|
-
reports `E_PERMISSION_PROTECTED_PATH` with no side effect. Reads remain allowed so that you can
|
|
590
|
-
review the rules. The POSIX list has eight entries — the same list **without `dsh-os7.ps1`**.
|
|
591
|
-
* The check runs on the **resolved and normalised** path, compared segment by segment rather than by
|
|
592
|
-
regular expression, because `..`, an 8.3 short name or a trailing dot all walk around a regex.
|
|
593
|
-
`file.move` and `file.copy` check **both ends**, since moving a protected file is also a way of
|
|
594
|
-
destroying it.
|
|
595
|
-
|
|
596
|
-
**Layer A — kernel ACL (Windows, explicitly enabled, requires elevation).**
|
|
597
|
-
|
|
598
|
-
`permission acl init` appends an explicit **DENY** for `Everyone` (`S-1-1-0`) to the DACL of
|
|
599
|
-
`permission.json`, covering `FILE_GENERIC_WRITE | DELETE | WRITE_DAC | WRITE_OWNER`. It is idempotent,
|
|
600
|
-
and reports `E_ELEVATION_REQUIRED` when it cannot act rather than failing vaguely.
|
|
601
|
-
|
|
602
|
-
`selfcheck` reports `aclEnforced` by **reading the file's real security descriptor** and walking the
|
|
603
|
-
DACL. An earlier version read a flag from a state file, which claimed an enforcement that was not in
|
|
604
|
-
effect; that implementation has been deleted. `aclReason` explains a `false`, so the result can be
|
|
605
|
-
acted on rather than merely wondered at.
|
|
606
|
-
|
|
607
|
-
### Four limitations that must be stated honestly
|
|
608
|
-
|
|
609
|
-
These are documented in `src/host/docs/permission-model.md` and repeated here because they change what
|
|
610
|
-
the guarantees above mean.
|
|
611
|
-
|
|
612
|
-
1. **Persistent rules are recorded but not enforced at the verb layer.** The rules in `permission.json`
|
|
613
|
-
are matched by the consent delegate and its `os_permission_query` tool; the script reads the file
|
|
614
|
-
for **display only** (`permission list/query/audit`). No verb consults a rule before acting: the
|
|
615
|
-
real gates are `--allow`, the protected-path check, and (if enabled) layer A. A `deny-always` rule
|
|
616
|
-
does **not** currently stop a verb that arrives with `--allow`.
|
|
617
|
-
2. **`E_PERMISSION_DENIED_BY_RULE` is registered but never thrown.** It is in the error table and maps
|
|
618
|
-
to exit code 3, but no code path produces it — consistent with point 1.
|
|
619
|
-
3. **The in-script rule reader is not deny-first.** The script's `permission query` takes the **first**
|
|
620
|
-
matching rule regardless of whether it allows or denies. The delegate implements deny-first
|
|
621
|
-
correctly and has test coverage, so the two readers can disagree, and the script side is the weaker
|
|
622
|
-
one.
|
|
623
|
-
4. **`execute` is a way around the protected-path boundary.** That boundary guards `file.*` and
|
|
624
|
-
`environment.*`. It cannot constrain a child process: `execute.synchronous` hands the command line
|
|
625
|
-
to `cmd.exe /d /s /c` on Windows and to `sh -c` on POSIX, so the program it starts can write
|
|
626
|
-
anything its own rights permit, `permission.json` included. Only layer A prevents that. When
|
|
627
|
-
`aclEnforced: false`, layer B cannot.
|
|
628
|
-
|
|
629
|
-
The consent delegate has one more **design gap** worth stating plainly: it registers five tools
|
|
630
|
-
(`os_permission_query`, `os_permission_grant`, `os_permission_revoke`, `os_permission_list`,
|
|
631
|
-
`os_permission_audit`), but it does **not** register an `approval/request` answerer. Its own design
|
|
632
|
-
document says it should, and that `permission grant` **must** go through a user-confirmed prompt. As
|
|
633
|
-
the code stands, the enforcement is a **tool description** telling the model to call
|
|
634
|
-
`os_permission_grant` only after the user has chosen; the code does not verify that a prompt ever
|
|
635
|
-
happened. Treat the delegate as trusted infrastructure, not as a check on the agent.
|
|
636
|
-
|
|
637
|
-
Two further points about layer A: it requires the harness and the delegate to run as **different
|
|
638
|
-
identities** (otherwise it degrades to layer B), and its POSIX equivalent is weaker — a mode change
|
|
639
|
-
that the same user can undo. `permission acl init` says as much in its own result.
|
|
640
|
-
|
|
641
|
-
### Reviewing what has been granted
|
|
642
|
-
|
|
643
|
-
```sh
|
|
644
|
-
dsh-os permission list # rules, defaults, and the MEASURED aclEnforced
|
|
645
|
-
dsh-os permission query file.delete "R:\project\**"
|
|
646
|
-
dsh-os permission audit --limit 50 # the append-only tail; the delegate trims to 200 entries
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
If `permission.json`'s `updatedBy` is not `consent-delegate`, the file is treated as **untrusted**: its
|
|
650
|
-
rules are ignored, everything falls back to the defaults, and the reason is reported. A malicious rule
|
|
651
|
-
hand-edited into that file therefore does **not** take effect — it is ignored, and you are told.
|
|
652
|
-
|
|
653
|
-
To **create** a persistent decision you must use the delegate's tools. A wildcard target
|
|
654
|
-
(`target.value === "*"`) is refused unless `acknowledgeBroad: true` is passed explicitly, because
|
|
655
|
-
"agree to everything, permanently" has to be a separate decision and not the side effect of one click.
|
|
656
|
-
|
|
657
|
-
---
|
|
658
|
-
|
|
659
|
-
## What is actually available
|
|
660
|
-
|
|
661
|
-
Method: the platform dispatch tables are extracted and compared against the declared verb table, entry
|
|
662
|
-
by entry. An unimplemented verb **never fails silently**: it returns a named `E_NOT_SUPPORTED`
|
|
663
|
-
envelope.
|
|
664
|
-
|
|
665
|
-
| Platform | Declared | Implemented |
|
|
666
|
-
|---|---|---|
|
|
667
|
-
| Windows PowerShell 5.1 (`src/host/bin/dsh-os.ps1`) | 86 | **82** |
|
|
668
|
-
| PowerShell 7.6 (`src/host/bin/dsh-os7.ps1`) | 86 | **82** |
|
|
669
|
-
| POSIX (`src/host/bin/dsh-os.sh`) | 86 | **61** |
|
|
670
|
-
|
|
671
|
-
The two PowerShell runtimes have **identical** implemented sets, established by comparing the two
|
|
672
|
-
dispatch tables entry by entry rather than by eye.
|
|
673
|
-
|
|
674
|
-
### Windows (5.1 and 7.6 alike)
|
|
675
|
-
|
|
676
|
-
**Implemented (82):** every `file.*` (including `file.encode`); `file verify.*`; `file.search`; all
|
|
677
|
-
five `execute` verbs (`command`, `synchronous`, `asynchronous`, `query`, `kill`); `system.*`; all of
|
|
678
|
-
`task.*` (including `task.launch`); `service.list/query`; the whole `environment` surface (including
|
|
679
|
-
`attribute.*`); the whole `permission` surface (including `grant`, `revoke`, `acl.init`); the whole
|
|
680
|
-
`window` surface; `screenshot`; `clipboard.read/write`; `wait.window/ms`; the whole `mouse` and
|
|
681
|
-
`keyboard` surfaces; `text`; `structure`; `selfcheck`.
|
|
682
|
-
|
|
683
|
-
**Not implemented (4):** `clipboard.paste`, `service.start`, `service.stop`, `service.restart`.
|
|
684
|
-
|
|
685
|
-
### POSIX
|
|
686
|
-
|
|
687
|
-
**Implemented (61):** the whole `file` surface except `file.encode`; `file verify.*`; `file.search`;
|
|
688
|
-
all five `execute` verbs (including `execute.command`, which the Windows side also has); `system.*`;
|
|
689
|
-
`task.list`/`query`/`show`/`kill` (**not `task.launch`**); the whole `environment` variable level
|
|
690
|
-
(including persistent writes) and all five `attribute.*` verbs; the whole `permission` surface;
|
|
691
|
-
`wait.ms`; the whole `mouse` surface (7 verbs) and `keyboard` surface (6 verbs); `selfcheck`.
|
|
692
|
-
|
|
693
|
-
Of those 61, **13 need an X display** (the 7 `mouse.*` and 6 `keyboard.*`); the other 48 do not.
|
|
694
|
-
|
|
695
|
-
**Not implemented (25):** `file.encode`; all eleven `window.*`; `screenshot`, `text`, `structure`;
|
|
696
|
-
`clipboard.read`, `clipboard.write`, `clipboard.paste`; `wait.window`; `task.launch`; and all five
|
|
697
|
-
`service.*` (`list`, `query`, `start`, `stop`, `restart`). These fall through the dispatch table's
|
|
698
|
-
default branch to a **typed** `E_NOT_SUPPORTED`; they are not silently absent.
|
|
699
|
-
|
|
700
|
-
> [!NOTE]
|
|
701
|
-
> POSIX has no kernel API for persisting a variable, so `environment add/set/delete --scope user`
|
|
702
|
-
> edits `${HOME}/.profile` through a **managed block** delimited by
|
|
703
|
-
> `# >>> dsh-os managed environment >>>` and `# <<< dsh-os managed environment <<<`. Every change
|
|
704
|
-
> regenerates everything between the markers, so these operations are idempotent and a user can undo
|
|
705
|
-
> the lot by deleting the block. Writing bare assignments into the file directly would append
|
|
706
|
-
> duplicates on the second call and could not be undone. `--scope machine` is unavailable and reports
|
|
707
|
-
> `E_NOT_SUPPORTED`.
|
|
708
|
-
>
|
|
709
|
-
> `environment attribute add/set/delete` are **process scope only**; a persistent scope is refused
|
|
710
|
-
> with `E_NOT_SUPPORTED`. That is deliberate: POSIX has no kernel API for a list-valued variable, so
|
|
711
|
-
> persisting one means rewriting a shell startup file, and a wrongly reconstructed `PATH` there can
|
|
712
|
-
> make a machine's shells unusable.
|
|
713
|
-
|
|
714
|
-
> [!IMPORTANT]
|
|
715
|
-
> **On POSIX only `mouse.*` (7) and `keyboard.*` (6) need an X display.** With no `DISPLAY` they return
|
|
716
|
-
> a **typed** `E_NO_DISPLAY`; with a display but no `xdotool` they return `E_NOT_SUPPORTED` naming the
|
|
717
|
-
> missing tool.
|
|
718
|
-
>
|
|
719
|
-
> **`wait.ms` does not need one** — it is a sleep, so it can be accepted in full on this machine. The
|
|
720
|
-
> five `environment attribute.*` verbs do not either (they act on environment variables, not on the
|
|
721
|
-
> desktop).
|
|
722
|
-
>
|
|
723
|
-
> On this machine (no `DISPLAY`, no `xdotool`) the golden suite therefore asserts them as
|
|
724
|
-
> `E_NO_DISPLAY`: **that asserts the absence path, not that they work.** The injection path is covered
|
|
725
|
-
> by `tests/smoke-desktop-sh.sh` with stand-in tools; see [Tests](#tests).
|
|
726
|
-
|
|
727
|
-
> [!NOTE]
|
|
728
|
-
> What remains **structurally absent** on POSIX is `window.*` (11), `screenshot`, `text`, `structure`,
|
|
729
|
-
> `clipboard.*`, `wait.window` and all of `service.*`. `native.sh` (515 bytes) and `service.sh`
|
|
730
|
-
> (333 bytes) are deliberate empty stubs whose headers explain why: their POSIX counterparts are
|
|
731
|
-
> external programs (`wmctrl`, `import`, `xclip`, `scrot`) rather than linkable libraries.
|
|
732
|
-
>
|
|
733
|
-
> **`window.sh` is no longer a stub** (26 KB): it now implements `wait.ms`, all of `mouse.*` and all of
|
|
734
|
-
> `keyboard.*`, driving X11 through `xdotool` and checking for the display and the tools before
|
|
735
|
-
> touching anything. The remaining desktop verbs still return `E_NOT_SUPPORTED` from the dispatch
|
|
736
|
-
> table's default branch.
|
|
737
|
-
|
|
738
|
-
### The two implementations are not equivalent
|
|
739
|
-
|
|
740
|
-
Same verb names and the same calling convention, but behaviour differs in several places. The
|
|
741
|
-
important ones:
|
|
742
|
-
|
|
743
|
-
| Aspect | Windows | POSIX |
|
|
744
|
-
|---|---|---|
|
|
745
|
-
| `execute.command` | implemented | implemented (real argv, no shell) |
|
|
746
|
-
| `file.encode` | implemented | not implemented |
|
|
747
|
-
| `file.delete` default | recycle bin | fails without `trash-put` |
|
|
748
|
-
| `file.search` literal match | case-insensitive | case-sensitive |
|
|
749
|
-
| `file.view` character boundaries | enforced (`E_BOUNDARY`) | **not checked at all** |
|
|
750
|
-
| `file.view --max-bytes` | honoured | ignored (`truncated` is always `false`) |
|
|
751
|
-
| files over 32 MiB | `E_TRUNCATED` | no limit |
|
|
752
|
-
| `--eol crlf` on create/append | exact bytes | adds one extra trailing CRLF |
|
|
753
|
-
| appending to a mixed-EOL file | existing endings untouched | the whole file is renormalised |
|
|
754
|
-
| a character that cannot be represented | silently written as `?` | `E_ENCODING_INVALID` |
|
|
755
|
-
| `--pretty` / `--quiet` | honoured | accepted and ignored |
|
|
756
|
-
| `--` | `E_USAGE` | end-of-options marker |
|
|
757
|
-
| protected-path comparison | case-insensitive, normalised | case-sensitive, as given |
|
|
758
|
-
| unregistered error code | exit code 7 | exit code 5 |
|
|
759
|
-
| `meta.durationMs` on failure | the real duration | `0` |
|
|
760
|
-
|
|
761
|
-
Measured here, because prose is not evidence: on POSIX,
|
|
762
|
-
`file view <utf-8 file> utf-8 0 2` returns `ok` with `"text":"\ufffd"` — an offset of 2 lands inside
|
|
763
|
-
the first three-byte character and the character is split, where Windows fails the same call with
|
|
764
|
-
`E_BOUNDARY`.
|
|
765
|
-
|
|
766
|
-
`file.search`'s `--type` is a latent defect on both implementations: the code reads it, but it is not
|
|
767
|
-
registered as a known flag, so behaviour depends on the parser's fallback path.
|
|
768
|
-
|
|
769
|
-
---
|
|
770
|
-
|
|
771
|
-
## Installation status and known defects
|
|
772
|
-
|
|
773
|
-
Stated plainly, so that nobody is caught out.
|
|
774
|
-
|
|
775
|
-
### The bundle's load path is now covered by three verifiers
|
|
776
|
-
|
|
777
|
-
This section previously recorded that the loader's end-to-end path was **unverified**. That gap has
|
|
778
|
-
been closed by three verifiers, and they found one real defect:
|
|
779
|
-
|
|
780
|
-
| Verifier | What it proves | Cases |
|
|
781
|
-
|---|---|---|
|
|
782
|
-
| `tools/verify-bundle.mjs` | the loader's **resolution** contract: the manifest header, the patch file, every row resolving from a profile-style `node_modules`, every resolved module exporting the Cordis shape the loader calls (`apply`/`inject`/`name`), the two rows resolving to **different** modules (a shared specifier registers the same tool twice and aborts the whole entry), and the version gate | 10 |
|
|
783
|
-
| `tools/verify-plugin-load.mjs` | the real `apply()` path: the host plugin registers `dsh_os` with a parameters schema whose **root type is `object`**, the permission plugin registers its five tools, and **the registered handler really does spawn the script** and return a real envelope (`selfcheck` → ok, `verbCount` 86); a mutating verb without `allow` is refused with a typed `E_PERMISSION` rather than throwing. It also covers the workflow row, whose failure would otherwise be silent: the tool and its prompt section **are** registered, and `start → plan → choose → run` runs through the real handler with its gates intact | 30 |
|
|
784
|
-
| `tools/verify-editions.mjs` | edition selection: the default is still PowerShell 5.1; `powershellEdition: '7.6'` and `DSH_OS_POWERSHELL_EDITION` select the 7.6 artefact; `'auto'` uses 7.6 only when the artefact and a working `pwsh` are present **together**; configuration beats the environment; an explicit `scriptPath` disables the choice. `-ExecutionPolicy` is passed only to the 5.1 executable | 14 |
|
|
785
|
-
|
|
786
|
-
The bundle verifier links the package into a throwaway profile sandbox by junction, so resolution
|
|
787
|
-
happens exactly as it would in a profile, and the **active profile is never written to**.
|
|
788
|
-
|
|
789
|
-
The defect it found: `bin/dsh-os7.ps1` was previously **unreachable from the plugin** — it existed,
|
|
790
|
-
built and tested, but `resolveInvocation` only ever looked for `dsh-os.ps1`, so all the 7.6 work was
|
|
791
|
-
dead code from the plugin's point of view. It is now selectable, and **the default is unchanged**
|
|
792
|
-
(still 5.1), because a deployment that already works should not change behaviour merely because a
|
|
793
|
-
plugin was upgraded.
|
|
794
|
-
|
|
795
|
-
**Now observed live, though not through this verifier:** the package has since been installed into an
|
|
796
|
-
active desktop profile, and the six registered tools (`dsh_os` and the five `os_permission_*` tools)
|
|
797
|
-
are present and callable in a running session. That corroborates the verifier; it does not replace it,
|
|
798
|
-
because it does not establish that the loader composed the bundle by the same contract the verifier
|
|
799
|
-
checks.
|
|
800
|
-
|
|
801
|
-
### The Windows golden suite's order dependency is fixed
|
|
802
|
-
|
|
803
|
-
This was recorded as `passed: 47 failed: 1` on a clean checkout, the failing case being
|
|
804
|
-
`parser/one-two-three-token-verbs`, which asserted `file length` against a fixture directory created
|
|
805
|
-
by a **later** case.
|
|
806
|
-
|
|
807
|
-
That defect is **fixed**: the parser case in `tests/golden.ps1` now creates its own fixture
|
|
808
|
-
(`$fixtureDir = Join-Path $sandbox 'parser\verify'`, then `New-Item` and `Set-Content`) and asserts
|
|
809
|
-
against it. The current measurement is **50 passed, 0 failed**.
|
|
810
|
-
|
|
811
|
-
### Two real POSIX defects found by `tests/smoke-desktop-sh.sh`
|
|
812
|
-
|
|
813
|
-
Neither was visible after writing, and not by review either — because this machine has no `DISPLAY`,
|
|
814
|
-
they normally showed up only as "an `E_NO_DISPLAY` either way". Opening the injection path with
|
|
815
|
-
stand-in tools exposed both:
|
|
816
|
-
|
|
817
|
-
1. **Validation ran after injection.** `mouse click 10 20 --count abc` **returned an error, but the
|
|
818
|
-
pointer had already been moved** — a call that reports failure had changed the state of the
|
|
819
|
-
machine. The Windows side validates every argument *before* injecting; the POSIX side now matches
|
|
820
|
-
it. (The same held for `mouse over abc 20`.)
|
|
821
|
-
|
|
822
|
-
2. **A `fail` inside `$(...)` did not stop the verb.** The coordinate validator called `fail`, but the
|
|
823
|
-
caller wrote `x="$(window_int_arg ...)"`, so the `exit` ended only the subshell: the verb continued
|
|
824
|
-
with an empty value and **injected anyway** — `mouse over abc 20` **moved the pointer and returned
|
|
825
|
-
`ok`**. The three validators that reject input now **assign a variable** (`WINDOW_INT`,
|
|
826
|
-
`WINDOW_BUTTON`, `WINDOW_KEY`) which is read in the **caller's own shell**.
|
|
827
|
-
|
|
828
|
-
**This trap was already documented in this repository's `environment` module** (the comment on
|
|
829
|
-
`env_resolve_scope` explains precisely why it assigns rather than prints), and it was still
|
|
830
|
-
repeated here. Both defects are now pinned by three assertions each: invalid input must be
|
|
831
|
-
**rejected with nothing injected**.
|
|
832
|
-
|
|
833
|
-
### Other gaps
|
|
834
|
-
|
|
835
|
-
* **`tests/conformance.ps1` did not exist**, although the header of `spec/verbs.tsv` claimed it did and
|
|
836
|
-
that it checked consistency with `command.txt`. **The file now exists** (see
|
|
837
|
-
[Conformance of the two tables](#conformance-of-the-two-tables)), so the header's claim is now a
|
|
838
|
-
fact. A comment naming a guard that is not there is worse than no comment, because it is read as a
|
|
839
|
-
guarantee.
|
|
840
|
-
* **The consent delegate never registers an approval answerer**, as described above.
|
|
841
|
-
* **The delegate reports `aclEnforced: false` unconditionally** in its `query` result instead of
|
|
842
|
-
measuring the ACL as the script does, so it **under-reports** protection when layer A is on.
|
|
843
|
-
* **This section previously recorded that `--shell`, `--env`, `--stdin` and `--background` were
|
|
844
|
-
accepted but never read, and that `--encoding` on Windows built a value that was never applied. Both
|
|
845
|
-
records were out of date, and both had been real defects; they are now fixed.**
|
|
846
|
-
* **Windows:** `--shell`, `--env`, `--stdin` and `--background` really are read (there are branches
|
|
847
|
-
for them in `execute.ps1`). `--encoding` genuinely **was inert** — it built `$enc` and never used
|
|
848
|
-
it, and `StandardOutputEncoding` appeared nowhere in the module set, so child output was always
|
|
849
|
-
decoded with the runtime default. It is now wired to the output and error streams. **Measured:**
|
|
850
|
-
with a child emitting the GBK bytes `D6 D0 CE C4`, the default decoding gives `����` (four
|
|
851
|
-
replacement characters) and `--encoding gbk` gives `中文` (two characters).
|
|
852
|
-
* **POSIX:** these flags genuinely were never read. They are now **explicitly refused** —
|
|
853
|
-
`--shell`, `--env`, `--stdin`, `--background` and `--encoding` all return `E_NOT_SUPPORTED` (exit
|
|
854
|
-
code 4) instead of being silently ignored. That is what this project's own contract requires: **an
|
|
855
|
-
unavailable capability must be an explicit error, never an empty success**, and a caller that
|
|
856
|
-
passed `--stdin`, had nothing fed in, and received a success envelope has been misled.
|
|
857
|
-
* **Some declared error codes really are unreachable.** A full-text search of both implementations
|
|
858
|
-
confirms **zero occurrences** of `E_ENCODING_INVALID` (for `file.encode`), `E_IS_A_DIR` (for
|
|
859
|
-
`file.delete`) and `E_ABORTED`. By contrast `E_CHARSET_MISMATCH` (3 sites), `E_BINARY_CONTENT` and
|
|
860
|
-
`E_TRUNCATED` all have real throw sites — so the table is not uniformly unreliable, but those three
|
|
861
|
-
should be read as unimplemented intentions rather than dependable return codes.
|
|
862
|
-
* **This section previously recorded that "`E_TIMEOUT` is never thrown by either side" — that record
|
|
863
|
-
was out of date and wrong.** It is thrown in `execute.ps1` (an `execute command` timeout) and in
|
|
864
|
-
`window.ps1` (a `wait window` timeout). Measured:
|
|
865
|
-
`execute command cmd.exe /c ping -n 10 127.0.0.1 --timeout 300 --allow` returns
|
|
866
|
-
`{"ok":false,...,"code":"E_TIMEOUT",...,"hint":"captured so far: stdout=86 bytes, stderr=0 bytes"}`
|
|
867
|
-
with exit code **6**. POSIX has `E_TIMEOUT` too (`json.sh` maps it to exit code 6).
|
|
868
|
-
* **`src/client/` is an untouched Vite scaffold.** It is not wired to the host: `index.html` refers to
|
|
869
|
-
`./src/main.ts` while the real file is `./src/client/main.ts`, and there is no `node_modules`, so
|
|
870
|
-
`npm run build` and `npm run dev` are currently meaningless.
|
|
871
|
-
* **`git` is not a required dependency, but it is not present in every target environment either**, so
|
|
872
|
-
no acceptance step depends on it. (The plugin's *installation* path does: the package manager
|
|
873
|
-
resolves the repository with `git ls-remote`, so a machine where the harness cannot find `git` on
|
|
874
|
-
`PATH` cannot install the bundle.)
|
|
875
|
-
* **PowerShell 7.6 is exercised, but not across the whole surface.** The 7.6 artefact definitely runs:
|
|
876
|
-
driving the plugin's real `resolveInvocation` path with `DSH_OS_POWERSHELL_EDITION=7.6` selects
|
|
877
|
-
`bin/dsh-os7.ps1` and the runtime that answers is **PowerShell 7.6.6**, while the default stays on
|
|
878
|
-
5.1.19041.3031 — that is what `tools/verify-editions.mjs` asserts and what the 14 cases measured here
|
|
879
|
-
confirm. What has *not* been done is a run of every verb against `dsh-os7.ps1`; its fidelity as a port
|
|
880
|
-
rests on the identical 82-verb dispatch sets, a per-module function-name comparison and the window
|
|
881
|
-
group's golden cases. That is a real gap in the evidence, not a claim of full parity.
|
|
882
|
-
|
|
883
|
-
### What is not proven
|
|
884
|
-
|
|
885
|
-
* That the loader composes the bundle **by the contract `tools/verify-bundle.mjs` checks**, rather than
|
|
886
|
-
by a path that happens to work on this machine. The weaker claim — that `dsh_os` and the five
|
|
887
|
-
`os_permission_*` tools appear and are callable in an active session — has since been observed.
|
|
888
|
-
* Complete verb parity between the two platforms — in neither direction.
|
|
889
|
-
* The enforcement effect of layer A on a machine where the harness and the delegate share one identity
|
|
890
|
-
— by construction that case degrades to layer B.
|
|
891
|
-
* Anything about macOS or non-Linux POSIX beyond code review: the POSIX suites are written for a GNU
|
|
892
|
-
userland and were not run anywhere else.
|
|
893
|
-
|
|
894
|
-
---
|
|
895
|
-
|
|
896
|
-
## Tests
|
|
897
|
-
|
|
898
|
-
```console
|
|
899
|
-
npm run test:all # runs everything below, in order
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
`test:all` chains `test:conformance`, `test:encoding`, `test` (the Windows golden suite),
|
|
903
|
-
`test:plugin`, `test:permission`, `test:bundle` (`tools/verify-bundle.mjs`), `test:load`
|
|
904
|
-
(`tools/verify-plugin-load.mjs` then `tools/verify-editions.mjs`) and `test:desktop`
|
|
905
|
-
(`bash tests/smoke-desktop-sh.sh`).
|
|
906
|
-
|
|
907
|
-
| Suite | Command | Cases | Measured result |
|
|
908
|
-
|---|---|---|---|
|
|
909
|
-
| Table conformance | `powershell -File tests/conformance.ps1` | 5 | **5 passed, 0 failed** |
|
|
910
|
-
| Encoding contracts | `powershell -File tests/encoding.ps1` | 32 build modules | **all contracts hold** |
|
|
911
|
-
| Windows golden | `powershell -File tests/golden.ps1` | 50 | **50 passed, 0 failed** |
|
|
912
|
-
| POSIX golden | `bash tests/golden-sh.sh` | 116 | **116 passed, 0 failed** |
|
|
913
|
-
| POSIX desktop injection | `bash tests/smoke-desktop-sh.sh` | 52 | **52 passed, 0 failed** |
|
|
914
|
-
| Plugin | `node tests/plugin.test.mjs` | 13 | **13 passed, 0 failed** |
|
|
915
|
-
| Takeover interceptions | `node tests/takeover.test.mjs` | 41 | **41 passed, 0 failed** |
|
|
916
|
-
| Permission delegate | `node tests/permission.test.mjs` | 18 | **18 passed, 0 failed** |
|
|
917
|
-
| Workflow | `node tests/workflow.test.mjs` | 31 | **31 passed, 0 failed** |
|
|
918
|
-
| Bundle resolution | `node tools/verify-bundle.mjs` | 16 | **16 passed, 0 failed** |
|
|
919
|
-
| Package completeness | `node tools/verify-package.mjs` | 21 | **21 passed, 0 failed** |
|
|
920
|
-
| Plugin load | `node tools/verify-plugin-load.mjs` | 30 | **30 passed, 0 failed** |
|
|
921
|
-
| Edition selection | `node tools/verify-editions.mjs` | 14 | **14 passed, 0 failed** |
|
|
922
|
-
| Build drift | `powershell -File tools/build.ps1 -Check` | 3 artefacts | **no drift** |
|
|
923
|
-
| Prefix cache (`npm run measure:cache`) | `node tools/measure-cache.mjs` | 2 claims | **PASS — 96.6% mean hit over four workflow calls** |
|
|
924
|
-
|
|
925
|
-
`measure:cache` is **not** part of `test:all`: it calls the real provider, so it needs
|
|
926
|
-
`DEEPSEEK_API_KEY` (read from `$DSH_HOME/.credentials.yaml`) and it costs tokens. It exits non-zero only
|
|
927
|
-
when a measured claim does not hold.
|
|
928
|
-
|
|
929
|
-
> [!NOTE]
|
|
930
|
-
> **`test:desktop` needs `bash`, and this machine has none.** `npm run test:all` therefore ends with
|
|
931
|
-
> `'bash' is not recognized` on a Windows host without Git Bash or WSL on `PATH`. That is an environment
|
|
932
|
-
> gap in the runner, not a failing assertion: every suite above it ran and passed in the same invocation.
|
|
933
|
-
> The POSIX suites remain unverified here for the same reason they always were — they need a POSIX host.
|
|
934
|
-
|
|
935
|
-
### Conformance of the two tables
|
|
936
|
-
|
|
937
|
-
The header of `spec/verbs.tsv` had always claimed that "`tests/conformance.ps1` asserts the two
|
|
938
|
-
agree", while that file **did not exist** — it has now been **added**, so the claim is a fact. The
|
|
939
|
-
guard resolves each line of `command.txt` to a verb by the **longest word-prefix that names a known
|
|
940
|
-
verb** (so `file verify charset path charset` resolves to `file.verify.charset` and not to a
|
|
941
|
-
nonexistent `file.verify`), then checks that the two tables hold each other's verbs, that the counts
|
|
942
|
-
are equal, that the R/W class agrees, and that the positional names agree in order.
|
|
943
|
-
|
|
944
|
-
**Writing it found five real inconsistencies**, and all but one of them were in `command.txt` (which is
|
|
945
|
-
the authoritative contract):
|
|
946
|
-
|
|
947
|
-
| Verb | Was declared | What the implementation actually does |
|
|
948
|
-
|---|---|---|
|
|
949
|
-
| `file.append` | `path content charset` | charset is an **optional** third positional; the old form implied it was required |
|
|
950
|
-
| `file.insert` | `path content charset offset` | charset is `--charset` and **deliberately not** a positional — as a positional, omitting it would silently shift `offset` |
|
|
951
|
-
| `wait.ms` | no positionals | takes `ms` |
|
|
952
|
-
| `keyboard.combination` | two lines, `... down a b c d` / `... up a b c d` | it is **one** verb with a `phase` positional; the two-line form read as two verbs |
|
|
953
|
-
| `execute.command` | tsv wrote `command\|arguments` | the entire positional list is argv; there is no separate `command` |
|
|
954
|
-
|
|
955
|
-
Four were corrected in `command.txt`, and `verbs.tsv`'s `execute.command` row became `arguments...`.
|
|
956
|
-
**Nothing about the running surface changed** — only what the two documents say — which is exactly
|
|
957
|
-
what such a conformance guard is for.
|
|
958
|
-
|
|
959
|
-
`tests/encoding.ps1` guards the contracts that make everything else meaningful: the `.ps1` artefacts
|
|
960
|
-
carry a BOM, the `.sh` artefacts do not, line endings follow `.gitattributes`, `command.txt` and the
|
|
961
|
-
`.tsv` tables stay pure ASCII + LF, and **stdout never carries a BOM**.
|
|
962
|
-
|
|
963
|
-
**The "four encoding violations" recorded earlier are gone, and this is not a relaxation of the
|
|
964
|
-
guard.** The rule itself never moved; its **scope** did. It used to require **every** `*.ps1` under
|
|
965
|
-
`src/host/lib/` to be BOM-free and ASCII-only, and two of those files are the standalone `git.ps1`
|
|
966
|
-
scripts — never concatenated into anything, and carrying a BOM because that is what makes their
|
|
967
|
-
Chinese prompt literals parse under Windows PowerShell 5.1. Applying that rule to files the build
|
|
968
|
-
never reads was a **false positive**. The guard now checks only the **32 modules `tools/build.ps1`
|
|
969
|
-
actually concatenates**, and **prints which files it skipped**, so the narrowing is visible rather
|
|
970
|
-
than silent.
|
|
971
|
-
|
|
972
|
-
> [!NOTE]
|
|
973
|
-
> **Neither the "116 passed" nor the "52 passed" for the POSIX desktop verbs is evidence that they
|
|
974
|
-
> drive a real window manager.** The two suites cover the same verbs from two directions, and this
|
|
975
|
-
> machine has no X server:
|
|
976
|
-
> * `tests/golden-sh.sh` asserts the **absence** path — with no `DISPLAY`, every desktop verb must
|
|
977
|
-
> return a typed `E_NO_DISPLAY` rather than an empty success.
|
|
978
|
-
> * `tests/smoke-desktop-sh.sh` asserts the **injection** path — it puts the recording
|
|
979
|
-
> `tests/fake-xdotool` on `PATH` and sets `DISPLAY`, so the verbs run their real code and the
|
|
980
|
-
> assertions are about the **exact argument vector** handed to the tool: whether `--button right`
|
|
981
|
-
> became button `3`, whether `Enter` became the keysym `Return`, whether a name outside the table
|
|
982
|
-
> (`XF86AudioMute`) is passed through unchanged, whether `--clearmodifiers` and `--repeat` land in
|
|
983
|
-
> the right place, and whether an `xdotool` failure is reported as `E_IO` rather than a false
|
|
984
|
-
> success.
|
|
985
|
-
>
|
|
986
|
-
> **That harness is what found the two real defects in the POSIX implementation** (see
|
|
987
|
-
> [What is actually available](#what-is-actually-available) and
|
|
988
|
-
> [Installation status](#installation-status-and-known-defects)). Neither suite proves that they
|
|
989
|
-
> operate a real desktop.
|
|
990
|
-
|
|
991
|
-
> [!WARNING]
|
|
992
|
-
> **The two golden suites must be run serially.** Both implementations stage a failure envelope in a
|
|
993
|
-
> **fixed-path** temporary file — `.temporary/dsh-os.err`, with no PID and no lock — so concurrent
|
|
994
|
-
> calls overwrite each other's pending result. Running `golden.ps1` and `golden-sh.sh` in parallel was
|
|
995
|
-
> measured to report 2 **false** failures, in which the exit code contradicted the envelope's own
|
|
996
|
-
> `error.code`; run serially, both disappear. Treat this as a defect, not as flakiness: that staging
|
|
997
|
-
> path is racy by construction.
|
|
998
|
-
|
|
999
|
-
---
|
|
1000
|
-
|
|
1001
|
-
## Contributing
|
|
1002
|
-
|
|
1003
|
-
`src/host/lib/powershell5.1/*`, `src/host/lib/powershell7.6/*` and `src/host/lib/*.sh` are the source.
|
|
1004
|
-
**Never hand-edit `src/host/bin/dsh-os.ps1`, `dsh-os7.ps1` or `.sh`** — they are generated. Edit the
|
|
1005
|
-
modules, then:
|
|
1006
|
-
|
|
1007
|
-
```console
|
|
1008
|
-
powershell -File tools/build.ps1 # regenerate all three scripts
|
|
1009
|
-
powershell -File tools/build.ps1 -Check # fails as soon as a committed script drifts
|
|
1010
|
-
```
|
|
1011
|
-
|
|
1012
|
-
There are three separate artefacts rather than "one script with a runtime branch" because the two
|
|
1013
|
-
runtimes differ in ways a branch cannot paper over cleanly: .NET Framework has no
|
|
1014
|
-
`ProcessStartInfo.ArgumentList`, and .NET 10 has no reason to keep carrying the hand-rolled Windows
|
|
1015
|
-
command-line escaping this project needed in order to work around its absence.
|
|
1016
|
-
|
|
1017
|
-
`git.ps1` / `git.sh` are **not** in `tools/build.ps1`'s module lists, so they are never concatenated
|
|
1018
|
-
into any artefact; they are separate, self-contained interactive tools — see
|
|
1019
|
-
[Standalone tools](#standalone-tools).
|
|
1020
|
-
|
|
1021
|
-
`.gitattributes` is part of the contract rather than a preference: `command.txt` and the `.tsv` tables
|
|
1022
|
-
must be LF so that both implementations parse byte-identical input; `dsh-os.bat` must be CRLF and
|
|
1023
|
-
ASCII-only, or `cmd.exe` mis-parses it; and `dsh-os.ps1` must be CRLF and UTF-8 **with** a BOM, or
|
|
1024
|
-
PowerShell 5.1 reads its non-ASCII literals in the ANSI code page.
|
|
1025
|
-
|
|
1026
|
-
Two lessons learned the hard way in this repository, worth inheriting:
|
|
1027
|
-
|
|
1028
|
-
* **A security boundary derived from a path must fail closed.** A refactor once made the repository
|
|
1029
|
-
root one level too high, so no protected path matched. The failure mode was not an error but
|
|
1030
|
-
**protection silently ceasing to apply** — the protected files really were deleted.
|
|
1031
|
-
* **Temporary files live under `.temporary/`**, golden-test fixtures included.
|
|
1032
|
-
|
|
1033
|
-
Module headers still refer to a `tools/split-once.ps1` that no longer exists; the current assembler is
|
|
1034
|
-
`tools/build.ps1`.
|
|
1035
|
-
|
|
1036
|
-
---
|
|
1037
|
-
|
|
1038
|
-
## Standalone tools
|
|
1039
|
-
|
|
1040
|
-
`src/host/lib/git.sh`, `src/host/lib/powershell5.1/git.ps1` and `src/host/lib/powershell7.6/git.ps1`
|
|
1041
|
-
are interactive git runners. They are deliberately **not** part of the verb surface: they are absent
|
|
1042
|
-
from `$modulesPs1` and `$modulesSh` in `tools/build.ps1`, so they are never concatenated into
|
|
1043
|
-
`bin/dsh-os.ps1`, `bin/dsh-os7.ps1` or `bin/dsh-os.sh`, and they emit no JSON envelope. Each is a
|
|
1044
|
-
single self-contained file, byte-independent of the others, kept in step by review rather than by a
|
|
1045
|
-
generator.
|
|
1046
|
-
|
|
1047
|
-
They locate git in three phases: `GIT_EXE`/`GIT_HOME`/`PATH`, then the known install locations
|
|
1048
|
-
(including the Git-for-Windows registry keys), then a **depth- and time-bounded** walk of every
|
|
1049
|
-
mounted file system. When nothing is found they list what was searched and exit 127. Arguments reach
|
|
1050
|
-
git as separate argv elements; no shell string is ever assembled. `git add` with an empty answer
|
|
1051
|
-
(directly pressing Enter) is equivalent to `git add .`. Each of the three obeys its own runtime's
|
|
1052
|
-
encoding contract — which is precisely why `git.sh` is plain ASCII (English prompts) and the two
|
|
1053
|
-
`git.ps1` files are not.
|
|
1054
|
-
|
|
1055
|
-
---
|
|
1056
|
-
|
|
1057
|
-
## Licence
|
|
1058
|
-
|
|
1059
|
-
**MIT**, declared in `package.json` (`"license": "MIT"`, author Eloy Kim) and distributed with the
|
|
1060
|
-
repository as a separate [`LICENSE`](LICENSE) file. Fully open source: the whole verb surface, the
|
|
1061
|
-
specification documents and the test suites are all in the repository. No telemetry, no account, no
|
|
1062
|
-
network access at run time.
|
|
1063
|
-
|
|
1064
|
-
```
|
|
1065
|
-
MIT License
|
|
1066
|
-
|
|
1067
|
-
Copyright (c) 2026 Eloy Kim
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
The licence file is pure ASCII, LF-terminated and BOM-free, consistent with the rest of the
|
|
1071
|
-
repository's encoding contracts. An earlier description here claimed it was **not** distributed because it
|
|
1072
|
-
is not listed in `package.json`'s `files` whitelist. That was wrong, and the published tarball proves it:
|
|
1073
|
-
`npm pack deepseek-coordination@0.1.5` contains `package/LICENSE` **and** `package/README.zh-CN.md`, because
|
|
1074
|
-
npm always includes those two names regardless of `files`. The whitelist has 20 entries and includes
|
|
1075
|
-
`src/host/bin/dsh-os7.ps1`; the two files simply do not need to be in it.
|
|
1076
|
-
|
|
1077
|
-
The verb contract and the specification documents are part of the source: a behaviour change should be
|
|
1078
|
-
reflected in `src/host/spec/verbs.tsv`, `src/host/command.txt` and `src/host/docs/os-spec.md`, not only
|
|
1079
|
-
in code.
|
|
1080
|
-
|
|
1081
|
-
One further note: the specification documents (`src/host/docs/os-spec.md`,
|
|
1082
|
-
`src/host/docs/permission-model.md`, `src/host/command.txt`) are written in **Chinese**, while this
|
|
1083
|
-
README and the verb table are in English.
|
|
1
|
+
# deepseek-coordination
|
|
2
|
+
|
|
3
|
+
**English** · [简体中文](README.zh-CN.md) · [Changelog](CHANGELOG.md) · [中文更新日志](CHANGELOG.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
A dual-face Cordis plugin for DeepSeek Harness: the host half owns a coordination SQLite database, seven model-facing tools and an HTTP surface; the web half mounts five composer dropdowns, a settings catalog and the service-level takeover dialog.
|
|
6
|
+
|
|
7
|
+
**Status: v0.10.0 (2026-10-18).** This release follows the v0.9.2 tooltip fix and lands the settings catalog, the durable file layer, the turn pipeline, phase freezes and the data-structure adoption pass. The package version, changelog and release tarball are the same tree; claims below were checked against source, not against the design documents alone.
|
|
8
|
+
|
|
9
|
+
| Status | Meaning in this README |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **Wired** | Registered by `src/index.ts` or called by the composed service, routes or tools. |
|
|
12
|
+
| **Library-only** | Implemented and tested as a module, but not driven by the current turn lifecycle or exposed through a route/tool. |
|
|
13
|
+
| **Design/planned** | Documented or declared in config/schema, with no runtime consumer yet. |
|
|
14
|
+
|
|
15
|
+
## What it is
|
|
16
|
+
|
|
17
|
+
| Piece | Location | Role |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Bundle | `cordis.patch.yml`, `dsh.bundle` in `package.json` | Inserts one `deepseek-coordination` row into a DSH profile roster. |
|
|
20
|
+
| Host half | `lib/index.mjs` (ESM, Node) | `CoordinationService`, the `coordination` SQLite database, 7 tools, 14 route paths and the agent hooks. |
|
|
21
|
+
| Client half | `lib/client.js` (CJS, browser, `__ModuleLoader__` wrapper) | React 18 + Material UI 9: composer dropdowns, settings page, takeover dialog. |
|
|
22
|
+
| Database | `%DSH_HOME%\coordination\coordination.db` by default | Plugin-owned SQLite file; 63 tables, 7 triggers, 63 indexes, schema version 24. Never DSH's `harness` database. |
|
|
23
|
+
|
|
24
|
+
The plugin does not replace DSH's agent loop. It records coordination state, locks one profession per task, exposes the composer selection and file metadata, asks DSH to compact when context pressure crosses a threshold, and relays approvals/questions to a human through its own dialog.
|
|
25
|
+
|
|
26
|
+
The five composer controls are **权限 (permission), 职能 (profession), 交互 (interaction), 情景 (scene), 验证 (validation)**. The command face (`/goal`, `/plan`, `/feedback`, `/compact`, `/permission`) is defined as a message prefix, not a sixth dropdown.
|
|
27
|
+
|
|
28
|
+
## Install and build
|
|
29
|
+
|
|
30
|
+
| Requirement | Detail |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Node.js | `package.json` declares `>=18`, but the host imports `node:sqlite`; use a Node build that ships it. Verified on Node 24.21.0. |
|
|
33
|
+
| npm | npm 11 (bundled with Node 24) installs from `package-lock.json` and drives the scripts and the release workflow. |
|
|
34
|
+
| DSH | A DSH installation with the `dsh` CLI and a web-capable profile (`@deepseek-ai/dsh-base` + `@deepseek-ai/dsh-web-app`). |
|
|
35
|
+
| Peer packages | See the table below; they come from the DSH installation, not from this package. |
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
cd deepseek-coordination
|
|
39
|
+
npm ci # or `npm install` in a fresh checkout
|
|
40
|
+
npm run typecheck # tsc -p tsconfig.json --noEmit
|
|
41
|
+
npm test # tsdown test build + node --test
|
|
42
|
+
npm run build # lib/index.mjs, lib/index.d.mts, lib/client.js
|
|
43
|
+
npm pack # prepack builds first; produces the installable tarball
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`npm run test:tsc` type-checks the test tree. `npm run pack:check` validates the packed runtime files and syntax-checks the host entry. `package-lock.json` is committed; `npm ci` reproduces it exactly.
|
|
47
|
+
|
|
48
|
+
| Peer package | Range | Side |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `@deepseek-ai/cordis` | `^4.0.1` | Host runtime |
|
|
51
|
+
| `@deepseek-ai/dsh-host-webserver` | `^0.1.0-rc.7 \|\| ^0.2.0-rc.0` | Host route registration |
|
|
52
|
+
| `@deepseek-ai/dsh-client-runtime`, `-locale`, `-ui-slots`, `-ui-settings` | `^0.1.0-rc.7 \|\| ^0.2.0-rc.0` | Browser module table |
|
|
53
|
+
| `react` | `^18.2.0` | Browser module table; never bundled |
|
|
54
|
+
|
|
55
|
+
### Install into a profile
|
|
56
|
+
|
|
57
|
+
Install the packed tarball, not the GitHub source:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
npm pack
|
|
61
|
+
dsh plugin --profile <name> add .\deepseek-coordination-0.10.0.tgz
|
|
62
|
+
dsh --profile <name> --dump-config # expect a deepseek-coordination layer
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Raw `github:eloyhere/deepseek-coordination` installs are unsupported.** `lib/` is gitignored, and the bundle is only built by `prepack` (or an explicit `npm run build`), which npm does not run for a git dependency; the installed package then fails at activation with `ERR_MODULE_NOT_FOUND` for `lib/index.mjs`. The tag-triggered GitHub Actions workflow (`.github/workflows/release.yml`) builds and attaches the verified tarball to the release; install that artifact.
|
|
66
|
+
|
|
67
|
+
**npm latest is still the old `0.1.9` dsh-os line.** Do not install by package name yet. A local checkout can be linked with `dsh plugin --profile <name> add .` after `npm run build`.
|
|
68
|
+
|
|
69
|
+
`dsh plugin add` writes the dependency and the bundle row. If the profile still records a different package spec, replace it rather than assuming the new code loaded; check `dsh --profile <name> --dump-config` and the installed `package.json` version.
|
|
70
|
+
|
|
71
|
+
| Artifact | Format | Size in this build |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `lib/index.mjs` | Host ESM, `platform: node` | 940,726 B |
|
|
74
|
+
| `lib/index.d.mts` | Host declarations | 9,095 B |
|
|
75
|
+
| `lib/client.js` | Browser CJS wrapped in `__ModuleLoader__.load()` | 1,170,887 B |
|
|
76
|
+
|
|
77
|
+
Material UI, emotion and `react-dom` are bundled into `lib/client.js`; React itself comes from the host module table. The inlined `react-dom` is the 18.3.1 line that matches the host's React 18 — npm refuses to auto-install a React 19 `react-dom` against a React 18 `react`. The client bundle is large because MUI and `react-dom` are inlined, not because it ships a second React.
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
Host half (`src/index.ts`):
|
|
82
|
+
|
|
83
|
+
- exports `name`, `inject = ['webServer', 'tools']` and `Config`; `apply()` resolves config, creates the service, recovers interrupted turns and installs every effect through `ctx.effect`.
|
|
84
|
+
- `CoordinationService` (`src/host/coordination.ts`) is the Cordis `coordination` service. It owns the database, module host and the composed modules: session, selection, turn, profession, plan, goal, compact, directive, permission, takeover/gate, provider, cache and command.
|
|
85
|
+
- SQLite is opened with `node:sqlite` (`DatabaseSync`), foreign keys, a 5 s busy timeout and WAL outside `:memory:`. Triggers refuse changing a locked profession and make `orchestration_trace` append-only.
|
|
86
|
+
- The `/deepseek-coordination` prefix route answers JSON `{ ok, value | error }`; request bodies are capped at 1 MiB and unknown paths return 404 with the route envelope.
|
|
87
|
+
- Seven tools are registered through `ctx.tools`; the three composer tools are re-registered after `POST /locale`, because their descriptions are part of the model-facing prompt.
|
|
88
|
+
- Hooks: `agent/pre-step` opens the coordination turn and runs the compaction governor; `agent/turn-stopping` closes it as `done`; `agent/error` closes it as failed; `approval/request` and `user-questions/request` feed the takeover gate. A 60 s timer deletes expired lock rows.
|
|
89
|
+
|
|
90
|
+
Client half (`src/client/index.ts`):
|
|
91
|
+
|
|
92
|
+
- `inject = ['slots', 'locale']`; `package.json`'s `dsh.client.inject` separately lists the four module-table packages.
|
|
93
|
+
- `settings.section` (order 50) mounts `CoordinationSettings`; `conversation.input.left` (order 40) mounts the composer controls and the takeover dialog as siblings.
|
|
94
|
+
- Language follows `ctx.locale.getLocale().active`, normalises `zh*`/`en*` onto `zh-CN`/`en-US`, and reports changes to `POST /locale`. The host rebuilds localized tool descriptions from that report.
|
|
95
|
+
- The client bridge uses same-origin `fetch` with a 3 s selection timeout and 2 s takeover/locale timeouts. Unreachable hosts fall back to a per-session `localStorage` copy for the five choices; a permission preset is never written offline.
|
|
96
|
+
- The composer components are memoised on `sessionId` only, because the slot also passes the composer input on every keystroke; re-rendering this row would make emotion touch `<head>` and close DSH's popover.
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
deepseek-coordination/
|
|
100
|
+
├─ src/
|
|
101
|
+
│ ├─ index.ts # host composition root: service, routes, tools, hooks
|
|
102
|
+
│ ├─ host/
|
|
103
|
+
│ │ ├─ coordination.ts # CoordinationService (Cordis service "coordination")
|
|
104
|
+
│ │ ├─ config.ts # Schemastery schema + resolveConfig()
|
|
105
|
+
│ │ ├─ database.ts # SQLite schema (24), 63 tables, triggers
|
|
106
|
+
│ │ ├─ dsh.ts # narrow DSH ports + turn lifecycle
|
|
107
|
+
│ │ ├─ tools.ts # 7 model-facing tools
|
|
108
|
+
│ │ ├─ chain.ts # bounded Chain projection engine
|
|
109
|
+
│ │ ├─ node-registry.ts # NodeCard registry and describe()
|
|
110
|
+
│ │ ├─ parse-service.ts # workspace-bound parse + read service
|
|
111
|
+
│ │ ├─ token.ts # TokenGovernor and pure policy
|
|
112
|
+
│ │ ├─ takeover-gate.ts # approval/question waterfall gate
|
|
113
|
+
│ │ ├─ selection.ts turn.ts profession.ts … # selection, lock, lifecycle
|
|
114
|
+
│ │ ├─ bayesian.ts comprehension.ts organization.ts … # library engines
|
|
115
|
+
│ │ └─ text/ # P0 parsers: JS/TS, CSS/HTML/MD, TeX, formulas, graph
|
|
116
|
+
│ ├─ client/
|
|
117
|
+
│ │ ├─ index.ts # slots, locale store, nav icon, takeover CSS
|
|
118
|
+
│ │ ├─ controls.ts # five composer dropdowns, tooltips, offline replay
|
|
119
|
+
│ │ ├─ settings.ts # 13-group, 151-control settings catalog
|
|
120
|
+
│ │ ├─ takeover.ts # approval/question dialog
|
|
121
|
+
│ │ ├─ bridge.ts store.ts i18n.ts theme.ts selection-cache.ts
|
|
122
|
+
│ └─ shared/ # entity/enum/selection/i18n/error + data structures
|
|
123
|
+
├─ test/ # 24 *.test.ts + helpers.ts
|
|
124
|
+
├─ lib/ # build output; gitignored, never edited by hand
|
|
125
|
+
├─ cordis.patch.yml # bundle row inserted into a profile
|
|
126
|
+
├─ scripts/check-pack.mjs # tarball runtime-file verification
|
|
127
|
+
└─ .github/workflows/release.yml
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Composer controls
|
|
131
|
+
|
|
132
|
+
| Control | Options | What it does |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| 权限 Permission | DSH preset catalog, or a neutral “follow DSH” state | Writes the chosen preset to DSH immediately through `ctx.permissionPresets`; never persists it in the coordination DB. Only shown while the local 权限接管 switch is on. |
|
|
135
|
+
| 职能 Profession | decision / outcome / resource / personnel / qualification / execution | Locked for the active turn. Selecting another value is refused with `PROFESSION_LOCKED` and opens the rebuild/abandon dialog. |
|
|
136
|
+
| 交互 Interaction | emotion / think / heuristic / converge / orchestrate | Stored for the next turn; frozen while the active turn has a non-cleared goal. |
|
|
137
|
+
| 情景 Scene | dialogue / writing / office / research / engineering / iterate | Stored for the next turn. `writing` sits between `dialogue` and `office`; the scene policy is read by the turn pipeline. |
|
|
138
|
+
| 验证 Validation | low / medium / high | Stored for the next turn; frozen while the active turn has a draft/scored/active plan. All three levels search. |
|
|
139
|
+
|
|
140
|
+
- Every control and every option has a tooltip. A single-owner `tooltipVisibility` store ensures only one composer tooltip is visible; placement is left/right, exit is a 160 ms fade, and the fade popper is pointer-transparent.
|
|
141
|
+
- Clicking a dropdown opens its menu on the first click; there is no state change on pointerdown/mousedown that would swallow the Select click.
|
|
142
|
+
- A locked profession trigger is disabled and carries an accessible name with the lock explanation; option `MenuItem`s carry the label plus hint for keyboard and screen-reader users.
|
|
143
|
+
- Phase freezes are re-derived from the host on every poll: turn → profession, plan → validation, goal → interaction. The select is disabled but stays focusable, its aria-label and tooltip name when the freeze lifts, and the server refuses a changed value with `INVALID_STATE` (replaying the stored value is allowed).
|
|
144
|
+
- Tooltips obey the host's `ui.showTooltip` gate; the local “show hints” preference can only narrow them, never resurrect a host-disabled tooltip.
|
|
145
|
+
- Saves are optimistic and debounced by 120 ms. A rejected save reverts to the last host-confirmed state; offline edits are replayed when the host returns. A full-access preset needs an explicit second confirmation.
|
|
146
|
+
- While the host state is loading, selects stay disabled and show a loading line. A missing preset catalog is shown as a missing catalog, not invented.
|
|
147
|
+
|
|
148
|
+
Validation budgets are policy, not a claim that the whole pipeline runs today:
|
|
149
|
+
|
|
150
|
+
| Level | Queries × results | Evidence scope | Interaction scope |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| low | 1 × 3 | Current turn only | Selected interaction |
|
|
153
|
+
| medium | 2 × 5 | Current session | Selected interaction |
|
|
154
|
+
| high | 4 × 8 | All local content | All interactions |
|
|
155
|
+
|
|
156
|
+
The budgets live in `src/host/comprehension.ts` and the shared `ValidationBudgetByLevel` table. `ComprehensionModule` is library-only and is not driven by the current `CoordinationService` turn lifecycle, so the composer persists the level without executing these searches yet.
|
|
157
|
+
|
|
158
|
+
### Scenes and the writing profile
|
|
159
|
+
|
|
160
|
+
| Scene | Thinking / effort | Plan | Outline | Revision | Score | Bash | Web | Max iterations |
|
|
161
|
+
|---|---|---|---|---|---|---|---|---|
|
|
162
|
+
| dialogue | disabled / medium | – | – | – | – | no | yes | 1 |
|
|
163
|
+
| writing | enabled / high | yes | yes | yes | yes | no | yes | 3 |
|
|
164
|
+
| office | disabled / medium | yes | – | – | – | no | yes | 1 |
|
|
165
|
+
| research | enabled / high | yes | – | – | – | no | yes | 3 |
|
|
166
|
+
| engineering | enabled / high | yes | – | – | – | yes | yes | 3 |
|
|
167
|
+
| iterate | enabled / high | – | – | – | yes | no | yes | 3 |
|
|
168
|
+
|
|
169
|
+
`writing` requires an outline and at least one revision round, requires a quality score, disallows bash, allows web search and caps the loop at three iterations. The pass threshold is `0.6`; `plan.ts` uses that writing threshold. The turn pipeline reads reasoning effort, the require-* flags, citation/evidence, `allowWeb` and `sandboxOnly`; `thinking`, `allowBash`, `maxIteration` and non-writing thresholds remain deferred diagnostics because no DSH port or consumer exists yet.
|
|
170
|
+
|
|
171
|
+
## Profession lock and turn lifecycle
|
|
172
|
+
|
|
173
|
+
- One task has exactly one profession. `turn.create` writes `profession NOT NULL`, `locked = 1` and one profession record plus one `lock` trace event.
|
|
174
|
+
- While a non-terminal turn exists, `SelectionModule.set` refuses a different profession with `ProfessionLockedError`; the HTTP layer answers 409 `PROFESSION_LOCKED`. The selection is not mutated.
|
|
175
|
+
- `POST /profession/rebuild` ends the active task as `abandoned`, creates a replacement turn with a new ordinal and profession, copies the prompt/command, and emits `rebuild` + `lock` trace events. It requires an active turn.
|
|
176
|
+
- `POST /profession/abandon` closes the active task without a replacement; the next task may choose again. Both reselect flags are forced true by `resolveConfig`.
|
|
177
|
+
- SQLite repeats the rule: `trg_turn_profession_lock`, `trg_profession_record_lock`, `trg_subtask_profession_lock` abort a profession change while locked; `trg_turn_profession_unlock` and `trg_subtask_profession_unlock` refuse clearing the flag.
|
|
178
|
+
|
|
179
|
+
| Hook | Effect |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `agent/pre-step` | Ensure the coordination session, open (or reuse) one Turn for the DSH turn; also runs the token governor before `next()`. |
|
|
182
|
+
| `agent/turn-stopping` | Close the Turn as `done`; the profession unlocks for the next turn. |
|
|
183
|
+
| `agent/error` | Close the Turn as `failed`. |
|
|
184
|
+
| Host start | `recoverInterrupted()` marks every non-terminal Turn failed with reason `turn was interrupted by a host restart`. |
|
|
185
|
+
| Every 60 s | Delete ended locks older than `defaultLease`. |
|
|
186
|
+
|
|
187
|
+
Terminal turn states (`done`, `failed`, `cancelled`, `abandoned`) are irreversible. The turn hook records turn metadata and an empty prompt row; it does not mirror the DSH conversation into the coordination database.
|
|
188
|
+
|
|
189
|
+
## Settings and configuration
|
|
190
|
+
|
|
191
|
+
The settings page is driven by `SETTINGS_CONTROLS` in `src/shared/selection.ts`: **151 controls in 13 groups, 134 writable host rows, covering 155 non-secret config leaves**. The 18 top-level config keys include `database` and `databasePath`, which are deliberately not projected or writable.
|
|
192
|
+
|
|
193
|
+
| Group | Defaults / scope | Write path |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `session` | `maxTurnPerSession=1000`, `defaultLease=30000 ms`, `clarifyTimeout=30000 ms` | Host rows through `/config` |
|
|
196
|
+
| `profession` | enabled/lock/rebuild/abandon forced `true`; default mirrors `defaults.profession`; audit/errorHint `true` | Forced flags read-only; audit/errorHint host rows (audit has no consumer) |
|
|
197
|
+
| `plan` | threshold `0.5`, maxRebuild `3`, 8 weights summing to 1, 8 dimension thresholds | Host rows; weights re-normalise on save |
|
|
198
|
+
| `orchestration` | maxReject `2`, maxSubTask `32`, groundTruth `true`, timeout `5000 ms`, shell ground truth `false`, modelRouting `true` | Host rows; `groundTruthTimeout` has no consumer |
|
|
199
|
+
| `cache` | enabled `true`, ttl `60000 ms`, fingerprintWindow `50`, effortWarning `true` | Host rows; `fingerprintWindow` has no consumer |
|
|
200
|
+
| `bayesian` | prior `0.5`, stopEntropy `0.05`, infoGain `0.01`, maxRoutes `12`, calibrationFloor `0.1` | Host rows; `calibrationFloor` has no consumer |
|
|
201
|
+
| `comprehension` | stageTimeout `30000 ms`, searchEnabled `true`, maxEvidence `8`, highHistoryWindow `50` | Host rows |
|
|
202
|
+
| `compaction` | auto `true`, soft `0.70`, hard `0.88`, reserve `2048`, recentTurns `6`, cooldown `30000 ms`, maxPerSession `20` | auto/soft/hard host rows; structural fields read-only |
|
|
203
|
+
| `ui` | toolbar `true` (deprecated alias), showTooltip `true`, takeover `false`, refreshInterval `5000 ms`, compact `true`, locale `auto` | Host rows plus the two local switches; toolbar/locale read-only |
|
|
204
|
+
| `scenes` | six scene profiles, 13 fields each; writing threshold `0.6` | Host rows; only the writing score threshold is read by `plan.ts` |
|
|
205
|
+
| `parser` | enabled `true`, 1 MiB, 200k tokens, depth `256`, 2 s, cache `256`, graph 20k/100k | enabled + four limits host rows; depth and graph caps planned |
|
|
206
|
+
| `chain` | depth `8`, links `50`, tokenBudget `1500` | Host rows |
|
|
207
|
+
| `tools` | maxResultBytes `0` | Planned/read-only; not consumed |
|
|
208
|
+
| `defaults` | profession `execution`, interaction `think`, scene `dialogue`, validation `medium`, command `none` | Host rows |
|
|
209
|
+
|
|
210
|
+
`GET /deepseek-coordination/config` returns `{ values, defaults }`; `defaults` is `resolveConfig(undefined)`, so the page never duplicates a default literal. `POST /config` accepts a flat dotted patch such as `{ "plan.threshold": 0.7 }`, validates it against the catalog and the real Schemastery schema, applies it to the live config, re-resolves invariants (weight normalisation, alias mirror, forced flags, compaction clamps), rebuilds `TokenGovernor` when compaction changes, and best-effort persists through `ctx.settings.update`.
|
|
211
|
+
|
|
212
|
+
Persistence is explicit: `persisted: true` only when DSH accepted the settings write; otherwise the change is live for this process and the page says so. Unknown keys, `databasePath` and `database` are rejected with `INVALID_ARGUMENT`. Known but non-editable keys are reported in `ignored` and never written. `POST /config/compaction` remains as a small compatibility route for the old auto/soft/hard patch.
|
|
213
|
+
|
|
214
|
+
The page is Material UI end to end: one default-expanded `Accordion` per group, a `List` of rows, six scene tables (no scene dropdown), `Checkbox` for booleans, `RadioGroup` for enums, `Slider` for ratio-like numbers, `TextField type="number"` for counts/bytes/milliseconds, and `Chip` only for the `主机 / 本机 / 强制 / 只读 / 计划中` status plus connection state. There is no save button; controls apply on change/blur. A forced row draws a disabled control, a read-only row shows a value, and a planned row says the runtime does not read it yet. The settings nav icon is a host-shell workaround: the shell accepts only `{ id, order, label }` for a section, so the plugin decorates its nav button with a CSS mask of the Material `sync` glyph.
|
|
215
|
+
|
|
216
|
+
## Service-level takeover
|
|
217
|
+
|
|
218
|
+
- With `ui.takeover` on, `TakeoverGate` listens to DSH's `approval/request` and `user-questions/request` waterfalls. It records a durable pending row, emits `takeover/pending`, and waits for a human answer delivered through the client routes.
|
|
219
|
+
- A timeout, an aborted request signal, an explicit cancel, or plugin unload calls `next()`; the gate never turns “nobody answered” into `allowed-once` or a fabricated answer. `unavailable` is the host's fail-closed outcome and is rejected as a user decision.
|
|
220
|
+
- The dialog polls `/takeover/pending` every 1 s only while the local 权限接管 switch is on, the session id is real and the document is visible. The first pending request is rendered; a `1 / N` badge appears for a queue.
|
|
221
|
+
- Approval offers 仅本次允许 / 拒绝 / 取消; only `allowed-once` and `rejected` are submitted, while cancel/Esc/backdrop use the cancel route. Questions render radios for single-select, checkboxes for multi-select and optional free text; submit stays disabled until every question is answered.
|
|
222
|
+
- **Both switches are needed**: host `ui.takeover` (default `false`) opens the gate; the local 权限接管 switch makes the dialog poll. With the host switch off, the gate delegates immediately and there is nothing to show.
|
|
223
|
+
|
|
224
|
+
## Auto-compaction and the token governor
|
|
225
|
+
|
|
226
|
+
`TokenGovernor` estimates context (CJK-aware O(n) estimator with a bounded 256-entry memo), plans a budget against `contextWindow - reserve`, and asks DSH's `compactIfNeeded` at `agent/pre-step` when utilisation crosses a threshold. Defaults: soft `0.70`, hard `0.88`, reserve `max(2048, 8% of window)` capped at 25%, recentTurns `6`, cooldown `30 s`, `maxPerSession 20`.
|
|
227
|
+
|
|
228
|
+
The soft threshold is editable as a 0.30–0.90 ratio; the hard threshold as 0.30–0.98 and strictly above soft. The old 60/70/80/88% values remain valid choices (`0.60`, `0.70`, `0.80`, `0.88`) even though the catalog now uses a slider rather than a four-item picker. Auto compaction is disabled by `compaction.auto=false`, but an explicit compact command is not governed by that switch.
|
|
229
|
+
|
|
230
|
+
Fail-safe rules are deliberate: the hook never blocks the step; a missing compaction port, `null`/`undefined` result, timeout, throw, spent per-session allowance or unelapsed cooldown all become a no-op, and the agent proceeds to `next()`. The governor snaps the policy at construction, so a live settings change also rebuilds it. DSH owns the actual safe range and replacement; this plugin only decides when it is worth asking.
|
|
231
|
+
|
|
232
|
+
## File tools, parser and Chain
|
|
233
|
+
|
|
234
|
+
The file layer is wired as four model-facing tools, alongside the three composer tools:
|
|
235
|
+
|
|
236
|
+
| Tool | Purpose |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `coordination_parse` | Parse a workspace file or supplied text. Exactly one of `path`/`text`; returns references, diagnostics, charset, truncation and node cards, never the file body. |
|
|
239
|
+
| `coordination_describe` | Read one `NodeCard` and optionally `summary`/`description`/`usage`/`purpose` by handle; never the body. |
|
|
240
|
+
| `coordination_chain` | Query one bounded relation chain: dependencies, dependents, satisfies, satisfied_by, baseline, prerequisites, unmet, path or cycle. Depth ≤ 8, links ≤ 50, ~1,500 tokens; never an AST or graph object. |
|
|
241
|
+
| `coordination_read` | The only tool that returns file content. Reads a byte range (end exclusive) or a line range (1-based inclusive), capped at 256 KiB / 2,000 lines / 16k tokens per call. |
|
|
242
|
+
|
|
243
|
+
P0 parsers are lexical and deterministic: JS/TS/JSX (imports, exports, `require`, dynamic import, symbols, comments), CSS/HTML/Markdown (`@import`/`url()`, script/link/img, links/images/headings/front matter/fenced divs/formulas), TeX (theorem-like environments, proofs, labels/refs, math, `\input`/`\include`), generic formulas (Markdown/TeX/spreadsheet/MathML/OMML/unknown; no evaluation), and a lexical dependency graph (external filtering, topological order, cycles, levels, isolated vertices). Malformed input produces diagnostics and `truncated`, not a crash.
|
|
244
|
+
|
|
245
|
+
Every parsed node is **blueprint-only**: `path`, UTF-8 `byteStart`/`byteEnd`, 1-based `lineStart`/`lineEnd`, `sha256:<hex>` fingerprint and `charset`; no `text`, `body`, `content` or AST. The same holds for `NodeCard` and `ChainLink`. `coordination_read` is the single content path; it rejects absolute paths, UNC paths, traversal and symlink escapes, refuses NUL/binary/undecodable content, and hashes what it returns.
|
|
246
|
+
|
|
247
|
+
| Limit | Value |
|
|
248
|
+
|---|---|
|
|
249
|
+
| Source text per parse | 1 MiB; prefix parsed and marked `truncated` |
|
|
250
|
+
| Tokens per parse | 200,000 |
|
|
251
|
+
| AST depth | 256 |
|
|
252
|
+
| Parse wall clock | 2,000 ms |
|
|
253
|
+
| Diagnostics retained | 100 |
|
|
254
|
+
| Node metadata per parse | 20,000 |
|
|
255
|
+
| Dependency graph | 20,000 vertices / 100,000 edges, fail-closed |
|
|
256
|
+
| Formulas | 5,000 per scan / 512 references |
|
|
257
|
+
| Parse cache | 256 files, LRU keyed `path+mtime+size+parserId` |
|
|
258
|
+
| Read range | 256 KiB / 2,000 lines / 16,000 estimated tokens |
|
|
259
|
+
| Chain projection | depth ≤ 8 / links ≤ 50 / ~1,500 tokens; 50k nodes / 200k edges fail-closed |
|
|
260
|
+
|
|
261
|
+
`parser.enabled` is read when the file tools are registered; parser limits and `chain.depth`/`links`/`tokenBudget` are captured when the lazy tool runtime creates its `ParseService`/`ChainEngine`. A live change through `/config` therefore does not retune an already-created runtime; it takes effect on the next registration (today a locale change or restart). The workspace root falls back to `process.cwd()` because the plugin entry does not receive DSH's workspace root; a host whose cwd is not the workspace must pass a root through tool options.
|
|
262
|
+
|
|
263
|
+
**No write or refresh tool exists.** The v24 write contract needs `expectedFingerprint` conflict detection, transactional reparse and range recomputation, explicit charset/BOM changes and a watcher/rename policy; none is implemented, so a writer would risk overwriting concurrent edits or returning stale ranges. `coordination_write` and `coordination_refresh` are design/planned.
|
|
264
|
+
|
|
265
|
+
## Data structures
|
|
266
|
+
|
|
267
|
+
The shared library in `src/shared/` is pure ES2022: hash/LRU, nodes, binary/B+ and red-black trees, heaps, stack/deque/queue, ordered maps/sets/skip lists, AST/token stream, directed graph, trie and sparse grid. The current runtime uses:
|
|
268
|
+
|
|
269
|
+
- `LruList` for the token-estimator memo, parse cache, node registry and chain projection cache.
|
|
270
|
+
- `BinaryHeap` for `selectContext`, preserving the old total order (value desc → created asc → id byte-asc → input index).
|
|
271
|
+
- `RedBlackTree` predecessor/successor for numeric agreement; the exact argmax proof is in the adoption document.
|
|
272
|
+
- `OrderedSet` for first-seen dedupe in comprehension/emotion/permission; `Queue` for the permission-command token stream.
|
|
273
|
+
- `DirectedGraph` for the dependency graph and one graph per chain relation.
|
|
274
|
+
|
|
275
|
+
The second adoption pass is measured and randomized-tested. Its honest caveat: on the 20k-chunk **wide** synthetic case the heap is not faster than V8 sort (17.7 ms vs 15.9 ms); it wins when the budget fills early and is never worse in result. `SparseGrid` and `BPlusTree` are regression-tested in the shared library but **not wired into a runtime path**; do not infer they are used.
|
|
276
|
+
|
|
277
|
+
## Tools and routes
|
|
278
|
+
|
|
279
|
+
| Tool | What it returns |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `coordination_selection` | Read or patch the five selections for a session (including `command` and DSH `preset`); returns `SelectionState` with lock, presets and config projection. |
|
|
282
|
+
| `coordination_profession` | Read the current profession record; set a profession only before/without an active task. A locked task returns `PROFESSION_LOCKED` and leaves the selection unchanged. |
|
|
283
|
+
| `coordination_professionlock` | Read the lock state/profession of the active turn, or of a given turn ordinal. |
|
|
284
|
+
| `coordination_parse` | Body-free parse projection (references, diagnostics, charset, truncation, cards). |
|
|
285
|
+
| `coordination_describe` | Body-free `NodeCard` and requested metadata fields. |
|
|
286
|
+
| `coordination_chain` | Bounded `ChainProjection` only. |
|
|
287
|
+
| `coordination_read` | The only content-returning tool; capped byte/line range. |
|
|
288
|
+
|
|
289
|
+
All routes live under `/deepseek-coordination`; there are **14 paths / 16 method handlers**:
|
|
290
|
+
|
|
291
|
+
| Method | Path | Purpose |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| GET | `/ping` | Liveness; returns plugin name, `pong`, current locale. |
|
|
294
|
+
| POST | `/echo` | Echo a JSON body (diagnostic). |
|
|
295
|
+
| GET, POST | `/selection` | Read `SelectionState`; patch the five choices. |
|
|
296
|
+
| GET | `/presets` | DSH permission-preset catalog projected for the dropdown. |
|
|
297
|
+
| POST | `/permission` | Apply one DSH preset immediately. |
|
|
298
|
+
| GET, POST | `/config` | Read `{ values, defaults }`; apply a flat config patch. |
|
|
299
|
+
| POST | `/config/compaction` | Compatibility patch for `auto`/`soft`/`hard`. |
|
|
300
|
+
| GET | `/takeover/pending` | Pending approval/question windows for a session. |
|
|
301
|
+
| POST | `/takeover/answer` | Deliver a validated human answer; invalid answers keep the window open. |
|
|
302
|
+
| POST | `/takeover/cancel` | Cancel a window and delegate to the host default. |
|
|
303
|
+
| POST | `/profession/rebuild` | End the active task and create a replacement with a new profession. |
|
|
304
|
+
| POST | `/profession/abandon` | End the active task without a replacement. |
|
|
305
|
+
| POST | `/locale` | Report the client language; re-registers localized tool descriptions. |
|
|
306
|
+
| GET | `/state` | Selection plus coordination metrics (`session`, `turn`, `plan`, `subtask`, `trace`, `takeover`, `schema`). |
|
|
307
|
+
|
|
308
|
+
There is no `coordination_write`, `coordination_refresh`, `outline`, `findSymbol`, `searchSummary` or `commentsOf` tool. The four file tools carry inline EN/zh-CN copy because the shared `ToolName` table has no keys for them yet; the three composer tools use the shared copy table.
|
|
309
|
+
|
|
310
|
+
## Data, privacy and safety
|
|
311
|
+
|
|
312
|
+
- The plugin owns an independent SQLite database. It never opens DSH's `harness` database. The default path is `%DSH_HOME%\coordination\coordination.db`; `database`/`databasePath` are deployment-only and are neither projected nor writable from `/config`.
|
|
313
|
+
- Parser nodes, `NodeCard`s, `ChainLink`s and their tool results never carry file bodies, attachments or ASTs. Only `coordination_read` returns text, and only for the requested range.
|
|
314
|
+
- File paths are workspace-relative; absolute paths, drive-rooted paths, UNC paths, `..` escapes and symlink escapes are rejected before a read. Binary/NUL/undecodable content is refused rather than guessed.
|
|
315
|
+
- The turn hook records turn metadata and an empty prompt row; it does not mirror the DSH transcript. No file content is copied into the coordination database.
|
|
316
|
+
- Permission presets are written to DSH immediately (full access needs a second confirmation) and are never stored in the offline localStorage copy. Offline replay covers only the five stored selection fields.
|
|
317
|
+
- Takeover never auto-approves. Timeout, abort, cancel and unload delegate with `next()`; only a human decision delivered through the answer route settles a window.
|
|
318
|
+
- Fail-closed behaviour is explicit: invalid takeover answers leave the window open; `PROFESSION_LOCKED` leaves the selection intact; parser uncertainty is `unverifiable`; graph/parse overflow is `truncated`; compaction failure is a no-op; chain `unknown` never auto-passes.
|
|
319
|
+
- The coordination DB stores task/audit rows (including `prompt` and trace rows), not credentials or provider secrets. The config projection contains no DSH credential, token or provider key.
|
|
320
|
+
|
|
321
|
+
## Testing, quality and limitations
|
|
322
|
+
|
|
323
|
+
| Check | Result |
|
|
324
|
+
|---|---|
|
|
325
|
+
| `npm test` | **473 tests, 472 pass, 1 skipped (GC-only), 0 fail**, ~3.3 s on Node 24.21.0 |
|
|
326
|
+
| `npm run typecheck` / `npm run test:tsc` | Clean |
|
|
327
|
+
| `npm run build` | Host + client bundles green; MUI `"use client"` directive warnings are non-fatal |
|
|
328
|
+
| `npm run pack:check` | OK: `deepseek-coordination-0.10.0.tgz` (538,358 B); four runtime files present |
|
|
329
|
+
| Release CI | `.github/workflows/release.yml` runs typecheck, test:tsc, tests, pack:check and attaches the tarball to a `v*` GitHub Release |
|
|
330
|
+
|
|
331
|
+
Parser and structure numbers are guards, not promises: a 100k-character parse test stays under 3 s; the data-structure report records B+ tree 10k inserts at 8.3/9.8 ms, skip-list 10k inserts at 6.4 ms, red-black 10k random set/delete at 13.5 ms, sparse grid 10k cells at 5.1 ms, and `hashString` 100k calls at 4.3 ms on the audit machine. The complexity audit records the ordinal `COUNT(*)` → `MAX(ordinal)` change (≈65–69 ms → ≈12–14 ms for 2,000 statements) and route ranking (2,400 → 200 posterior statements for 200 × 12 routes).
|
|
332
|
+
|
|
333
|
+
Known limitations, stated rather than hidden:
|
|
334
|
+
|
|
335
|
+
1. **Durable graph is bounded.** `node`/`edge`/`snapshot` are now schema 24 and survive a new runtime; rehydration is bounded to the newest 10k nodes / 50k edges, removed symbols are not pruned, and a handle older than the rehydration window is `NOT_FOUND`.
|
|
336
|
+
2. **Domain extraction is parser-level; the chain projection is partial.** Book/Article/Part/Chapter/Section/Paragraph, Person/Venue, Condition, Sheet/Cell/Range, TeX proof status and CSV/TSV sheets are emitted and projected onto node cards. `partOf`/`condition` edges are not yet queryable through `coordination_chain`; xlsx/docx/pptx, Sign promotion, Lean/Coq/Isabelle status and cross-file Book composition remain deferred.
|
|
337
|
+
3. **The core turn pipeline is wired, the wider engines are not.** Comprehension → Organization → Plan/score and required Predict rows now run for `command=none` turns, with the scene policy applied where ports exist. Standalone Emotion/Bayesian/Cognition/Orchestration/Learning/Resource/Score, plan rebuild loops and decompose/dispatch/verify/synthesize remain library-only. `thinking`, `allowBash`, `maxIteration` and non-writing score thresholds are recorded as deferred diagnostics, not faked.
|
|
338
|
+
4. **Configured caps are live.** `parser.maxAstDepth`, both dependency-graph caps and `tools.maxResultBytes` are consumed; parser/chain limits retune an existing runtime, and a config change no longer leaves a stale tool runtime.
|
|
339
|
+
5. **Workspace root is resolved, not hard-coded.** Tool option → service root → host root → sandbox-policy root → `process.cwd()` only as the last fallback.
|
|
340
|
+
6. **`parser.enabled` still needs registration.** The other file-layer limits retune live; turning the parser off fails closed immediately, but adding/removing the four tools requires the next registration (restart).
|
|
341
|
+
7. **No browser E2E test.** The UI is covered by types, build, the settings-catalog tests and the takeover/bridge/integration tests; visual verification remains manual/screenshot-based.
|
|
342
|
+
8. **Node engine is `>=22.13.0`.** `node:sqlite` is unflagged from 22.13.0/23.4.0; CI and this verification use 24.21.0.
|
|
343
|
+
9. **A profile can still record an older package spec.** Verify the installed version (`dsh --profile <name> --dump-config` or the profile's `package.json`); install the 0.10.0 tarball or link the built checkout rather than assuming a previous install was upgraded.
|
|
344
|
+
|
|
345
|
+
## Repository history and license
|
|
346
|
+
|
|
347
|
+
`0.1.x` is **dsh-os**, a different OS-interaction project, no longer on the branch tip; `0.2.0` became the DSH plugin bundle; `0.3`–`0.6` moved the UI from Angular to MDUI to plain React to Material UI; `0.7`–`0.9` added the five dropdowns, i18n, profession lock, takeover, compaction, the P0 parser, the domain model and the shared data structures; `0.10.0` adds the settings catalog, the four file tools, the second data-structure pass and the verified tarball. Earlier states remain in git history and tags.
|
|
348
|
+
|
|
349
|
+
MIT © 2026 Eloy Kim. See [LICENSE](LICENSE). Source: [github.com/eloyhere/deepseek-coordination](https://github.com/eloyhere/deepseek-coordination).
|