dsh-plugin-wsl-env 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +274 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/cordis.patch.yml +306 -0
- package/examples/profile.cordis.patch.yml +36 -0
- package/lib/auto-preset.js +198 -0
- package/lib/fs.js +5 -0
- package/lib/index.js +871 -0
- package/lib/listing.js +112 -0
- package/lib/paths.js +265 -0
- package/lib/picker.js +350 -0
- package/lib/sandbox.js +162 -0
- package/lib/shell-env.js +74 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.js +230 -0
- package/lib/wsl.js +320 -0
- package/package.json +103 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
The reasoning and the measurements behind each entry are in the archived
|
|
8
|
+
engineering record,
|
|
9
|
+
[docs/archive/engineering-record.zh.md](docs/archive/engineering-record.zh.md)
|
|
10
|
+
(Chinese, repository only — it is history, never a second README). Every `§`
|
|
11
|
+
reference below points at that record's numbering.
|
|
12
|
+
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
## [0.1.0] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
First release. A WSL distro can be picked as a workspace, read and written
|
|
18
|
+
with the model's own file tools, and run commands in; the GUI terminal opens
|
|
19
|
+
inside it; commands are confined by a Linux-side sandbox built in the distro;
|
|
20
|
+
and the package installs as a bundle that contributes its own profile layer.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Distro-side sandboxing** (`lib/sandbox.js`, and `sandbox: true` on both
|
|
25
|
+
providers): WSL commands are now confined instead of running unconfined. The
|
|
26
|
+
Windows ACL runner can never reach WSL — its restricted token fails `wsl.exe`
|
|
27
|
+
with `Wsl/E_ACCESSDENIED` — so the confinement is built on the host and
|
|
28
|
+
executed *inside* the distro: `wsl.exe … --exec bwrap --ro-bind / / --dev /dev
|
|
29
|
+
--unshare-pid --proc /proc --die-with-parent [--tmpfs /tmp --bind <workspace>
|
|
30
|
+
<workspace>] -- <shell> -lc <cmd>`. The profile arguments, the denial dialect
|
|
31
|
+
(`read-only file system`) and the runner-failure rules mirror
|
|
32
|
+
`@deepseek-ai/dsh-sandbox-local`'s Linux rung, so a denial reads the same
|
|
33
|
+
whether the command ran on a Linux host or in a distro. `WslShellExecutor` and
|
|
34
|
+
`WslFileSystem` advertise the mode through their `sandboxMode` capability fact,
|
|
35
|
+
which brings back the Permissions selector and the denied→escalate flow
|
|
36
|
+
(`danger-full-access` skips the wrap entirely). Enforcement is reported as
|
|
37
|
+
**`partial`**, not `full`: a distro process can still execute a Windows binary
|
|
38
|
+
through interop, which bubblewrap does not govern — measured, not assumed, by
|
|
39
|
+
the probe below. Without `bubblewrap` in the distro the providers fail closed
|
|
40
|
+
with `SANDBOX_UNAVAILABLE`; `sandbox: false` restores the old posture and
|
|
41
|
+
removes the capability fact with it.
|
|
42
|
+
- Sandbox probes. `test/probe/sandbox.sh` (`npm run probe:sandbox`) needs no
|
|
43
|
+
harness: it applies the exact profile arguments and asserts what bubblewrap
|
|
44
|
+
governs — a write outside the workspace refused with EROFS, an ephemeral
|
|
45
|
+
sandbox `/tmp`, a read-only `/mnt/c`, and the interop escape, recorded as
|
|
46
|
+
`INFO` because a Linux sandbox cannot govern a Windows process.
|
|
47
|
+
`test/probe/sandbox-shell.sh` (`npm run probe:sandbox-shell`) boots the
|
|
48
|
+
throwaway profile with the executor mounted and drives the same three modes
|
|
49
|
+
through `ctx.shell`, asserting the result's `denied`/`enforcement` facts.
|
|
50
|
+
- `WslSubprocessRuntime` (`ctx.subprocess`, subpath
|
|
51
|
+
`dsh-plugin-wsl-env/subprocess`): the GUI's right-sidebar terminal window now
|
|
52
|
+
opens a shell *inside the distro*, in the Session workspace, instead of
|
|
53
|
+
`cmd.exe` in a UNC directory. It is a `LocalSubprocessRuntime` subclass whose
|
|
54
|
+
only override is `spawnTerminal`, so every ordinary command, the host ripgrep
|
|
55
|
+
search and every other subprocess consumer keeps the shipped implementation.
|
|
56
|
+
The `wsl.exe` launch is rewritten to `wsl.exe -d <distro> --cd <linux dir>`,
|
|
57
|
+
with the request's `DSH_*` facts forwarded through `WSLENV`, and the distro's
|
|
58
|
+
own login shell is left to `wsl.exe` unless the provider pins one.
|
|
59
|
+
- Terminal probe (`test/probe/terminal.sh`, `npm run probe:terminal`): boots the
|
|
60
|
+
throwaway Web profile with the provider in place, asks for the same
|
|
61
|
+
`spawnTerminal` request the GUI makes, and asserts the distro, the translated
|
|
62
|
+
initial directory, and the forwarded `DSH_SESSION_ID`.
|
|
63
|
+
|
|
64
|
+
- `WslShellExecutor` (`ctx.shell`): runs every command inside the distro as
|
|
65
|
+
`wsl.exe -d <distro> --cd <linux dir> --exec <shell> -lc <cmd>`, where the
|
|
66
|
+
shell is the distro user's login shell rather than a hardcoded bash. (§3, §12)
|
|
67
|
+
- `WslFileSystem` (`ctx.fs`): maps Linux paths onto the distro's UNC share so the
|
|
68
|
+
host fs stack — including the packaged ripgrep — operates on real distro files,
|
|
69
|
+
with Linux paths as the model-facing `displayPath` and the UNC path as the
|
|
70
|
+
opaque `targetKey`. (§3)
|
|
71
|
+
- Symlinked paths resolve by asking the distro for `readlink -f` and retrying
|
|
72
|
+
once, which is what makes `/etc/os-release`, `/bin` and `/lib` readable over a
|
|
73
|
+
share that cannot traverse POSIX symlinks. (§10.4)
|
|
74
|
+
- `WslDirectoryPicker` (`ctx.directoryPicker`): lists Windows home and every
|
|
75
|
+
installed distro at the root level, so the shipped GUI dialog becomes the
|
|
76
|
+
single workspace picker for host and distro folders alike. It reports
|
|
77
|
+
`kind: 'browse'` — a third kind would invalidate the three Remote verbs that
|
|
78
|
+
require it. (§8)
|
|
79
|
+
- `auto-preset`: binds the `wsl` agent preset when a new session's workspace is
|
|
80
|
+
inside a distro, inside the `ensureSession` frame so the very first mount is
|
|
81
|
+
already correct and the host ACL sandbox never touches a 9p path. (§13, §15)
|
|
82
|
+
- `shell-env`: contributes `DSH_WSL_DISTRO`, `DSH_WSL_SHELL` and `DSH_WSL_HOME`
|
|
83
|
+
to the managed `DSH_*` namespace, so the model can read the real environment
|
|
84
|
+
instead of inferring it from a tool name. (§14)
|
|
85
|
+
- Behavioural probe harness under `test/probe/`, with two throwaway profiles and
|
|
86
|
+
a negative-control run. (§16.5)
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- `WslFileSystem` fences its mutations by `ctx.sandboxPolicy`: `writeText` and
|
|
90
|
+
`editText` check the policy before any I/O, re-resolve the target so the checked
|
|
91
|
+
identity is the mutated one, refuse `read-only` and anything outside the
|
|
92
|
+
writable roots with `FS_SANDBOX_DENIED`, and refuse nothing at all under
|
|
93
|
+
`danger-full-access`. The writable roots are the workspace plus the temp area of
|
|
94
|
+
the world that workspace lives in — a distro workspace grants the distro's
|
|
95
|
+
`/tmp`, a Windows-folder workspace keeps upstream's `/tmp` + `os.tmpdir()`.
|
|
96
|
+
`@deepseek-ai/dsh-sandbox` returns as a peer: the plugin imports the shared
|
|
97
|
+
`canonicalPath`/`writableRoots` and the runner-failure/diagnostic classifiers
|
|
98
|
+
rather than restating them.
|
|
99
|
+
- The README's "one constraint that shapes everything" section is gone: it said
|
|
100
|
+
the plugin inherits `sandboxMode === undefined`, which is no longer true. The
|
|
101
|
+
replacement states the mechanism (Linux bubblewrap inside the distro), the mode
|
|
102
|
+
table, the `partial` enforcement and its interop cause, the `bubblewrap`
|
|
103
|
+
requirement and the fail-closed behaviour.
|
|
104
|
+
- An empty `cwd` now means **the distro user's home** in both WSL providers,
|
|
105
|
+
instead of the host path the shipped default resolves to. `WslFileSystem`'s
|
|
106
|
+
default was `process.cwd()` — a Windows directory no `wsl.exe --cd` accepts —
|
|
107
|
+
and `cordis.patch.yml` papered over it with the author's own `/home/andy`, a
|
|
108
|
+
value that is wrong on every other machine. `WslShellExecutor.resolve()` keeps
|
|
109
|
+
the seam's synchronous contract (`ctx.shell.execute(ctx.shell.resolve(…))` is
|
|
110
|
+
called with no `await` in between) and marks the case with an empty `workdir`;
|
|
111
|
+
`execute()` fills it from the distro's `$HOME`, one cached `wsl.exe` query per
|
|
112
|
+
provider instance. Only relative input consults the base at all, so an
|
|
113
|
+
absolute path never pays for the query, and `DSH_WSL_HOME` now takes the POSIX
|
|
114
|
+
home directly (`linuxHomePath`) instead of converting the world path back. The
|
|
115
|
+
`wslfs` probe follows: its profile no longer pins a `cwd`, and `fs-probe.mjs`
|
|
116
|
+
resolves a relative name and asserts it lands under the home the distro itself
|
|
117
|
+
reports — which is the only place that fallback was reachable at all, since the
|
|
118
|
+
tool layer always passes an absolute Session directory.
|
|
119
|
+
- The package is now a DSH **bundle**, so installing it applies the layer instead
|
|
120
|
+
of merely dropping code into a profile. `package.json` declares
|
|
121
|
+
`dsh.bundle.patch` → `./cordis.patch.yml`, which is the declaration
|
|
122
|
+
`dsh plugin --profile <name> add dsh-plugin-wsl-env` reads; `private: true` is
|
|
123
|
+
gone (npm refuses to publish it otherwise), and `repository`/`homepage`/`bugs`
|
|
124
|
+
now point at the GitHub repository. The package is plain JS with no
|
|
125
|
+
dependencies, so a published install needs no `allowBuilds` permission and no
|
|
126
|
+
build step. README's Install section follows: create the profile from the Web
|
|
127
|
+
template, `dsh plugin … add`, then `--dump-config`.
|
|
128
|
+
- `cordis.patch.yml` is now only the plugin's own layer. The four machine-local
|
|
129
|
+
rows it used to carry — `agent-default-model`, `ui-settings-account`,
|
|
130
|
+
`ui-chat`, `ui-settings` — moved to
|
|
131
|
+
[examples/profile.cordis.patch.yml](examples/profile.cordis.patch.yml), which
|
|
132
|
+
documents what a reader's own `$DSH_HOME/profiles/<name>/cordis.patch.yml` is
|
|
133
|
+
for and what a by-id row override looks like.
|
|
134
|
+
- The terminal is bound at the **composition** level, not in the `wsl` preset.
|
|
135
|
+
`dsh-api-terminal-controller` resolves its execution world with
|
|
136
|
+
`agent.ctx.get("subprocess")`, and an Agent's context is created by the agent
|
|
137
|
+
loop under the root realm (`createScope(loopCtx, …)`, `loopCtx` = the
|
|
138
|
+
root-mounted `ctx.agents`), while a preset's isolate realms belong to
|
|
139
|
+
`dsh-agent-preset-registry`'s own context (`createScope(this.owner, …)`). A
|
|
140
|
+
`subprocess` provider inside `preset-wsl` is therefore invisible to the
|
|
141
|
+
terminal window — the registry's `agentPresets.serviceFor(agent, name)` is the
|
|
142
|
+
sanctioned channel for that gap, and the controller does not use it.
|
|
143
|
+
`cordis.patch.yml` consequently disables the shipped `subprocess` row and
|
|
144
|
+
inserts this provider, and configures `terminal-controller` with
|
|
145
|
+
`shell: { path: wsl.exe, name: WSL }` and `shellCandidates: []`.
|
|
146
|
+
- `@deepseek-ai/dsh-subprocess-local` is now a declared peer. A linked plugin's
|
|
147
|
+
bare imports are routed through its `peerDependencies` by the runtime's
|
|
148
|
+
resolution interception, so an undeclared peer is not merely untidy — the
|
|
149
|
+
module fails to import with `failed to import`.
|
|
150
|
+
- The README is now **current state only**. The five sections describing designs
|
|
151
|
+
that this one replaced — the per-process `DSH_WSL` switch and the three
|
|
152
|
+
abandoned tool-naming routes — moved to
|
|
153
|
+
[docs/archive/design-history.zh.md](docs/archive/design-history.zh.md), which
|
|
154
|
+
keeps their original numbering and records where each conclusion lives now.
|
|
155
|
+
The README renumbered accordingly; its §0.1 explains the split. The one part of
|
|
156
|
+
the old desktop section that is still current (regenerating the generated
|
|
157
|
+
`preset-wsl` block after an app upgrade) stayed, as §13.9.
|
|
158
|
+
- Static review: the manifest now says only what the shipped code does.
|
|
159
|
+
- `main` pointed at `lib/index.js` while `exports["."]` resolved to
|
|
160
|
+
`lib/shell.js`. Only the latter exposes a default export, so a tool that
|
|
161
|
+
read `main` instead of `exports` got a module with no plugin in it.
|
|
162
|
+
- `lib/index.js` re-exported path helpers that nothing could reach: the
|
|
163
|
+
`exports` map does not expose `lib/index.js`, and the helpers already have
|
|
164
|
+
their own `./paths` and `./wsl` subpaths.
|
|
165
|
+
- Dropped an unused `ENV_OVERRIDES` import.
|
|
166
|
+
### Known limitations
|
|
167
|
+
|
|
168
|
+
- The sandbox does not govern WSL interop: a confined distro command can still
|
|
169
|
+
execute a Windows binary from `/mnt/c`, and that process runs outside
|
|
170
|
+
bubblewrap under the ordinary Windows token. Enforcement is therefore reported
|
|
171
|
+
as `partial`, and `npm run probe:sandbox` re-measures the escape. Closing it
|
|
172
|
+
would mean denying execution under `/mnt`, which breaks `/mnt/c/…` Sessions.
|
|
173
|
+
- `bubblewrap` must be installed in the distro, and both providers **fail
|
|
174
|
+
closed** without it (`SANDBOX_UNAVAILABLE` rather than a silent unconfined
|
|
175
|
+
run). `sandbox: false` is the documented opt-out. A `workspace-write` profile
|
|
176
|
+
also refuses to bind a workspace root that does not exist, so a Session whose
|
|
177
|
+
workspace directory was deleted reports a runner diagnostic instead of being
|
|
178
|
+
recreated.
|
|
179
|
+
- The GUI terminal is an interactive shell and is not wrapped in the sandbox,
|
|
180
|
+
exactly like the shipped terminal provider.
|
|
181
|
+
- The runtime mirror's destination lies outside every Session workspace, so the
|
|
182
|
+
sync step of `npm run probe` (and of a manual `sync-to-windows.sh`) needs an
|
|
183
|
+
approved `danger-full-access` escalation when an agent runs it.
|
|
184
|
+
- The terminal follows the composition, not the Session: a Session whose
|
|
185
|
+
workspace is a Windows folder gets the same distro terminal, started in that
|
|
186
|
+
folder as `/mnt/<drive>/…`.
|
|
187
|
+
- Terminal activity reporting stops at `wsl.exe`, so the controller's unattended
|
|
188
|
+
idle reclamation never fires for these terminals; close the tab to release the
|
|
189
|
+
process.
|
|
190
|
+
|
|
191
|
+
See the archived record's §7 for the full list, including the 9p
|
|
192
|
+
performance caveat, unsupported `watch()`, and the missing executable bit on
|
|
193
|
+
freshly created files.
|
|
194
|
+
### Removed
|
|
195
|
+
|
|
196
|
+
- `README.zh.md` stopped being a README: the file moved to
|
|
197
|
+
`docs/archive/engineering-record.zh.md` and left the package's `files` list, so
|
|
198
|
+
npm and GitHub now render exactly one README. The reasoning is under Fixed
|
|
199
|
+
below; nothing was deleted, only renamed out of the slot that made it look
|
|
200
|
+
current.
|
|
201
|
+
- Three peer declarations this plugin neither imports nor injects:
|
|
202
|
+
`@deepseek-ai/dsh-sandbox`, `@deepseek-ai/dsh-shell` and
|
|
203
|
+
`@deepseek-ai/dsh-tools`. They were required by the abandoned tool-renaming
|
|
204
|
+
designs (archive §15/§18), which could only stub the sandbox symbols locally
|
|
205
|
+
(archive §15.5). The remaining set is checkable in one command —
|
|
206
|
+
`grep -rho 'from "@deepseek-ai/[^"]*"' lib/ | sort -u` — plus
|
|
207
|
+
`@deepseek-ai/dsh-subprocess` (the service `static inject` requires) and
|
|
208
|
+
`@deepseek-ai/cordis`, which every DSH plugin declares.
|
|
209
|
+
(`@deepseek-ai/dsh-sandbox` came back in this same release: the distro-side
|
|
210
|
+
sandbox imports `canonicalPath`/`writableRoots` and the runner diagnostics, so
|
|
211
|
+
the declaration is earned again rather than vestigial — and that same check is
|
|
212
|
+
what surfaced it, by listing the package this entry had removed.)
|
|
213
|
+
### Fixed
|
|
214
|
+
|
|
215
|
+
- `lib/listing.js` built breadcrumbs with the platform-default
|
|
216
|
+
`dirname`/`basename` from `node:path` rather than the `win32` flavour its own
|
|
217
|
+
comments describe. On Linux — where the CI runners run, and where this
|
|
218
|
+
checkout itself lives — a UNC path parses as a single opaque POSIX segment,
|
|
219
|
+
so the ancestry chain collapsed to `['.', <whole path>]` and three
|
|
220
|
+
`test/listing.test.mjs` checks failed. The helpers serve Windows paths only;
|
|
221
|
+
they now call `win32.dirname`/`win32.basename` explicitly, and the suite
|
|
222
|
+
passes on Linux again.
|
|
223
|
+
|
|
224
|
+
Documentation that a reader would have acted on, and that was no longer true:
|
|
225
|
+
|
|
226
|
+
- The repository briefly carried two READMEs. `README.zh.md` called itself the
|
|
227
|
+
current description of the plugin while its conclusions had been superseded
|
|
228
|
+
three times over — most sharply by the sandbox work below — which is exactly
|
|
229
|
+
the ambiguity a second README creates. It is now
|
|
230
|
+
[docs/archive/engineering-record.zh.md](docs/archive/engineering-record.zh.md):
|
|
231
|
+
the same content, renamed out of the README slot and banner-headed as history,
|
|
232
|
+
with README.md the single statement of current state. Nothing was deleted from
|
|
233
|
+
the record; §0.1 names the conclusions that are now historical.
|
|
234
|
+
- §2.1 and §3.4 of that record conclude that "any WSL plugin must run on a
|
|
235
|
+
non-sandboxing provider" and that both providers report `sandboxMode:
|
|
236
|
+
undefined` (§9's recorded self-check prints exactly that). The Windows ACL
|
|
237
|
+
constraint in §2.1 holds and is unchanged; the *conclusion* it drew about our
|
|
238
|
+
own confinement does not, because the sandbox now runs inside the distro — see
|
|
239
|
+
the README's *Sandboxing* section.
|
|
240
|
+
- §9.4's three-step install ends with writing the plugin's rows into the profile's
|
|
241
|
+
own `cordis.patch.yml` by hand. The package is a bundle now, so that step is
|
|
242
|
+
gone (and the row set it describes has since gained the sandbox config); the
|
|
243
|
+
current install is in README.md.
|
|
244
|
+
- §9.6 and §10.5 claimed the `wsl` and `wsltest` profiles were still available,
|
|
245
|
+
and §13.6 said the same about `envweb`. All three are gone.
|
|
246
|
+
- The desktop section documented the superseded `build-desktop-patch.mjs`
|
|
247
|
+
regeneration flow instead of `build-preset-wsl.mjs`.
|
|
248
|
+
- §15.5 answered "nothing to do after an app upgrade" while the desktop section
|
|
249
|
+
said the generated preset copy has to be regenerated.
|
|
250
|
+
- References to `dsh-wsl-research/` now say it is a machine-local directory that
|
|
251
|
+
is not part of the repository.
|
|
252
|
+
|
|
253
|
+
- `wsl.exe` was handed the command without `--exec`, so the distro's default
|
|
254
|
+
shell expanded it once first: `$x`, `$?`, `$(...)`, `${...}` and backticks were
|
|
255
|
+
silently eaten. (§11)
|
|
256
|
+
- The shell was hardcoded to bash, discarding the PATH and rc setup of a user
|
|
257
|
+
whose login shell is zsh, fish or dash. (§12)
|
|
258
|
+
- Guarded creation (`createIfAbsent`) failed with `FS_IO_ERROR`, because the host
|
|
259
|
+
backend publishes it with a hard link and the share rejects hard links with
|
|
260
|
+
`ENOTSUP`; the guard is now checked by this backend and publication is a plain
|
|
261
|
+
rename. (§2.2)
|
|
262
|
+
- The managed `DSH_*` namespace never reached the distro. WSL imports only the
|
|
263
|
+
names listed in `WSLENV`, and the executor forwarded a fixed allowlist; it now
|
|
264
|
+
forwards the whole prefix, and marks the two Windows-path facts `DSH_HOME` and
|
|
265
|
+
`DSH_PROFILE_DIR` with `/p` so the distro sees `/mnt/c/...`. (§14.7)
|
|
266
|
+
- **Every write to an existing file, and therefore every edit, failed** with
|
|
267
|
+
`EIO: GetFileSecurityW EIO (Win32 1)`. `dsh-fs-local` takes a Windows branch
|
|
268
|
+
whenever the destination exists — to inherit the replaced file's DACL — and a
|
|
269
|
+
9p share carries no Windows security descriptor. Publication now uses the POSIX
|
|
270
|
+
path. (§16)
|
|
271
|
+
- Overwriting or editing a file silently dropped its POSIX mode, so editing a
|
|
272
|
+
script made it non-executable: the share ignores a host-side `chmod`, and a
|
|
273
|
+
host-side `stat` always reports 0666. The target's real mode is now copied onto
|
|
274
|
+
the staged temp file inside the distro, where a rename preserves it. (§16.3)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andy Zhao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# dsh-plugin-wsl-env
|
|
2
|
+
|
|
3
|
+
Run a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) session
|
|
4
|
+
against a WSL distro. Commands execute *inside* the distro, the model's file tools
|
|
5
|
+
read and write real distro files, and the GUI's folder dialog can open one.
|
|
6
|
+
|
|
7
|
+
> The engineering record this file summarises — design rationale, measurements,
|
|
8
|
+
> and every discarded design with the reason it was abandoned — is archived at
|
|
9
|
+
> **[docs/archive/engineering-record.zh.md](docs/archive/engineering-record.zh.md)**
|
|
10
|
+
> (Chinese). It is history, not a second README: where it and this file disagree,
|
|
11
|
+
> **this file is the current state**. Its §0.1 lists which of its conclusions the
|
|
12
|
+
> later sandbox work superseded.
|
|
13
|
+
|
|
14
|
+
## What it does
|
|
15
|
+
|
|
16
|
+
DSH is a Cordis application whose capabilities are exposed as *service seams*.
|
|
17
|
+
The model-facing tools (`bash`, `read`, `write`, `edit`, `glob`, `grep`) consume
|
|
18
|
+
those seams and never touch a filesystem or a shell directly. A WSL integration
|
|
19
|
+
is therefore not a new tool; it is two seam providers:
|
|
20
|
+
|
|
21
|
+
| Seam | Provider | Effect |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `ctx.shell` | `WslShellExecutor` | every command runs as `wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd>`, wrapped in a distro-side `bwrap` sandbox |
|
|
24
|
+
| `ctx.fs` | `WslFileSystem` | Linux paths map onto the distro's UNC share, so the host fs stack (and the packaged ripgrep) operates on real distro files, fenced by the same policy |
|
|
25
|
+
| `ctx.subprocess` | `WslSubprocessRuntime` | the GUI's right-sidebar terminal window opens a shell *inside the distro*, in the Session workspace, instead of `cmd.exe` in a UNC directory |
|
|
26
|
+
|
|
27
|
+
It also ships three smaller integrations:
|
|
28
|
+
|
|
29
|
+
| Package | Provides |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `dsh-plugin-wsl-env/picker` | `ctx.directoryPicker` — lists Windows home *and* every installed distro at the root level, so one dialog opens a host folder or a distro folder |
|
|
32
|
+
| `dsh-plugin-wsl-env/auto-preset` | binds the `wsl` agent preset when a new session's workspace is inside a distro, in the `ensureSession` frame, so the very first mount is already correct and the host ACL sandbox never touches a 9p path |
|
|
33
|
+
| `dsh-plugin-wsl-env/shell-env` | contributes `DSH_WSL_DISTRO`, `DSH_WSL_SHELL`, `DSH_WSL_HOME` to the managed `DSH_*` namespace the model reads |
|
|
34
|
+
|
|
35
|
+
## Why the terminal provider sits at the composition level
|
|
36
|
+
|
|
37
|
+
`ctx.shell` and `ctx.fs` are served *per Session*: they live in the `wsl` agent
|
|
38
|
+
preset's isolate realm, so a host workspace keeps the shipped sandboxed
|
|
39
|
+
PowerShell environment while a WSL workspace gets the distro, concurrently.
|
|
40
|
+
|
|
41
|
+
The terminal window cannot be served that way, and the reason is not visible in
|
|
42
|
+
the profile YAML. `dsh-api-terminal-controller` resolves its execution world from
|
|
43
|
+
the Session's own context — `agent.ctx.get("subprocess")` — and an Agent's
|
|
44
|
+
context is created by the agent loop under the **root** realm
|
|
45
|
+
(`createScope(loopCtx, …)`, where `loopCtx` is the root-mounted `ctx.agents`),
|
|
46
|
+
while a preset's isolate realms are created by `dsh-agent-preset-registry` under
|
|
47
|
+
the **registry's** context (`createScope(this.owner, …)`). The two subtrees never
|
|
48
|
+
meet, so a `subprocess` provider mounted inside `preset-wsl` is invisible to the
|
|
49
|
+
terminal window. The registry ships `agentPresets.serviceFor(agent, name)` for
|
|
50
|
+
exactly that gap; the terminal controller does not use it.
|
|
51
|
+
|
|
52
|
+
So [`cordis.patch.yml`](cordis.patch.yml) replaces the composition-level
|
|
53
|
+
`subprocess` row instead, with a subclass whose only override is
|
|
54
|
+
`spawnTerminal`: a `wsl.exe` launch is rewritten to
|
|
55
|
+
`wsl.exe -d <distro> --cd <linux dir>`, and everything else — every ordinary
|
|
56
|
+
`spawn()`, the host ripgrep search, the pwsh executor, the LSP host — reaches the
|
|
57
|
+
shipped implementation untouched.
|
|
58
|
+
|
|
59
|
+
Consequence worth stating plainly: the terminal follows the **composition**, not
|
|
60
|
+
the Session. A Session whose workspace is a Windows folder gets the same distro
|
|
61
|
+
terminal, started in that folder as `/mnt/<drive>/…`. That is the honest limit of
|
|
62
|
+
a per-session execution world that no Session identity reaches; and because
|
|
63
|
+
`terminalEnvironment()` and `resolveExecutable()` receive no Session at all, the
|
|
64
|
+
controller is configured with the single shell profile that matters
|
|
65
|
+
(`shell: { path: wsl.exe }`) rather than having executable lookup rewritten for
|
|
66
|
+
every root consumer to make one shell menu prettier.
|
|
67
|
+
|
|
68
|
+
## Sandboxing: a different mechanism, not the Windows one
|
|
69
|
+
|
|
70
|
+
On Windows, DSH confines commands with `dsh-sandbox-windows-acl`: a restricted,
|
|
71
|
+
low-integrity token plus a write allowlist. **That token cannot reach WSL at
|
|
72
|
+
all** — `wsl.exe` fails with `Wsl/E_ACCESSDENIED` and `\\wsl.localhost\<distro>`
|
|
73
|
+
reports access denied. Both work normally outside the sandbox. There is no way
|
|
74
|
+
to run a distro command under that token, so the WSL execution world cannot
|
|
75
|
+
inherit the Windows sandbox.
|
|
76
|
+
|
|
77
|
+
It gets the Linux one instead. `lib/sandbox.js` builds a **bubblewrap** profile
|
|
78
|
+
on the host and hands it to `wsl.exe --exec`, so the confinement is created and
|
|
79
|
+
enforced *inside* the distro:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
wsl.exe -d <distro> --cd <linux dir> --exec bwrap \
|
|
83
|
+
--ro-bind / / --dev /dev --unshare-pid --proc /proc --die-with-parent \
|
|
84
|
+
[--tmpfs /tmp --bind <workspace> <workspace>] -- <shell> -lc <cmd>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
That is DSH's own Linux rung, argument for argument (`dsh-sandbox-local`), which
|
|
88
|
+
is why the semantics and the diagnostics line up with a Linux host:
|
|
89
|
+
|
|
90
|
+
| Mode | What the distro command gets |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `read-only` | the whole distro read-only, `/dev/null` writable — the sink a shell needs |
|
|
93
|
+
| `workspace-write` | the above plus the Session workspace bound read-write and an ephemeral `/tmp` |
|
|
94
|
+
| `danger-full-access` | no wrap at all; the approved escalation |
|
|
95
|
+
|
|
96
|
+
`WslFileSystem` fences the same policy on its own mutation path (`writeText`,
|
|
97
|
+
`editText`) against the same writable roots, so "the write tool cannot write
|
|
98
|
+
/tmp but bash can" asymmetries do not arise. Both providers report the mode
|
|
99
|
+
through their `sandboxMode` capability fact, which is what brings back the
|
|
100
|
+
Permissions selector and the *denied → request escalation* flow.
|
|
101
|
+
|
|
102
|
+
**Enforcement is `partial`, and this is the honest part.** A WSL distro process
|
|
103
|
+
can execute a *Windows* binary through interop (`/mnt/c/…/*.exe`). That process
|
|
104
|
+
is not a Linux process: bubblewrap does not govern it, and it runs under your
|
|
105
|
+
ordinary Windows token, able to write anywhere you can. `npm run probe:sandbox`
|
|
106
|
+
demonstrates exactly that, and keeps demonstrating it. Closing the hole means
|
|
107
|
+
denying execution under `/mnt`, which would also break `/mnt/c/…` Sessions — so
|
|
108
|
+
it is stated rather than papered over. Network and process visibility are
|
|
109
|
+
outside `SandboxMode`'s vocabulary on every platform.
|
|
110
|
+
|
|
111
|
+
Two consequences worth knowing:
|
|
112
|
+
|
|
113
|
+
- **`bubblewrap` is required in the distro** (`sudo apt install bubblewrap`).
|
|
114
|
+
Without it the provider *fails closed*: every confined command reports
|
|
115
|
+
`SANDBOX_UNAVAILABLE` instead of quietly running unconfined. The capability
|
|
116
|
+
fact also disappears with the enforcement — an unusable runner never leaves a
|
|
117
|
+
"confined" claim behind.
|
|
118
|
+
- Configure `sandbox: false` on either provider to get the pre-sandbox
|
|
119
|
+
behaviour back: commands run unconfined and `sandboxMode` returns `undefined`,
|
|
120
|
+
so the tool layer tells the model these operations are not confined.
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
- Windows with WSL2 and at least one distro.
|
|
125
|
+
- `bubblewrap` inside the distro, for the sandbox (see above).
|
|
126
|
+
- DSH `0.2.0-rc.2`-era packages (`@deepseek-ai/dsh-base`, `dsh-web-app`).
|
|
127
|
+
- **No dependencies.** Everything the package needs is a peer, supplied by the
|
|
128
|
+
profile that mounts it.
|
|
129
|
+
|
|
130
|
+
## Install
|
|
131
|
+
|
|
132
|
+
The package is a DSH **bundle**: its `package.json` declares
|
|
133
|
+
`dsh.bundle.patch`, so `dsh plugin add` installs the code *and* applies
|
|
134
|
+
[`cordis.patch.yml`](cordis.patch.yml) as a configuration layer. There is no
|
|
135
|
+
patch to merge by hand. Start from the Web template, because the layer
|
|
136
|
+
substitutes web-surface rows — the composition-level `subprocess` provider, the
|
|
137
|
+
terminal controller, the directory picker:
|
|
138
|
+
|
|
139
|
+
```powershell
|
|
140
|
+
dsh wsl --from-default-profile web --dump-config # create the profile from the Web template
|
|
141
|
+
dsh plugin --profile wsl add dsh-plugin-wsl-env # npm; or link:C:\path\to\checkout while developing
|
|
142
|
+
dsh --profile wsl --dump-config # compose only, no boot: the fastest check
|
|
143
|
+
dsh --profile wsl
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`cordis.patch.yml` is commented line by line and is the authoritative install
|
|
147
|
+
reference, including the generated `preset-wsl` block. Two of its rows are not
|
|
148
|
+
additions but substitutions: it **disables** the shipped composition-level
|
|
149
|
+
`subprocess` provider and inserts this package's in its place, and it gives
|
|
150
|
+
`terminal-controller` the `wsl.exe` shell profile. Both are needed for the
|
|
151
|
+
terminal window and neither can live in the preset — see above.
|
|
152
|
+
|
|
153
|
+
Machine-local preferences do not belong in that file. Layers compose as
|
|
154
|
+
bundles, then the profile's own `cordis.patch.yml`, then the home-level one,
|
|
155
|
+
then `--patch` overlays, and a later layer wins per row — so override a row by
|
|
156
|
+
id in `$DSH_HOME/profiles/<name>/cordis.patch.yml`.
|
|
157
|
+
[`examples/profile.cordis.patch.yml`](examples/profile.cordis.patch.yml) is one
|
|
158
|
+
such layer, and shows what a row override looks like.
|
|
159
|
+
|
|
160
|
+
> A running process caches ES modules. **Restart the app after changing `lib/`**,
|
|
161
|
+
> or the old code stays loaded.
|
|
162
|
+
|
|
163
|
+
## Layout
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
lib/paths.js pure path translation between the three coordinate systems
|
|
167
|
+
lib/wsl.js wsl.exe interop primitives (no DSH imports)
|
|
168
|
+
lib/listing.js pure directory-listing and breadcrumb helpers (no DSH imports)
|
|
169
|
+
lib/index.js WslShellExecutor (ctx.shell) + WslFileSystem (ctx.fs)
|
|
170
|
+
lib/sandbox.js the distro-side bwrap confinement both providers apply
|
|
171
|
+
lib/picker.js WslDirectoryPicker (ctx.directoryPicker)
|
|
172
|
+
lib/subprocess.js WslSubprocessRuntime (ctx.subprocess) — the terminal window
|
|
173
|
+
lib/auto-preset.js per-session environment selection
|
|
174
|
+
lib/shell-env.js DSH_WSL_* environment facts
|
|
175
|
+
lib/{shell,fs}.js one-line subpath entry points
|
|
176
|
+
cordis.patch.yml the bundle patch layer (dsh.bundle), with comments
|
|
177
|
+
examples/ a machine-local profile layer, for contrast
|
|
178
|
+
test/ unit tests and the behavioural probes
|
|
179
|
+
docs/archive/ the designs this one replaced, and why
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm test # syntax check + unit tests — dependency-free, runs anywhere
|
|
186
|
+
npm run probe:sandbox # the bwrap profile's semantics, measured inside the distro
|
|
187
|
+
npm run probe # filesystem probe against a real distro (Windows + WSL only)
|
|
188
|
+
npm run probe:sandbox-shell # shell probe: drives the confined executor through a real boot
|
|
189
|
+
npm run probe:terminal # terminal probe: opens a PTY through the provider
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`npm test` covers the pure modules plus a `--check` parse pass over every shipped
|
|
193
|
+
module. It cannot import the service modules: they need DSH peers that a bare
|
|
194
|
+
checkout does not have. That leaves an evaluation-time gap which only booting the
|
|
195
|
+
harness closes — the archived record's §15.4 documents the five rounds of
|
|
196
|
+
misdiagnosis that gap once caused.
|
|
197
|
+
|
|
198
|
+
`test/probe/sandbox.sh` needs no harness at all: it applies the exact profile
|
|
199
|
+
arguments `lib/sandbox.js` builds and asserts what bubblewrap does and does not
|
|
200
|
+
govern — including the interop escape, which it records as `INFO` rather than a
|
|
201
|
+
failure because a Linux sandbox cannot govern a Windows process.
|
|
202
|
+
|
|
203
|
+
`test/probe/` drives the seams inside throwaway profiles bound to the distro.
|
|
204
|
+
The filesystem probe asserts the whole publication path — create, read, version
|
|
205
|
+
guard, edit, overwrite, mode preservation, and the two guard rejections — plus
|
|
206
|
+
the fence: a write outside the policy root and a `read-only` write are both
|
|
207
|
+
refused with `FS_SANDBOX_DENIED`, and `danger-full-access` is not fenced. The
|
|
208
|
+
shell probe boots the same profile with the WSL executor mounted and asserts the
|
|
209
|
+
same three modes through `ctx.shell`, including the denial classification the
|
|
210
|
+
tool layer renders. The terminal probe boots the Web profile with the provider in
|
|
211
|
+
place, asks for the same `spawnTerminal` request the GUI makes, and asserts the
|
|
212
|
+
shell it lands in: distro, initial directory, and the `DSH_*` fact forwarded
|
|
213
|
+
through `WSLENV`.
|
|
214
|
+
|
|
215
|
+
Those probes need a Windows-side profile whose `node_modules/dsh-plugin-wsl-env`
|
|
216
|
+
points at the checkout; `test/probe/run.sh` documents the one-time setup in its
|
|
217
|
+
header, and `terminal.sh` and `sandbox-shell.sh` reuse that same profile.
|
|
218
|
+
|
|
219
|
+
## Verified
|
|
220
|
+
|
|
221
|
+
Local, on Windows 11 + WSL2 (Ubuntu 26.04):
|
|
222
|
+
|
|
223
|
+
- the seam wiring, UNC primitives and picker behaviour, in and out of process;
|
|
224
|
+
- the plugin mounted in a real profile, end to end;
|
|
225
|
+
- a **real model turn** in a WSL-only headless profile:
|
|
226
|
+
`write → chmod → read → edit → execute`, with the executable bit surviving the
|
|
227
|
+
edit;
|
|
228
|
+
- the same `write`/`edit` tools in the daily GUI profile;
|
|
229
|
+
- the **terminal provider**, booted in the Web composition:
|
|
230
|
+
`spawnTerminal({ argv: ["wsl.exe"], cwd: <UNC workspace> })` lands in
|
|
231
|
+
`ubuntu` as the distro user's login shell (`/usr/bin/zsh`), in the workspace's
|
|
232
|
+
Linux path, with the request's `DSH_SESSION_ID` forwarded. `npm run
|
|
233
|
+
probe:terminal` re-runs exactly that, and it is what the throwaway Web boot
|
|
234
|
+
showed;
|
|
235
|
+
- the same provider **in the daily GUI profile**: pressing *New terminal* starts,
|
|
236
|
+
from the harness process, exactly
|
|
237
|
+
`wsl.exe -d ubuntu --cd /home/andy/Projects/dsh/plugins/dsh-plugin-wsl-env`
|
|
238
|
+
(recorded from the live process table), and the window lands in the distro
|
|
239
|
+
user's zsh.
|
|
240
|
+
- the **sandbox**, three ways: the profile arguments measured directly in the
|
|
241
|
+
distro (`npm run probe:sandbox`: a write outside the workspace is refused with
|
|
242
|
+
EROFS, the sandbox's `/tmp` is ephemeral, `/mnt/c` is read-only, and the
|
|
243
|
+
interop escape is recorded); the filesystem fence (`npm run probe`: a write
|
|
244
|
+
outside the policy root and a `read-only` write both refused with
|
|
245
|
+
`FS_SANDBOX_DENIED`, `danger-full-access` not fenced); and the shell path
|
|
246
|
+
(`npm run probe:sandbox-shell`: `sandboxMode` advertised, the same three modes
|
|
247
|
+
through `ctx.shell`, the refusal classified as a denial with
|
|
248
|
+
`enforcement: partial`).
|
|
249
|
+
- the sandbox **in the daily GUI profile**, from the agent's own session: a
|
|
250
|
+
command that writes outside the Session workspace is refused inside the distro
|
|
251
|
+
(`zsh: read-only file system: /home/andy/…`), and the tool layer answers with
|
|
252
|
+
the escalation hint that `sandbox_permissions` then grants — which is how the
|
|
253
|
+
runtime mirror gets synced now that its destination sits outside the workspace.
|
|
254
|
+
|
|
255
|
+
42 unit assertions, 18 filesystem-probe assertions and 10 shell-probe checks
|
|
256
|
+
pass; the bwrap probe adds 10 measured expectations plus the recorded escape, and
|
|
257
|
+
the terminal probe asserts three more.
|
|
258
|
+
|
|
259
|
+
## Known limitations
|
|
260
|
+
|
|
261
|
+
- **The sandbox does not govern WSL interop.** A confined distro command can
|
|
262
|
+
still execute a Windows binary from `/mnt/c`, and that Windows process runs
|
|
263
|
+
outside bubblewrap under your ordinary token. Closing it would mean denying
|
|
264
|
+
execution under `/mnt`, which breaks `/mnt/c/…` Sessions; the provider reports
|
|
265
|
+
`enforcement: partial` for exactly this reason, and `npm run probe:sandbox`
|
|
266
|
+
re-measures it. Network and process visibility are outside the mode vocabulary
|
|
267
|
+
on every platform.
|
|
268
|
+
- **`bubblewrap` must be installed in the distro**, and the providers fail
|
|
269
|
+
closed without it (`SANDBOX_UNAVAILABLE` for every confined command). That is
|
|
270
|
+
deliberate: the alternative is a silent run without the boundary the model was
|
|
271
|
+
told it has. `sandbox: false` is the documented opt-out.
|
|
272
|
+
- A `workspace-write` profile binds the workspace root read-write, and bubblewrap
|
|
273
|
+
refuses a bind whose source does not exist — so a Session whose workspace
|
|
274
|
+
directory has been deleted fails closed with a runner diagnostic rather than
|
|
275
|
+
being recreated.
|
|
276
|
+
- Freshly created files get the distro umask default (0644), not an executable
|
|
277
|
+
bit: a host-side `chmod` over the share is silently ignored. Overwrites and
|
|
278
|
+
edits *do* preserve the mode. `chmod +x` from inside the distro when needed.
|
|
279
|
+
- Guards are check-then-act, not atomic: this backend checks the caller's guard
|
|
280
|
+
itself because the host backend's guarded publication uses a hard link the
|
|
281
|
+
share rejects. The race is the one `dsh-fs-sandbox` already documents.
|
|
282
|
+
- `watch()` is refused outright rather than armed unreliably over 9p.
|
|
283
|
+
- `glob`/`grep` run the host ripgrep over the share: correct, but not fast, and
|
|
284
|
+
the `.gitignore` semantics are the host's.
|
|
285
|
+
- `editText` reads and rewrites the whole file in memory.
|
|
286
|
+
- The terminal is a **composition** property, not a Session one (see above): a
|
|
287
|
+
Windows-folder Session gets the distro terminal at `/mnt/<drive>/…`, and the
|
|
288
|
+
shell menu is deliberately reduced to the one configured profile.
|
|
289
|
+
- The GUI terminal is an interactive shell and is **not** wrapped in the
|
|
290
|
+
sandbox, exactly like the shipped terminal provider: the mode vocabulary
|
|
291
|
+
governs command execution and file mutations, not a human's interactive
|
|
292
|
+
session.
|
|
293
|
+
- Terminal activity reporting stops at `wsl.exe`. The shipped provider's shell
|
|
294
|
+
integration runs only for a direct `bash`/`zsh` launch on a POSIX host, so a
|
|
295
|
+
distro terminal reports `unknown` activity and the controller's unattended idle
|
|
296
|
+
reclamation never fires for it — close the tab to release the process.
|
|
297
|
+
|
|
298
|
+
## Repository notes
|
|
299
|
+
|
|
300
|
+
The checkout is developed inside the distro, but the harness is a Windows process
|
|
301
|
+
and a profile can only link a Windows path (`link:\\wsl.localhost\…` is rewritten
|
|
302
|
+
to a broken `/wsl.localhost/…` symlink by pnpm). The Windows copy under
|
|
303
|
+
`default-workspace/dsh-plugin-wsl` is therefore a **runtime mirror**; keep the two
|
|
304
|
+
in sync with `test/probe/sync-to-windows.sh` before launching the app.
|
|
305
|
+
|
|
306
|
+
That destination is outside every Session workspace, so an agent running the sync
|
|
307
|
+
through a confined shell is refused by the workspace-write policy and has to
|
|
308
|
+
approve `danger-full-access` for that one command. That is the sandbox working as
|
|
309
|
+
described above, not a broken script — run it from a plain distro terminal when
|
|
310
|
+
the prompt is unwelcome.
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT — see [LICENSE](LICENSE).
|