deepseek-coordination 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eloy Kim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,425 @@
1
- # Temporary Holding Version
1
+ # deepseek-coordination
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Cross-platform **atomic OS-interaction surface** for the DeepSeek Harness.
4
+
5
+ The problem this project solves is narrow and concrete: an agent that talks to the operating
6
+ system through *strings* gets the wrong answer whenever a path contains a space, a file carries a
7
+ UTF-8 BOM, a line ending is CRLF, a document is not UTF-8, or a command is assembled by
8
+ concatenation. This project replaces string assembly with an **argument-stable verb set** and makes
9
+ encoding, byte order marks and line endings explicit and byte-exact.
10
+
11
+ ## What is here
12
+
13
+ | Path | What it is |
14
+ |---|---|
15
+ | `src/host/lib/powershell5.1/*.ps1` | **The source of truth** for Windows PowerShell 5.1: the 16 modules listed in `tools/build.ps1`, plus the standalone `git.ps1` |
16
+ | `src/host/lib/powershell7.6/*.ps1` | **The source of truth** for PowerShell 7.6: the same 16 modules ported to .NET, plus the standalone `git.ps1` |
17
+ | `src/host/lib/*.sh` | **The source of truth** for POSIX: the 16 shell modules, plus the standalone `git.sh` |
18
+ | `src/host/bin/dsh-os.ps1` | **Generated** Windows PowerShell 5.1 implementation (218,514 bytes) |
19
+ | `src/host/bin/dsh-os7.ps1` | **Generated** PowerShell 7.6 implementation (255,565 bytes) |
20
+ | `src/host/bin/dsh-os.sh` | **Generated** POSIX implementation (142,744 bytes) |
21
+ | `src/host/bin/dsh-os.bat` | ASCII-only `cmd.exe` shim: sets code page 65001 and forwards arguments to the `.ps1`. Hand-written, not generated |
22
+ | `src/host/command.txt` | The authoritative verb contract, one line per verb |
23
+ | `src/host/spec/verbs.tsv` | Machine-readable verb table: positionals, flags, permission class, error codes. **86 verbs** |
24
+ | `src/host/spec/charsets.tsv` | Charset table: IANA name to .NET code page to `iconv` name, with BOM bytes. **15 charsets** |
25
+ | `src/host/docs/os-spec.md` | Full specification: contracts, error codes, platform constraints, golden matrix |
26
+ | `src/host/docs/permission-model.md` | Authorization model: the four decisions, the file schema, the two enforcement layers |
27
+ | `src/host/lib/powershell.md` | PowerShell 5.1 to 7.6 API-change reference. Not referenced by code, tests or `package.json`; not published |
28
+ | `tools/build.ps1` | Assembles the generated scripts from the modules; `-Check` fails if they drift |
29
+ | `tools/parse-check.ps1` | Parse-checks the PowerShell modules under the runtime that will execute them; also reports non-ASCII bytes and a BOM. Skips files that are not build inputs |
30
+ | `tools/json-field.mjs` | Reads one field of an envelope by dotted path. Used by the shell harnesses, because `sed` cannot see into a nested object |
31
+ | `tools/verify-bundle.mjs` | Verifies the loader's resolution contract without touching a live profile. **10 cases** |
32
+ | `tools/verify-plugin-load.mjs` | Runs the real `apply()` path against a stub registry. **13 cases** |
33
+ | `tools/verify-editions.mjs` | Proves both PowerShell artefacts are reachable and that the default is unchanged. **14 cases** |
34
+ | `tests/golden.ps1` | Windows golden cases driving the real script through argv. **49 cases** |
35
+ | `tests/golden-sh.sh` | POSIX golden cases, including that a failing call still emits an envelope. **116 cases** |
36
+ | `tests/smoke-desktop-sh.sh` | Asserts what the POSIX desktop verbs actually *inject*, using the fakes below. **52 cases** |
37
+ | `tests/fake-xdotool`, `tests/fake-wmctrl` | Recording stand-ins for the X tools, so the desktop verbs can be verified on a machine with no X server |
38
+ | `tests/encoding.ps1` | Encoding-contract guard (BOM, ASCII-only, line endings, BOM-free stdout). **32 build modules + the generated artefacts** |
39
+ | `tests/conformance.ps1` | Asserts `command.txt` and `spec/verbs.tsv` describe the same surface: same verbs both ways, same count, same R/W class, same positional names. **5 checks** |
40
+ | `tests/plugin.test.mjs` | Plugin execute path, argv handling, and bundle manifest shape. **13 cases** |
41
+ | `tests/permission.test.mjs` | Consent delegate: rule matching, deny-first, trust, audit bounds, atomic write. **18 cases** |
42
+ | `src/plugin/index.js` | Host plugin: registers the `dsh_os` tool |
43
+ | `src/permission/index.js` | Consent delegate: the only writer of `permission.json` |
44
+ | `src/client/` | Web client surface (Vite scaffold, not yet wired to the host) |
45
+
46
+ The two PowerShell runtimes are kept in separate directories because they differ in ways that are not
47
+ cosmetic: .NET Framework 4.8 has no `ProcessStartInfo.ArgumentList` and needs an ANSI-aware encoding
48
+ path, while .NET 10 has both. Neither file has to carry the other's workarounds.
49
+
50
+ ## Modules
51
+
52
+ `src/host/lib/` holds one module pair per concern. `bin/dsh-os.ps1` and `bin/dsh-os.sh` are
53
+ generated from them by `tools/build.ps1`, so never edit the generated files.
54
+
55
+ | Module | Concern |
56
+ |---|---|
57
+ | `preamble` | strict mode, encoding pins, layout and repository-root discovery |
58
+ | `json` | error model, JSON serialisation, the envelope writer |
59
+ | `command` | argument parsing (positional and `name="value"`), assertions, coercion |
60
+ | `charset` | charset table, BOM policy, byte-level text model |
61
+ | `native` | P/Invoke declarations and atomic replace (Windows only; empty on POSIX) |
62
+ | `paths` | path resolution and the protected-path boundary |
63
+ | `file` | `file.*`, `file verify.*`, `file search` |
64
+ | `system` | `system.*` queries |
65
+ | `execute` | `execute.command` / `.synchronous` / `.asynchronous` |
66
+ | `task` | `task.*` |
67
+ | `environment` | `environment.*` and `environment attribute.*` |
68
+ | `service` | `service.*` |
69
+ | `permission` | `permission.*` (three read-only verbs, three delegate-only writes) and `selfcheck` |
70
+ | `window` | `window.*` / `mouse.*` / `keyboard.*` / `clipboard.*` / `wait.*` / observe |
71
+ | `dispatch` | verb resolution and the dispatch table |
72
+ | `entry` | run, emit, exit |
73
+
74
+ A build step rather than dot-sourcing the modules at runtime, deliberately: the shipped script must
75
+ stay one self-contained file that the harness and a human can invoke directly, with no module
76
+ resolution and no relative-path assumptions. On Windows PowerShell 5.1 a sourced file without a BOM
77
+ is decoded in the ANSI code page, which is the exact encoding fault this project exists to remove.
78
+
79
+ ## Design rules
80
+
81
+ 1. **Values are separate argv elements.** Commands are never assembled into a shell string. This is
82
+ what removes the Windows space-in-path problem: `C:\Program Files\a b.exe` is one argument.
83
+ 2. **Byte-exact text handling.** Reads strip the BOM; every write restores the file's original BOM
84
+ presence and line-ending convention. A BOM is never allowed to leak into text a caller sees.
85
+ 3. **Assertions report, they do not throw.** `file verify charset` returns `match: false` alongside
86
+ the observed value, so a caller can tell "the file is wrong" from "I asked wrongly".
87
+ 4. **Read-only verbs never prompt; mutating verbs require explicit authorization** (`--allow`).
88
+ 5. **Unsupported is never silent.** An unimplemented verb or an unavailable platform capability
89
+ returns a typed error code, rather than succeeding with an empty result.
90
+
91
+ ## Usage
92
+
93
+ ```powershell
94
+ # Windows
95
+ src\host\bin\dsh-os.ps1 selfcheck
96
+ src\host\bin\dsh-os.ps1 file charset "C:\path with spaces\file.txt"
97
+ src\host\bin\dsh-os.ps1 file verify charset .\file.txt utf-8
98
+ src\host\bin\dsh-os.ps1 environment attr get PATH --scope process
99
+ ```
100
+
101
+ Every call writes exactly one JSON envelope to stdout, UTF-8 without a BOM:
102
+
103
+ ```json
104
+ {"ok":true,"verb":"file.charset","result":{"charset":"utf-8","bom":false,"eol":"crlf"},"meta":{}}
105
+ ```
106
+
107
+ `error.code` is authoritative; exit codes are a coarse class (0 ok, 1 usage, 2 type, 3 authorization,
108
+ 4 unsupported, 5 target/encoding, 6 timeout, 7 internal).
109
+
110
+ ## Tests
111
+
112
+ ```powershell
113
+ npm run test:all # encoding guard, Windows golden, plugin, permission, the three verifiers, desktop smoke
114
+ ```
115
+
116
+ `test:all` chains `test:conformance`, `test:encoding`, `test` (the Windows golden suite), `test:plugin`,
117
+ `test:permission`, `test:bundle` (`tools/verify-bundle.mjs`), `test:load`
118
+ (`tools/verify-plugin-load.mjs` then `tools/verify-editions.mjs`) and `test:desktop`
119
+ (`bash tests/smoke-desktop-sh.sh`), in that order. The POSIX golden suite is run directly with
120
+ `bash tests/golden-sh.sh`, because it is a POSIX acceptance suite and not part of the Windows-side `npm`
121
+ chain.
122
+
123
+ Current state, measured on this checkout:
124
+
125
+ | Suite | Command | Cases | Result |
126
+ |---|---|---|---|
127
+ | Table conformance | `powershell -File tests/conformance.ps1` | 5 | **5 passed, 0 failed** |
128
+ | Encoding contracts | `powershell -File tests/encoding.ps1` | 32 build modules | **all contracts hold** |
129
+ | Windows golden | `powershell -File tests/golden.ps1` | 49 | **49 passed, 0 failed** |
130
+ | POSIX golden | `bash tests/golden-sh.sh` | 116 | **116 passed, 0 failed** |
131
+ | POSIX desktop smoke | `bash tests/smoke-desktop-sh.sh` | 52 | **52 passed, 0 failed** |
132
+ | Plugin | `node tests/plugin.test.mjs` | 13 | **13 passed, 0 failed** |
133
+ | Permission delegate | `node tests/permission.test.mjs` | 18 | **18 passed, 0 failed** |
134
+ | Bundle resolution | `node tools/verify-bundle.mjs` | 10 | **10 passed, 0 failed** |
135
+ | Plugin load | `node tools/verify-plugin-load.mjs` | 13 | **13 passed, 0 failed** |
136
+ | Edition selection | `node tools/verify-editions.mjs` | 14 | **14 passed, 0 failed** |
137
+ | Build drift | `powershell -File tools/build.ps1 -Check` | 3 artefacts | **no drift** |
138
+
139
+ 86 verbs are declared in the table; **82 are implemented on Windows PowerShell 5.1 and on PowerShell
140
+ 7.6 (identical sets), and 61 on POSIX**.
141
+
142
+ The POSIX desktop verbs need an X display, which this machine does not have. `tests/golden-sh.sh`
143
+ therefore asserts the **absence** path — with no `DISPLAY`, every one of them must return a typed
144
+ `E_NO_DISPLAY` rather than an empty success. `tests/smoke-desktop-sh.sh` covers the other half without a
145
+ display server: it puts the recording `tests/fake-xdotool` on `PATH` and sets `DISPLAY`, so the verbs run
146
+ their real code and the assertions are about the **exact argument vector** handed to the tool — that
147
+ `--button right` became button `3`, that `Enter` became the keysym `Return`, that a name outside the
148
+ table (`XF86AudioMute`) is not mangled, that `--clearmodifiers` and `--repeat` land in the right place,
149
+ and that an `xdotool` failure is reported as `E_IO` rather than a false success. That harness is what
150
+ found two real defects in the POSIX implementation (see below). Neither suite is evidence that the verbs
151
+ drive a real window manager.
152
+
153
+ `tests/encoding.ps1` requires ASCII-only, BOM-free files **of the 32 modules `tools/build.ps1`
154
+ concatenates**, because a non-ASCII byte there would be decoded in the ANSI code page and corrupt the
155
+ generated artefact. It deliberately does not require it of the two standalone `git.ps1` scripts, which
156
+ are never concatenated into anything and are UTF-8 **with** a BOM so their Chinese prose parses under
157
+ Windows PowerShell 5.1. An earlier version of the guard applied the rule to every `*.ps1` under `lib/`
158
+ and reported four violations that were a contract conflict rather than a defect; the guard now scopes
159
+ the rule to build inputs and prints which files it skipped, so the narrowing is visible rather than
160
+ silent.
161
+
162
+ `tests/conformance.ps1` exists because `spec/verbs.tsv` had claimed since it was written that
163
+ "tests/conformance.ps1 asserts the two agree" while no such file existed — a comment naming a guard that
164
+ is not there is worse than no comment, because it is read as a guarantee. It resolves each `command.txt`
165
+ line to a verb by the longest word-prefix that names a known verb (so `file verify charset path charset`
166
+ resolves to `file.verify.charset` rather than to a nonexistent `file.verify`), then checks that the two
167
+ tables list the same verbs, the same count, the same R/W class and the same positional names in order.
168
+ Writing it found **five real inconsistencies**, all of them in `command.txt` except one:
169
+
170
+ | Verb | Was declared | Actual, per the implementation |
171
+ |---|---|---|
172
+ | `file.append` | `path content charset` | charset is an **optional** third positional, so the declaration implied it was required |
173
+ | `file.insert` | `path content charset offset` | charset is `--charset`, deliberately **not** a positional — an omitted positional there would silently shift `offset` |
174
+ | `wait.ms` | no positionals | takes `ms` |
175
+ | `keyboard.combination` | two lines, `... down a b c d` / `... up a b c d` | one verb with a **`phase`** positional; the two-line form read as two verbs |
176
+ | `execute.command` | tsv `command\|arguments` | the entire positional list is argv, so there is no separate `command` |
177
+
178
+ `command.txt` is the authoritative contract, so four were corrected there; `verbs.tsv`'s
179
+ `execute.command` row was corrected to `arguments...`. Nothing about the running surface changed — only
180
+ what the two documents say — which is exactly the class of drift a conformance guard is for.
181
+
182
+ ## Using it from the harness
183
+
184
+ `package.json` declares `dsh.bundle.patch`, so the package installs into a DSH profile as a bundle and
185
+ registers one model-facing tool:
186
+
187
+ | Tool | Purpose |
188
+ |---|---|
189
+ | `dsh_os` | Run one verb of the surface and return its JSON envelope verbatim |
190
+
191
+ One tool rather than one per verb: the verb table is the contract and lives in `spec/verbs.tsv`, so
192
+ the schema does not need regenerating when a verb is added or renamed; an agent discovers the whole
193
+ surface with a single `verb: "selfcheck"` call; and the envelope is already the stable result shape,
194
+ so there is no second contract to drift from the script's.
195
+
196
+ ```jsonc
197
+ // call shape
198
+ { "verb": "file charset", "arguments": ["path=\"./a.txt\""], "allow": false }
199
+ { "verb": "environment attribute get", "arguments": ["PATH"], "flags": ["--scope", "process"] }
200
+ ```
201
+
202
+ The implementation is spawned by absolute path with an argv array, never through a shell string, so a
203
+ value containing spaces stays one value. On Windows the `.ps1` is launched through `powershell.exe -File`
204
+ rather than the `.bat`: `cmd.exe` interprets a batch file, and spawning one with `shell: false` fails
205
+ with `EINVAL`. On POSIX the `.sh` is launched through `/bin/bash` so it need not be marked executable.
206
+
207
+ ## Status
208
+
209
+ - Implemented and tested on Windows: `file`, `file verify`, `file search`, `system`, `task`
210
+ (list/query), `service` (list/query), `environment` including `environment attribute`,
211
+ `permission` (read-only) and `selfcheck`.
212
+ - Implemented and tested on POSIX: the same surface except `service`, `window`, `screenshot`,
213
+ `clipboard`, `wait`, `mouse` and `keyboard`, plus `file search`, `execute` (command / synchronous /
214
+ asynchronous), the process verbs `execute.query`, `execute.kill`, `task.query`, `task.show` and
215
+ `task.kill`, and the whole `permission.*` group including `permission acl init`.
216
+ - POSIX layer A is a mode change rather than a Windows deny ACE, and the equivalence is weaker: the
217
+ same user can restore write access. `permission acl init` says so in its own result, and its
218
+ `restore` field names the command that undoes it.
219
+ - `selfcheck` reports on the two platforms differently, and the difference is worth knowing before
220
+ reading its output as a parity check. On POSIX it derives the implemented list from its own dispatch
221
+ table and emits an `implementedCount`. On **Windows it does not**: `Invoke-Selfcheck` walks every row
222
+ of `spec/verbs.tsv` and emits only `verbCount`, with no implemented/unimplemented distinction at all.
223
+ So a Windows `selfcheck` naming a verb does not mean that verb is implemented — the dispatch tables
224
+ above are the authority.
225
+ - Implemented and tested on Windows for `window`, `screenshot`, `clipboard` and `wait`: enumeration,
226
+ matching, move, resize, minimize, maximize, restore, z-order, close, screen capture to png/jpeg/bmp,
227
+ clipboard text round-trip including non-ASCII, an explicit sleep, and waiting for a window to appear
228
+ or to go away.
229
+ - Both goldens include the failure path, which is the point: a failing call must still deliver exactly
230
+ one parseable envelope on stdout, because a bare exit code tells an agent nothing.
231
+ - Implemented and tested on Windows for `mouse` and `keyboard`: cursor read-back, move, click,
232
+ right-click, wheel, button down/up, key down/up, a self-releasing press, chords in both phases, and
233
+ text entry by Unicode key events (so it is layout-independent and accepts non-ASCII).
234
+ - Implemented and tested on Windows for `text` and `structure` through UI Automation: the element at a
235
+ point, its text via ValuePattern / TextPattern / LegacyIAccessiblePattern / Name in that order, and
236
+ the control tree around it. Both are honest about the environment - a missing UIA stack or a refusal
237
+ is a typed E_ACCESSIBILITY_UNAVAILABLE / E_ACCESSIBILITY_DENIED, never an empty result.
238
+ - Missing on Windows: `clipboard.paste` and the `service` lifecycle verbs (`start`, `stop`, `restart`)
239
+ — four verbs. Everything else in the table is dispatched on both PowerShell runtimes, with identical
240
+ verb sets. Every listed verb is declared in the table and returns a typed `E_NOT_SUPPORTED` rather
241
+ than a silent success.
242
+ - Missing on POSIX: 25 verbs — `file.encode`; all eleven `window.*`; `screenshot`, `text`, `structure`;
243
+ `clipboard.read`, `clipboard.write`, `clipboard.paste`; `wait.window`; `task.launch`; and all five
244
+ `service.*`. These fall through the dispatch table to a typed `E_NOT_SUPPORTED`; they are not silently
245
+ absent.
246
+ - Implemented on POSIX and needing **no display server**: the five `environment attribute` verbs
247
+ (`add`, `set`, `delete`, `list`, `get`) and `wait.ms`. The attribute mutations are `--scope process`
248
+ only, and refuse a persistent scope with `E_NOT_SUPPORTED` rather than half-applying it, because POSIX
249
+ has no kernel API for a list variable and rewriting `PATH` in a startup file can make a machine's
250
+ shells unusable.
251
+ - Implemented on POSIX and needing **an X display**: all seven `mouse.*` and all six `keyboard.*`, which
252
+ drive X11 through `xdotool`. With no `DISPLAY` each returns a typed `E_NO_DISPLAY`; with no `xdotool`
253
+ installed each returns `E_NOT_SUPPORTED` naming the missing tool. Key names are translated from the
254
+ Windows spelling to the X keysym spelling (`Enter`→`Return`, `PgUp`→`Prior`, `win`→`super`) and
255
+ anything outside the table is passed through unchanged, so any X keysym name works. A Windows virtual
256
+ key *number* is refused, because `13` means Return to a Windows caller and nothing to X.
257
+ - Both goldens include the failure path, which is the point: a failing call must still deliver exactly
258
+ one parseable envelope on stdout, because a bare exit code tells an agent nothing.
259
+ - Windows and PowerShell 7.6 are at exact parity: both dispatch the same 82 verbs, checked by
260
+ comparing the two dispatch tables rather than by eye.
261
+ - Every Windows verb that a caller can reach is also reachable on POSIX or reports a typed refusal. The
262
+ POSIX-only weak spot is that the desktop verbs are verified against recording fakes rather than a real
263
+ X server; see *Tests*.
264
+ - The four `execute` verbs that the README previously listed as unwritten are in fact implemented on
265
+ Windows — `execute.command`, `execute.query`, `execute.kill` and `task.launch` are dispatched to real
266
+ bodies. So are `permission.grant`, `permission.revoke` and `permission.acl.init` on **both**
267
+ platforms. `permission.json` therefore does have writing verbs; see the authorization section.
268
+ - The consent delegate (`src/permission/index.js`, row id `dsh-os-permission`) is written and tested:
269
+ it is the only component that writes `permission.json`, matching is deny-first regardless of rule
270
+ order, a file whose `updatedBy` is not the delegate is untrusted and falls back to defaults, a
271
+ wildcard target needs explicit acknowledgement, and writes go through a temp file plus rename so a
272
+ crash cannot leave a half-written file.
273
+ - The A layer (kernel ACL enforcement) is implemented on Windows: `permission acl init` appends an
274
+ explicit deny for Everyone to the authorization file DACL, is idempotent, requires elevation
275
+ (reporting `E_ELEVATION_REQUIRED` rather than failing silently), and `selfcheck` reports the strength
276
+ it MEASURED from the file security descriptor plus a reason, never a stored flag.
277
+
278
+ ### Verified or not
279
+
280
+ Proven by tests and verifiers in this repository:
281
+
282
+ - the scripts on both platforms, including that a failing call still emits exactly one envelope;
283
+ - the build's determinism: `tools/build.ps1 -Check` reports the three generated artefacts matching
284
+ their modules byte for byte (218,514 + 255,565 + 142,744 bytes);
285
+ - the plugin's execute path: entry-point resolution, argv construction, a path containing spaces
286
+ reaching the script as one argument, flag forwarding, authorization, and error propagation;
287
+ - the loader's **resolution** contract, by `tools/verify-bundle.mjs`: the manifest header, the patch
288
+ file, each row resolving from a profile-style `node_modules`, each resolved module exporting the
289
+ Cordis shape the loader calls, the two rows resolving to **distinct** modules, and the version gate.
290
+ The package is linked into a throwaway profile sandbox by junction, so resolution happens as it
291
+ would in a profile and the live profile is never written to;
292
+ - the real `apply()` path, by `tools/verify-plugin-load.mjs`: the host plugin registers `dsh_os` with a
293
+ parameters schema whose root type is `object`, the permission plugin registers its five tools, the
294
+ **registered handler actually spawns the script** and returns a real envelope (`selfcheck` → ok,
295
+ `verbCount` 86), and a mutating verb without `allow` is refused with a typed `E_PERMISSION` rather
296
+ than throwing;
297
+ - edition selection, by `tools/verify-editions.mjs`: the default stays on PowerShell 5.1,
298
+ `powershellEdition: '7.6'` and `DSH_OS_POWERSHELL_EDITION` select the 7.6 artefact, `'auto'` uses
299
+ 7.6 only when both the artefact and a working `pwsh` are present, config beats environment, and an
300
+ explicit `scriptPath` disables the choice. `-ExecutionPolicy` is passed only to the 5.1 executable,
301
+ being a Windows PowerShell option.
302
+
303
+ Measured on this checkout, stated exactly as observed:
304
+
305
+ | Suite | Cases | Result |
306
+ |---|---|---|
307
+ | Table conformance | 5 | **5 passed, 0 failed** |
308
+ | Windows golden | 49 | **49 passed, 0 failed** |
309
+ | POSIX golden | 116 | **116 passed, 0 failed** |
310
+ | POSIX desktop smoke | 52 | **52 passed, 0 failed** |
311
+ | Plugin | 13 | **13 passed, 0 failed** |
312
+ | Permission delegate | 18 | **18 passed, 0 failed** |
313
+ | Bundle resolution | 10 | **10 passed, 0 failed** |
314
+ | Plugin load | 13 | **13 passed, 0 failed** |
315
+ | Edition selection | 14 | **14 passed, 0 failed** |
316
+ | Encoding contracts | 32 modules | **all contracts hold** |
317
+
318
+ - Every suite above passes. The four encoding violations this section used to report are gone, because
319
+ the guard now scopes the ASCII-only rule to the files the assembler actually reads; the rule was never
320
+ relaxed, its scope was corrected, and the guard prints which files it skipped.
321
+ - The PowerShell 7.6 artefact builds, is reachable, and the version of PowerShell 7 on this machine is
322
+ **7.6.6**, which is what the edition verifier drove. Beyond that, the 7.6 modules are established as a
323
+ faithful port by comparing the two dispatch tables (identical 82-verb sets), by a per-module
324
+ function-name comparison, and by running the window group's golden cases against `dsh-os7.ps1` — rather
325
+ than by a per-verb run of the whole surface on 7.6. That is a real gap in the evidence, not a claim of
326
+ full parity.
327
+ - Two defects found by `tests/smoke-desktop-sh.sh` after the POSIX mouse and keyboard verbs were written,
328
+ both recorded here because they are the kind review does not catch:
329
+ 1. **Validation ran after injection.** `mouse click 10 20 --count abc` returned an error *and had
330
+ already moved the pointer*. The Windows side validates every argument before injecting; the POSIX
331
+ side now does too.
332
+ 2. **A rejection inside `$(...)` did not stop the verb.** The coordinate validator called `fail`, but
333
+ the caller invoked it as `x="$(window_int_arg ...)"`, so the `exit` ended only the subshell: the verb
334
+ carried on with an empty value and injected anyway — `mouse over abc 20` moved the pointer and
335
+ returned `ok`. The validators now assign a variable (`WINDOW_INT`, `WINDOW_BUTTON`, `WINDOW_KEY`)
336
+ and are called in the caller's own shell. This trap was already documented in the environment
337
+ module, and it was still repeated here.
338
+
339
+ Not proven, and not claimed: that the **harness's own loader** composes these bundles in a live
340
+ profile. That would mean installing this package into the user's active desktop profile and restarting
341
+ the harness, which has not been done.
342
+
343
+ ## Authorization
344
+
345
+ Read-only verbs never prompt. Every mutating verb requires an explicit `--allow`; without it the call
346
+ returns `E_PERMISSION` and nothing happens.
347
+
348
+ `permission.json` is written by the consent delegate (`src/permission/index.js`, row id
349
+ `dsh-os-permission`) and by nothing else. The table declares three write verbs —
350
+ `permission.grant`, `permission.revoke` and `permission.acl.init`, all `W` — and three read-only ones
351
+ (`query`, `list`, `audit`). `grant` and `revoke` accept only the `delegate` flag besides `allow`, so
352
+ the script itself cannot create a persistent authorization: with no delegate present they return
353
+ `E_PERMISSION_DELEGATE_ONLY`.
354
+
355
+ Two enforcement layers protect "the files that decide what this layer may do":
356
+
357
+ - **Layer B, structural** — a hard protected-path boundary rejects any modification of
358
+ `permission.json` (and `.bak`), `src/host/command.txt`, the three `src/host/bin/dsh-os.*` entry
359
+ points, `src/host/spec/verbs.tsv` and `src/host/spec/charsets.tsv`, plus any same-named temporary
360
+ staged beside one of them. Violations report `E_PERMISSION_PROTECTED_PATH` with no side effect;
361
+ reads are still allowed so you can review the rules. The check runs on the **resolved, normalised**
362
+ path, compared segment by segment rather than by regex, because `..`, 8.3 short names or a trailing
363
+ dot all walk around a regex. `file.move` and `file.copy` check **both ends**, since moving a
364
+ protected file is a way of destroying it. The POSIX list has eight entries: the same list without
365
+ `dsh-os7.ps1`. `dsh-os7.ps1` was added to the Windows list only after the gap was found destructively —
366
+ the golden suite's protected-path case moved the artefact into a sandbox it then deleted, because the
367
+ list named the 5.1 and shell entry points but not the 7.6 one. Every entry point belongs to the same
368
+ boundary, and the two lists now protect both PowerShell scripts.
369
+ - **Layer A, kernel ACL** — Windows only, opt-in and requiring elevation. `permission acl init`
370
+ appends an explicit `DENY` for `Everyone` (`S-1-1-0`) to the DACL of `permission.json`, covering
371
+ `FILE_GENERIC_WRITE | DELETE | WRITE_DAC | WRITE_OWNER`. It is idempotent and reports
372
+ `E_ELEVATION_REQUIRED` rather than failing opaquely. `selfcheck` reports `aclEnforced` by reading
373
+ the file's real security descriptor and walking the DACL, never from a stored flag, and `aclReason`
374
+ explains a `false`. The POSIX equivalent is weaker by construction: it is a mode change, and the
375
+ same user can restore write access. `permission acl init` says so in its own result and its
376
+ `restore` field names the command that undoes it.
377
+
378
+ Four limitations, stated because they change what the guarantees above mean:
379
+
380
+ 1. **Persistent rules are recorded but not enforced at the verb layer.** The rules in
381
+ `permission.json` are matched by the consent delegate and its `os_permission_query` tool. The
382
+ script reads the file for **display only** (`permission list/query/audit`); no verb consults a rule
383
+ before acting. The real gates are `--allow`, the protected-path check and (if enabled) layer A. A
384
+ `deny-always` rule does **not** currently stop a verb that arrives with `--allow`.
385
+ 2. **`E_PERMISSION_DENIED_BY_RULE` is declared but never thrown.** It is in the error table and maps
386
+ to exit class 3, but no code path produces it — consistent with point 1.
387
+ 3. **The in-script rule reader is not deny-first.** `permission query` takes the **first** matching
388
+ rule without regard to whether it allows or denies. The delegate implements deny-first correctly
389
+ and has test coverage; the two readers can therefore disagree, and the script side is the weaker
390
+ one.
391
+ 4. **`execute` is a way around the protected-path boundary.** That boundary guards `file.*` and
392
+ `environment.*`. It cannot constrain a child process: `execute.synchronous` hands the command line
393
+ to `cmd.exe /d /s /c` on Windows and to `sh -c` on POSIX, so the program it starts can write
394
+ anything its own rights permit, `permission.json` included. Only layer A prevents that, and layer B
395
+ cannot when `aclEnforced` is false.
396
+
397
+ The consent delegate has a **design gap** worth stating plainly: it registers five tools
398
+ (`os_permission_query`, `os_permission_grant`, `os_permission_revoke`, `os_permission_list`,
399
+ `os_permission_audit`) but does **not** register an `approval/request` answerer, although its own
400
+ header explains that reaching the user is why it is a plugin rather than a script subcommand. As the
401
+ code stands, the enforcement is a **tool description** telling the model to call `os_permission_grant`
402
+ only after the user chooses; the code does not verify that a prompt ever happened. Treat the delegate
403
+ as trusted infrastructure rather than as a check on the agent. It also reports
404
+ `aclEnforced: false` unconditionally in its `query` result instead of measuring the ACL as the script
405
+ does, so it **under-reports** protection when layer A is on.
406
+
407
+ ## Standalone tools
408
+
409
+ `src/host/lib/git.sh`, `src/host/lib/powershell5.1/git.ps1` and `src/host/lib/powershell7.6/git.ps1`
410
+ are interactive git runners. They are deliberately **not** part of the verb surface: they are absent
411
+ from `$modulesPs1` and `$modulesSh` in `tools/build.ps1`, so they are never concatenated into
412
+ `bin/dsh-os.ps1`, `bin/dsh-os7.ps1` or `bin/dsh-os.sh`, and they emit no JSON envelope. Each one is a
413
+ single self-contained file, byte-independent of the others, kept in step by review rather than by a
414
+ generator.
415
+
416
+ They locate git in three phases — `GIT_EXE`/`GIT_HOME`/`PATH`, then the known install locations
417
+ (including the Git-for-Windows registry keys), then a depth- and time-bounded walk of every mounted
418
+ file system — and exit 127 with the searched locations when nothing is found. Arguments reach git as
419
+ separate argv elements; no shell string is ever assembled. `git add` with an empty answer becomes
420
+ `git add .`. All three are ASCII-or-BOM per their own runtime's encoding contract, which is why the
421
+ shell module is English-only and the PowerShell ones are not.
422
+
423
+ ## License
424
+
425
+ MIT