dsh-wsl-desktop 0.3.7 → 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/README.en.md +7 -5
- package/README.md +7 -5
- package/lib/index.js +345 -5
- package/lib/wsl/confined-argv.js +63 -0
- package/lib/wsl/preset.js +127 -15
- package/lib/wsl/pty.js +572 -14
- package/lib/wsl/sandbox.js +185 -0
- package/lib/wsl/search-tool.js +436 -0
- package/lib/wsl/search.js +288 -0
- package/lib/wsl/subprocess.js +5 -4
- package/lib/wsl/watch.js +14 -6
- package/lib/wsl/world.js +11 -0
- package/package.json +1 -1
package/lib/wsl/preset.js
CHANGED
|
@@ -35,9 +35,19 @@ export const WORLD_ROWS = new Set([
|
|
|
35
35
|
* depth. Beyond the five canonical rows, the persistent/terminal family ships
|
|
36
36
|
* NESTED in the minimal preset's `persistent-shell` group with non-canonical
|
|
37
37
|
* ids — an enabled host PowerShell persistent tool inside a preset whose name
|
|
38
|
-
* and persona assert distro-contained execution.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* and persona assert distro-contained execution.
|
|
39
|
+
*
|
|
40
|
+
* `tool-bash` and `tool-fs` are deliberately absent, and the reason is NOT "their
|
|
41
|
+
* WSL forms are re-mounted under the same package name". `tool-fs` IS re-mounted
|
|
42
|
+
* from `@deepseek-ai/dsh-tool-fs`; the realm's shell is
|
|
43
|
+
* `@deepseek-ai/dsh-tool-bash-persistent`, so a base row naming
|
|
44
|
+
* `@deepseek-ai/dsh-tool-bash` under a non-canonical id SURVIVES this transform
|
|
45
|
+
* (reported through `unclassified`, not removed, because the transform must not
|
|
46
|
+
* guess a row away) and would register a second `bash` beside the realm's own.
|
|
47
|
+
* What actually keeps exactly one owner is the row set in `buildWorldGroup`
|
|
48
|
+
* below, which mounts no one-shot shell row — not this list. Widening this set
|
|
49
|
+
* is therefore a separate decision with its own cost (it would also drop such a
|
|
50
|
+
* row from presets where it is the only shell), and it is not made here.
|
|
41
51
|
*/
|
|
42
52
|
export const WORLD_MODULE_NAMES = new Set([
|
|
43
53
|
'@deepseek-ai/dsh-tool-pwsh',
|
|
@@ -191,6 +201,25 @@ export function hasClassifierVerdict(name) {
|
|
|
191
201
|
))
|
|
192
202
|
}
|
|
193
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The persistent `bash` tool's model-facing description, restated LITERALLY.
|
|
206
|
+
*
|
|
207
|
+
* Nothing carries it into the variant: `prune()` records `toolConfig` only for the five
|
|
208
|
+
* canonical `WORLD_ROWS` ids plus `str-replace-editor`, while the persistent row is removed
|
|
209
|
+
* by MODULE NAME — so a bare re-mount would silently answer with the package default instead
|
|
210
|
+
* of the text the deployment declares. Source of truth for the text:
|
|
211
|
+
* `packages/bundle/web-app/presets/minimal.patch.yml:52-59`.
|
|
212
|
+
*/
|
|
213
|
+
export const PERSISTENT_BASH_DESCRIPTION = [
|
|
214
|
+
'Run commands in a bash shell',
|
|
215
|
+
'* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
|
|
216
|
+
'* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.',
|
|
217
|
+
'* State is persistent across command calls and discussions with the user.',
|
|
218
|
+
"* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.",
|
|
219
|
+
'* Please avoid commands that may produce a very large amount of output.',
|
|
220
|
+
"* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
|
|
221
|
+
].join('\n')
|
|
222
|
+
|
|
194
223
|
/**
|
|
195
224
|
* Build the `wsl-world` isolate group as a plugin-entry OBJECT.
|
|
196
225
|
*
|
|
@@ -202,12 +231,14 @@ export function hasClassifierVerdict(name) {
|
|
|
202
231
|
* @param {string} options.subprocessPath - module specifier of the WSL subprocess provider.
|
|
203
232
|
* @param {string} options.shellPath - module specifier of the WSL shell executor.
|
|
204
233
|
* @param {string} options.fsPath - module specifier of the WSL filesystem provider.
|
|
234
|
+
* @param {string} options.sandboxPath - module specifier of the WSL confinement provider.
|
|
235
|
+
* @param {string} options.searchPath - module specifier of this plugin's WSL search tools.
|
|
205
236
|
* @param {string | undefined} options.distro - distribution for paths that do not name one.
|
|
206
237
|
* @param {boolean} options.includeEditor - whether the source preset mounted the string-replace editor.
|
|
207
238
|
* @param {Map<string, object>} [options.toolConfig] - config the pruned host-world rows carried, keyed by row id.
|
|
208
239
|
* @returns {object} the group entry.
|
|
209
240
|
*/
|
|
210
|
-
export function buildWorldGroup({ subprocessPath, shellPath, fsPath, distro, includeEditor, toolConfig }) {
|
|
241
|
+
export function buildWorldGroup({ subprocessPath, shellPath, fsPath, sandboxPath, searchPath, distro, includeEditor, toolConfig }) {
|
|
211
242
|
// The pin belongs under `config`, NOT as a sibling row key. The loader hands a
|
|
212
243
|
// plugin ONLY `options.config` (`registry.plugin(plugin, this.options.config, …)`),
|
|
213
244
|
// and nothing relocates row keys into it — so as a bare `distro` key every
|
|
@@ -226,22 +257,101 @@ export function buildWorldGroup({ subprocessPath, shellPath, fsPath, distro, inc
|
|
|
226
257
|
{ id: 'subprocess-wsl', name: subprocessPath, ...distroConfig },
|
|
227
258
|
{ id: 'shell-wsl', name: shellPath, ...distroConfig },
|
|
228
259
|
{ id: 'fs-wsl', name: fsPath, ...distroConfig },
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
260
|
+
// The realm's own confinement provider, and the reason this group isolates `sandbox`
|
|
261
|
+
// (below). Two things are settled by mounting it HERE:
|
|
262
|
+
//
|
|
263
|
+
// - the realm stops asking the HOST. `ctx.get('sandbox')` inside an unisolated group
|
|
264
|
+
// resolved to `@deepseek-ai/dsh-sandbox-local`, whose Windows rung wraps the argv in
|
|
265
|
+
// an ACL restricted-token runner and grants the workspace root — an ACL grant on a
|
|
266
|
+
// 9P UNC spelling fails with `GetNamedSecurityInfoW … (Win32 1)`, so every terminal
|
|
267
|
+
// spawn in the realm died there rather than starting a shell;
|
|
268
|
+
// - the terminal stack gets the SAME in-distro fence the realm's shell executor
|
|
269
|
+
// already applies. `terminal-bash`'s `spawnArgv` wraps its argv through
|
|
270
|
+
// `ctx.get('sandbox')` for every mode but `danger-full-access`, and this provider
|
|
271
|
+
// builds that wrapper from `./confinement.js` + the distribution's own
|
|
272
|
+
// `dsh-wsl-confine` helper — not a second mechanism. Without this row the caller's
|
|
273
|
+
// argv reaches the PTY UNFENCED, which is the security face Task 1 measured.
|
|
274
|
+
{ id: 'sandbox-wsl', name: sandboxPath, ...distroConfig },
|
|
275
|
+
// The realm's terminal stack, HAND-WRITTEN — never a copy of the harness's rows: those
|
|
276
|
+
// carry `disabled: !!js process.platform === 'win32'` (`minimal.patch.yml:44,49`), which
|
|
277
|
+
// is evaluated in THIS host process and would mount the row DISABLED on this Windows
|
|
278
|
+
// desktop (F12). The removal rule and `prune` are untouched: these entries are appended
|
|
279
|
+
// inside the group AFTER the prune pass, so WORLD_MODULE_NAMES still owns their
|
|
280
|
+
// BASE-preset twins (design §3's two directions).
|
|
281
|
+
//
|
|
282
|
+
// `dsh-terminal` provides the `terminals` service; an unisolated service provider lands
|
|
283
|
+
// in the ROOT realm and the mount is refused, so the group declares `terminals: true`
|
|
284
|
+
// below (F11). The backend registers its PTY mechanism into that registry and spawns
|
|
285
|
+
// through `ctx.subprocess` — which inside this realm IS this plugin's distribution-side
|
|
286
|
+
// PTY provider (`lib/wsl/subprocess.js:269`), so the shell really is a PTY inside the
|
|
287
|
+
// distribution (F7).
|
|
288
|
+
{ id: 'pty', name: '@deepseek-ai/dsh-terminal' },
|
|
289
|
+
{ id: 'terminal-bash', name: '@deepseek-ai/dsh-terminal-bash' },
|
|
290
|
+
// ONE owner of the model-facing name `bash`. The persistent row registers `bash`, and so
|
|
291
|
+
// did the one-shot row that used to stand further down; the harness's own minimal preset
|
|
292
|
+
// resolves the same question the same way — it mounts no one-shot shell row BECAUSE it
|
|
293
|
+
// mounts the persistent stack (`apps/cli/tests/windows-shell.spec.ts:127-134`).
|
|
294
|
+
//
|
|
295
|
+
// WHICH of the two answered here was MEASURED before the one-shot row was removed: the
|
|
296
|
+
// live probe read M1 = P2 — the persistent shell completed `tools.bashPersist.b` and read
|
|
297
|
+
// back the cwd the first call made — so the persistent row is the realm's `bash` owner.
|
|
298
|
+
// The one-shot row was NOT inert, though: the pre-change inventory showed BOTH `bash`
|
|
299
|
+
// rows `fiberState: 2` with no `broken`, so re-registration did not throw in this realm
|
|
300
|
+
// (the store's duplicate contract at `packages/core/scope/src/store.ts:43-45` describes
|
|
301
|
+
// the store, not the observed mount — ruling R33). It was a second live registrant that
|
|
302
|
+
// could re-win the name on a registration-order change, which is why it had to go.
|
|
303
|
+
//
|
|
304
|
+
// Removing it also removes what that package provided and this one does not: the
|
|
305
|
+
// `tool:bash` systemPrompt section (`packages/shell/tool-bash/src/index.ts:251-255`) and
|
|
306
|
+
// the jobs-backed background path. The harness's minimal preset makes the same trade
|
|
307
|
+
// (`packages/bundle/web-app/presets/minimal.patch.yml`), so this is parity, stated rather
|
|
308
|
+
// than discovered later.
|
|
309
|
+
//
|
|
310
|
+
// Sources for the measurement: the ledger's 活体验证 section (ruling R29) and
|
|
311
|
+
// `artifacts/live-M1-M3-c1.txt:1550`. NOT this brief and not Task 1's report — neither
|
|
312
|
+
// recorded a measurement.
|
|
313
|
+
//
|
|
314
|
+
// The config is restated literally: `toolRow()` cannot carry it (see
|
|
315
|
+
// PERSISTENT_BASH_DESCRIPTION above), and the operator-visible timeout is part of the
|
|
316
|
+
// contract the model lives with (the harness's own row sets `timeoutMs: 300000`).
|
|
317
|
+
{ id: 'persistent-bash', name: '@deepseek-ai/dsh-tool-bash-persistent', config: {
|
|
318
|
+
timeoutMs: 300_000,
|
|
319
|
+
description: PERSISTENT_BASH_DESCRIPTION,
|
|
320
|
+
} },
|
|
321
|
+
// The Web terminal controller spawns through
|
|
322
|
+
// `agent.ctx.get('subprocess').spawnTerminal(...)`, so it needs no terminal row of its
|
|
323
|
+
// own: the realm's subprocess provider already puts its PTY inside the distribution.
|
|
324
|
+
// The operator's config for these rows travels with them: re-declaring them bare
|
|
325
|
+
// silently dropped settings the base preset had set (a non-default maxOutputChars on
|
|
326
|
+
// the editor, …), so a value honoured in the host world vanished in the WSL world.
|
|
237
327
|
toolRow('tool-fs', '@deepseek-ai/dsh-tool-fs'),
|
|
238
328
|
...(includeEditor ? [toolRow('str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor')] : []),
|
|
329
|
+
// Searching is re-mounted the same way fs is: the two NAMES the model already knows,
|
|
330
|
+
// over a module of THIS plugin. It cannot be a carried host row — the host's
|
|
331
|
+
// search package is removed by WORLD_MODULE_NAMES, so naming it back would recreate the
|
|
332
|
+
// row the transform exists to drop (its argv[0] is a packaged Windows ripgrep binary).
|
|
333
|
+
//
|
|
334
|
+
// Both rows name ONE module, so the row's config is what tells that instance which tool
|
|
335
|
+
// to register: toolRow() is deliberately NOT used here — it would reapply the pruned
|
|
336
|
+
// host search row's config (a `sampleOverCapGlobResults`/caps shape this module does not
|
|
337
|
+
// read, and whose `tool` field would be absent), failing the row at mount time instead of
|
|
338
|
+
// mounting it. The tool name is also exactly `grep`/`glob`: callers invoke tools by name,
|
|
339
|
+
// and the row id is a separate thing that happens to spell the same.
|
|
340
|
+
{ id: 'grep', name: searchPath, config: { tool: 'grep' } },
|
|
341
|
+
{ id: 'glob', name: searchPath, config: { tool: 'glob' } },
|
|
239
342
|
]
|
|
240
343
|
return {
|
|
241
344
|
id: 'wsl-world',
|
|
242
345
|
name: 'cordis:group',
|
|
243
346
|
group: true,
|
|
244
|
-
|
|
347
|
+
// `terminals` and `sandbox` are both required keys, for the same reason: the rows above
|
|
348
|
+
// PROVIDE those services, and the loader refuses a provider inside a group that does not
|
|
349
|
+
// isolate it (`mount.ts:213`) — but the isolation is also what makes each service answer
|
|
350
|
+
// from inside the realm. `sandbox` is the one that changes an observable: isolated, the
|
|
351
|
+
// terminal stack can no longer reach the host's sandbox provider and its Windows ACL
|
|
352
|
+
// runner, which fails on this realm's UNC workspace. `shell`, `fs` and `subprocess` are
|
|
353
|
+
// the realm's own execution seams.
|
|
354
|
+
isolate: { shell: true, fs: true, subprocess: true, terminals: true, sandbox: true },
|
|
245
355
|
config,
|
|
246
356
|
}
|
|
247
357
|
}
|
|
@@ -306,11 +416,13 @@ function cloneRow(row) {
|
|
|
306
416
|
* @param {string} options.subprocessPath - module specifier of the WSL subprocess provider.
|
|
307
417
|
* @param {string} options.shellPath - module specifier of the WSL shell executor.
|
|
308
418
|
* @param {string} options.fsPath - module specifier of the WSL filesystem provider.
|
|
419
|
+
* @param {string} options.sandboxPath - module specifier of the WSL confinement provider.
|
|
420
|
+
* @param {string} options.searchPath - module specifier of this plugin's WSL search tools.
|
|
309
421
|
* @param {string | undefined} options.distro - distribution for paths that do not name one.
|
|
310
422
|
* @param {string | undefined} options.sourceDir - directory the source preset lives in.
|
|
311
423
|
* @returns {{ plugins: object[], removed: string[], unclassified: { id: string | null, name: unknown }[] }} the variant rows, what was dropped, and the rows kept without a classifier verdict — an unreadable specifier, or a tool-family module that is neither removed nor verdicted-kept.
|
|
312
424
|
*/
|
|
313
|
-
export function buildVariantPlugins(basePlugins, { subprocessPath, shellPath, fsPath, distro, sourceDir }) {
|
|
425
|
+
export function buildVariantPlugins(basePlugins, { subprocessPath, shellPath, fsPath, sandboxPath, searchPath, distro, sourceDir }) {
|
|
314
426
|
const kept = []
|
|
315
427
|
const removed = []
|
|
316
428
|
/** Rows kept without a classifier verdict — both shapes the prune() branches record. */
|
|
@@ -392,6 +504,6 @@ export function buildVariantPlugins(basePlugins, { subprocessPath, shellPath, fs
|
|
|
392
504
|
const pruned = prune(row)
|
|
393
505
|
if (pruned !== null) kept.push(pruned)
|
|
394
506
|
}
|
|
395
|
-
kept.push(buildWorldGroup({ subprocessPath, shellPath, fsPath, distro, includeEditor, toolConfig }))
|
|
507
|
+
kept.push(buildWorldGroup({ subprocessPath, shellPath, fsPath, sandboxPath, searchPath, distro, includeEditor, toolConfig }))
|
|
396
508
|
return { plugins: kept, removed, unclassified }
|
|
397
509
|
}
|