dsh-plugin-wsl-env 0.4.0 → 0.6.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/CHANGELOG.md +126 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +43 -17
- package/agent/wsl-agent.sh +41 -8
- package/cordis.patch.yml +6 -4
- package/docs/ARCHITECTURE.md +11 -6
- package/docs/CONFIGURATION.md +2 -0
- package/docs/LIMITATIONS.md +10 -0
- package/docs/PARITY.md +4 -0
- package/docs/README.zh.md +35 -10
- package/docs/UPSTREAM-TERMINAL-TITLE.md +81 -0
- package/lib/index.js +26 -10
- package/lib/sandbox-core.js +157 -0
- package/lib/sandbox.js +11 -131
- package/lib/watcher.js +63 -12
- package/lib/wsl.js +24 -2
- package/locale/auto-preset/en.json +6 -0
- package/locale/auto-preset/zh.json +6 -0
- package/locale/en.json +6 -0
- package/locale/picker/en.json +6 -0
- package/locale/picker/zh.json +6 -0
- package/locale/shell-env/en.json +6 -0
- package/locale/shell-env/zh.json +6 -0
- package/locale/subprocess/en.json +6 -0
- package/locale/subprocess/zh.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,132 @@ reference below points at that record's numbering.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.6.0] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
One theme: the P2 backlog from the 2026-10-03 review, closed with the same
|
|
18
|
+
evidence bar as its P0/P1 — every fix carries a check that fails without it,
|
|
19
|
+
and the real-distro probes run green over the changed paths — plus the
|
|
20
|
+
import-time regression that backlog carried, caught at the first
|
|
21
|
+
sync-and-restart: `watchMaxDepth` was written `z.number().int()`, a member the
|
|
22
|
+
pinned schemastery does not publish, which took `wsl-shell` and `wsl-fs` down
|
|
23
|
+
in every distro workspace session. The key now uses `z.natural()`, and
|
|
24
|
+
`test/syntax.mjs` reads the chained half of the schemastery surface so the
|
|
25
|
+
next such member dies in CI instead of in the app.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **The in-distro watcher no longer fails silently (review M10).** Four defects
|
|
30
|
+
in `lib/watcher.js`, fixed together because they share one loop:
|
|
31
|
+
the stamp file is removed on every way out (EXIT trap; TERM/INT/HUP routed
|
|
32
|
+
through a plain exit so the trap runs); a target that vanishes mid-watch now
|
|
33
|
+
ends the loop with a report through the error channel instead of arming
|
|
34
|
+
forever (`E` protocol line, unit- and probe-tested); the scan's depth is
|
|
35
|
+
boundable through the new `wsl-fs.watchMaxDepth` key (`0`, the default,
|
|
36
|
+
keeps the whole-tree walk); and the loop's stderr is drained continuously
|
|
37
|
+
with its tail kept for error context, so a chatty `sh` can no longer stall
|
|
38
|
+
the watch at the 64 KiB pipe limit.
|
|
39
|
+
- **`watchMaxDepth` no longer takes both preset rows down at import (a
|
|
40
|
+
regression the M10 entry above introduced, caught after the first
|
|
41
|
+
sync-and-restart).** The key was written `z.number().int().min(0)`, but the
|
|
42
|
+
pinned schemastery publishes no `int` on any schema instance, and a Config
|
|
43
|
+
schema is a class field — so `lib/index.js` threw
|
|
44
|
+
`TypeError: z.number(...).int is not a function` while it was still being
|
|
45
|
+
imported, the loader left `wsl-shell` and `wsl-fs` fiber-less, and the preset
|
|
46
|
+
audit reported exactly those two rows as `never started`: every session in a
|
|
47
|
+
distro workspace, new or historical, failed at the first message. The key is
|
|
48
|
+
now `z.natural().default(0)` — the pinned spelling of "integer >= 0", the
|
|
49
|
+
idiom `picker` and `auto-preset` already use — and `test/syntax.mjs` now
|
|
50
|
+
reads the chained half of the schemastery surface (a balanced walk that
|
|
51
|
+
survives nested call arguments, string literals and template
|
|
52
|
+
interpolations), so a member the peer does not publish fails the suite
|
|
53
|
+
instead of waiting for the Desktop app. Reproduced and verified in a
|
|
54
|
+
peer-stub harness: the `lib/index.js` import went from the `TypeError` to
|
|
55
|
+
clean with the fix, and the new check fails on the old line.
|
|
56
|
+
- **A wedged WSL service can no longer hang a tool call forever (review M11).**
|
|
57
|
+
Every `wsl.exe` resolution call in `lib/wsl.js` now runs under a deadline
|
|
58
|
+
(`DEFAULT_WSL_DEADLINE_MS`, 60 s — generous on purpose for cold boots, `0`
|
|
59
|
+
waits forever), and the deadline kill is named for what it is in the error,
|
|
60
|
+
with the `wsl.exe --shutdown` remedy. Separately, the agent execution path's
|
|
61
|
+
`confine()` now receives the request's abort signal, the same one the
|
|
62
|
+
one-shot path always passed.
|
|
63
|
+
- **A timed-out command can no longer leave descendants running (review M12).**
|
|
64
|
+
EXEC requests run as their own session and process group (`setsid`, detected
|
|
65
|
+
at startup with a documented fallback), and the in-distro timeout and KILL
|
|
66
|
+
frames signal the whole group — TERM, then KILL after the grace, descendants
|
|
67
|
+
included. Proven by a new real-script check: a command that backgrounds a
|
|
68
|
+
marked shell and outlives its budget leaves zero survivors.
|
|
69
|
+
- **Documentation drift cluster (review M9).** ARCHITECTURE.md's service table
|
|
70
|
+
claimed the share substrate was the default (it is `agent`), pointed at
|
|
71
|
+
`lib/sandbox.js` for `bwrapProfileArgs` (built in `lib/bwrap.js`), and kept
|
|
72
|
+
the long-gone `watch()` refusal in the error-code table; the `substrate`
|
|
73
|
+
schema docstring claimed watches run through the substrate (both substrates
|
|
74
|
+
ride the same `lib/watcher.js` poll loop); PARITY.md now records the rg
|
|
75
|
+
interception as designed-but-never-implemented instead of leaving it
|
|
76
|
+
implied; and the probe checklists (both READMEs, CONTRIBUTING, RELEASING,
|
|
77
|
+
the PR template, CI's comment) list the agent, watch, substrate and exec
|
|
78
|
+
probes that had shipped without being listed anywhere.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- **Test structure (review M15).** The sandbox checks run everywhere now:
|
|
83
|
+
the probe-and-confine logic moved to the peer-free `lib/sandbox-core.js`
|
|
84
|
+
(one implementation, still shipped), `lib/sandbox.js` binds it to the peer's
|
|
85
|
+
`SandboxUnavailableError`, and the tests inject a stand-in with the same
|
|
86
|
+
`code` — CI no longer skips them. `FS_SANDBOX_DENIED`'s write-only
|
|
87
|
+
classification carries three unit checks (write denial classified, the same
|
|
88
|
+
signature ignored off the write path, ordinary permission failures keep
|
|
89
|
+
theirs). The probes stopped carrying inline bwrap profiles: `exec.sh` and
|
|
90
|
+
`sandbox.sh` derive their profiles from `lib/bwrap.js` itself, so a profile
|
|
91
|
+
change fails the probe instead of hiding behind a copy. `lib/wsl.js`'s
|
|
92
|
+
spawning half — previously probe-only — gained `test/wsl.test.mjs`.
|
|
93
|
+
`test/probe/agent.sh` is wired as `npm run probe:agent` like its siblings.
|
|
94
|
+
|
|
95
|
+
## [0.5.0] - 2026-10-04
|
|
96
|
+
|
|
97
|
+
One theme: telling the truth, in the reader's language. The Plugins page now
|
|
98
|
+
renders localized titles and descriptions for every row this bundle owns, the
|
|
99
|
+
README runs shallow to deep and carries an Architecture section in both
|
|
100
|
+
languages, the tab-title limitation is documented with its upstream proposal,
|
|
101
|
+
and the statements `hostSessions` had already falsified are gone.
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- **The plugin page can speak the bundle's language.** The bundle and each of
|
|
106
|
+
its four inserted rows (`subprocess`, `picker`, `shell-env`, `auto-preset`)
|
|
107
|
+
now export localized display metadata — `locale/<lang>.json` with a
|
|
108
|
+
`meta.title` / `meta.description` pair, exposed through `exports` per row —
|
|
109
|
+
which is what the Plugins page reads before falling back to a package.json
|
|
110
|
+
`description`. A Chinese UI now renders Chinese titles and descriptions for
|
|
111
|
+
every row this package owns; previously the rows fell back to raw module
|
|
112
|
+
specifiers (no description at all), and the two official rows it inserts
|
|
113
|
+
keep their upstream English text, which only upstream locale files can fix.
|
|
114
|
+
- **docs/UPSTREAM-TERMINAL-TITLE.md — the upstream proposal behind a documented
|
|
115
|
+
limitation, posted as
|
|
116
|
+
[deepseek-ai/deepseek-harness#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769).** `subprocess-wsl.hostSessions` routes a Windows-folder session's
|
|
117
|
+
terminal to `powershell.exe`, but the tab still reads `WSL`: the controller
|
|
118
|
+
titles a tab from the composition's single shell profile before the provider
|
|
119
|
+
is consulted, the terminal spawn spec carries no display name, and a plugin
|
|
120
|
+
cannot bridge the gap (a patch layer asserts rather than renames a row's
|
|
121
|
+
module, and disabling the `terminal-controller` row fails web boot — its id
|
|
122
|
+
is what the client's `webTerminals` mount waits behind). The doc records the
|
|
123
|
+
gap, the three closed escape hatches with the web-boot failure that closed
|
|
124
|
+
the second one live, and the smallest upstream change that would close it.
|
|
125
|
+
LIMITATIONS.md gains the matching limitation paragraph with the workarounds
|
|
126
|
+
that do work: double-click rename, and a user-layer `shellCandidates` entry
|
|
127
|
+
whose manual selection is titled after itself.
|
|
128
|
+
- **The README carries an Architecture section** (both languages): the two
|
|
129
|
+
mount heights — composition-level `subprocess-wsl` and the preset realm's
|
|
130
|
+
`wsl-shell` / `wsl-fs` — with the per-session terminal routing and the
|
|
131
|
+
command and file execution paths in one diagram and two paragraphs.
|
|
132
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) remains the full reference.
|
|
133
|
+
|
|
134
|
+
### Fixed
|
|
135
|
+
|
|
136
|
+
- Stale statements that `hostSessions` had already falsified were corrected in
|
|
137
|
+
place: `cordis.patch.yml`'s consequence note and ARCHITECTURE.md's "the
|
|
138
|
+
terminal follows the app configuration" paragraph still described the
|
|
139
|
+
pre-routing behaviour.
|
|
140
|
+
|
|
15
141
|
## [0.4.0] - 2026-10-03
|
|
16
142
|
|
|
17
143
|
One theme: the enterprise review's remediation cycle completes. The
|
package/CONTRIBUTING.md
CHANGED
|
@@ -69,6 +69,10 @@ terminal if you would rather not see the prompt.
|
|
|
69
69
|
| `npm run probe:sandbox` | what bubblewrap confines and what it does not | no |
|
|
70
70
|
| `npm run probe:sandbox-shell` | the confined executor through a real boot | yes |
|
|
71
71
|
| `npm run probe:terminal` | the terminal provider | yes |
|
|
72
|
+
| `npm run probe:substrate` | the agent filesystem substrate over a real `wsl.exe` transport | yes; inside the distro |
|
|
73
|
+
| `npm run probe:watch` | the in-distro watcher over a real directory | yes; inside the distro |
|
|
74
|
+
| `npm run probe:agent` | the resident-vs-one-shot fallback parity legs | yes; inside the distro |
|
|
75
|
+
| `npm run probe:exec` | the agent-backed execution handle: timeout, kill, cwd failure | yes; inside the distro |
|
|
72
76
|
| `npm run probe:missing-wsl` | how a `wslPath` that cannot start is reported | yes |
|
|
73
77
|
| `npm run probe:picker` | the picker's root level, its refusals and its cap | yes |
|
|
74
78
|
| `npm run probe:mode` | which POSIX-mode facts survive the share | no; Windows Node |
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This plugin lets [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harn
|
|
|
10
10
|
|
|
11
11
|
**Windows + WSL2 only.** One install command, no dependencies of its own. The environment applies per session, so a session on a Windows folder keeps its normal Windows environment.
|
|
12
12
|
|
|
13
|
-
[Install](#install) · [Using it](#using-it) · [Configure](#configure) · [Sandbox](#sandbox) ·
|
|
13
|
+
[Install](#install) · [Using it](#using-it) · [Configure](#configure) · [Recipes](#recipes) · [Architecture](#architecture) · [Sandbox](#sandbox) ·
|
|
14
14
|
[Troubleshooting](#troubleshooting) · [Development](#development) · [Documentation](#documentation)
|
|
15
15
|
|
|
16
16
|
## Install
|
|
@@ -42,6 +42,7 @@ Open `\\wsl.localhost\ubuntu\home\you\project` as the workspace, then ask the mo
|
|
|
42
42
|
- **Commands** run as `wsl.exe -d <distro> --cd <linux dir> --exec <your login shell> -lc <command>`, so your `PATH`, `nvm`, `cargo`, `pyenv` and rc files apply. The shell is not hardcoded to bash.
|
|
43
43
|
- **Files are the distro's real files.** `/home/you/x` and `\\wsl.localhost\ubuntu\home\you\x` are the same file, and `/mnt/c/...` reaches the Windows disk.
|
|
44
44
|
- **The terminal** (right sidebar, then *New terminal*) opens a shell inside the distro, in the session's folder.
|
|
45
|
+
- **Port visibility.** The model sees which ports have a listener inside the distro (`DSH_WSL_PORTS`, refreshed about every 10 s), so it can hand you the exact URL of a dev server it just started; WSL2's localhost forwarding makes it reachable from Windows directly.
|
|
45
46
|
- **The model sees its shell environment.** The plugin registers `DSH_WSL_DISTRO`, `DSH_WSL_SHELL` and `DSH_WSL_HOME` in the managed `DSH_*` namespace.
|
|
46
47
|
- **Permissions work as on a Linux host.** The Permissions selector switches between `read-only`, `workspace-write` (the default) and `danger-full-access`. A refused command or write comes back with an offer to run it with wider permissions; if you approve, that one call runs without the sandbox.
|
|
47
48
|
|
|
@@ -62,22 +63,6 @@ Override a row by id in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. The keys w
|
|
|
62
63
|
|
|
63
64
|
[`cordis.patch.yml`](cordis.patch.yml) is the commented reference for every shipped value. [docs/CONFIGURATION.md](docs/CONFIGURATION.md) lists the rest, including `shell`, `loginShell`, `cwd`, `timeoutMs`, `resolveSymlinks`, `preferredDistro` and `maxEntries`; [examples/profile.cordis.patch.yml](examples/profile.cordis.patch.yml) is a machine-local layer to copy from.
|
|
64
65
|
|
|
65
|
-
## Sandbox
|
|
66
|
-
|
|
67
|
-
Commands are confined by `bubblewrap` inside the distro, and file writes are checked against the same policy. The Windows ACL sandbox cannot be used here: its restricted token cannot reach WSL at all.
|
|
68
|
-
|
|
69
|
-
| Mode | What a command inside the distro can do |
|
|
70
|
-
|---|---|
|
|
71
|
-
| `read-only` | read the whole distro; a fresh `/dev` is mounted writable, so `/dev/null` and `/dev/shm` work, and nothing else does |
|
|
72
|
-
| `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
|
|
73
|
-
| `danger-full-access` | no sandbox; used for an approved wider-permission request |
|
|
74
|
-
|
|
75
|
-
**`bubblewrap` is required, and the failure is closed.** Without it every confined command reports `SANDBOX_UNAVAILABLE` instead of running unconfined. Set `sandbox: false` on either provider to opt out; the tool layer then tells the model these operations have no sandbox.
|
|
76
|
-
|
|
77
|
-
**The reported enforcement is `partial`, not `full`.** A process inside the distro can still run a Windows program through WSL interop, for example `/mnt/c/.../*.exe`, and bubblewrap does not govern it. `npm run probe:sandbox` demonstrates the boundary on your machine.
|
|
78
|
-
|
|
79
|
-
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for everything the plugin does not do.
|
|
80
|
-
|
|
81
66
|
## Recipes
|
|
82
67
|
|
|
83
68
|
- **Git credential sharing**: let git inside the distro use the Windows-side Git
|
|
@@ -95,6 +80,44 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [do
|
|
|
95
80
|
two Windows-path ones (`DSH_HOME`, `DSH_PROFILE_DIR`) with `/p`. `PATH` is
|
|
96
81
|
deliberately never forwarded — it would shadow the distro's own PATH.
|
|
97
82
|
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
One DSH process serves both kinds of session at once — a workspace on a Windows folder, and a workspace inside the distro — because its providers mount at two levels:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
composition (app level, one per process)
|
|
89
|
+
├─ subprocess-wsl the GUI terminal's execution world
|
|
90
|
+
│ WSL-folder session → the distro shell, in the session's Linux directory
|
|
91
|
+
│ Windows-folder session → powershell.exe, in the session's Windows directory
|
|
92
|
+
├─ directory-picker-wsl installed distros listed beside the Windows home
|
|
93
|
+
├─ wsl-shell-env the DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS facts the model sees
|
|
94
|
+
└─ auto-preset binds the wsl preset when a session opens a distro folder
|
|
95
|
+
|
|
96
|
+
preset-wsl (the wsl agent preset; its services run in isolate realms)
|
|
97
|
+
├─ wsl-shell ctx.shell — wsl.exe --exec <login shell>, confined by bubblewrap inside the distro
|
|
98
|
+
└─ wsl-fs ctx.fs — real distro files; agent substrate on ext4, or the 9p share
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Why two levels.** `wsl-shell` and `wsl-fs` live inside the `wsl` agent preset, which `auto-preset` binds whenever a session's workspace is inside the distro: a Windows-folder session keeps the stock providers, a distro session gets the WSL ones — the environment is a property of the session, not of the process. The terminal controller is the exception: it resolves its execution world through the root context, which never sees a preset's isolate realms, so `subprocess-wsl` sits at the composition level instead.
|
|
102
|
+
|
|
103
|
+
**Commands and files.** A command runs as `wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd>` inside a distro-side bubblewrap profile assembled with the same arguments as DSH's own Linux runner, so confinement semantics and error messages match a Linux host. The file tools read and write real distro files on one of two substrates: the default `agent` — a resident in-distro process, where reads, writes and identities run on ext4 with native symlinks and mode bits — or `share`, the opt-out: the Windows-side host stack over the 9p share. Both check writes against the same policy the command sandbox enforces.
|
|
104
|
+
|
|
105
|
+
## Sandbox
|
|
106
|
+
|
|
107
|
+
Commands are confined by `bubblewrap` inside the distro, and file writes are checked against the same policy. The Windows ACL sandbox cannot be used here: its restricted token cannot reach WSL at all.
|
|
108
|
+
|
|
109
|
+
| Mode | What a command inside the distro can do |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `read-only` | read the whole distro; a fresh `/dev` is mounted writable, so `/dev/null` and `/dev/shm` work, and nothing else does |
|
|
112
|
+
| `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
|
|
113
|
+
| `danger-full-access` | no sandbox; used for an approved wider-permission request |
|
|
114
|
+
|
|
115
|
+
**`bubblewrap` is required, and it fails closed.** Without it every confined command reports `SANDBOX_UNAVAILABLE` instead of running unconfined. Set `sandbox: false` on either provider to opt out; the tool layer then tells the model these operations have no sandbox.
|
|
116
|
+
|
|
117
|
+
**The reported enforcement is `partial`, not `full`.** A process inside the distro can still run a Windows program through WSL interop, for example `/mnt/c/.../*.exe`, and bubblewrap does not govern it. `npm run probe:sandbox` demonstrates the boundary on your machine.
|
|
118
|
+
|
|
119
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for everything the plugin does not do.
|
|
120
|
+
|
|
98
121
|
## Troubleshooting
|
|
99
122
|
|
|
100
123
|
| Symptom | Cause | Fix |
|
|
@@ -120,6 +143,9 @@ npm run probe # filesystem probe against a real distro (Windows +
|
|
|
120
143
|
npm run probe:sandbox-shell # boot a real harness and drive the confined executor
|
|
121
144
|
npm run probe:terminal # open a PTY through the terminal provider
|
|
122
145
|
npm run probe:substrate # drive the agent filesystem substrate over a real wsl.exe transport (from inside the distro)
|
|
146
|
+
npm run probe:watch # arm the in-distro watcher over a real directory (from inside the distro)
|
|
147
|
+
npm run probe:agent # the resident-vs-one-shot fallback parity legs (from inside the distro)
|
|
148
|
+
npm run probe:exec # the agent-backed execution handle: timeout, kill, cwd failure (from inside the distro)
|
|
123
149
|
npm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
|
|
124
150
|
npm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
|
|
125
151
|
npm run probe:mode # which POSIX-mode facts survive the share (Windows node only)
|
package/agent/wsl-agent.sh
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
# With a cap, stdout and stderr are each cut at that many bytes and
|
|
28
28
|
# the flags say which streams were cut; 0 means the whole capture.
|
|
29
29
|
# or ERR|<id>|<reason>|<b64 message> (reason: cwd)
|
|
30
|
+
# An EXEC request runs as its own session and process group (`setsid`),
|
|
31
|
+
# so a timeout or a KILL frame takes the whole descendant tree — not
|
|
32
|
+
# just the direct child, which is how a build tool's daemonized
|
|
33
|
+
# grandchild outlives the kill.
|
|
30
34
|
# FS|<id>|<op>|<timeout seconds|0>|<n args>
|
|
31
35
|
# followed by <n args> lines of base64, one argument each — arguments
|
|
32
36
|
# arrive still base64 so a write's content never passes through a shell
|
|
@@ -81,31 +85,58 @@ b64enc_file() {
|
|
|
81
85
|
|
|
82
86
|
kill_current() {
|
|
83
87
|
if [ -n "$CURRENT_PID" ]; then
|
|
84
|
-
|
|
88
|
+
kill_request TERM "$CURRENT_PID"
|
|
85
89
|
fi
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
# Signal one in-flight request, descendants included. With `setsid` (EXEC
|
|
93
|
+
# requests, below) the request is its own process-group leader, so the
|
|
94
|
+
# negative PID signals the WHOLE group — every descendant that did not make
|
|
95
|
+
# its own session. Without it the group does not exist, the negative-PID kill
|
|
96
|
+
# fails with ESRCH, and the direct kill keeps the old guarantee. A shell whose
|
|
97
|
+
# `kill` builtin cannot parse `-- -PID` at all also lands here.
|
|
98
|
+
kill_request() {
|
|
99
|
+
# $1 = signal name, $2 = request PID.
|
|
100
|
+
kill -s "$1" -- "-$2" 2>/dev/null || kill -s "$1" "$2" 2>/dev/null || :
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
shutdown() {
|
|
89
104
|
kill_current
|
|
90
105
|
exit 0
|
|
91
106
|
}
|
|
92
107
|
trap shutdown TERM INT HUP
|
|
93
108
|
|
|
109
|
+
# Whether EXEC requests can run as their own session. `setsid` makes the
|
|
110
|
+
# request process a session AND process-group leader (pid = pgid), which is
|
|
111
|
+
# what lets kill_request signal the whole descendant tree; the agent's own
|
|
112
|
+
# shell cannot do this portably — `set -m` in a non-interactive dash has been
|
|
113
|
+
# observed skipping the setpgid entirely, and a kill against a PID that is
|
|
114
|
+
# not a group leader is a no-op. A distro without `setsid` keeps working:
|
|
115
|
+
# requests then run in the agent's own group and the direct-child kill of the
|
|
116
|
+
# pre-setsid behaviour applies.
|
|
117
|
+
SETSID=
|
|
118
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
119
|
+
SETSID=setsid
|
|
120
|
+
fi
|
|
121
|
+
|
|
94
122
|
timeout_watcher() {
|
|
95
|
-
# $1 = seconds, $2 = pid, $3 = grace seconds. TERM first
|
|
96
|
-
#
|
|
97
|
-
# the
|
|
123
|
+
# $1 = seconds, $2 = pid, $3 = grace seconds. TERM first — the whole group,
|
|
124
|
+
# so a command that ignores TERM takes its descendants with it when the
|
|
125
|
+
# KILL lands; a process that ignores TERM gets KILLed after the grace, so a
|
|
126
|
+
# stuck command cannot hang the request forever.
|
|
98
127
|
sleep "$1"
|
|
99
|
-
|
|
128
|
+
kill_request TERM "$2"
|
|
100
129
|
sleep "${3:-3}"
|
|
101
|
-
|
|
130
|
+
kill_request KILL "$2"
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
# Capture one request's output and answer with RES. $1 = request id, $2 =
|
|
105
134
|
# timeout seconds ('' or 0 = none), $3 = per-stream output cap in bytes (0 or
|
|
106
135
|
# empty = unlimited), $4 = launch mode: `exec` runs "$@" (argv words) as the
|
|
107
|
-
# request process
|
|
108
|
-
# the
|
|
136
|
+
# request process, in its own session via `setsid` when the distro has it —
|
|
137
|
+
# the timeout and KILL then signal the whole descendant group; `subshell` runs
|
|
138
|
+
# ( "$@" ) in a forked subshell, which is how the FS handlers — shell
|
|
139
|
+
# functions, not executables — run.
|
|
109
140
|
run_capture() {
|
|
110
141
|
local req_id timeout_s maxout mode rc watcher_pid out_cut err_cut
|
|
111
142
|
req_id=$1
|
|
@@ -121,6 +152,8 @@ run_capture() {
|
|
|
121
152
|
CURRENT_ID=$req_id
|
|
122
153
|
if [ "$mode" = subshell ]; then
|
|
123
154
|
( "$@" ) >"$OUT_FILE" 2>"$ERR_FILE" &
|
|
155
|
+
elif [ -n "$SETSID" ]; then
|
|
156
|
+
( exec setsid "$@" ) >"$OUT_FILE" 2>"$ERR_FILE" &
|
|
124
157
|
else
|
|
125
158
|
"$@" >"$OUT_FILE" 2>"$ERR_FILE" &
|
|
126
159
|
fi
|
package/cordis.patch.yml
CHANGED
|
@@ -43,10 +43,12 @@
|
|
|
43
43
|
# directory, so offering host executables there would advertise shells this
|
|
44
44
|
# terminal never starts.
|
|
45
45
|
#
|
|
46
|
-
# Consequence:
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
46
|
+
# Consequence: a terminal's EXECUTION WORLD follows the Session — a WSL-folder
|
|
47
|
+
# session runs the distro's shell, a Windows-folder session runs PowerShell in
|
|
48
|
+
# its own directory (`hostSessions` on `subprocess-wsl`) — while the shell menu
|
|
49
|
+
# and the tab title stay the composition's single `WSL` profile, which no
|
|
50
|
+
# Session identity reaches. The title that implies is documented in
|
|
51
|
+
# docs/LIMITATIONS.md and bridged upstream in docs/UPSTREAM-TERMINAL-TITLE.md.
|
|
50
52
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
51
53
|
- id: subprocess
|
|
52
54
|
disabled: true
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -19,7 +19,7 @@ smaller integrations.
|
|
|
19
19
|
| Service | Class | File | What it does |
|
|
20
20
|
|---|---|---|---|
|
|
21
21
|
| `ctx.shell` | `WslShellExecutor` | [`lib/index.js`](../lib/index.js) | Runs each command as `wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd>`, wrapped in a distro-side `bwrap` sandbox. |
|
|
22
|
-
| `ctx.fs` | `WslFileSystem` | [`lib/index.js`](../lib/index.js) | Serves the file tools from real distro files, on one of two I/O substrates (see below): the Windows-side host stack over the distro's UNC share
|
|
22
|
+
| `ctx.fs` | `WslFileSystem` | [`lib/index.js`](../lib/index.js) | Serves the file tools from real distro files, on one of two I/O substrates (see below): the resident in-distro agent on ext4 (default), or the Windows-side host stack over the distro's UNC share. |
|
|
23
23
|
| `ctx.subprocess` | `WslSubprocessRuntime` | [`lib/subprocess.js`](../lib/subprocess.js) | Opens the GUI terminal inside the distro, in the session workspace. |
|
|
24
24
|
|
|
25
25
|
`WslShellExecutor` extends the shipped `LocalBashExecutor`, and `WslFileSystem`
|
|
@@ -74,9 +74,13 @@ So the layer disables the shipped `subprocess` row and inserts
|
|
|
74
74
|
overrides `spawnTerminal` only. Ordinary `spawn()`, the host ripgrep search, the
|
|
75
75
|
pwsh executor, and the LSP host reach the shipped implementation unchanged.
|
|
76
76
|
|
|
77
|
-
The
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
The terminal's execution world follows the session (`hostSessions`, default on):
|
|
78
|
+
a WSL-folder session runs the distro shell, a Windows-folder session runs
|
|
79
|
+
`powershell.exe` in its own directory. What still follows the app configuration
|
|
80
|
+
is the shell menu and the tab title, both derived from the composition's single
|
|
81
|
+
`WSL` shell profile — so a host session's tab reads `WSL` over a PowerShell
|
|
82
|
+
process. See [LIMITATIONS.md](LIMITATIONS.md) and
|
|
83
|
+
[UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md).
|
|
80
84
|
|
|
81
85
|
## Path coordinates
|
|
82
86
|
|
|
@@ -142,7 +146,8 @@ wsl.exe -d <distro> --cd <linux dir> --exec bwrap \
|
|
|
142
146
|
|
|
143
147
|
These are the same arguments DSH's Linux runner uses (`dsh-sandbox-local`), so the
|
|
144
148
|
behaviour and the error messages match a Linux host. The function
|
|
145
|
-
`bwrapProfileArgs` in [`lib/
|
|
149
|
+
`bwrapProfileArgs` in [`lib/bwrap.js`](../lib/bwrap.js) builds them — the
|
|
150
|
+
peer-free builder every confinement site composes from.
|
|
146
151
|
|
|
147
152
|
| Mode | What a command inside the distro can do |
|
|
148
153
|
|---|---|
|
|
@@ -204,7 +209,7 @@ finds it here.
|
|
|
204
209
|
| `FS_OUTSIDE_DISTRO` | `WslFileSystem.worldPath` | `restrictToDistro` is on and the path names **another distro**'s share — a configuration fence, not a sandbox decision | open a session in that distro, or set `restrictToDistro: false`. A wider permission does **not** lift it |
|
|
205
210
|
| `FS_NOT_OBSERVED` | `WslFileSystem.assertGuard` | a guarded create (`createIfAbsent`) targeted a file that exists and had not been read first | read the file, then overwrite with the version guard or edit it |
|
|
206
211
|
| `FS_STALE_VERSION` | `WslFileSystem.assertGuard` | the version the caller holds no longer matches: the file changed, or is gone | read it again and retry with the fresh version |
|
|
207
|
-
| `FS_IO_ERROR` | `WslFileSystem.watch` |
|
|
212
|
+
| `FS_IO_ERROR` | `WslFileSystem.watch` | the watch target is not a path inside the pinned distro, so there is nothing the in-distro poll loop can observe | watch a distro path; the watch itself runs where the files live (`lib/watcher.js`) |
|
|
208
213
|
| `FS_NOT_FOUND` | the host filesystem stack | the path does not exist — the ordinary answer, passed through | — |
|
|
209
214
|
|
|
210
215
|
Two failures come from `wsl.exe` itself rather than from a code of ours, and both used
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -26,12 +26,14 @@ are schema defaults, listed because they are the ones worth knowing.
|
|
|
26
26
|
| | `substrate` | `agent` *(shipped)* | which I/O substrate serves the file tools: `agent` — the resident in-distro agent (`lib/fs-substrate.js`), where reads, writes and identities run on ext4: native symlinks, native mode bits, and the mutation guard survives to the write's atomic publication, kernel-enforced by the confined resident when the policy is confined. `share` — the opt-out: the Windows-side host stack over the 9p share, with the symlink-retry and publication hooks. Both substrates fence writes with the same host-side policy check. See [LIMITATIONS.md](LIMITATIONS.md) for what changes between them |
|
|
27
27
|
| | `resolveSymlinks` | `true` | *(share substrate only)* follow Linux symlinks that the share cannot traverse, such as `/etc/os-release` and `/bin` — the agent substrate follows them natively |
|
|
28
28
|
| | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
|
|
29
|
+
| | `watchMaxDepth` | `0` | how deep the in-distro watch scan walks; `0` walks the whole tree. Bound it (e.g. `4`) when the watched tree holds a `node_modules` — a change below the bound is seen only when another change above it fires the same callback |
|
|
29
30
|
| `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
|
|
30
31
|
| | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
|
|
31
32
|
| | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
|
|
32
33
|
| `subprocess-wsl` | `distro` | `''` *(shipped)* | which distro the GUI terminal opens in |
|
|
33
34
|
| | `shell` | `''` | pin a shell; empty lets `wsl.exe` decide |
|
|
34
35
|
| | `loginShell` | `true` | whether a pinned shell uses login semantics |
|
|
36
|
+
| | `hostSessions` | `true` | give a session opened on a Windows folder a host `powershell.exe` in its own directory instead of the distro shell at `/mnt/<drive>/…`; `false` restores the composition-owned distro terminal for every session |
|
|
35
37
|
|
|
36
38
|
You can override the other keys in the same way; they keep their defaults. Those are
|
|
37
39
|
`wslPath`, `hostCwd` and `forwardEnv` on the shell and terminal rows,
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -110,5 +110,15 @@ Everything below applies to the share substrate unless it says otherwise.
|
|
|
110
110
|
default on): a WSL-folder session gets the distro shell, a Windows-folder session gets
|
|
111
111
|
`powershell.exe` in its own directory. The shell MENU remains the single configured
|
|
112
112
|
profile, and a directory-less launch still lands in the distro.
|
|
113
|
+
- A host session's TAB TITLE still reads `WSL`, the composition's shell profile
|
|
114
|
+
name, over a PowerShell process. The controller titles a tab from that profile
|
|
115
|
+
before the provider rewrites the launch, and a plugin cannot correct it: a
|
|
116
|
+
patch layer asserts rather than renames a row's module, and the row's id is
|
|
117
|
+
load-bearing for the web composition (disabling it fails web boot with
|
|
118
|
+
"waiting for service: webTerminals"). The full account and the proposed
|
|
119
|
+
upstream change are in [UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md).
|
|
120
|
+
Meanwhile, double-click a tab's title to rename it, or add `shellCandidates`
|
|
121
|
+
to the `terminal-controller` row in your own profile layer — a manually
|
|
122
|
+
selected shell is titled after itself.
|
|
113
123
|
- Terminal activity reporting stops at `wsl.exe`, so the controller never reclaims
|
|
114
124
|
these terminals when they are idle.
|
package/docs/PARITY.md
CHANGED
|
@@ -40,6 +40,10 @@ shaped a phase of the plan.
|
|
|
40
40
|
distro's share, rewrite the spawn to run a distro-side `rg` (falling back
|
|
41
41
|
to `grep -rE` + `find` when absent) with identical stdout conventions.
|
|
42
42
|
Under the agent, that exec rides the agent's `exec` capability.
|
|
43
|
+
**Not implemented, and recorded so the gap is a decision, not an
|
|
44
|
+
omission:** search still runs the packaged Windows rg over the share. The
|
|
45
|
+
interception was designed (above) but never landed; nothing in the phase
|
|
46
|
+
table below delivers it, and no phase claims it.
|
|
43
47
|
3. **`dsh-fs-local` watches with chokidar over the share** — the behaviour
|
|
44
48
|
this plugin replaces: chokidar on 9p is the unreliability that made
|
|
45
49
|
`WslFileSystem.watch` refuse. The in-distro watcher replaces it wholesale.
|
package/docs/README.zh.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**只支持 Windows + WSL2。** 安装只需一条命令,插件自身没有依赖。环境按会话生效:打开 Windows 文件夹的会话继续使用原来的 Windows 环境。
|
|
12
12
|
|
|
13
|
-
[安装](#安装) · [使用](#使用) · [配置](#配置) · [沙箱](#沙箱) · [常见问题](#常见问题) ·
|
|
13
|
+
[安装](#安装) · [使用](#使用) · [配置](#配置) · [配方](#配方) · [架构](#架构) · [沙箱](#沙箱) · [常见问题](#常见问题) ·
|
|
14
14
|
[开发](#开发) · [文档](#文档)
|
|
15
15
|
|
|
16
16
|
## 安装
|
|
@@ -63,6 +63,36 @@ dsh --profile wsl # 4. 启动
|
|
|
63
63
|
|
|
64
64
|
[`cordis.patch.yml`](../cordis.patch.yml) 是每个随包值的注释参考。[docs/CONFIGURATION.md](CONFIGURATION.md) 列出其余键,包括 `shell`、`loginShell`、`cwd`、`timeoutMs`、`resolveSymlinks`、`preferredDistro` 和 `maxEntries`;[examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) 是一份可照抄的本机层。
|
|
65
65
|
|
|
66
|
+
## 配方
|
|
67
|
+
|
|
68
|
+
- **Git 凭据共享**:让子系统里的 git 使用 Windows 侧的 Git Credential Manager,避免每次输密码:
|
|
69
|
+
`git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"`
|
|
70
|
+
(路径按 Windows 侧 Git 的安装位置调整;WSL2 的 localhost 转发是平台行为,子系统内监听的端口 Windows 直接可达。)
|
|
71
|
+
- **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`npm run bootstrap -- <子系统>` 会一并报告 ripgrep / inotifywait(搜索与监视的后端)是否就位。
|
|
72
|
+
- **WSLENV 透传**:WSL 只导入 `WSLENV` 中列出的变量。本插件按前缀放行托管的 `DSH_*` 命名空间,其中带 Windows 路径的两个(`DSH_HOME`、`DSH_PROFILE_DIR`)加 `/p` 让 WSL 翻译成 `/mnt/c/...`。`PATH` 故意不透传——否则 Windows 的 PATH 会覆盖子系统自身的 PATH。
|
|
73
|
+
|
|
74
|
+
## 架构
|
|
75
|
+
|
|
76
|
+
一个 DSH 进程同时服务两类会话:工作区开在 Windows 目录的,和工作区在子系统内的。能做到这一点,是因为 provider 分两层挂载:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
组合层(composition,每个进程一份)
|
|
80
|
+
├─ subprocess-wsl GUI 终端的执行世界
|
|
81
|
+
│ 子系统目录会话 → 子系统 shell,落在会话的 Linux 目录
|
|
82
|
+
│ Windows 目录会话 → powershell.exe,落在会话自己的目录
|
|
83
|
+
├─ directory-picker-wsl 在 Windows 家目录旁列出已安装的子系统
|
|
84
|
+
├─ wsl-shell-env 向模型暴露 DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS
|
|
85
|
+
└─ auto-preset 会话打开子系统目录时自动绑定 wsl preset
|
|
86
|
+
|
|
87
|
+
preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
|
|
88
|
+
├─ wsl-shell ctx.shell — wsl.exe --exec <登录 shell>,在子系统内经 bubblewrap 约束
|
|
89
|
+
└─ wsl-fs ctx.fs — 子系统的真实文件;agent 基座落在 ext4,或走 9p 共享
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**为什么分两层。** `wsl-shell` 和 `wsl-fs` 位于 `wsl` agent preset 内,`auto-preset` 在会话工作区位于子系统内时自动绑定它:Windows 目录的会话继续使用原生 provider,子系统会话拿到 WSL provider——环境是会话的属性,而不是进程的。终端控制器是例外:它经根上下文解析执行世界,永远看不到 preset 的 isolate realm,所以 `subprocess-wsl` 必须挂在组合层。
|
|
93
|
+
|
|
94
|
+
**命令与文件。** 命令以 `wsl.exe -d <子系统> --cd <Linux 目录> --exec <登录 shell> -lc <命令>` 执行,外层是子系统内的 bubblewrap profile,参数与 DSH 自家 Linux runner 完全一致——约束语义与报错文案都和 Linux 主机相同。文件工具读写子系统的真实文件,基座二选一:默认 `agent`——常驻子系统内的进程,读写与路径解析落在 ext4,原生符号链接与权限位;或 `share`——退出项:Windows 侧宿主文件栈走 9p 共享。两个基座与命令沙箱使用同一份策略检查写入。
|
|
95
|
+
|
|
66
96
|
## 沙箱
|
|
67
97
|
|
|
68
98
|
命令由**子系统内的 `bubblewrap`** 约束,文件写入按同一份策略检查。Windows ACL 沙箱在这里用不了:它的受限令牌完全到不了 WSL。
|
|
@@ -73,20 +103,12 @@ dsh --profile wsl # 4. 启动
|
|
|
73
103
|
| `workspace-write` | 在上一行基础上,把会话工作区绑定为可写,并把 `/tmp` 挂成临时目录 |
|
|
74
104
|
| `danger-full-access` | 不加沙箱;用于批准后的放宽权限请求 |
|
|
75
105
|
|
|
76
|
-
**bubblewrap
|
|
106
|
+
**bubblewrap 是必需项,缺失即失败关闭。** 没有它时,每条受限命令都报 `SANDBOX_UNAVAILABLE`,而不是不受约束地运行。要退出约束就在任一 provider 上设 `sandbox: false`,工具层会如实告诉模型这些操作没有沙箱。
|
|
77
107
|
|
|
78
108
|
**上报的强制程度是 `partial` 而不是 `full`。** 子系统里的进程仍可经 WSL interop 执行 Windows 程序(例如 `/mnt/c/.../*.exe`),bubblewrap 管不到它。`npm run probe:sandbox` 会在你的机器上演示这条边界。
|
|
79
109
|
|
|
80
110
|
设计见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#sandbox),插件不做的事情见 [docs/LIMITATIONS.md](LIMITATIONS.md)。
|
|
81
111
|
|
|
82
|
-
## 配方
|
|
83
|
-
|
|
84
|
-
- **Git 凭据共享**:让子系统里的 git 使用 Windows 侧的 Git Credential Manager,避免每次输密码:
|
|
85
|
-
`git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"`
|
|
86
|
-
(路径按 Windows 侧 Git 的安装位置调整;WSL2 的 localhost 转发是平台行为,子系统内监听的端口 Windows 直接可达。)
|
|
87
|
-
- **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`npm run bootstrap -- <子系统>` 会一并报告 ripgrep / inotifywait(搜索与监视的后端)是否就位。
|
|
88
|
-
- **WSLENV 透传**:WSL 只导入 `WSLENV` 中列出的变量。本插件按前缀放行托管的 `DSH_*` 命名空间,其中带 Windows 路径的两个(`DSH_HOME`、`DSH_PROFILE_DIR`)加 `/p` 让 WSL 翻译成 `/mnt/c/...`。`PATH` 故意不透传——否则 Windows 的 PATH 会覆盖子系统自身的 PATH。
|
|
89
|
-
|
|
90
112
|
## 常见问题
|
|
91
113
|
|
|
92
114
|
| 现象 | 原因 | 处理 |
|
|
@@ -112,6 +134,9 @@ npm run probe # 文件系统探针,需要真实子系统(仅
|
|
|
112
134
|
npm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
|
|
113
135
|
npm run probe:terminal # 通过终端 provider 打开一个 PTY
|
|
114
136
|
npm run probe:substrate # 用真实 wsl.exe 传输驱动 agent 文件基座(在子系统内运行)
|
|
137
|
+
npm run probe:watch # 在真实目录上装上 distro 内轮询 watcher(在子系统内运行)
|
|
138
|
+
npm run probe:agent # resident 与 one-shot 的回退不变量对比腿(在子系统内运行)
|
|
139
|
+
npm run probe:exec # agent 执行句柄:超时、杀停、cwd 失败(在子系统内运行)
|
|
115
140
|
npm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
|
|
116
141
|
npm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
|
|
117
142
|
npm run probe:mode # 哪些 POSIX 权限事实能穿过共享层(仅需 Windows node)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Upstream proposal: a terminal tab title that follows the session's shell
|
|
2
|
+
|
|
3
|
+
Status: proposed upstream as
|
|
4
|
+
[deepseek-ai/deepseek-harness#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769)
|
|
5
|
+
(targeting `packages/api/terminal-controller`, version line `0.2.0-rc.x`). This
|
|
6
|
+
is the record of a gap this plugin worked around, could not work around, and
|
|
7
|
+
now documents: the GUI terminal titles every tab from the composition's single
|
|
8
|
+
shell profile, so a per-session shell choice is invisible in the title.
|
|
9
|
+
|
|
10
|
+
## The gap
|
|
11
|
+
|
|
12
|
+
`TerminalController.spawn` fixes a terminal's display title from the resolved
|
|
13
|
+
shell profile before the provider is ever consulted:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const info: WebTerminalInfo = {
|
|
17
|
+
id: request.id, shell, title: shell.name, cwd: environment.cwd, ...
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`shell` is `resolveShell(subprocess, this.config.shell, signal)` — the
|
|
22
|
+
composition's configured profile (`shell.name: WSL` in this plugin's layer),
|
|
23
|
+
kept verbatim; `resolveExecutable` swaps only the `path`. Two properties of the
|
|
24
|
+
seam make the title unable to follow a per-session shell:
|
|
25
|
+
|
|
26
|
+
- `SubprocessTerminalSpawnSpec` carries no display-name field, so a provider
|
|
27
|
+
that rewrites the launch (this plugin routes a Windows-folder session's
|
|
28
|
+
`wsl.exe` launch to `powershell.exe` — `subprocess-wsl.hostSessions`) cannot
|
|
29
|
+
say what it actually started.
|
|
30
|
+
- The controller never reads the effective program back from the
|
|
31
|
+
`SubprocessTerminalHandle`, so the rewrite is invisible to `info.title`.
|
|
32
|
+
|
|
33
|
+
The result on this plugin's default configuration: a terminal that runs
|
|
34
|
+
PowerShell under a tab that reads `WSL`.
|
|
35
|
+
|
|
36
|
+
## Why a plugin cannot bridge it
|
|
37
|
+
|
|
38
|
+
Three escape hatches were evaluated against `0.2.0-rc.2` and each is closed:
|
|
39
|
+
|
|
40
|
+
1. **Replace the controller row's module with a subclass** that corrects the
|
|
41
|
+
title after `super.spawn` (the class exports itself; `BrowserTerminal.rename`
|
|
42
|
+
is public and lands before `create()` publishes the terminal). Closed by the
|
|
43
|
+
patch-layer contract: *"A truthy name asserts the existing plugin name
|
|
44
|
+
rather than renaming it"* (`packages/boot/app-boot/src/config-schema/document.ts`)
|
|
45
|
+
— an id-targeted patch can replace `config`, but not the module.
|
|
46
|
+
2. **Disable the shipped row and insert the subclass under a new id.** Closed
|
|
47
|
+
by the web composition: the row id is load-bearing. The web-app bundle
|
|
48
|
+
inserts `terminal-controller`, and the client model that the
|
|
49
|
+
`dsh-client-ui-sidebar-terminal` entry waits for mounts with
|
|
50
|
+
`inject = ['remote', 'remote.terminal']` — the `terminal` namespace the host
|
|
51
|
+
controller registers in its constructor. Disabling the row fails web boot:
|
|
52
|
+
`1 entry did not activate: @deepseek-ai/dsh-client-ui-sidebar-terminal:
|
|
53
|
+
pending (waiting for service: webTerminals)`. Verified live against
|
|
54
|
+
`dsh-desktop 0.2.0-rc.2`.
|
|
55
|
+
3. **Per-session config.** The `shell` profile is composition state; `!!js`
|
|
56
|
+
expressions evaluate once at config resolution and reach no Session
|
|
57
|
+
identity.
|
|
58
|
+
|
|
59
|
+
## The proposal
|
|
60
|
+
|
|
61
|
+
Any one of these closes the gap; the first is the smallest and keeps the
|
|
62
|
+
profile name as the fallback:
|
|
63
|
+
|
|
64
|
+
- `SubprocessTerminalSpawnSpec` gains an optional `displayTitle` (or the handle
|
|
65
|
+
exposes the launched `argv`), and `create()` uses it for `info.title` when
|
|
66
|
+
present — falling back to `shell.name`. A provider that rewrites a launch
|
|
67
|
+
states the program it started; every other launch keeps the profile name.
|
|
68
|
+
- Alternatively, the controller derives the title from the effective program
|
|
69
|
+
itself: title `executableName(effectiveArgv[0])` when it differs from
|
|
70
|
+
`shell.path`, else `shell.name`.
|
|
71
|
+
|
|
72
|
+
With either, this plugin deletes the limitation paragraph in
|
|
73
|
+
[LIMITATIONS.md](LIMITATIONS.md) and the `WSL`-over-PowerShell title, for any
|
|
74
|
+
provider that rewrites launches — not just WSL.
|
|
75
|
+
|
|
76
|
+
## Workarounds until then
|
|
77
|
+
|
|
78
|
+
- Double-click a tab's title to rename it (a shipped client feature; the
|
|
79
|
+
controller's `rename` Remote API accepts 1–120 characters).
|
|
80
|
+
- Add `shellCandidates` to the `terminal-controller` row in a user layer: a
|
|
81
|
+
manually selected shell is titled after itself.
|