@banbolee/dsh-fish-shell 0.7.0 → 0.8.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +318 -13
  3. package/README.zh.md +101 -4
  4. package/THIRD-PARTY-NOTICES.md +91 -0
  5. package/cordis.patch.yml +56 -0
  6. package/node_modules/@deepseek-ai/cosmokit/LICENSE +21 -0
  7. package/node_modules/@deepseek-ai/cosmokit/README.md +30 -0
  8. package/node_modules/@deepseek-ai/cosmokit/lib/index.js +418 -0
  9. package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts +15 -0
  10. package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts.map +1 -0
  11. package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts +13 -0
  12. package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts.map +1 -0
  13. package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts +41 -0
  14. package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts.map +1 -0
  15. package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts +89 -0
  16. package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts.map +1 -0
  17. package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts +19 -0
  18. package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts.map +1 -0
  19. package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts +42 -0
  20. package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts.map +1 -0
  21. package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts +40 -0
  22. package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts.map +1 -0
  23. package/node_modules/@deepseek-ai/cosmokit/package.json +32 -0
  24. package/node_modules/@deepseek-ai/cosmokit/src/array.ts +42 -0
  25. package/node_modules/@deepseek-ai/cosmokit/src/index.ts +12 -0
  26. package/node_modules/@deepseek-ai/cosmokit/src/misc.ts +78 -0
  27. package/node_modules/@deepseek-ai/cosmokit/src/string.ts +113 -0
  28. package/node_modules/@deepseek-ai/cosmokit/src/time.ts +92 -0
  29. package/node_modules/@deepseek-ai/cosmokit/src/types.ts +168 -0
  30. package/node_modules/@deepseek-ai/cosmokit/src/volatile.ts +86 -0
  31. package/node_modules/@deepseek-ai/dsh-tool-terminal/LICENSE +21 -0
  32. package/node_modules/@deepseek-ai/dsh-tool-terminal/README.i18n.yaml +100 -0
  33. package/node_modules/@deepseek-ai/dsh-tool-terminal/README.md +186 -0
  34. package/node_modules/@deepseek-ai/dsh-tool-terminal/README.zh.md +186 -0
  35. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/index.js +646 -0
  36. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/background.d.ts +20 -0
  37. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/index.d.ts +32 -0
  38. package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/render.d.ts +81 -0
  39. package/node_modules/@deepseek-ai/dsh-tool-terminal/package.json +63 -0
  40. package/node_modules/@deepseek-ai/schemastery/LICENSE +21 -0
  41. package/node_modules/@deepseek-ai/schemastery/README.md +393 -0
  42. package/node_modules/@deepseek-ai/schemastery/lib/index.cjs +635 -0
  43. package/node_modules/@deepseek-ai/schemastery/lib/index.mjs +635 -0
  44. package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts +211 -0
  45. package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts.map +1 -0
  46. package/node_modules/@deepseek-ai/schemastery/package.json +39 -0
  47. package/node_modules/@deepseek-ai/schemastery/src/index.ts +962 -0
  48. package/node_modules/@standard-schema/spec/LICENSE +21 -0
  49. package/node_modules/@standard-schema/spec/README.md +198 -0
  50. package/node_modules/@standard-schema/spec/dist/index.cjs +18 -0
  51. package/node_modules/@standard-schema/spec/dist/index.d.cts +119 -0
  52. package/node_modules/@standard-schema/spec/dist/index.d.ts +119 -0
  53. package/node_modules/@standard-schema/spec/dist/index.js +0 -0
  54. package/node_modules/@standard-schema/spec/package.json +52 -0
  55. package/package.json +56 -42
  56. package/terminal-tools.js +259 -0
  57. package/terminal.js +297 -0
  58. package/tool.js +1 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@banbolee/dsh-fish-shell` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.8.1] - 2026-10-14
9
+
10
+ This is a **patch** release (`0.8.0` → `0.8.1`): the bundle now targets the
11
+ `0.2.0-rc.2` DSH family, with no behavioural change of its own.
12
+
13
+ ### Changed
14
+
15
+ - The `@deepseek-ai/*` dependency family moves to `0.2.0-rc.2` (dsh
16
+ `0.2.0-rc.2`): the peer and dev ranges are `^0.2.0-rc.2`, and the bundled
17
+ `@deepseek-ai/dsh-tool-terminal` pin is `0.2.0-rc.2`. `0.8.0` cannot be
18
+ installed against a `0.2.0-rc.2` harness.
19
+
20
+ ### Documentation
21
+
22
+ - README (both languages) names the `0.2.0-rc.2` family in the support matrix;
23
+ `THIRD-PARTY-NOTICES.md` follows the bundled pin.
24
+
25
+ ## [0.8.0] - 2026-10-09
26
+
27
+ This is a **minor** release (`0.7.0` → `0.8.0`): a new capability, no breaking
28
+ change — the one-shot `fish` path is untouched.
29
+
30
+ ### Added
31
+
32
+ - Interactive terminal sessions (L2): the bundle patch now appends a
33
+ `fish-terminal-group` (`cordis:group` + `isolate: { terminals: true }`) that
34
+ mounts the official `@deepseek-ai/dsh-terminal`, this bundle's `terminal.js`
35
+ (the official `@deepseek-ai/dsh-terminal-bash` backend driven with fish argv,
36
+ plus the harness home contract below), the official
37
+ `@deepseek-ai/dsh-tool-terminal`, and this bundle's `terminal-tools` policy.
38
+ The model gets six `terminal_*` tools — `terminal_open`, `terminal_send`,
39
+ `terminal_read`, `terminal_signal`, `terminal_close`, `terminal_list` — a
40
+ real fish PTY instead of only one-shot `fish -c`.
41
+ - **Harness home contract in interactive sessions.** `terminal.js` adds
42
+ `DSH_HOME` — and `DSH_PROFILE` / `DSH_PROFILE_DIR` when the profile context
43
+ carries them — to the PTY child environment, from the same `ctx.shellEnv`
44
+ registry the one-shot `fish` tool reads. A `dsh` nested inside an interactive
45
+ session now starts on the same harness home and plugin set out of the box;
46
+ before this change it resolved the default `~/.dsh` and had to be pointed at
47
+ the real home by hand (`env -u DSH_SESSION_ID … DSH_HOME=<home> dsh --profile
48
+ <profile>`).
49
+ - The `terminalTools` switch (`terminal-tools.js`, exported as
50
+ `@banbolee/dsh-fish-shell/terminal-tools`): `allow` (default) or `deny`
51
+ (restrict the six explicitly enumerated tool names per agent).
52
+
53
+ ### Changed
54
+
55
+ - `@deepseek-ai/dsh-tool-terminal@0.2.0-rc.1` now ships with the package: it is
56
+ a `dependencies` + `bundledDependencies` entry, so packing requires the
57
+ hoisted linker (`pnpm install --prod --config.node-linker=hoisted` →
58
+ `pnpm pack --config.node-linker=hoisted`). An offline profile install
59
+ resolves it from inside the tarball instead of the profile plane.
60
+ - `scripts/sync-to-profile.sh` now takes a `<profile>` argument and deploys the
61
+ packed tarball (`dsh plugin --profile <profile> add -w <tgz> --offline
62
+ --config.auto-install-peers=false`) instead of copying a hand-written list of
63
+ source files.
64
+ - The L2 backend row keeps the official backend's configuration contract:
65
+ `terminal.js` re-exports the official `Config` schema and reproduces the
66
+ official configuration resolution and validation (per-dialect defaults,
67
+ validation messages), so an empty `shellPath`/`shellArgs` gets the official
68
+ defaults and an invalid value is rejected exactly as the official backend
69
+ rejects it. `@deepseek-ai/dsh-pwsh-local`, which the pwsh dialect default
70
+ resolves through, is declared as a peer dependency.
71
+ - A mounted home registry is the only source of the injected home facts: a
72
+ failing `ctx.shellEnv.collect()` fails the spawn instead of silently falling
73
+ back to the ambient `DSH_HOME`, and a fact the registry does not declare is
74
+ left undeclared rather than borrowed from another source. The public
75
+ `dsh-home-paths` / `profileContext` fallback applies only when no registry is
76
+ mounted at all.
77
+
78
+ ### Documentation
79
+
80
+ - README (both languages): support matrix, security model, expanded Known
81
+ Limitations and a troubleshooting section (`ERR_MODULE_NOT_FOUND`,
82
+ `ERR_PNPM_NO_OFFLINE_TARBALL`).
83
+ - `THIRD-PARTY-NOTICES.md`: origin, version and license of every package
84
+ bundled into the tarball.
85
+
86
+ ### Unchanged
87
+
88
+ - The legacy one-shot surface keeps its exact code path: `index.js`, `tool.js`,
89
+ `persistent.js` and `terminal-fish.js` are untouched, and so are the
90
+ host-plane patch rows (`bash-sandbox`/`tool-bash` disabled;
91
+ `fish-shell`/`tool-fish`/`fish-preset-policy` inserted).
package/README.md CHANGED
@@ -3,10 +3,15 @@
3
3
  **English** | [中文](./README.zh.md)
4
4
 
5
5
  Fish shell executors and tool for DeepSeek Harness: run commands with
6
- **fish** instead of bash. Distribution-ready and surface-agnostic: works in
7
- any profile that mounts it — preset-roster based (dsh-tui, web) or
8
- host-tool based (headless) — and under **any agent preset** (standard, ptc,
9
- cordis, minimal, or a third-party preset).
6
+ **fish** instead of bash. The bundle is surface-agnostic by construction —
7
+ preset-roster based (dsh-tui, web) or host-tool based (headless) — and the
8
+ per-agent policy is written to apply under any agent preset (standard, ptc,
9
+ cordis, minimal, or a third-party preset). The **verified envelope is narrower
10
+ than the design**: the real lanes cover `dsh-base`-class profiles (an isolated
11
+ profile whose bundle layers are `@deepseek-ai/dsh-base` + this bundle), the
12
+ `dsh-tui` composition, and the official `minimal` preset. Every other
13
+ preset/profile combination is **unverified**, so read **Known Limitations**
14
+ below for the authoritative scope before deploying elsewhere.
10
15
 
11
16
  ## What this bundle does
12
17
 
@@ -101,6 +106,40 @@ cordis, minimal, or a third-party preset).
101
106
  realm). Command quoting uses fish single-quote escaping and `eval` with
102
107
  one string argument (fish 4 has no `$'...'` ANSI-C quoting and no
103
108
  `eval --`), verified against real fish 4.0.0.
109
+ - **Interactive terminal sessions** (`terminal-tools.js`, exported as
110
+ `@banbolee/dsh-fish-shell/terminal-tools`): the `terminalTools: 'allow' |
111
+ 'deny'` switch for the interactive (L2) surface, whose patch group mounts the
112
+ official `@deepseek-ai/dsh-terminal` registry, this bundle's `terminal.js`
113
+ (the official `@deepseek-ai/dsh-terminal-bash` backend driven with fish argv,
114
+ plus the harness home contract in the PTY child environment), and the
115
+ official `@deepseek-ai/dsh-tool-terminal` (six `terminal_*` tools). See
116
+ **Interactive terminal sessions (L2)** below.
117
+
118
+ ## Support matrix
119
+
120
+ | Item | Supported | Verified |
121
+ |---|---|---|
122
+ | `@deepseek-ai/*` dsh family | `0.2.0-rc.2` — the exact pins live in the repository root `package.json` (`pnpm.overrides`) | yes: real isolated profiles in the S0 probe runs |
123
+ | `fish` | `3.7+` | yes on `3.7.1`; lower versions are unverified |
124
+ | Platform | POSIX only: Linux and macOS. Windows is not supported (the `pwsh` family covers it) | Linux: the real lane with `DSH_REAL_FISH_PTY=1` in the `test` job; macOS: the `fish-pty-macos` job runs the real L2 lane, including the `workspace-write` denial outside the workspace |
125
+ | Node.js | `>=22` (`engines.node`) | yes |
126
+
127
+ ## Test coverage matrix
128
+
129
+ The promises above are only as good as the lanes that assert them. These are
130
+ the lanes, what they boot, and what they cover:
131
+
132
+ | Lane | Boots | Covers |
133
+ |---|---|---|
134
+ | `plugins/fish-shell/tests/terminal-session-real.spec.ts` | a real isolated `dsh-base` profile, through the `dsh` CLI | six tools; `open`/`send`/`read`/`close`; `terminal_signal`; a background `terminal_send` job with `job_output`/`job_kill`; the mode-switch fence; the `workspace-write` denial outside the workspace; the `read-only` semantics; owner isolation (`FOREIGN_SESSION`); the harness home contract (the session's `$DSH_HOME` is the profile's own home, a nested `dsh --profile <profile> --dump-config` composes that profile, and the same command under a foreign home fails) |
135
+ | `tests/composition/fish-pty-dsh-tui.spec.ts` | a real `dsh-tui` composition | the bundle group activates (no pending L2 rows); six tools visible; `open`/`send`/`read`/`close` |
136
+ | `tests/composition/fish-pty-minimal.spec.ts` | the official `minimal` preset over `dsh-base` | two `terminals` realms (host fish group + preset `/bin/bash` persistent shell); six tools exactly once; the persistent-fish swap; `open`/`send`/`read`/`close` |
137
+ | `tests/composition/fish-pty-bundled-row.spec.ts` | a profile patch that references `@deepseek-ai/dsh-tool-terminal` directly | resolution from inside this bundle only; the row stays `pending (waiting for service: terminals)`; no second registration; the surviving registration still drives a PTY |
138
+ | `scripts/qa/fish-packed-offline.mjs` | a packed tarball installed into a temporary profile with `--offline` | the install succeeds and really loads: six tools visible, `listBackends()` non-empty |
139
+
140
+ CI runs the real lane in the `test` job (Linux) and in `fish-pty-macos`
141
+ (macOS), the composition lanes in the `composition` job, and the packed/offline
142
+ lane in `fish-packed-offline`.
104
143
 
105
144
  ## Install
106
145
 
@@ -116,13 +155,18 @@ From this checkout, per profile:
116
155
  dsh plugin --profile <name> add ./plugins/fish-shell
117
156
  ```
118
157
 
119
- Add it to every profile that should run fish (dsh-tui, web, headless, …).
120
- The bundle patch (`cordis.patch.yml`) is one safe patch across surfaces: it
121
- disables the host bash executor and `tool-bash` rows, mounts the fish
122
- executor and the host-global `fish` tool, and inserts the
123
- `fish-preset-policy` row, which hides the preset-inherited `bash` tool per
124
- agent (restriction + empty `tool:bash` prompt shadow, persistent-fish swap
125
- under `minimal`). The patch no longer touches the preset roster.
158
+ Add it to every profile that should run fish (dsh-tui, web, headless, …) —
159
+ remembering the envelope above: `dsh-base`-class profiles, the `dsh-tui`
160
+ composition and the official `minimal` preset are verified today, every other
161
+ preset/profile combination is unverified (see **Known Limitations**). The
162
+ bundle patch
163
+ (`cordis.patch.yml`) is one safe patch across surfaces: it disables the host
164
+ bash executor and `tool-bash` rows, mounts the fish executor and the
165
+ host-global `fish` tool, and inserts the `fish-preset-policy` row, which hides
166
+ the preset-inherited `bash` tool per agent (restriction + empty `tool:bash`
167
+ prompt shadow, persistent-fish swap under `minimal`). It also appends the
168
+ `fish-terminal-group` for interactive terminal sessions (see **Interactive
169
+ terminal sessions (L2)** below). The patch no longer touches the preset roster.
126
170
 
127
171
  ### Deployed copy and the symlink chain
128
172
 
@@ -132,11 +176,146 @@ runtime instance the harness uses — the launcher-maintained
132
176
  `profiles/node_modules/@deepseek-ai/*` symlink chain. A plain `link:` to this
133
177
  checkout (outside the profile tree) would resolve no `@deepseek-ai` package,
134
178
  so the deployed copy lives at `profiles/node_modules/@banbolee/dsh-fish-shell` (inside
135
- the tree). `scripts/sync-to-profile.sh` copies the plugin there after edits.
179
+ the tree). `scripts/sync-to-profile.sh <profile>` packs the plugin with the hoisted
180
+ linker (`pnpm install --prod --config.node-linker=hoisted` then
181
+ `pnpm pack --config.node-linker=hoisted`) and installs the resulting tarball
182
+ into the profile with
183
+ `dsh plugin --profile <profile> add -w <tgz> --offline --config.auto-install-peers=false`.
136
184
  A pnpm `file:` dependency points each profile at the deployed copy. A
137
185
  package published to npm installs normally (its realpath already lies inside
138
186
  the profile tree).
139
187
 
188
+ ### Release and provenance
189
+
190
+ Releases are expected to ship with **npm provenance**: `publishConfig` carries
191
+ `provenance: true`, so publishing works from an OIDC-capable CI (GitHub
192
+ Actions), or with an explicit
193
+ `npm publish --provenance --access public`. A local publish has no OIDC
194
+ provider to attest the build, so a maintainer releasing from a workstation
195
+ has to turn provenance off for that one run with `--no-provenance`. **2FA is a
196
+ release prerequisite** — provenance does not replace it and npm checks it on
197
+ the account/organization side, which this repository cannot verify. Before
198
+ publishing, turn the `[Unreleased]` section of `CHANGELOG.md` into the version
199
+ being released (this `0.8.1` cut did exactly that).
200
+
201
+ The published artefact has to be packed from a staged tree installed with
202
+ `--config.package-import-method=copy`: pnpm's default import method hardlinks
203
+ store files, and npmjs rejects a tarball carrying hardlink members with
204
+ `415 Unsupported Media Type — Hard link is not allowed`. `npm publish --dry-run`
205
+ cannot catch that (a dry run never uploads), so the release workflow asserts the
206
+ shape explicitly and `plugins/fish-shell/tests/packed-files.spec.ts` asserts the
207
+ same on its own `npm pack` artefact.
208
+
209
+ ## Interactive terminal sessions (L2)
210
+
211
+ Next to the one-shot `fish` tool, the bundle mounts an interactive terminal
212
+ surface: a real fish PTY the model drives with `terminal_open`,
213
+ `terminal_send`, `terminal_read`, `terminal_signal`, `terminal_close` and
214
+ `terminal_list`. It reuses the official terminal stack: the backend row mounts
215
+ `terminal.js`, a subclass of the official `BashTerminalBackend` whose only
216
+ difference is the harness home contract in the child environment (**The harness
217
+ home contract** below). `spawn()` is inherited, so argv confinement, the
218
+ sandbox-mode fence and `session.initialize()` readiness are the official
219
+ code.
220
+
221
+ The patch appends one `insert` group, `fish-terminal-group`, at the end of
222
+ `cordis.patch.yml`:
223
+
224
+ | Row id | Package | Config |
225
+ |---|---|---|
226
+ | `pty` | `@deepseek-ai/dsh-terminal` | — |
227
+ | `terminal-fish-pty` | `@banbolee/dsh-fish-shell/terminal` | `shellPath: fish`, `shellArgs: ["--no-config","-i","-C", <FISH_PROMPT_SETUP>]`, `timeoutMs: 300000` |
228
+ | `terminal-tools` | `@deepseek-ai/dsh-tool-terminal` | — |
229
+ | `fish-terminal-tools` | `@banbolee/dsh-fish-shell/terminal-tools` | `terminalTools: allow` |
230
+
231
+ The group row is `cordis:group` with `group: true` and
232
+ `isolate: { terminals: true }`: the `terminals` service has to be self-contained
233
+ inside the group, because a host-plane row would stay pending in every profile
234
+ whose host plane has no `terminals` service. `tools` is deliberately **not**
235
+ isolated, so the six `terminal_*` tools registered by `terminal-tools` land in
236
+ the host tool registry and are visible to agents.
237
+
238
+ The prompt setup travels in the `-C` argument (the same `FISH_PROMPT_SETUP`
239
+ string that `terminal-fish.js` exports): startup submits nothing, so the motd
240
+ stays clean, whereas feeding the same setup through PTY input gets it echoed
241
+ back by the fish line editor.
242
+
243
+ **The harness home contract.** `terminal.js` exists for exactly one reason: a
244
+ PTY child's environment is the backend overlay plus `scrubbedParentEnv()`,
245
+ which drops every inherited `DSH_*` name, so a `dsh` nested inside an
246
+ interactive session used to resolve the default `~/.dsh` while the one-shot
247
+ `fish` tool ran on the real harness home. The subclass adds `DSH_HOME` — and
248
+ `DSH_PROFILE` / `DSH_PROFILE_DIR` when the context carries them — from the SAME
249
+ registry the one-shot tool reads, `ctx.shellEnv.collect()`, through the official
250
+ constructor's public `spawnTerminal` seam (the layer the subprocess provider
251
+ applies after the ambient scrub). The snapshot is taken at spawn time because
252
+ the three home facts are execution-independent: `collect` only reads the
253
+ execution to add `DSH_SESSION_ID`, so the session facts (`DSH_SHELL`,
254
+ `DSH_SESSION_ID`, `DSH_PTY_SESSION_ID`) stay the official overlay's own values.
255
+ Nothing else differs: the config surface is the official schema re-exported
256
+ verbatim (`Config`), so `backendType` keeps its `shell` default, which is what
257
+ `listBackends()` reports and `terminal_open { type: 'shell' }` opens. The
258
+ overlay has exactly ONE source per mounted state: when the registry is not
259
+ mounted at all (a custom composition outside `dsh-base`) the same facts come
260
+ from their public owners — `resolveDshHome()` and `ctx.get('profileContext')`,
261
+ which are the registry's own sources — while a mounted registry is
262
+ authoritative, so a `collect` error propagates (like the one-shot path) instead
263
+ of silently resolving another home, and a key its snapshot does not declare
264
+ stays undeclared instead of being borrowed from the ambient environment.
265
+ **Maintenance note:** the row is ours only
266
+ because the official row exposes no `env` config field; if
267
+ `@deepseek-ai/dsh-terminal-bash` grows one, the row goes back to the official
268
+ package and `terminal.js` is deleted.
269
+
270
+ `fish-terminal-tools` is the only entry this plugin adds for the surface: the
271
+ `terminalTools` switch implemented in `terminal-tools.js`. Its default is
272
+ `allow` (do nothing, matching every other tool in the deployment); `deny` takes
273
+ the six tools away per agent — see **Security model**.
274
+
275
+ **Why the one-shot path is untouched.** The appended group is purely additive.
276
+ The host-plane rows above it stay byte-identical (`bash-sandbox`/`tool-bash`
277
+ disabled, `fish-shell`/`tool-fish`/`fish-preset-policy` inserted), and
278
+ `ctx.shell` (`index.js`), the one-shot `fish` tool (`tool.js`), the `minimal`
279
+ preset's persistent tool (`persistent.js`) and the `terminal-fish.js` library
280
+ keep their exact code paths: nothing about a one-shot command changes because
281
+ an interactive surface exists.
282
+
283
+ **The bundled `@deepseek-ai/dsh-tool-terminal`.** The `terminal-tools` row
284
+ resolves the official tool package from inside this package, not from the
285
+ profile plane: `@deepseek-ai/dsh-tool-terminal@0.2.0-rc.2` is declared in
286
+ `dependencies` and `bundledDependencies`, so packing ships it, and its
287
+ transitive dependencies, as real files under `node_modules/` inside the
288
+ tarball. That is what makes an offline profile install work. It must not be
289
+ moved to `peerDependencies`: peer resolution would look for a profile-plane
290
+ copy, which is exactly what this design avoids. Versions, origins and licenses
291
+ are listed in `./THIRD-PARTY-NOTICES.md`.
292
+
293
+ ## Security model
294
+
295
+ - **Default: `terminalTools: 'allow'`.** The interactive surface is on by
296
+ default, exactly like every other tool the deployment mounts; a fish profile
297
+ with this bundle installed behaves like any other profile that has the
298
+ official terminal tools. `deny` is the opt-out: set the
299
+ `fish-terminal-tools` row's config to `terminalTools: deny`, and at
300
+ `agent/created`, and again on every `tools/change`, the policy restricts the
301
+ six names per agent through the official
302
+ `agent.ctx.tools.restrict({ deny: [...] })`. The names are
303
+ enumerated one by one (never a `terminal_*` wildcard), so an upstream seventh
304
+ tool cannot silently pass the policy.
305
+ - **The six tools**: `terminal_open`, `terminal_send`, `terminal_read`,
306
+ `terminal_signal`, `terminal_close`, `terminal_list`.
307
+ - **No privilege escalation.** This plugin registers no tool of its own, wraps
308
+ or replaces no host object, monkey-patches nothing, and offers no
309
+ sudo/elevation entry point. A session process is `fish` running with the
310
+ permissions of the user that launched the profile.
311
+ - **Sandbox inheritance.** Confinement is the host's: a `workspace-write`
312
+ profile fences the interactive session through the same sandbox backend as
313
+ any other command (measured: a write to `/etc` is denied). The plugin does
314
+ not bypass the fence and caches nothing outside the sandbox.
315
+ - **Secret handling and audit are bounded** — **Known Limitations** below
316
+ states the hidden-input boundary, the unsupported interaction surface and the
317
+ best-effort audit scope.
318
+
140
319
  ## Per-surface behavior
141
320
 
142
321
  - **Preset-roster surfaces** (dsh-tui, web): the roster default is untouched
@@ -180,7 +359,11 @@ the profile tree).
180
359
  `start()` and the official `dsh-tool-bash`): it runs until `job_kill`,
181
360
  cancellation, or composition teardown. Requires the jobs services
182
361
  (`@deepseek-ai/dsh-jobs` + `@deepseek-ai/dsh-tool-jobs`) to be composed;
183
- the tool fails loud when they are not.
362
+ the tool fails loud when they are not. Killing a job while it is still
363
+ running is verified by the real PTY lane
364
+ (`plugins/fish-shell/tests/terminal-session-real.spec.ts`): `job_output`
365
+ reads a live `terminal_send` job and `job_kill` settles it to the terminal
366
+ `killed` status.
184
367
  - Under a sandboxing executor, a denied command can be re-run wider in the
185
368
  same turn with `sandbox_permissions` (the narrowest wider mode that
186
369
  suffices) plus a one-sentence `justification`; the approval prompt raised
@@ -216,5 +399,127 @@ the profile tree).
216
399
  escalation (`sandbox_permissions`/`justification` via `ctx.approval`),
217
400
  terminal/generic UI presentation, and the `tool:fish` exit-code prompt
218
401
  section are all supported.
402
+ - **Secrets: only true hidden input is hidden.** A command line, an argument or
403
+ a `terminal_send` payload is public to the session: the fish line editor
404
+ echoes what it receives and the session keeps the scrollback, so anything
405
+ that entered non-hidden can be read back by `terminal_read`. Never pass a
406
+ secret as a command argument or through `terminal_send`; a real hidden read
407
+ (`read -s` in fish) is the only supported way to keep text out of the echoed
408
+ stream. `stty -echo` is **not** a mitigation: fish's line editor repaints and
409
+ the sentinel stays in the scrollback (measured).
410
+ - **Audit is best-effort, not a lifecycle guarantee.** The plugin observes only
411
+ what the model's explicit `terminal_open`/`terminal_close` calls expose
412
+ (session id, PID, status transitions). It does **not** cover host-side
413
+ reclamation, timeout kills, a process exiting on its own, other agents' or
414
+ owners' sessions, or the group's `terminals` realm internals (invisible at
415
+ the host plane). There is no full-lifecycle promise: the official
416
+ `TerminalSessionService` exposes no open/exit/dispose events, and the plugin
417
+ does not invent an audit surface of its own.
418
+ - **Text interaction only.** `resize`, named keys (arrow keys, named Ctrl
419
+ combinations), full-screen TUIs (`vim`, `htop`) and `TERM=dumb`-style
420
+ degraded terminals are not supported by the official session API; supporting
421
+ them would require a plugin-owned backend, which is out of scope.
422
+ - **Verified L2 behaviors.** Each of these was an open question when the
423
+ interactive surface was first documented; each now has a lane that asserts the
424
+ recorded behavior (the lane index is **Test coverage matrix**):
425
+ - **Preset compositions.** Two `terminals` realms coexist under the official
426
+ `minimal` preset — the host L2 group's (fish) and the preset's own
427
+ persistent-shell group's (`/bin/bash`) — and each of the six `terminal_*`
428
+ tools is visible to the preset-bound agent exactly once; the `dsh-tui`
429
+ composition mounts the same group and drives `open`/`send`/`read`/`close`
430
+ (`tests/composition/fish-pty-minimal.spec.ts`,
431
+ `tests/composition/fish-pty-dsh-tui.spec.ts`).
432
+ - **Mode switch with a live session.** The standing-policy switch is refused
433
+ synchronously with `cannot change sandbox mode … while persistent terminal
434
+ sessions are open`: the mode event is not committed, the effective mode is
435
+ unchanged, the live session keeps working, and the same switch succeeds once
436
+ the session is closed
437
+ (`plugins/fish-shell/tests/terminal-session-real.spec.ts`).
438
+ - **`read-only` mode.** `read-only` is a file-effect mode, not a capability
439
+ gate: the six tools stay visible and the PTY still spawns (motd `dsh>`), but
440
+ the resolved policy has no writable root at all, so even a write inside the
441
+ session workspace is denied — the workspace is not a write boundary under
442
+ `read-only` (same lane).
443
+ - **Cross-agent owner isolation.** A second agent in the same profile sees an
444
+ empty `terminal_list` and cannot read, write or close the first agent's
445
+ session: the tool error is `belongs to another agent` and the registry code
446
+ behind it is `FOREIGN_SESSION`, while the owner's session survives untouched
447
+ (same lane).
448
+ - **A profile patch referencing the bundled package directly.** The name
449
+ resolves from inside this bundle only
450
+ (`…/@banbolee/dsh-fish-shell/node_modules/@deepseek-ai/dsh-tool-terminal`),
451
+ never from the dsh installation; resolution is not activation — because the
452
+ group isolates `terminals`, that row stays
453
+ `pending (waiting for service: terminals)`, so the six tools are never
454
+ registered twice, and the boot reports the pending row on stderr instead of
455
+ swallowing it (`tests/composition/fish-pty-bundled-row.spec.ts`).
456
+ - **Unverified (do not read as supported)**:
457
+ - the `FishTerminalBackend` submitted-setup path converging on fish 3.7.1:
458
+ that library is not mounted by the patch, so the L2 surface is unaffected,
459
+ and the legacy persistent path keeps its own specs;
460
+ - a first install in a fully offline, cold-store environment: the packed lane
461
+ (`scripts/qa/fish-packed-offline.mjs`) installs with `--offline`, but its
462
+ store was warmed by the repository install first.
219
463
  - POSIX only: the `fish` binary and the underlying process-group semantics
220
464
  are not available on Windows (the pwsh family covers Windows).
465
+
466
+ ## Troubleshooting
467
+
468
+ ### `ERR_MODULE_NOT_FOUND` for `@deepseek-ai/dsh-tool-terminal`
469
+
470
+ The bundled tool package did not make it into the installed package, or it was
471
+ resolved as a peer (profile-plane) dependency. Build with the hoisted linker:
472
+ `pnpm install --prod --config.node-linker=hoisted` and then
473
+ `pnpm pack --config.node-linker=hoisted`. Without it pnpm refuses to pack a
474
+ bundled dependency and fails with
475
+ `ERR_PNPM_BUNDLED_DEPENDENCIES_WITHOUT_HOISTED`. Check the artefact: the
476
+ tarball must contain
477
+ `package/node_modules/@deepseek-ai/dsh-tool-terminal/package.json`. One
478
+ upstream cause is not ours to fix: `@deepseek-ai/dsh-tool-terminal@0.2.0-rc.2`
479
+ declares `exports` entries for `./src/*` that the published tarball does not
480
+ contain, so anything resolving through that subpath fails with
481
+ `ERR_MODULE_NOT_FOUND`.
482
+
483
+ ### `ERR_PNPM_NO_OFFLINE_TARBALL`
484
+
485
+ The offline install ran against a cold pnpm store: with `--offline`, pnpm
486
+ refuses to go to the network for a tarball it has not cached. Warm the store
487
+ once with a networked install (or run the same `dsh plugin ... add` without
488
+ `--offline` for the first install), then re-run the offline command. The
489
+ plugin's own runtime dependency travels inside the plugin tarball, so this is a
490
+ store-state problem, not a missing bundled file.
491
+
492
+ ### Nested `dsh` inside an interactive session
493
+
494
+ As of this bundle the session carries the harness home contract: its child
495
+ environment gets `DSH_HOME` (plus `DSH_PROFILE` / `DSH_PROFILE_DIR` when the
496
+ profile context has them) from the same `ctx.shellEnv` registry the one-shot
497
+ `fish` tool reads, so the two surfaces cannot drift — **The harness home
498
+ contract** above says where and why. The real lane asserts the outcome: inside a
499
+ session `echo $DSH_HOME` prints the profile's own home, and
500
+ `dsh --profile <profile> --dump-config` composes that profile's tree, while the
501
+ same command under a foreign `DSH_HOME` fails. A nested `dsh` therefore starts
502
+ on the same home and the same plugin set as the session it was opened in.
503
+
504
+ Older bundles — and any deployment whose `terminal-fish-pty` row is still the
505
+ official `@deepseek-ai/dsh-terminal-bash` — have no such contract, and there was
506
+ no config-only fix for them: that row exposes no `env` config field, so an
507
+ `env:` written on it was accepted by the schema and silently ignored, and the
508
+ backend's `childEnvironment()` is module-private. A nested `dsh` there resolved
509
+ `~/.dsh`, which can hold profile and plugin state from an older release — a
510
+ stale plugin there can fail the nested boot or disable its rows. Pass the home
511
+ explicitly — replace `/path/to/dsh-home` with the Harness home the outer session
512
+ was launched with (a one-shot `fish` call prints it: `echo $DSH_HOME`):
513
+
514
+ ```
515
+ env -u DSH_SESSION_ID -u DSH_PTY_SESSION_ID -u DSH_SHELL -u DSH_PERMISSION_MODE DSH_HOME=/path/to/dsh-home dsh --profile <profile>
516
+ ```
517
+
518
+ **Maintenance note.** Owning the row (`terminal.js`) is the only reason the
519
+ contract can be added without editing the official package; if
520
+ `@deepseek-ai/dsh-terminal-bash` grows an `env` config field (or the registry
521
+ forwards the home facts itself), the row goes back to the official package,
522
+ `terminal.js` is deleted, and only the first paragraph of this entry remains —
523
+ the real lane keeps its assertions either way. That retirement also has to
524
+ update the `NESTED_DSH_ENTRY` contract in
525
+ `tests/docs-shape-fish-shell.spec.ts`, which names this module.