@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.
- package/CHANGELOG.md +91 -0
- package/README.md +318 -13
- package/README.zh.md +101 -4
- package/THIRD-PARTY-NOTICES.md +91 -0
- package/cordis.patch.yml +56 -0
- package/node_modules/@deepseek-ai/cosmokit/LICENSE +21 -0
- package/node_modules/@deepseek-ai/cosmokit/README.md +30 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/index.js +418 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts +15 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/array.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts +13 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/index.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts +41 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/misc.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts +89 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/string.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts +19 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/time.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts +42 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/types.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts +40 -0
- package/node_modules/@deepseek-ai/cosmokit/lib/types/volatile.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/cosmokit/package.json +32 -0
- package/node_modules/@deepseek-ai/cosmokit/src/array.ts +42 -0
- package/node_modules/@deepseek-ai/cosmokit/src/index.ts +12 -0
- package/node_modules/@deepseek-ai/cosmokit/src/misc.ts +78 -0
- package/node_modules/@deepseek-ai/cosmokit/src/string.ts +113 -0
- package/node_modules/@deepseek-ai/cosmokit/src/time.ts +92 -0
- package/node_modules/@deepseek-ai/cosmokit/src/types.ts +168 -0
- package/node_modules/@deepseek-ai/cosmokit/src/volatile.ts +86 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/LICENSE +21 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/README.i18n.yaml +100 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/README.md +186 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/README.zh.md +186 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/index.js +646 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/background.d.ts +20 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/index.d.ts +32 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/lib/types/render.d.ts +81 -0
- package/node_modules/@deepseek-ai/dsh-tool-terminal/package.json +63 -0
- package/node_modules/@deepseek-ai/schemastery/LICENSE +21 -0
- package/node_modules/@deepseek-ai/schemastery/README.md +393 -0
- package/node_modules/@deepseek-ai/schemastery/lib/index.cjs +635 -0
- package/node_modules/@deepseek-ai/schemastery/lib/index.mjs +635 -0
- package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts +211 -0
- package/node_modules/@deepseek-ai/schemastery/lib/types/index.d.ts.map +1 -0
- package/node_modules/@deepseek-ai/schemastery/package.json +39 -0
- package/node_modules/@deepseek-ai/schemastery/src/index.ts +962 -0
- package/node_modules/@standard-schema/spec/LICENSE +21 -0
- package/node_modules/@standard-schema/spec/README.md +198 -0
- package/node_modules/@standard-schema/spec/dist/index.cjs +18 -0
- package/node_modules/@standard-schema/spec/dist/index.d.cts +119 -0
- package/node_modules/@standard-schema/spec/dist/index.d.ts +119 -0
- package/node_modules/@standard-schema/spec/dist/index.js +0 -0
- package/node_modules/@standard-schema/spec/package.json +52 -0
- package/package.json +56 -42
- package/terminal-tools.js +259 -0
- package/terminal.js +297 -0
- 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.
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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.
|