dsh-plugin-wsl-env 0.3.0 → 0.5.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 +175 -0
- package/README.md +40 -17
- package/agent/wsl-agent.sh +63 -19
- package/cordis.patch.yml +6 -4
- package/docs/ARCHITECTURE.md +7 -3
- package/docs/CONFIGURATION.md +1 -0
- package/docs/LIMITATIONS.md +17 -1
- package/docs/PARITY.md +5 -4
- package/docs/README.zh.md +32 -10
- package/docs/UPSTREAM-TERMINAL-TITLE.md +81 -0
- package/lib/agent-confined.js +9 -3
- package/lib/agent.js +69 -4
- package/lib/bwrap.js +18 -1
- package/lib/fs-decisions.js +132 -0
- package/lib/fsio-agent.js +2 -2
- package/lib/index.js +36 -24
- package/lib/paths.js +14 -0
- package/lib/shell-env.js +6 -1
- 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 +16 -4
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,181 @@ reference below points at that record's numbering.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.5.0] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
One theme: telling the truth, in the reader's language. The Plugins page now
|
|
18
|
+
renders localized titles and descriptions for every row this bundle owns, the
|
|
19
|
+
README runs shallow to deep and carries an Architecture section in both
|
|
20
|
+
languages, the tab-title limitation is documented with its upstream proposal,
|
|
21
|
+
and the statements `hostSessions` had already falsified are gone.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **The plugin page can speak the bundle's language.** The bundle and each of
|
|
26
|
+
its four inserted rows (`subprocess`, `picker`, `shell-env`, `auto-preset`)
|
|
27
|
+
now export localized display metadata — `locale/<lang>.json` with a
|
|
28
|
+
`meta.title` / `meta.description` pair, exposed through `exports` per row —
|
|
29
|
+
which is what the Plugins page reads before falling back to a package.json
|
|
30
|
+
`description`. A Chinese UI now renders Chinese titles and descriptions for
|
|
31
|
+
every row this package owns; previously the rows fell back to raw module
|
|
32
|
+
specifiers (no description at all), and the two official rows it inserts
|
|
33
|
+
keep their upstream English text, which only upstream locale files can fix.
|
|
34
|
+
- **docs/UPSTREAM-TERMINAL-TITLE.md — the upstream proposal behind a documented
|
|
35
|
+
limitation, posted as
|
|
36
|
+
[deepseek-ai/deepseek-harness#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769).** `subprocess-wsl.hostSessions` routes a Windows-folder session's
|
|
37
|
+
terminal to `powershell.exe`, but the tab still reads `WSL`: the controller
|
|
38
|
+
titles a tab from the composition's single shell profile before the provider
|
|
39
|
+
is consulted, the terminal spawn spec carries no display name, and a plugin
|
|
40
|
+
cannot bridge the gap (a patch layer asserts rather than renames a row's
|
|
41
|
+
module, and disabling the `terminal-controller` row fails web boot — its id
|
|
42
|
+
is what the client's `webTerminals` mount waits behind). The doc records the
|
|
43
|
+
gap, the three closed escape hatches with the web-boot failure that closed
|
|
44
|
+
the second one live, and the smallest upstream change that would close it.
|
|
45
|
+
LIMITATIONS.md gains the matching limitation paragraph with the workarounds
|
|
46
|
+
that do work: double-click rename, and a user-layer `shellCandidates` entry
|
|
47
|
+
whose manual selection is titled after itself.
|
|
48
|
+
- **The README carries an Architecture section** (both languages): the two
|
|
49
|
+
mount heights — composition-level `subprocess-wsl` and the preset realm's
|
|
50
|
+
`wsl-shell` / `wsl-fs` — with the per-session terminal routing and the
|
|
51
|
+
command and file execution paths in one diagram and two paragraphs.
|
|
52
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) remains the full reference.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Stale statements that `hostSessions` had already falsified were corrected in
|
|
57
|
+
place: `cordis.patch.yml`'s consequence note and ARCHITECTURE.md's "the
|
|
58
|
+
terminal follows the app configuration" paragraph still described the
|
|
59
|
+
pre-routing behaviour.
|
|
60
|
+
|
|
61
|
+
## [0.4.0] - 2026-10-03
|
|
62
|
+
|
|
63
|
+
One theme: the enterprise review's remediation cycle completes. The
|
|
64
|
+
security-decision layer gains its own peer-free module and unit tests, the
|
|
65
|
+
coverage gate measures every unit file again, the fallback invariant is probed
|
|
66
|
+
for real, and the file tools pick up the audit's fixes — a killed write no
|
|
67
|
+
longer strands state, large reads stream instead of staging, `/tmp` writes
|
|
68
|
+
land where readers read, cross-distro workdirs are refused, the resident's
|
|
69
|
+
Windows environment is pinned, and `maskWindowsDrive` offers the interop
|
|
70
|
+
mitigation.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **The filesystem provider's policy decisions are a peer-free module with
|
|
75
|
+
their own unit tests.** The security-decision layer — which paths are
|
|
76
|
+
refused for naming another distro, which mode denies a mutation outright,
|
|
77
|
+
which write guards refuse which intents, how a substrate failure maps onto
|
|
78
|
+
the surfaced refusal, and whether a mutation routes to the confined
|
|
79
|
+
resident — used to live inline in `lib/index.js`, a file no unit test can
|
|
80
|
+
import. It is now `lib/fs-decisions.js` (pure in, refusal descriptor out,
|
|
81
|
+
byte-identical messages), with `test/fs-decisions.test.mjs` covering every
|
|
82
|
+
branch, and `lib/index.js` delegating at one `FsError` choke point.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **The fallback invariant is now probed, not just promised.** PARITY.md's
|
|
87
|
+
hard rule — "`agent: false` must behave like the pre-agent release" — cited
|
|
88
|
+
a probe leg that did not exist, and spelled the key as `wsl.agent: 'off'`
|
|
89
|
+
when it is a boolean. `test/probe/agent.sh` now runs the same operations
|
|
90
|
+
through the resident AND through the one-shot `wsl.exe` path and compares
|
|
91
|
+
outcomes: exit code and stdout bytes (including `$`-metacharacters), stderr
|
|
92
|
+
and working directory, and NUL/CRLF binary safety. All three legs pass
|
|
93
|
+
against a real distro; the doc now names the key correctly and describes
|
|
94
|
+
what the probe actually compares.
|
|
95
|
+
|
|
96
|
+
- **The coverage gate measures every unit file again.** `test:coverage` listed
|
|
97
|
+
six of the sixteen test files — the thresholds (85/85/70) were met by the
|
|
98
|
+
narrowed set while the full suite sat at 82.45% branches, so branch
|
|
99
|
+
regressions anywhere outside the six were invisible to the gate that claimed
|
|
100
|
+
to guard them. The gate now runs the whole unit suite (95.83% lines / 85.95%
|
|
101
|
+
branches / 91.01% functions), which got there the honest way: new tests for
|
|
102
|
+
the seams the audit found dark — the deny-dialect classification,
|
|
103
|
+
aborted-signal mapping, the transport env pin, handshake mismatch, protocol
|
|
104
|
+
garbage, cwd rejections, pre-aborted signals, idle retirement, mid-flight
|
|
105
|
+
over-cap buffers, failed rebuilds, watcher teardown and error paths, the
|
|
106
|
+
cross-distro workdir predicate, staging sweeps, setuid publication, and the
|
|
107
|
+
real-`/tmp` consistency the file tools now have.
|
|
108
|
+
|
|
109
|
+
- **Reading a large file no longer copies its remainder to a temp file for
|
|
110
|
+
every window.** `fs_read` staged `tail -c +offset` to disk before `head`
|
|
111
|
+
took the requested bytes — so a full read of a 1 GB file performed ~1000
|
|
112
|
+
window round trips AND ~500 GB of aggregate temp-file writes inside the
|
|
113
|
+
distro, and a window that took over 30 seconds died as an opaque
|
|
114
|
+
"distro I/O failure". The remainder now streams through a `tail | head`
|
|
115
|
+
pipe (the request's own bound is the only staging), read failures still
|
|
116
|
+
classify through the same stderr dialect, and windowed reads are verified
|
|
117
|
+
byte-exact including the final partial window.
|
|
118
|
+
- **A request the transport cannot deliver now fails its caller instead of
|
|
119
|
+
stranding it.** `writeLines` used to drop a frame silently when the
|
|
120
|
+
`wsl.exe` process was gone (a thin race between exit handling and the next
|
|
121
|
+
request), leaving a pending entry nothing would ever answer — the caller
|
|
122
|
+
waited forever when it had no timeout, and the watchdog had no process
|
|
123
|
+
left to kill. An undeliverable frame now rejects with
|
|
124
|
+
`AgentUnavailableError` ("the agent exited before the request could be
|
|
125
|
+
sent; it never ran"), the rebuild relay fails undeliverable re-sends the
|
|
126
|
+
same way, and `close()` no longer writes `SHUTDOWN` into an ended stdin
|
|
127
|
+
(whose async write-after-end error could crash the process during its own
|
|
128
|
+
shutdown).
|
|
129
|
+
|
|
130
|
+
- **A confined write to `/tmp` now lands in the distro's real `/tmp`, where
|
|
131
|
+
every reader reads.** The fence grants `/tmp` as a writable root, but the
|
|
132
|
+
confined resident's profile mounted an ephemeral tmpfs over it — so the
|
|
133
|
+
write reported success into a private tmpfs, the read tools (unconfined)
|
|
134
|
+
could not see it, the post-write stat produced a `missing:` version that
|
|
135
|
+
made the next edit fail with `FS_STALE_VERSION`, and the file evaporated
|
|
136
|
+
when the resident retired. The file-writes resident's profile now binds the
|
|
137
|
+
real `/tmp` read-write (`realTmp`, on by default for the confined factory):
|
|
138
|
+
the write lands where the fence said it would, reads and edits agree, and
|
|
139
|
+
user terminals see the file. Commands keep their ephemeral per-run tmpfs —
|
|
140
|
+
the fresh-temp-area property (and its anti-poisoning guarantee) is
|
|
141
|
+
unchanged, so a command still cannot see files the write tools placed in
|
|
142
|
+
`/tmp` — the documented command-vs-tools split, now safe in both
|
|
143
|
+
directions. Verified live: confined write → plain read returns the content;
|
|
144
|
+
a confined `cat` still reports it absent, as documented.
|
|
145
|
+
|
|
146
|
+
- **A killed write no longer leaves its staging directory in your files.** A
|
|
147
|
+
`KILL`/`SIGTERM` mid-write could not be trapped, so the private 0700
|
|
148
|
+
staging dir (`.<name>.<pid>.<rand>.tmpdir`) sat in the target's own
|
|
149
|
+
directory forever — and listings showed the debris to the model. The EXIT
|
|
150
|
+
trap now removes the in-flight staging along with the agent's temp dir, and
|
|
151
|
+
each write sweeps the target directory's leftovers whose creating agent's
|
|
152
|
+
PID (it rides the name) is no longer running — a live agent's staging is
|
|
153
|
+
left alone, PID reuse included.
|
|
154
|
+
- **A write to a typo'd path refuses instead of materializing the chain.**
|
|
155
|
+
The staging `mkdir -p` silently created every missing parent directory and
|
|
156
|
+
reported success, where the peer's Node-fs publication fails `ENOENT` —
|
|
157
|
+
masking model errors as wins. Parents are no longer created: the refusal is
|
|
158
|
+
`FS_NOT_FOUND`, an existing directory stays fine, and a concurrent creator
|
|
159
|
+
of the directory still wins.
|
|
160
|
+
- **Setuid, setgid and sticky bits survive an overwrite or edit.** The mode
|
|
161
|
+
was masked to `0o777` at the stat parse and again at the write, so a
|
|
162
|
+
`4755` binary became `755` and a `1777` directory file lost its sticky bit
|
|
163
|
+
— while LIMITATIONS claimed the bits were kept. The full mode now rides
|
|
164
|
+
stat and write end-to-end (`0o7777`), matching the share substrate's
|
|
165
|
+
`chmod --reference` behaviour.
|
|
166
|
+
|
|
167
|
+
- **A shell command whose workdir names another WSL distro is refused, where
|
|
168
|
+
it used to run in the pinned distro against whatever tree shares the path.**
|
|
169
|
+
`toLinuxPath` strips a `\\wsl.localhost\<name>\…` UNC's distro name, and the
|
|
170
|
+
shell executor never compared it — so a `debian` workspace's commands ran
|
|
171
|
+
inside the pinned `ubuntu`, silently, while the fs side refused the same
|
|
172
|
+
workspace with `FS_OUTSIDE_DISTRO` ("open a session in that distro" — which
|
|
173
|
+
the user had done). The shell now refuses with the fs side's wording, and
|
|
174
|
+
the shell-env facts stay silent for a foreign-distro session instead of
|
|
175
|
+
presenting another distro's home and shell as this session's.
|
|
176
|
+
|
|
177
|
+
- **The resident agent no longer inherits the Windows environment's WSLENV
|
|
178
|
+
forwarding.** Spawned bare, `wsl.exe` carried this process's whole
|
|
179
|
+
environment with it, and whatever the user's `WSLENV` names — dev machines
|
|
180
|
+
routinely route secrets (`GITHUB_TOKEN/u` is the documented example) — was
|
|
181
|
+
forwarded into the distro, where every model command inherited it. The
|
|
182
|
+
transport's Windows env is now pinned: the essentials `wsl.exe` itself
|
|
183
|
+
needs, the managed `DSH_*` facts, `WSL_UTF8=1`, and a `WSLENV` that names
|
|
184
|
+
only the managed entries with their original flags. Verified live: a secret
|
|
185
|
+
injected on the Windows side with a forwarding flag appears zero times in
|
|
186
|
+
the distro environment. `forwardEnv` remains one-shot-only today — the
|
|
187
|
+
resident path never forwarded it, and the pin keeps that posture instead of
|
|
188
|
+
widening it silently.
|
|
189
|
+
|
|
15
190
|
### Added
|
|
16
191
|
|
|
17
192
|
- **`maskWindowsDrive` on `wsl-shell` and `wsl-fs`** — the mitigation knob for
|
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 |
|
package/agent/wsl-agent.sh
CHANGED
|
@@ -57,11 +57,15 @@ PROTO_VERSION=3
|
|
|
57
57
|
TMPDIR_AGENT=$(mktemp -d "${TMPDIR:-/tmp}/wsl-agent.XXXXXX")
|
|
58
58
|
OUT_FILE="$TMPDIR_AGENT/out"
|
|
59
59
|
ERR_FILE="$TMPDIR_AGENT/err"
|
|
60
|
-
trap 'rm -rf "$TMPDIR_AGENT"; exit 0' EXIT
|
|
60
|
+
trap 'rm -rf "$TMPDIR_AGENT"; [ -z "${CURRENT_STAGING:-}" ] || rm -rf -- "$CURRENT_STAGING"; exit 0' EXIT
|
|
61
61
|
|
|
62
62
|
# The request currently in flight, for KILL and for the exit trap.
|
|
63
63
|
CURRENT_PID=
|
|
64
64
|
CURRENT_ID=
|
|
65
|
+
# The staged-publication directory of an in-flight FS write. TERM/INT would
|
|
66
|
+
# otherwise orphan it inside the target's own directory; a KILL cannot be
|
|
67
|
+
# trapped, which is what the sweep in fs_write is for.
|
|
68
|
+
CURRENT_STAGING=
|
|
65
69
|
|
|
66
70
|
b64dec() {
|
|
67
71
|
# `base64 -d` with GNU and BusyBox spellings; a decode failure yields empty.
|
|
@@ -256,18 +260,22 @@ fs_realpath() {
|
|
|
256
260
|
fs_read() {
|
|
257
261
|
# $1 = base64 path, $2 = base64 byte offset, $3 = base64 max bytes. The
|
|
258
262
|
# window is the bound: at most $3 bytes leave the distro per request; the
|
|
259
|
-
# host loops.
|
|
260
|
-
|
|
263
|
+
# host loops. The remainder is STREAMED through the pipe, never staged —
|
|
264
|
+
# staging copied the whole tail to a temp file once per window, which made
|
|
265
|
+
# a full read of a large file quadratic in the file's own size. tail's
|
|
266
|
+
# stderr lands in ERR_FILE (its SIGPIPE death once `head` has had its fill
|
|
267
|
+
# writes nothing there), so a real read failure is still classified.
|
|
268
|
+
local p offset max err
|
|
261
269
|
p=$(b64dec "$1")
|
|
262
270
|
offset=$(b64dec "$2")
|
|
263
271
|
max=$(b64dec "$3")
|
|
264
272
|
[ "$max" -gt 0 ] || return 0
|
|
265
|
-
|
|
266
|
-
tail -c +"$(( offset + 1 ))" -- "$p" >"$
|
|
273
|
+
: >"$ERR_FILE"
|
|
274
|
+
tail -c +"$(( offset + 1 ))" -- "$p" 2>"$ERR_FILE" | head -c "$max" -- 2>/dev/null
|
|
275
|
+
if [ -s "$ERR_FILE" ]; then
|
|
267
276
|
err=$(cat "$ERR_FILE")
|
|
268
277
|
fs_fail "$(fs_classify "$err")" "$err"
|
|
269
|
-
|
|
270
|
-
head -c "$max" -- "$rd"
|
|
278
|
+
fi
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
norm_time() {
|
|
@@ -299,6 +307,30 @@ fs_version() {
|
|
|
299
307
|
printf '%s:%s:%s:%s:%s' "$dev" "$ino" "$size" "$(norm_time "$mtime")" "$(norm_time "$ctime")"
|
|
300
308
|
}
|
|
301
309
|
|
|
310
|
+
drop_staging() {
|
|
311
|
+
# Remove the current write's staging dir and forget it, so the EXIT trap
|
|
312
|
+
# never re-removes (or, worse, removes a newer write's staging) after it.
|
|
313
|
+
rm -rf -- "$staging"
|
|
314
|
+
CURRENT_STAGING=
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
sweep_staging() {
|
|
318
|
+
# $1 = directory, $2 = base name. Remove this directory's leftover staging
|
|
319
|
+
# dirs whose creating agent is gone — the PID rides the name, and a process
|
|
320
|
+
# that is not running cannot clean up after its own KILL. A live PID keeps
|
|
321
|
+
# its dir (the safe direction, PID reuse included).
|
|
322
|
+
local d pid
|
|
323
|
+
for d in "$1/.$2."*.*.tmpdir; do
|
|
324
|
+
[ -d "$d" ] || continue
|
|
325
|
+
pid=${d#"$1/.$2."}
|
|
326
|
+
pid=${pid%%.*}
|
|
327
|
+
case $pid in
|
|
328
|
+
''|*[!0-9]*) continue ;;
|
|
329
|
+
esac
|
|
330
|
+
kill -0 "$pid" 2>/dev/null || rm -rf -- "$d"
|
|
331
|
+
done
|
|
332
|
+
}
|
|
333
|
+
|
|
302
334
|
fs_write() {
|
|
303
335
|
# $1 = base64 path, $2 = base64 mode (`-` keeps none), $3 = base64
|
|
304
336
|
# `replace` or `no-replace`, $4 = expected version (`-` for none) — the
|
|
@@ -315,10 +347,21 @@ fs_write() {
|
|
|
315
347
|
dir=$(dirname -- "$path")
|
|
316
348
|
base=$(basename -- "$path")
|
|
317
349
|
[ -n "$base" ] && [ "$base" != "/" ] || fs_fail notfound "empty file name"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
350
|
+
# Leftover staging dirs from a KILLed write (a KILL cannot be trapped) would
|
|
351
|
+
# sit in the target's directory forever and show up in listings. The creating
|
|
352
|
+
# agent's PID rides the name: one whose process is gone is an orphan, so
|
|
353
|
+
# sweep it before staging this write.
|
|
354
|
+
sweep_staging "$dir" "$base"
|
|
355
|
+
if [ ! -d "$dir" ]; then
|
|
356
|
+
# Deliberately NO `mkdir -p`: a typo'd parent chain must refuse (the peer's
|
|
357
|
+
# Node-fs publication fails ENOENT there), not materialize silently. An
|
|
358
|
+
# existing directory after a failed mkdir is a concurrent creator, not an
|
|
359
|
+
# error.
|
|
360
|
+
mkdir -- "$dir" 2>"$ERR_FILE" || {
|
|
361
|
+
err=$(cat "$ERR_FILE")
|
|
362
|
+
[ -d "$dir" ] || fs_fail "$(fs_classify "$err")" "$err"
|
|
363
|
+
}
|
|
364
|
+
fi
|
|
322
365
|
u=$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
|
323
366
|
staging="$dir/.$base.$$.${u:-0}.tmpdir"
|
|
324
367
|
mkdir -- "$staging" 2>"$ERR_FILE" || {
|
|
@@ -327,12 +370,13 @@ fs_write() {
|
|
|
327
370
|
err=$(cat "$ERR_FILE")
|
|
328
371
|
fs_fail "$(fs_classify "$err")" "$err"
|
|
329
372
|
}
|
|
373
|
+
CURRENT_STAGING=$staging
|
|
330
374
|
chmod 700 -- "$staging" 2>/dev/null
|
|
331
375
|
tmp="$staging/$base.tmp"
|
|
332
376
|
# Decode straight to the file: the base64 text is the one form that crosses
|
|
333
377
|
# a shell variable without a NUL-byte or quoting hazard.
|
|
334
378
|
printf '%s' "$5" | base64 -d >"$tmp" 2>/dev/null || {
|
|
335
|
-
|
|
379
|
+
drop_staging
|
|
336
380
|
fs_fail io "cannot stage content"
|
|
337
381
|
}
|
|
338
382
|
chmod 600 -- "$tmp"
|
|
@@ -340,7 +384,7 @@ fs_write() {
|
|
|
340
384
|
if [ "$mode" != "-" ]; then
|
|
341
385
|
chmod "$mode" -- "$tmp" 2>"$ERR_FILE" || {
|
|
342
386
|
err=$(cat "$ERR_FILE")
|
|
343
|
-
|
|
387
|
+
drop_staging
|
|
344
388
|
fs_fail io "$err"
|
|
345
389
|
}
|
|
346
390
|
fi
|
|
@@ -351,7 +395,7 @@ fs_write() {
|
|
|
351
395
|
# trips the stat/find skew a list-derived version would.
|
|
352
396
|
version=$(fs_version "$path")
|
|
353
397
|
if [ "$version" != "$expected" ]; then
|
|
354
|
-
|
|
398
|
+
drop_staging
|
|
355
399
|
if [ -n "$version" ]; then
|
|
356
400
|
fs_fail stale "cannot write \"$path\": file changed since it was read"
|
|
357
401
|
fi
|
|
@@ -363,12 +407,12 @@ fs_write() {
|
|
|
363
407
|
# it) where the peer's link(2) refuses any existing path, so the existence
|
|
364
408
|
# guard is checked before the call and the landing spot verified after it.
|
|
365
409
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
366
|
-
|
|
410
|
+
drop_staging
|
|
367
411
|
fs_fail exists "cannot write \"$path\": it already exists"
|
|
368
412
|
fi
|
|
369
413
|
ln -- "$tmp" "$path" 2>"$ERR_FILE" || {
|
|
370
414
|
err=$(cat "$ERR_FILE")
|
|
371
|
-
|
|
415
|
+
drop_staging
|
|
372
416
|
# A concurrent creator wins the race; classify by what is on disk now.
|
|
373
417
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
374
418
|
fs_fail exists "$err"
|
|
@@ -379,17 +423,17 @@ fs_write() {
|
|
|
379
423
|
# ln linked BESIDE the target (a directory appeared under us): undo the
|
|
380
424
|
# stray link and refuse, the guarded create did not publish.
|
|
381
425
|
rm -f -- "$path/${base}.tmp"
|
|
382
|
-
|
|
426
|
+
drop_staging
|
|
383
427
|
fs_fail exists "cannot write \"$path\": it was created concurrently"
|
|
384
428
|
fi
|
|
385
429
|
else
|
|
386
430
|
mv -f -- "$tmp" "$path" 2>"$ERR_FILE" || {
|
|
387
431
|
err=$(cat "$ERR_FILE")
|
|
388
|
-
|
|
432
|
+
drop_staging
|
|
389
433
|
fs_fail "$(fs_classify "$err")" "$err"
|
|
390
434
|
}
|
|
391
435
|
fi
|
|
392
|
-
|
|
436
|
+
drop_staging
|
|
393
437
|
}
|
|
394
438
|
|
|
395
439
|
fs_dispatch() {
|
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
|
@@ -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
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -32,6 +32,7 @@ are schema defaults, listed because they are the ones worth knowing.
|
|
|
32
32
|
| `subprocess-wsl` | `distro` | `''` *(shipped)* | which distro the GUI terminal opens in |
|
|
33
33
|
| | `shell` | `''` | pin a shell; empty lets `wsl.exe` decide |
|
|
34
34
|
| | `loginShell` | `true` | whether a pinned shell uses login semantics |
|
|
35
|
+
| | `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
36
|
|
|
36
37
|
You can override the other keys in the same way; they keep their defaults. Those are
|
|
37
38
|
`wslPath`, `hostCwd` and `forwardEnv` on the shell and terminal rows,
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -22,6 +22,9 @@ host stack over the distro's 9p share. What differs:
|
|
|
22
22
|
rather than quietly degrading to share semantics that cannot follow symlinks
|
|
23
23
|
or keep mode bits. The share substrate dies with the distro anyway; the
|
|
24
24
|
agent only adds its own failure mode, which is the one honest way to fail.
|
|
25
|
+
The failure message names the opt-out: set `substrate: "share"` in the
|
|
26
|
+
provider config to keep working on the Windows-side share while the agent
|
|
27
|
+
is out.
|
|
25
28
|
- **The mutation guard survives to the write.** `createIfAbsent` publishes with
|
|
26
29
|
a no-replace link inside the distro, which closes the check-then-write window
|
|
27
30
|
for creates outright. An overwrite or edit carries the version it was based
|
|
@@ -90,7 +93,10 @@ Everything below applies to the share substrate unless it says otherwise.
|
|
|
90
93
|
- The check before a write is "check, then write", which is not atomic. This backend
|
|
91
94
|
checks the caller's guard itself, because the Windows backend publishes a guarded
|
|
92
95
|
new file with a hard link, and the share does not support hard links. This is the
|
|
93
|
-
race that `dsh-fs-sandbox` already documents
|
|
96
|
+
race that `dsh-fs-sandbox` already documents — and one reason the share substrate
|
|
97
|
+
is the documented FALLBACK rather than the main path: its publication rides the
|
|
98
|
+
unconfined agent, so it never receives the confined resident's kernel-enforced
|
|
99
|
+
containment. The agent substrate does.
|
|
94
100
|
- `watch()` observes from inside the distro (a `find -newer` poll loop, see
|
|
95
101
|
`lib/watcher.js`), so events are coarse invalidation, not per-file notifications,
|
|
96
102
|
and they arrive on the poll cadence, not instantly. A file created with an mtime
|
|
@@ -104,5 +110,15 @@ Everything below applies to the share substrate unless it says otherwise.
|
|
|
104
110
|
default on): a WSL-folder session gets the distro shell, a Windows-folder session gets
|
|
105
111
|
`powershell.exe` in its own directory. The shell MENU remains the single configured
|
|
106
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.
|
|
107
123
|
- Terminal activity reporting stops at `wsl.exe`, so the controller never reclaims
|
|
108
124
|
these terminals when they are idle.
|
package/docs/PARITY.md
CHANGED
|
@@ -11,7 +11,7 @@ distro. Everything users love — native ext4 semantics, inotify, native
|
|
|
11
11
|
`rg`, low latency — is a derivative. This plugin's 9p-UNC plus one-shot
|
|
12
12
|
`wsl.exe` model can only approach it. The plan therefore lands a resident
|
|
13
13
|
in-distro agent and migrates the `fs` and `shell` seams onto it, keeping the
|
|
14
|
-
current paths as the documented fallback (`wsl.agent:
|
|
14
|
+
current paths as the documented fallback (`wsl.agent: false` must equal
|
|
15
15
|
today's behaviour byte for byte).
|
|
16
16
|
|
|
17
17
|
Not applicable, recorded so the gap is a decision and not an omission:
|
|
@@ -64,7 +64,8 @@ shaped a phase of the plan.
|
|
|
64
64
|
|
|
65
65
|
## Fallback invariant
|
|
66
66
|
|
|
67
|
-
Every phase must keep `wsl.agent:
|
|
67
|
+
Every phase must keep `wsl.agent: false` (and, per capability, any runtime
|
|
68
68
|
fallback) behaviourally identical to the pre-phase release. Probes assert
|
|
69
|
-
this: `agent.sh` runs the same
|
|
70
|
-
compares outcomes,
|
|
69
|
+
this: `agent.sh` runs the same operations through the resident and through
|
|
70
|
+
the one-shot `wsl.exe` path and compares outcomes — exit code, stdout bytes,
|
|
71
|
+
stderr bytes, working directory — not internals.
|
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
|
| 现象 | 原因 | 处理 |
|