dsh-wsl-desktop 0.7.2 → 0.7.4
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 +1 -0
- package/README.md +1 -0
- package/lib/client.js +21 -0
- package/lib/host/http.js +110 -0
- package/lib/host/presets.js +97 -0
- package/lib/host/state.js +58 -0
- package/lib/index.js +132 -280
- package/lib/wsl/confinement.js +18 -2
- package/lib/wsl/distros.js +190 -0
- package/lib/wsl/dsh-wsl-confine.sh +37 -4
- package/lib/wsl/fs.js +32 -0
- package/lib/wsl/preset.js +7 -0
- package/lib/wsl/proc.js +182 -0
- package/lib/wsl/sandbox.js +11 -4
- package/lib/wsl/shell.js +2 -1
- package/lib/wsl/subprocess.js +2 -1
- package/lib/wsl/world.js +4 -341
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -13,19 +13,25 @@
|
|
|
13
13
|
* @module dsh-wsl-desktop
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
19
|
-
import { homedir } from 'node:os'
|
|
16
|
+
import { watch as nativeFsWatch } from 'node:fs'
|
|
17
|
+
import { mkdir, readFile } from 'node:fs/promises'
|
|
20
18
|
import { dirname, join } from 'node:path'
|
|
21
19
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
22
20
|
import z from '@deepseek-ai/schemastery'
|
|
23
21
|
import {
|
|
24
22
|
MAX_BODY_BYTES, DEV_TOKEN_HEADER, bodyAdmission, dispatchAdmission, fenceRejection, methodAdmission, preflight, tokenMatches,
|
|
25
23
|
} from './http-admission.js'
|
|
24
|
+
import {
|
|
25
|
+
connectionOf, devToken, isDeveloperCaller, readJsonBody, sendJson, sendRejection,
|
|
26
|
+
} from './host/http.js'
|
|
27
|
+
import {
|
|
28
|
+
GENERATED_PREFIX, basePluginsOf, runShellSpec, sourceDirFor, variantIdFor,
|
|
29
|
+
} from './host/presets.js'
|
|
30
|
+
import { bindingTape, hostSession, presetRun } from './host/state.js'
|
|
26
31
|
import { buildVariantPlugins } from './wsl/preset.js'
|
|
27
32
|
import { setLocalSubprocess } from './wsl/host-refs.js'
|
|
28
|
-
import { checkLinuxPath,
|
|
33
|
+
import { checkLinuxPath, defaultWorkspaceUnc, listLinuxDir, resolveDistroHome, runWslShell } from './wsl/world.js'
|
|
34
|
+
import { defaultDistro, listDistros, wslMajorVersion } from './wsl/distros.js'
|
|
29
35
|
import { joinWslUnc, parseWslUnc, shellQuote, windowsToMntPath } from './wsl/paths.js'
|
|
30
36
|
import { WslRouteError, browserFailure, routeErrorFor } from './route-errors.js'
|
|
31
37
|
import { CHECK_IDS, CHECK_LABELS, CHECK_REMEDIES, DOCS, ERROR_CODES, FEATURE_IDS, FEATURE_LABELS } from './protocol.js'
|
|
@@ -87,23 +93,6 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
87
93
|
*/
|
|
88
94
|
const SELF_MODULE = fileURLToPath(import.meta.url)
|
|
89
95
|
|
|
90
|
-
/** Prefix of every preset this plugin owns; its own outputs are never sources. */
|
|
91
|
-
const GENERATED_PREFIX = 'wsl-'
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The generated variant id for a resolved base preset.
|
|
95
|
-
*
|
|
96
|
-
* A preset that is already a variant keeps its id; anything else gets the prefix. The
|
|
97
|
-
* selftest and createWorkspace both derived it this way — the second site's own comment
|
|
98
|
-
* admitted it resolved exactly like the first — so a change to the prefix rule had two
|
|
99
|
-
* homes.
|
|
100
|
-
* @param {{ id: string }} preset - the resolved base preset.
|
|
101
|
-
* @returns {string} the variant id.
|
|
102
|
-
*/
|
|
103
|
-
function variantIdFor(preset) {
|
|
104
|
-
return preset.id.startsWith(GENERATED_PREFIX) ? preset.id : `${GENERATED_PREFIX}${preset.id}`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
96
|
/**
|
|
108
97
|
* Module specifiers of the WSL providers, as the preset rows must name them.
|
|
109
98
|
* The 0.1.7 loader imports row names via `new URL(name, baseUrl)` or a bare
|
|
@@ -132,30 +121,6 @@ const SANDBOX_MODULE = pathToFileURL(join(HERE, 'wsl', 'sandbox.js')).href
|
|
|
132
121
|
*/
|
|
133
122
|
const SEARCH_MODULE = pathToFileURL(join(HERE, 'wsl', 'search-tool.js')).href
|
|
134
123
|
|
|
135
|
-
/** Last preset materialization outcome, reported through the route. */
|
|
136
|
-
let presetState = { status: 'pending' }
|
|
137
|
-
|
|
138
|
-
/** Recent preset-binding decisions, reported through the route. */
|
|
139
|
-
const bindingLog = []
|
|
140
|
-
|
|
141
|
-
/** Most binding decisions kept for inspection. */
|
|
142
|
-
const BINDING_LOG_LIMIT = 50
|
|
143
|
-
|
|
144
|
-
/** The plugin context, captured so route methods can reach host services. */
|
|
145
|
-
let hostCtx
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The configured `wsl.exe` path, captured where the route methods can read it.
|
|
149
|
-
*
|
|
150
|
-
* The methods table is module-level (the route dispatcher calls into it without
|
|
151
|
-
* carrying the plugin's config), so the configured path is held beside the
|
|
152
|
-
* captured context rather than threaded through every signature.
|
|
153
|
-
*/
|
|
154
|
-
let routeWslPath = 'wsl.exe'
|
|
155
|
-
|
|
156
|
-
/** Whether the preset registry has been reached; `bootstrap` waits briefly for it. */
|
|
157
|
-
let hostReady = false
|
|
158
|
-
|
|
159
124
|
/** The plugin version, read from the shipped package.json once. */
|
|
160
125
|
let pluginVersionValue = null
|
|
161
126
|
|
|
@@ -184,21 +149,21 @@ async function pluginVersion() {
|
|
|
184
149
|
* @returns {Promise<object>} the probe, or a pending marker.
|
|
185
150
|
*/
|
|
186
151
|
async function probeHostWhenReady() {
|
|
187
|
-
if (
|
|
152
|
+
if (hostSession.ctx === undefined) {
|
|
188
153
|
return { status: 'pending', features: {}, missing: [], checkedAt: new Date().toISOString() }
|
|
189
154
|
}
|
|
190
155
|
const deadline = Date.now() + 5_000
|
|
191
|
-
while (!
|
|
156
|
+
while (!hostSession.ready && Date.now() < deadline) {
|
|
192
157
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
193
158
|
}
|
|
194
|
-
if (!
|
|
159
|
+
if (!hostSession.ready) {
|
|
195
160
|
// The registry never appeared. Probing the context anyway reports the missing
|
|
196
161
|
// feature as degraded; returning `pending` here would leave the picker
|
|
197
162
|
// proceeding against a world it cannot name a preset in.
|
|
198
|
-
if (
|
|
163
|
+
if (hostSession.ctx.get('agentPresets') === undefined) return probeHost(hostSession.ctx)
|
|
199
164
|
return { status: 'pending', features: {}, missing: [], checkedAt: new Date().toISOString() }
|
|
200
165
|
}
|
|
201
|
-
return probeHost(
|
|
166
|
+
return probeHost(hostSession.ctx)
|
|
202
167
|
}
|
|
203
168
|
|
|
204
169
|
/**
|
|
@@ -209,14 +174,6 @@ const DISCOVERY_METHODS = new Set([
|
|
|
209
174
|
'listDistros', 'defaultDistro', 'listDir', 'checkPath', 'resolve', 'resolveHome', 'wslPresetFor',
|
|
210
175
|
])
|
|
211
176
|
|
|
212
|
-
/**
|
|
213
|
-
* The harness home that holds the user preset root.
|
|
214
|
-
* @returns {string} the resolved `$DSH_HOME`.
|
|
215
|
-
*/
|
|
216
|
-
function dshHome() {
|
|
217
|
-
return process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
177
|
/**
|
|
221
178
|
* Resolve a location the caller supplied into one distribution plus Linux path.
|
|
222
179
|
* @param {unknown} location - UNC path, absolute Linux path, or Windows drive path.
|
|
@@ -235,53 +192,6 @@ async function resolveLocation(location, distroHint) {
|
|
|
235
192
|
return { distro, linuxPath, uncPath: joinWslUnc(distro, linuxPath) }
|
|
236
193
|
}
|
|
237
194
|
|
|
238
|
-
/**
|
|
239
|
-
* Locate the directory a preset was discovered under.
|
|
240
|
-
*
|
|
241
|
-
* A preset may name a row by a path relative to its own directory, so the
|
|
242
|
-
* variant needs that base to rewrite the name to something that still resolves
|
|
243
|
-
* from where the variant lives.
|
|
244
|
-
*
|
|
245
|
-
* Both spellings are historical: the package is `@deepseek-ai/dsh-agent-preset`
|
|
246
|
-
* (singular — the plural name never existed, so this resolve always threw), and
|
|
247
|
-
* since the presets moved into profile YAML declarations the package ships no
|
|
248
|
-
* `presets/<id>/agent.cordis.yml` tree at all. Neither branch is expected to
|
|
249
|
-
* hit on a current desktop; a base preset that DOES use relative row names
|
|
250
|
-
* therefore has no resolvable source directory, and `buildVariantPlugins`
|
|
251
|
-
* leaves the name as it found it. This stays a lookup rather than a guess so
|
|
252
|
-
* the failure mode is unchanged if a deployment ever ships that tree again.
|
|
253
|
-
* @param {string} id - preset id.
|
|
254
|
-
* @returns {string | undefined} the directory, or undefined when it cannot be found.
|
|
255
|
-
*/
|
|
256
|
-
function sourceDirFor(id) {
|
|
257
|
-
const local = join(dshHome(), '.agent-presets', id)
|
|
258
|
-
if (existsSync(join(local, 'agent.cordis.yml'))) return local
|
|
259
|
-
try {
|
|
260
|
-
const manifest = fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-agent-preset/package.json'))
|
|
261
|
-
const shipped = join(dirname(manifest), 'presets', id)
|
|
262
|
-
if (existsSync(join(shipped, 'agent.cordis.yml'))) return shipped
|
|
263
|
-
} catch {
|
|
264
|
-
// A deployment without the preset package leaves relative names as they are.
|
|
265
|
-
}
|
|
266
|
-
return undefined
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Read a base preset's declared plugin rows as OBJECTS.
|
|
271
|
-
*
|
|
272
|
-
* 0.1.7 keeps each declaration's `PresetDefinition` — with `plugins` as entry
|
|
273
|
-
* objects — in a registry map that is TS-private but reachable in-process
|
|
274
|
-
* (ctx.get returns the service instance). No YAML parser is needed on this
|
|
275
|
-
* path; the public readDocument only exposes a re-dumped text form.
|
|
276
|
-
* @param {object} presets - the agentPresets service.
|
|
277
|
-
* @param {string} id - base preset id.
|
|
278
|
-
* @returns {readonly object[] | null} the declared rows, or null when unreachable.
|
|
279
|
-
*/
|
|
280
|
-
function basePluginsOf(presets, id) {
|
|
281
|
-
const plugins = presets.definitions?.get?.(id)?.config?.plugins
|
|
282
|
-
return Array.isArray(plugins) ? plugins : null
|
|
283
|
-
}
|
|
284
|
-
|
|
285
195
|
/**
|
|
286
196
|
* Derive and register the WSL variant presets (0.1.7-native).
|
|
287
197
|
*
|
|
@@ -303,24 +213,6 @@ function basePluginsOf(presets, id) {
|
|
|
303
213
|
* refuses duplicates).
|
|
304
214
|
* @returns {Promise<object>} the registration outcome for the route.
|
|
305
215
|
*/
|
|
306
|
-
/**
|
|
307
|
-
* Run a shell spec through whichever seam the host exposes, and always hand back a
|
|
308
|
-
* handle whose `result()` is awaitable.
|
|
309
|
-
*
|
|
310
|
-
* 0.1.7 hosts expose `execute`, which resolves with a live handle; older ones have
|
|
311
|
-
* only `run`, whose settled result carries no `result()` projection. Both selftest
|
|
312
|
-
* steps used to spell that dual dispatch themselves, and the second one called
|
|
313
|
-
* `.result()` on `run`'s settled result — a TypeError on exactly the hosts that
|
|
314
|
-
* branch exists to support. One owner, so the two steps cannot drift again.
|
|
315
|
-
* @param {object} shell - the realm's shell service.
|
|
316
|
-
* @param {object} spec - the resolved spec to run.
|
|
317
|
-
* @returns {Promise<{ result: () => Promise<object> }>} a result()-able handle.
|
|
318
|
-
*/
|
|
319
|
-
function runShellSpec(shell, spec) {
|
|
320
|
-
if (typeof shell.execute === 'function') return shell.execute(spec)
|
|
321
|
-
return shell.run(spec).then((settled) => ({ result: async () => settled }))
|
|
322
|
-
}
|
|
323
|
-
|
|
324
216
|
async function materializePresets(ctx, distro, generation, previous) {
|
|
325
217
|
const presets = ctx.get('agentPresets')
|
|
326
218
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
@@ -415,12 +307,6 @@ async function materializePresets(ctx, distro, generation, previous) {
|
|
|
415
307
|
return { written, skipped, hostCompat }
|
|
416
308
|
}
|
|
417
309
|
|
|
418
|
-
/** Serialises refreshPresets so boot-time and developer-token calls cannot interleave. */
|
|
419
|
-
let refreshMutex = Promise.resolve()
|
|
420
|
-
|
|
421
|
-
/** The live registration generation; teardown flips its disposed flag. */
|
|
422
|
-
let currentGeneration = null
|
|
423
|
-
|
|
424
310
|
/**
|
|
425
311
|
* Refresh the presets, recording the outcome for the route.
|
|
426
312
|
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin context.
|
|
@@ -429,7 +315,7 @@ let currentGeneration = null
|
|
|
429
315
|
* generation's lifecycle token (see materializePresets).
|
|
430
316
|
* @returns {Promise<object>} the new preset state.
|
|
431
317
|
*/
|
|
432
|
-
async function refreshPresets(ctx, distro, generation =
|
|
318
|
+
async function refreshPresets(ctx, distro, generation = presetRun.generation) {
|
|
433
319
|
const run = async () => {
|
|
434
320
|
// A caller may omit the generation (developer-token regenerate before the
|
|
435
321
|
// inject ran): mint a transient one so every disposer has an owner.
|
|
@@ -437,19 +323,19 @@ async function refreshPresets(ctx, distro, generation = currentGeneration) {
|
|
|
437
323
|
// Withdrawal targets the PREVIOUS generation's disposers; a self-refresh
|
|
438
324
|
// (developer-token regenerate) withdraws its own — the exact behaviour of
|
|
439
325
|
// the historical shared map, minus the cross-generation teardown theft.
|
|
440
|
-
const previous =
|
|
441
|
-
|
|
442
|
-
|
|
326
|
+
const previous = presetRun.generation === gen ? gen : presetRun.generation
|
|
327
|
+
presetRun.generation = gen
|
|
328
|
+
presetRun.state = { status: 'running' }
|
|
443
329
|
try {
|
|
444
330
|
const result = await materializePresets(ctx, distro, gen, previous)
|
|
445
|
-
|
|
331
|
+
presetRun.state = { status: 'ready', ...result }
|
|
446
332
|
} catch (error) {
|
|
447
|
-
|
|
333
|
+
presetRun.state = { status: 'failed', error: error instanceof Error ? error.message : String(error) }
|
|
448
334
|
}
|
|
449
|
-
return
|
|
335
|
+
return presetRun.state
|
|
450
336
|
}
|
|
451
|
-
const result =
|
|
452
|
-
|
|
337
|
+
const result = presetRun.mutex.then(run, run)
|
|
338
|
+
presetRun.mutex = result.then(() => {}, () => {})
|
|
453
339
|
return result
|
|
454
340
|
}
|
|
455
341
|
|
|
@@ -510,8 +396,8 @@ const METHODS = {
|
|
|
510
396
|
return healthFor({
|
|
511
397
|
distro,
|
|
512
398
|
username,
|
|
513
|
-
wslPath:
|
|
514
|
-
wslVersionProbe: ({ distro: name }) => wslMajorVersion(name, { wslPath:
|
|
399
|
+
wslPath: hostSession.wslPath,
|
|
400
|
+
wslVersionProbe: ({ distro: name }) => wslMajorVersion(name, { wslPath: hostSession.wslPath }),
|
|
515
401
|
})
|
|
516
402
|
},
|
|
517
403
|
|
|
@@ -519,7 +405,7 @@ const METHODS = {
|
|
|
519
405
|
* List the installed distributions.
|
|
520
406
|
* @returns {Promise<string[]>} distribution names.
|
|
521
407
|
*/
|
|
522
|
-
listDistros: () => listDistros({ wslPath:
|
|
408
|
+
listDistros: () => listDistros({ wslPath: hostSession.wslPath }),
|
|
523
409
|
|
|
524
410
|
/**
|
|
525
411
|
* Report the default distribution.
|
|
@@ -536,7 +422,7 @@ const METHODS = {
|
|
|
536
422
|
assertDistro(distro)
|
|
537
423
|
assertLinuxPath(path ?? '/')
|
|
538
424
|
try {
|
|
539
|
-
return await listLinuxDir(distro, path ?? '/', { wslPath:
|
|
425
|
+
return await listLinuxDir(distro, path ?? '/', { wslPath: hostSession.wslPath })
|
|
540
426
|
} catch (error) {
|
|
541
427
|
throw routeErrorFor(error, { distro, path })
|
|
542
428
|
}
|
|
@@ -554,7 +440,7 @@ const METHODS = {
|
|
|
554
440
|
const uncPath = joinWslUnc(distro, path)
|
|
555
441
|
let facts
|
|
556
442
|
try {
|
|
557
|
-
facts = await checkLinuxPath(distro, path, { wslPath:
|
|
443
|
+
facts = await checkLinuxPath(distro, path, { wslPath: hostSession.wslPath })
|
|
558
444
|
} catch (error) {
|
|
559
445
|
throw routeErrorFor(error, { distro, path })
|
|
560
446
|
}
|
|
@@ -601,7 +487,7 @@ const METHODS = {
|
|
|
601
487
|
assertDistro(distro)
|
|
602
488
|
assertUser(username)
|
|
603
489
|
try {
|
|
604
|
-
return await resolveDistroHome(distro, username, { wslPath:
|
|
490
|
+
return await resolveDistroHome(distro, username, { wslPath: hostSession.wslPath })
|
|
605
491
|
} catch (error) {
|
|
606
492
|
throw routeErrorFor(error, { distro, user: username })
|
|
607
493
|
}
|
|
@@ -617,7 +503,7 @@ const METHODS = {
|
|
|
617
503
|
* @returns {Promise<{ agentPreset: string, base: string }>} the variant to request.
|
|
618
504
|
*/
|
|
619
505
|
wslPresetFor: async ({ base } = {}) => {
|
|
620
|
-
const presets =
|
|
506
|
+
const presets = hostSession.ctx?.get('agentPresets')
|
|
621
507
|
if (presets === undefined) throw new WslRouteError('host-capability-missing', { feature: 'preset-generation' })
|
|
622
508
|
let resolved
|
|
623
509
|
try {
|
|
@@ -669,7 +555,7 @@ const METHODS = {
|
|
|
669
555
|
...(username !== undefined ? { username } : {}),
|
|
670
556
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
671
557
|
...(raw === true ? { raw: true } : {}),
|
|
672
|
-
wslPath:
|
|
558
|
+
wslPath: hostSession.wslPath,
|
|
673
559
|
})
|
|
674
560
|
return { ...result, target }
|
|
675
561
|
},
|
|
@@ -679,7 +565,7 @@ const METHODS = {
|
|
|
679
565
|
* @returns {Promise<object>} the state, plus `deployed` — the path the running host loaded.
|
|
680
566
|
*/
|
|
681
567
|
presetStatus: () => Promise.resolve({
|
|
682
|
-
...
|
|
568
|
+
...presetRun.state,
|
|
683
569
|
deployed: { modulePath: SELF_MODULE },
|
|
684
570
|
}),
|
|
685
571
|
|
|
@@ -708,8 +594,8 @@ const METHODS = {
|
|
|
708
594
|
let handle
|
|
709
595
|
let agent
|
|
710
596
|
try {
|
|
711
|
-
const agents =
|
|
712
|
-
const presets =
|
|
597
|
+
const agents = hostSession.ctx.get('agents')
|
|
598
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
713
599
|
if (agents === undefined || presets === undefined) throw new Error('agents / agentPresets 服务不可用')
|
|
714
600
|
// NOT /tmp: the fence refuses a workspace root under /tmp by design (the
|
|
715
601
|
// confinement mounts a private tmpfs over /tmp AFTER binding the workspace,
|
|
@@ -721,7 +607,7 @@ const METHODS = {
|
|
|
721
607
|
// throws before any probe — and then handed its {user, home} result to a
|
|
722
608
|
// function that requires a path string. Nothing reddened: every acceptance
|
|
723
609
|
// path passes cwd. The default is now exercised by verify-world/check-2.
|
|
724
|
-
const workspaceCwd = cwd ?? (await defaultWorkspaceUnc({ wslPath:
|
|
610
|
+
const workspaceCwd = cwd ?? (await defaultWorkspaceUnc({ wslPath: hostSession.wslPath })).uncPath
|
|
725
611
|
const sessionId = `wsl-selftest-${Date.now()}`
|
|
726
612
|
handle = await agents.create({ sessionId, meta: { cwd: workspaceCwd, agentPreset: preset } })
|
|
727
613
|
agent = handle?.agent ?? handle
|
|
@@ -739,7 +625,7 @@ const METHODS = {
|
|
|
739
625
|
if (presets.composedPreset(agent.ctx) !== preset) {
|
|
740
626
|
try {
|
|
741
627
|
await presets.select(agent, preset)
|
|
742
|
-
record('select', { ok: true, composed: presets.composedPreset(agent.ctx) ?? null, bindings:
|
|
628
|
+
record('select', { ok: true, composed: presets.composedPreset(agent.ctx) ?? null, bindings: bindingTape.log.length })
|
|
743
629
|
} catch (error) {
|
|
744
630
|
record('select', { ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
745
631
|
}
|
|
@@ -896,11 +782,17 @@ const METHODS = {
|
|
|
896
782
|
// Created through the shell this block already runs its mutations with. A create
|
|
897
783
|
// that FAILED must not read as a false green, so its outcome is reported in the
|
|
898
784
|
// row: an absent path would notify from its arming baseline and pass on nothing.
|
|
899
|
-
|
|
785
|
+
// The path is shellQuote'd, not interpolated inside hand-written quotes: the Linux
|
|
786
|
+
// path grammar refuses control characters and backslashes but NOT an apostrophe
|
|
787
|
+
// (measured), so `'${path}'` would end the word early and the remainder would run
|
|
788
|
+
// as shell syntax. Every other command in this file already goes through the
|
|
789
|
+
// helper; these four were the exception.
|
|
790
|
+
const probePath = shellQuote(fs.processPath(fileTarget))
|
|
791
|
+
const probeCreated = await runShellSpec(shell, shell.resolve({ command: `printf 'x\\n' > ${probePath}`, workdir: workspaceCwd, sandboxPolicy: workspaceWritePolicy }))
|
|
900
792
|
.then((execution) => execution.result())
|
|
901
793
|
.catch((error) => ({ exitCode: null, stderr: { text: String(error?.message ?? error) } }))
|
|
902
794
|
record('fs.watch.file', probeCreated.exitCode === 0
|
|
903
|
-
? await observeWatch(fileTarget, `printf 'x\\n' >>
|
|
795
|
+
? await observeWatch(fileTarget, `printf 'x\\n' >> ${probePath}`)
|
|
904
796
|
: {
|
|
905
797
|
notified: 0,
|
|
906
798
|
watchError: `the probe file could not be created: exit ${probeCreated.exitCode ?? 'null'} ${probeCreated.stderr?.text?.slice(0, 200) ?? ''}`.trim(),
|
|
@@ -908,9 +800,10 @@ const METHODS = {
|
|
|
908
800
|
})
|
|
909
801
|
|
|
910
802
|
const dirTarget = await fs.resolve('.', { cwd: workspaceCwd })
|
|
911
|
-
const
|
|
803
|
+
const dirEntry = shellQuote(`${fs.processPath(dirTarget)}/dsh-wsl-watch-entry.txt`)
|
|
804
|
+
const dirCommand = `printf 'x\\n' > ${dirEntry}`
|
|
912
805
|
record('fs.watch.dir', await observeWatch(dirTarget, dirCommand))
|
|
913
|
-
await runShellSpec(shell, shell.resolve({ command: `rm -f
|
|
806
|
+
await runShellSpec(shell, shell.resolve({ command: `rm -f ${dirEntry}`, workdir: workspaceCwd, sandboxPolicy: workspaceWritePolicy }))
|
|
914
807
|
.then((execution) => execution.result()).catch(() => {})
|
|
915
808
|
|
|
916
809
|
// Why the override exists, recorded rather than assumed: if a future host or
|
|
@@ -958,9 +851,21 @@ const METHODS = {
|
|
|
958
851
|
|
|
959
852
|
// The model calls tools, not providers. Driving the registry is what makes
|
|
960
853
|
// the acceptance cover the layer a session actually uses.
|
|
961
|
-
const tools =
|
|
854
|
+
const tools = hostSession.ctx.get('tools')
|
|
962
855
|
if (tools !== undefined && agent !== undefined) {
|
|
963
856
|
const controller = new AbortController()
|
|
857
|
+
/**
|
|
858
|
+
* The text one tool outcome reports, capped for the receipt.
|
|
859
|
+
*
|
|
860
|
+
* The `.text`-first rule lives here rather than repeated at seven call sites: every
|
|
861
|
+
* row wants the tool's OWN output, and falls back to the serialized result only when
|
|
862
|
+
* the outcome carries no text (a failure has no `value` to serialize). The cap
|
|
863
|
+
* differs per row because the rows read different amounts of that output.
|
|
864
|
+
* @param {{ text?: string, result?: unknown }} outcome - one `invoke` result.
|
|
865
|
+
* @param {number} limit - maximum characters kept.
|
|
866
|
+
* @returns {string} the text to record.
|
|
867
|
+
*/
|
|
868
|
+
const outcomeText = (outcome, limit) => (outcome.text ?? JSON.stringify(outcome.result ?? null)).slice(0, limit)
|
|
964
869
|
/**
|
|
965
870
|
* Execute one tool on the session's behalf.
|
|
966
871
|
* @param {string} toolName - registered tool name.
|
|
@@ -969,14 +874,47 @@ const METHODS = {
|
|
|
969
874
|
*/
|
|
970
875
|
const invoke = async (toolName, args) => {
|
|
971
876
|
try {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
877
|
+
// Host 0.2.1-rc.3 changed `tools.execute` from ONE call to a BATCH: it now
|
|
878
|
+
// takes `{ calls: ToolCallBlock[], signal, agent? }` and answers
|
|
879
|
+
// `{ results, additionalContexts, concluded }`. The previous single-call
|
|
880
|
+
// object is rejected with `ToolBatchExecutionError` ("Tool batch execution
|
|
881
|
+
// failed"), which every tool row below reported as a null result.
|
|
882
|
+
//
|
|
883
|
+
// `ToolCallBlock.arguments` is the RAW JSON STRING the model produces, not an
|
|
884
|
+
// object — the block is the wire shape, not a convenience wrapper.
|
|
885
|
+
const batch = await tools.execute({
|
|
886
|
+
calls: [{
|
|
887
|
+
type: 'tool-call',
|
|
888
|
+
id: `wsl-selftest-${toolName}-${Date.now()}`,
|
|
889
|
+
name: toolName,
|
|
890
|
+
arguments: JSON.stringify(args ?? {}),
|
|
891
|
+
}],
|
|
976
892
|
agent,
|
|
977
893
|
signal: controller.signal,
|
|
978
894
|
})
|
|
979
|
-
|
|
895
|
+
const outcome = batch?.results?.[0]
|
|
896
|
+
if (outcome === undefined) return { ok: false, error: 'the batch returned no result for this call' }
|
|
897
|
+
// `ToolCallResult` is `{ callId, result: ToolExecutionResult }`, and
|
|
898
|
+
// `ToolExecutionResult` is a DISCRIMINATED UNION on `isError`: a success carries
|
|
899
|
+
// `{ isError: false, value }` and a failure carries `{ isError: true, error }`
|
|
900
|
+
// where `error` is a `ToolFailure { message, info? }` and `value` is `never`.
|
|
901
|
+
// Switching on the discriminant is the contract; probing for `error` on a
|
|
902
|
+
// success is reading a field the type says cannot be there.
|
|
903
|
+
const execution = outcome.result
|
|
904
|
+
if (execution === undefined || execution === null) {
|
|
905
|
+
return { ok: false, error: 'the batch returned a result-shaped null for this call', text: 'null' }
|
|
906
|
+
}
|
|
907
|
+
// Branch ONCE on the discriminant. A failure has no `value` to serialize, so its
|
|
908
|
+
// own message is what the row reports as text — which is also what makes a failed
|
|
909
|
+
// row readable instead of showing `"null"`.
|
|
910
|
+
if (execution.isError === true) {
|
|
911
|
+
const message = String(execution.error?.message ?? 'the tool reported an error')
|
|
912
|
+
return { ok: false, error: message, text: message }
|
|
913
|
+
}
|
|
914
|
+
const text = typeof execution.value === 'string'
|
|
915
|
+
? execution.value
|
|
916
|
+
: JSON.stringify(execution.value ?? null)
|
|
917
|
+
return { ok: true, result: JSON.parse(JSON.stringify(execution)), text }
|
|
980
918
|
} catch (error) {
|
|
981
919
|
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
982
920
|
}
|
|
@@ -985,7 +923,7 @@ const METHODS = {
|
|
|
985
923
|
record('tools.bash', {
|
|
986
924
|
ok: bashTool.ok,
|
|
987
925
|
error: bashTool.error ?? null,
|
|
988
|
-
text:
|
|
926
|
+
text: outcomeText(bashTool, 400),
|
|
989
927
|
})
|
|
990
928
|
// Derived from the workspace, not hardcoded: the fixture used to live at
|
|
991
929
|
// /tmp/dsh-wsl-selftest.txt, which only worked while the workspace was
|
|
@@ -1003,13 +941,13 @@ const METHODS = {
|
|
|
1003
941
|
record('tools.read', {
|
|
1004
942
|
ok: readTool.ok,
|
|
1005
943
|
error: readTool.error ?? null,
|
|
1006
|
-
text:
|
|
944
|
+
text: outcomeText(readTool, 400),
|
|
1007
945
|
})
|
|
1008
946
|
const pwshTool = await invoke('pwsh', { command: 'echo host-side', description: 'selftest: this tool must not exist in a WSL session' })
|
|
1009
947
|
record('tools.pwsh', {
|
|
1010
948
|
ok: pwshTool.ok,
|
|
1011
949
|
error: pwshTool.error ?? null,
|
|
1012
|
-
text:
|
|
950
|
+
text: outcomeText(pwshTool, 200),
|
|
1013
951
|
})
|
|
1014
952
|
|
|
1015
953
|
// The realm's own `grep`/`glob`: the host removes both names from a WSL world
|
|
@@ -1085,13 +1023,13 @@ const METHODS = {
|
|
|
1085
1023
|
record('tool.search.grep', {
|
|
1086
1024
|
ok: grepSearch.ok && searchFixtureError === null,
|
|
1087
1025
|
error: grepSearch.error ?? searchFixtureError,
|
|
1088
|
-
text:
|
|
1026
|
+
text: outcomeText(grepSearch, 800),
|
|
1089
1027
|
})
|
|
1090
1028
|
const globSearch = await invoke('glob', { pattern: 'src/**/*.ts' })
|
|
1091
1029
|
record('tool.search.glob', {
|
|
1092
1030
|
ok: globSearch.ok && searchFixtureError === null,
|
|
1093
1031
|
error: globSearch.error ?? searchFixtureError,
|
|
1094
|
-
text:
|
|
1032
|
+
text: outcomeText(globSearch, 800),
|
|
1095
1033
|
})
|
|
1096
1034
|
|
|
1097
1035
|
// A tool resolves its policy from the session's logged permission state
|
|
@@ -1148,7 +1086,7 @@ const METHODS = {
|
|
|
1148
1086
|
record('tools.bashPersist.a', {
|
|
1149
1087
|
ok: persistA.ok,
|
|
1150
1088
|
error: persistA.error ?? null,
|
|
1151
|
-
text:
|
|
1089
|
+
text: outcomeText(persistA, 400),
|
|
1152
1090
|
})
|
|
1153
1091
|
const persistB = await invoke('bash', {
|
|
1154
1092
|
command: `pwd; echo "$DSH_WSL_PERSIST_PROBE"; echo probe > "$HOME/.dsh-wsl-persist-outside"; echo write-exit=$?`,
|
|
@@ -1157,7 +1095,7 @@ const METHODS = {
|
|
|
1157
1095
|
record('tools.bashPersist.b', {
|
|
1158
1096
|
ok: persistB.ok,
|
|
1159
1097
|
error: persistB.error ?? null,
|
|
1160
|
-
text:
|
|
1098
|
+
text: outcomeText(persistB, 800),
|
|
1161
1099
|
})
|
|
1162
1100
|
|
|
1163
1101
|
// F8's guard, observed from the realm: once a persistent terminal session exists, a
|
|
@@ -1216,7 +1154,7 @@ const METHODS = {
|
|
|
1216
1154
|
// through this runner; that pin now covers THIS form structurally as well
|
|
1217
1155
|
// (`insideCall` + `carrier` in the structural row beside the count table), so dropping
|
|
1218
1156
|
// the spread reddens it instead of silently reverting to the default `wsl.exe`.
|
|
1219
|
-
const sweepSelect = { wslPath:
|
|
1157
|
+
const sweepSelect = { wslPath: hostSession.wslPath }
|
|
1220
1158
|
const sweepShell = (command) => runWslShell({ distro: sweepDistro, linuxCwd: '/', loginShell: false, command, ...sweepSelect })
|
|
1221
1159
|
const sweepList = async () => String((await sweepShell('ls -1 /tmp/dsh-pty-*.fifo 2>/dev/null; echo END')).stdout ?? '')
|
|
1222
1160
|
.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('/tmp/dsh-pty-'))
|
|
@@ -1260,7 +1198,7 @@ const METHODS = {
|
|
|
1260
1198
|
const [distroSecondsText, distroIso] = String(clock.stdout ?? '').trim().split('\n')
|
|
1261
1199
|
await sweepPlant()
|
|
1262
1200
|
const planted = await sweepList()
|
|
1263
|
-
const removedFirst = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath:
|
|
1201
|
+
const removedFirst = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath: hostSession.wslPath })
|
|
1264
1202
|
const afterFirst = await sweepList()
|
|
1265
1203
|
// The host-side shift. `Date.now` is the only host clock this process offers the
|
|
1266
1204
|
// sweep, so patching it is the whole of "the host's reading changed".
|
|
@@ -1280,7 +1218,7 @@ const METHODS = {
|
|
|
1280
1218
|
let removedShifted
|
|
1281
1219
|
try {
|
|
1282
1220
|
Date.now = () => realNow() + HOST_SHIFT_MS
|
|
1283
|
-
removedShifted = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath:
|
|
1221
|
+
removedShifted = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath: hostSession.wslPath })
|
|
1284
1222
|
} finally {
|
|
1285
1223
|
Date.now = realNow
|
|
1286
1224
|
}
|
|
@@ -1375,17 +1313,17 @@ const METHODS = {
|
|
|
1375
1313
|
* @returns {Promise<object>} what each step observed.
|
|
1376
1314
|
*/
|
|
1377
1315
|
workspaceFlow: async ({ distro, linuxPath }) => {
|
|
1378
|
-
const registry =
|
|
1316
|
+
const registry = hostSession.ctx.get('workspaceRegistry')
|
|
1379
1317
|
if (registry === undefined) throw new Error('workspaceRegistry 服务不可用')
|
|
1380
1318
|
// Default the same way the selftest does — the distribution home, never /tmp:
|
|
1381
1319
|
// the fence refuses a workspace root under /tmp (confinement mounts a private
|
|
1382
1320
|
// tmpfs over it after binding the workspace), so a workspace registered there
|
|
1383
1321
|
// could never be used as a writable root. This method's old default handed the
|
|
1384
1322
|
// registry exactly such a path.
|
|
1385
|
-
const location = linuxPath ?? (await defaultWorkspaceUnc({ wslPath:
|
|
1323
|
+
const location = linuxPath ?? (await defaultWorkspaceUnc({ wslPath: hostSession.wslPath })).uncPath
|
|
1386
1324
|
const target = await resolveLocation(location, distro)
|
|
1387
|
-
const listing = await listLinuxDir(target.distro, target.linuxPath, { wslPath:
|
|
1388
|
-
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath:
|
|
1325
|
+
const listing = await listLinuxDir(target.distro, target.linuxPath, { wslPath: hostSession.wslPath })
|
|
1326
|
+
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath: hostSession.wslPath })
|
|
1389
1327
|
const existing = await registry.resolveByPath(target.uncPath)
|
|
1390
1328
|
const workspace = existing ?? await registry.create(target.uncPath, `wsl-selftest ${target.linuxPath}`)
|
|
1391
1329
|
const created = existing === undefined
|
|
@@ -1408,7 +1346,7 @@ const METHODS = {
|
|
|
1408
1346
|
* Report recent preset-binding decisions.
|
|
1409
1347
|
* @returns {Promise<object>} the decisions, newest last.
|
|
1410
1348
|
*/
|
|
1411
|
-
bindingLog: () => Promise.resolve({ entries:
|
|
1349
|
+
bindingLog: () => Promise.resolve({ entries: bindingTape.log }),
|
|
1412
1350
|
|
|
1413
1351
|
/**
|
|
1414
1352
|
* Read the harness's own view of every preset's composition.
|
|
@@ -1418,7 +1356,7 @@ const METHODS = {
|
|
|
1418
1356
|
* @returns {Promise<object>} the composition inventory.
|
|
1419
1357
|
*/
|
|
1420
1358
|
presetInventory: async () => {
|
|
1421
|
-
const presets =
|
|
1359
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
1422
1360
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
1423
1361
|
return { inventory: await presets.compositionInventory() }
|
|
1424
1362
|
},
|
|
@@ -1428,7 +1366,7 @@ const METHODS = {
|
|
|
1428
1366
|
* @returns {Promise<object>} the roster.
|
|
1429
1367
|
*/
|
|
1430
1368
|
presetRoster: async () => {
|
|
1431
|
-
const presets =
|
|
1369
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
1432
1370
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
1433
1371
|
const roster = await presets.list()
|
|
1434
1372
|
return { roster: roster.map((entry) => ({ id: entry.id, name: entry.name, broken: entry.broken ?? null, isDefault: entry.isDefault ?? false })) }
|
|
@@ -1439,7 +1377,7 @@ const METHODS = {
|
|
|
1439
1377
|
* @param {{ distro?: string }} params - optional distribution to pin.
|
|
1440
1378
|
* @returns {Promise<object>} the new state.
|
|
1441
1379
|
*/
|
|
1442
|
-
regeneratePresets: ({ distro }) => refreshPresets(
|
|
1380
|
+
regeneratePresets: ({ distro }) => refreshPresets(hostSession.ctx, distro),
|
|
1443
1381
|
|
|
1444
1382
|
/**
|
|
1445
1383
|
* Create (or find) the workspace for a WSL directory.
|
|
@@ -1451,9 +1389,9 @@ const METHODS = {
|
|
|
1451
1389
|
*/
|
|
1452
1390
|
createWorkspace: async ({ distro, path, title }) => {
|
|
1453
1391
|
const target = await resolveLocation(path, distro)
|
|
1454
|
-
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath:
|
|
1392
|
+
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath: hostSession.wslPath })
|
|
1455
1393
|
if (!facts.isDirectory) throw new Error(`${target.linuxPath} 不是一个存在的目录`)
|
|
1456
|
-
const registry =
|
|
1394
|
+
const registry = hostSession.ctx.get('workspaceRegistry')
|
|
1457
1395
|
if (registry === undefined) throw new Error('workspaceRegistry 服务不可用')
|
|
1458
1396
|
const existing = await registry.resolveByPath(target.uncPath)
|
|
1459
1397
|
const workspace = existing ?? await registry.create(target.uncPath, title ?? target.linuxPath)
|
|
@@ -1463,7 +1401,7 @@ const METHODS = {
|
|
|
1463
1401
|
// degrades to the default spelling instead of failing the creation.
|
|
1464
1402
|
let presetId = 'wsl-standard'
|
|
1465
1403
|
try {
|
|
1466
|
-
const presets =
|
|
1404
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
1467
1405
|
if (presets !== undefined) {
|
|
1468
1406
|
const resolved = await presets.resolve()
|
|
1469
1407
|
const candidate = variantIdFor(resolved)
|
|
@@ -1492,7 +1430,7 @@ const METHODS = {
|
|
|
1492
1430
|
listPresetDirectories: async () => {
|
|
1493
1431
|
// 0.1.6 read generated directories off disk; the registry is the source of
|
|
1494
1432
|
// truth now. The method name is kept for the developer surface.
|
|
1495
|
-
const presets =
|
|
1433
|
+
const presets = hostSession.ctx?.get('agentPresets')
|
|
1496
1434
|
if (presets === undefined) return { root: null, names: [] }
|
|
1497
1435
|
const roster = await presets.list()
|
|
1498
1436
|
const names = roster
|
|
@@ -1511,7 +1449,7 @@ const METHODS = {
|
|
|
1511
1449
|
if (typeof id !== 'string' || !/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
1512
1450
|
throw new Error(`readPreset: 非法的预设 id ${JSON.stringify(id)}`)
|
|
1513
1451
|
}
|
|
1514
|
-
const presets =
|
|
1452
|
+
const presets = hostSession.ctx?.get('agentPresets')
|
|
1515
1453
|
if (presets === undefined) throw new Error('readPreset: agentPresets 服务不可用')
|
|
1516
1454
|
// 0.1.7: readDocument returns the declaration re-dumped as YAML content.
|
|
1517
1455
|
const doc = await presets.readDocument(id)
|
|
@@ -1519,102 +1457,17 @@ const METHODS = {
|
|
|
1519
1457
|
},
|
|
1520
1458
|
}
|
|
1521
1459
|
|
|
1522
|
-
/**
|
|
1523
|
-
* Read one JSON request body, bounded.
|
|
1524
|
-
* @param {import('node:http').IncomingMessage} req - the request to drain.
|
|
1525
|
-
* @returns {Promise<{ text: string | null, byteLength: number }>} the body, or null past the ceiling.
|
|
1526
|
-
*/
|
|
1527
|
-
async function readJsonBody(req) {
|
|
1528
|
-
const chunks = []
|
|
1529
|
-
let size = 0
|
|
1530
|
-
for await (const chunk of req) {
|
|
1531
|
-
size += chunk.byteLength
|
|
1532
|
-
if (size > MAX_BODY_BYTES) {
|
|
1533
|
-
// Drain the remainder so the refusal is a readable response, not a socket cut.
|
|
1534
|
-
req.resume()
|
|
1535
|
-
return { text: null, byteLength: size }
|
|
1536
|
-
}
|
|
1537
|
-
chunks.push(chunk)
|
|
1538
|
-
}
|
|
1539
|
-
return { text: Buffer.concat(chunks, size).toString('utf8'), byteLength: size }
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
/**
|
|
1543
|
-
* Answer one request with a JSON envelope and an explicit status.
|
|
1544
|
-
* @param {import('node:http').ServerResponse} res - the response to complete.
|
|
1545
|
-
* @param {number} status - the HTTP status.
|
|
1546
|
-
* @param {{ ok: boolean }} payload - the envelope, success or failure.
|
|
1547
|
-
*/
|
|
1548
|
-
function sendJson(res, status, payload) {
|
|
1549
|
-
const body = JSON.stringify(payload)
|
|
1550
|
-
res.writeHead(status, {
|
|
1551
|
-
'content-type': 'application/json; charset=utf-8',
|
|
1552
|
-
'content-length': Buffer.byteLength(body),
|
|
1553
|
-
'cache-control': 'no-store',
|
|
1554
|
-
})
|
|
1555
|
-
res.end(body)
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* Answer with one admission rejection.
|
|
1560
|
-
* @param {import('node:http').ServerResponse} res - the response to complete.
|
|
1561
|
-
* @param {import('./http-admission.js').Rejection} rejection - the refusal.
|
|
1562
|
-
*/
|
|
1563
|
-
function sendRejection(res, rejection) {
|
|
1564
|
-
sendJson(res, rejection.status, { ok: false, code: rejection.code, error: rejection.message })
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
/** Trust surface consumed here; the browser-side connection package owns the full type. */
|
|
1568
|
-
function connectionOf(ctx) {
|
|
1569
|
-
return Reflect.get(ctx, 'connection')
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
/**
|
|
1573
|
-
* This installation's development token, minted on first use.
|
|
1574
|
-
*
|
|
1575
|
-
* The file is owner-only and lives outside the repository. A web page cannot
|
|
1576
|
-
* read it, which is what keeps the acceptance surface unreachable from a
|
|
1577
|
-
* cross-site request even though the route itself is on loopback.
|
|
1578
|
-
* @returns {Promise<string>} the token.
|
|
1579
|
-
*/
|
|
1580
|
-
async function devToken() {
|
|
1581
|
-
const path = join(dshHome(), 'wsl-desktop-dev-token')
|
|
1582
|
-
try {
|
|
1583
|
-
const existing = (await readFile(path, 'utf8')).trim()
|
|
1584
|
-
if (existing.length > 0) return existing
|
|
1585
|
-
// An empty file is unusual — treat it like absent and re-mint below.
|
|
1586
|
-
} catch (error) {
|
|
1587
|
-
// ENOENT = first use. Any other read failure (ACL, AV lock, disk) must
|
|
1588
|
-
// fail loud rather than silently rotate the credential and revoke every
|
|
1589
|
-
// scripted caller.
|
|
1590
|
-
if (error?.code !== 'ENOENT') throw error
|
|
1591
|
-
}
|
|
1592
|
-
const minted = randomBytes(32).toString('hex')
|
|
1593
|
-
await mkdir(dirname(path), { recursive: true })
|
|
1594
|
-
await writeFile(path, `${minted}\n`, { mode: 0o600 })
|
|
1595
|
-
return minted
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
/**
|
|
1599
|
-
* Whether one request may use the acceptance surface.
|
|
1600
|
-
* @param {import('node:http').IncomingMessage} req - the request.
|
|
1601
|
-
* @param {{ developerTools: boolean }} config - resolved plugin config.
|
|
1602
|
-
* @returns {Promise<boolean>} true for a caller presenting this installation's token.
|
|
1603
|
-
*/
|
|
1604
|
-
async function isDeveloperCaller(req, config) {
|
|
1605
|
-
if (config.developerTools !== true) return false
|
|
1606
|
-
return tokenMatches(req.headers[DEV_TOKEN_HEADER], await devToken(), timingSafeEqual)
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
1460
|
/**
|
|
1610
1461
|
* Register the WSL route and generate the WSL presets.
|
|
1611
1462
|
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin context.
|
|
1612
1463
|
* @param {{ routePath: string, developerTools: boolean, wslPath?: string }} config - resolved plugin config.
|
|
1613
1464
|
*/
|
|
1614
1465
|
export function apply(ctx, config) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1466
|
+
hostSession.ctx = ctx
|
|
1467
|
+
// The declared default, not `undefined`: every route method passes this to
|
|
1468
|
+
// wsl.exe, and an undefined path is not the same as the default one.
|
|
1469
|
+
hostSession.wslPath = config.wslPath ?? 'wsl.exe'
|
|
1470
|
+
hostSession.ready = false
|
|
1618
1471
|
// Bound concurrent route methods: each discovery call spawns wsl.exe, and a
|
|
1619
1472
|
// misbehaving page could fan out without limit. Named because a bare "8" here
|
|
1620
1473
|
// was once mis-read as a wsl.exe SPAWN cap — it is not: one route method may
|
|
@@ -1744,10 +1597,10 @@ export function apply(ctx, config) {
|
|
|
1744
1597
|
// The probe is allowed to run even after a failed materialization: the result
|
|
1745
1598
|
// then says which capability is missing, instead of leaving the browser on
|
|
1746
1599
|
// "pending" forever.
|
|
1747
|
-
|
|
1600
|
+
hostSession.ready = true
|
|
1748
1601
|
// One startup line, as the spec asks: the post-restart gate and a bug report
|
|
1749
1602
|
// both need the capability state without opening the dialog.
|
|
1750
|
-
void probeHost(
|
|
1603
|
+
void probeHost(hostSession.ctx).then((status) => {
|
|
1751
1604
|
ctx.logger?.info?.(`wsl-desktop: capabilities ${JSON.stringify(status)}`)
|
|
1752
1605
|
}, () => {})
|
|
1753
1606
|
})
|
|
@@ -1758,7 +1611,7 @@ export function apply(ctx, config) {
|
|
|
1758
1611
|
// This listener is the LAST RESORT for a WSL-workspace session created
|
|
1759
1612
|
// without a preset (some other entry point): it tries a post-hoc select,
|
|
1760
1613
|
// which is refused for any session that has already taken a turn, and both
|
|
1761
|
-
// outcomes land in `
|
|
1614
|
+
// outcomes land in `bindingTape.log` as an anomaly signal.
|
|
1762
1615
|
//
|
|
1763
1616
|
// ONE listener, not two. `agent/created` is a scoped event, but a listener
|
|
1764
1617
|
// registered without a scope is admitted globally, so registering both paths
|
|
@@ -1776,8 +1629,7 @@ export function apply(ctx, config) {
|
|
|
1776
1629
|
* @param {object} entry - the decision to record.
|
|
1777
1630
|
*/
|
|
1778
1631
|
function recordBinding(entry) {
|
|
1779
|
-
|
|
1780
|
-
if (bindingLog.length > BINDING_LOG_LIMIT) bindingLog.splice(0, bindingLog.length - BINDING_LOG_LIMIT)
|
|
1632
|
+
bindingTape.record(entry)
|
|
1781
1633
|
}
|
|
1782
1634
|
|
|
1783
1635
|
/**
|