dsh-crate-web 0.1.1 → 0.1.2-preview.3

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.
Files changed (34) hide show
  1. package/README.md +12 -289
  2. package/README.zh-CN.md +2 -303
  3. package/THIRD-PARTY-LICENSE.txt +21 -0
  4. package/cordis.patch.yml +1 -6
  5. package/core/dsh_pack/_version.py +1 -0
  6. package/core/dsh_pack/bundle.py +74 -0
  7. package/core/dsh_pack/cli.py +81 -18
  8. package/core/dsh_pack/desktop_verify.py +304 -0
  9. package/core/dsh_pack/diagnostics.py +1170 -0
  10. package/core/dsh_pack/errors.py +16 -1
  11. package/core/dsh_pack/export.py +86 -103
  12. package/core/dsh_pack/export_plan.py +96 -0
  13. package/core/dsh_pack/identity.py +25 -0
  14. package/core/dsh_pack/importer.py +468 -217
  15. package/core/dsh_pack/network.py +5 -4
  16. package/core/dsh_pack/pack.py +14 -2
  17. package/core/dsh_pack/portability.py +392 -0
  18. package/core/dsh_pack/preflight.py +63 -16
  19. package/core/dsh_pack/troubleshooting.py +94 -0
  20. package/core/dsh_pack/user_cli.py +16 -0
  21. package/core/dsh_pack/validation.py +64 -4
  22. package/core/dsh_pack/verify.py +343 -194
  23. package/core/dsh_pack/windows_verify.py +177 -0
  24. package/desktop-patch-helper.mjs +140 -0
  25. package/lib/client.js +292 -102
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +1121 -229
  28. package/package.json +44 -7
  29. package/skills/dsh-crate-troubleshooting/SKILL.md +172 -0
  30. package/skills/dsh-crate-troubleshooting/compatibility.json +11 -0
  31. package/skills/dsh-crate-troubleshooting/reference/diagnostics.generated.md +96 -0
  32. package/skills/dsh-crate-troubleshooting/reference/repair-boundaries.md +60 -0
  33. package/skills/dsh-crate-troubleshooting/reference/stages.generated.md +22 -0
  34. package/verify-desktop.mjs +314 -0
@@ -0,0 +1,314 @@
1
+ /** Owned, zero-model Desktop Verify controller. Only stdio protocol and local CDP. */
2
+ import { createRequire } from 'node:module'
3
+ import { spawn } from 'node:child_process'
4
+ import { createInterface } from 'node:readline'
5
+ import { createHash, randomUUID } from 'node:crypto'
6
+ import { open, mkdir, readdir, lstat, readFile, writeFile } from 'node:fs/promises'
7
+ import { createServer } from 'node:net'
8
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
9
+ import { pathToFileURL } from 'node:url'
10
+
11
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
12
+ const inside = (root, path) => { const part = relative(resolve(root), resolve(path)); return part === '' || (!part.startsWith('..') && !isAbsolute(part)) }
13
+ const send = value => process.stdout.write(JSON.stringify(value) + '\n')
14
+
15
+ export async function copyRegularTree(source, target, limit = { files: 0, bytes: 0 }) {
16
+ const info = await lstat(source)
17
+ if (info.isSymbolicLink()) throw new Error('Verify refuses reparse links in the source Profile')
18
+ if (info.isDirectory()) {
19
+ await mkdir(target, { recursive: false })
20
+ for (const item of await readdir(source)) await copyRegularTree(join(source, item), join(target, item), limit)
21
+ } else if (info.isFile()) {
22
+ limit.files += 1; limit.bytes += info.size
23
+ if (limit.files > 15000 || limit.bytes > 1024 ** 3) throw new Error('Verify Profile copy exceeds its bounded file/byte limit')
24
+ await writeFile(target, await readFile(source), { flag: 'wx', mode: 0o600 })
25
+ } else throw new Error('Verify Profile contains an unsupported filesystem entry')
26
+ return limit
27
+ }
28
+
29
+ export function isolatedEnvironment(source, job, home) {
30
+ const result = {}
31
+ for (const [name, value] of Object.entries(source)) {
32
+ if (/secret|token|password|credential|authorization|api.?key|cookie|^auth$|private/i.test(name) || /^(DSH_|NODE_|ELECTRON_|CRATE_TEST_|PYTHONPATH$|PYTHONHOME$|PYTHONUSERBASE$)/i.test(name)) continue
33
+ if (typeof value === 'string' && !/:\/\/[^/@\s]+:[^/@\s]+@/.test(value)) result[name] = value
34
+ }
35
+ Object.assign(result, { DSH_HOME: home, HOME: join(job, 'user'), USERPROFILE: join(job, 'user'),
36
+ APPDATA: join(job, 'roaming'), LOCALAPPDATA: join(job, 'local'), TEMP: join(job, 'tmp'), TMP: join(job, 'tmp'),
37
+ XDG_CACHE_HOME: join(job, 'cache'), XDG_CONFIG_HOME: join(job, 'config'), XDG_DATA_HOME: join(job, 'data'),
38
+ NARB_NATIVE_CACHE_DIR: join(job, 'native-cache'), npm_config_cache: join(job, 'npm-cache'),
39
+ npm_config_userconfig: join(job, 'npmrc'), npm_config_fetch_retries: '0', npm_config_registry: 'http://127.0.0.1:9',
40
+ PNPM_CONFIG_STORE_DIR: join(job, 'pnpm-store'), PNPM_CONFIG_CACHE_DIR: join(job, 'pnpm-cache'),
41
+ PNPM_CONFIG_STATE_DIR: join(job, 'pnpm-state'), PNPM_CONFIG_CONFIG_DIR: join(job, 'pnpm-config'),
42
+ PNPM_CONFIG_OFFLINE: 'true', PNPM_CONFIG_FETCH_RETRIES: '0', PYTHONDONTWRITEBYTECODE: '1' })
43
+ return result
44
+ }
45
+
46
+ async function freePort() {
47
+ const server = createServer()
48
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) })
49
+ const port = server.address().port
50
+ await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
51
+ return port
52
+ }
53
+
54
+ export function validateSurface(value, home, webPort) {
55
+ const officialDesktop = value.protocol === 'dsh-app:' && value.hostname === 'app'
56
+ const localWeb = value.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(value.hostname)
57
+ if ((!officialDesktop && !localWeb) || !value.accountMenu || value.httpStatus !== 200 || value.backendHttpStatus !== 200 || value.backendStatus !== 'ok') {
58
+ throw new Error('Verification workspace/authenticated Web ready failed; see surface.observation')
59
+ }
60
+ if (typeof value.runtimeHome !== 'string' || resolve(value.runtimeHome).toLowerCase() !== resolve(home).toLowerCase() || value.runtimeProfile !== 'desktop' || !Number.isInteger(value.runtimePid) || value.runtimePid <= 0) {
61
+ throw new Error('Verification backend identity does not match its isolated desktop')
62
+ }
63
+ if (!Number.isInteger(webPort) || webPort < 1 || webPort > 65535) throw new Error('Owned backend Web port missing from startup log')
64
+ return { ...value, webPort, authenticationContext: 'official owned desktop protocol and authenticated profiles route; credentials not exported' }
65
+ }
66
+
67
+ class Cdp {
68
+ constructor(url) { this.url = url; this.pending = new Map(); this.sequence = 0 }
69
+ async connect() {
70
+ const parsed = new URL(this.url)
71
+ if (parsed.protocol !== 'ws:' || parsed.hostname !== '127.0.0.1') throw new Error('CDP is restricted to the owned local endpoint')
72
+ this.socket = new WebSocket(this.url)
73
+ this.socket.addEventListener('message', event => {
74
+ const data = JSON.parse(event.data)
75
+ if (!this.pending.has(data.id)) return
76
+ const pending = this.pending.get(data.id); this.pending.delete(data.id); clearTimeout(pending.timer)
77
+ data.error ? pending.reject(new Error(data.error.message)) : pending.resolve(data.result)
78
+ })
79
+ this.socket.addEventListener('close', () => {
80
+ for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(new Error('CDP connection closed')) }
81
+ this.pending.clear()
82
+ })
83
+ await new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => reject(new Error('local CDP connection deadline')), 10000)
85
+ this.socket.addEventListener('open', () => { clearTimeout(timer); resolve() }, { once: true })
86
+ this.socket.addEventListener('error', () => { clearTimeout(timer); reject(new Error('local CDP connection failed')) }, { once: true })
87
+ })
88
+ }
89
+ call(method, params = {}, sessionId) {
90
+ const id = ++this.sequence
91
+ return new Promise((resolve, reject) => {
92
+ const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('CDP command deadline: ' + method)) }, 10000)
93
+ this.pending.set(id, { resolve, reject, timer })
94
+ this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }))
95
+ })
96
+ }
97
+ async evaluate(expression) {
98
+ const response = await this.call('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }, this.pageSession)
99
+ if (response.exceptionDetails) throw new Error('owned page evaluation failed')
100
+ return response.result.value
101
+ }
102
+ disconnect() { if (this.socket && this.socket.readyState < 2) this.socket.close() }
103
+ }
104
+
105
+ export class DesktopController {
106
+ constructor(config) { this.config = config; this.generation = 0; this.child = null; this.originalHashes = {} }
107
+ async event(name, values = {}) {
108
+ await writeFile(join(this.config.job, 'controller-events.jsonl'), JSON.stringify({ atUtc: new Date().toISOString(), event: name, generation: this.generation, ...values }) + '\n', { flag: 'a', mode: 0o600 })
109
+ }
110
+ async prepare() {
111
+ const { job, sourceHome, sourceProfile, runtimeRoot } = this.config
112
+ if (!/^[A-Za-z0-9._-]+$/.test(sourceProfile)) throw new Error('invalid Profile name')
113
+ if (!inside(join(sourceHome, '.dsh-pack', 'verify-jobs'), job)) throw new Error('Verify job escaped its isolated workspace')
114
+ this.home = join(job, 'home'); this.documents = join(job, 'documents')
115
+ for (const name of ['home', 'home/profiles', 'documents', 'electron', 'user', 'roaming', 'local', 'tmp', 'cache', 'config', 'data', 'native-cache', 'npm-cache', 'pnpm-store', 'pnpm-cache', 'pnpm-state', 'pnpm-config']) await mkdir(join(job, name), { recursive: true })
116
+ await writeFile(join(job, 'npmrc'), '', { flag: 'wx' })
117
+ this.profile = join(this.home, 'profiles', 'desktop')
118
+ const source = join(sourceHome, 'profiles', sourceProfile)
119
+ if ((await lstat(source)).isSymbolicLink()) throw new Error('source Profile root is a reparse link')
120
+ await mkdir(this.profile)
121
+ let copied = { files: 0, bytes: 0 }
122
+ for (const name of ['package.json', 'pnpm-lock.yaml', 'node_modules']) {
123
+ let data
124
+ try { data = await lstat(join(source, name)) } catch (error) { if (error.code === 'ENOENT') continue; throw error }
125
+ copied = await copyRegularTree(join(source, name), join(this.profile, name), copied)
126
+ if (data.isFile()) this.originalHashes[name] = createHash('sha256').update(await readFile(join(source, name))).digest('hex')
127
+ }
128
+ const require = createRequire(import.meta.url)
129
+ const yaml = require(join(runtimeRoot, 'node_modules', 'js-yaml'))
130
+ const patchPath = join(this.profile, 'cordis.patch.yml')
131
+ // Parse configuration before writing: credentials in config are never copied verbatim.
132
+ const rows = yaml.load(await readFile(join(source, 'cordis.patch.yml'), 'utf8')) ?? []
133
+ if (!Array.isArray(rows)) throw new Error('Profile patch must be a patch-list array')
134
+ const workspace = rows.find(row => row?.id === 'workspace-controller')
135
+ const oldDocuments = workspace?.config?.documentsDirectory
136
+ const rewrite = value => {
137
+ if (typeof value === 'string') {
138
+ let result = value.split(sourceHome).join(this.home)
139
+ if (typeof oldDocuments === 'string' && oldDocuments) result = result.split(oldDocuments).join(this.documents)
140
+ if (isAbsolute(result) && ![job, dirname(this.config.desktopExecutable), process.env.SystemRoot ?? 'C:\\Windows'].some(root => inside(root, result))) throw new Error('Profile has an absolute path outside Verify isolation')
141
+ return result
142
+ }
143
+ if (Array.isArray(value)) return value.map(rewrite)
144
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key,
145
+ /^(api.?key|token|password|secret|authorization|cookie|credential|private.?key)$/i.test(key) ? 'CRATE_VERIFY_REDACTED' : rewrite(item)]))
146
+ return value
147
+ }
148
+ let rewritten = rewrite(rows)
149
+ const put = (id, change) => { let row = rewritten.find(item => item?.id === id); if (!row) { row = { id }; rewritten.push(row) } change(row) }
150
+ put('workspace-controller', row => { row.config = { ...row.config, documentsDirectory: this.documents } })
151
+ for (const id of ['session-title-llm', 'session-telemetry-otel', 'desktop-product-telemetry', 'product-analytics', 'llm-deepseek']) put(id, row => { row.disabled = true })
152
+ const providerRow = rewritten.find(row => row?.id === 'llm-pi-ai')
153
+ if (providerRow) {
154
+ const row = providerRow
155
+ for (const provider of Object.values(row.config?.providers ?? {})) if (provider && typeof provider === 'object') {
156
+ provider.baseURL = 'http://127.0.0.1:9/v1'; provider.apiKey = 'CRATE_VERIFY_NO_MODEL_REQUESTS'
157
+ provider.retryPolicy = { ...provider.retryPolicy, maxRetries: 0 }
158
+ }
159
+ }
160
+ this.inspectorPort = await freePort()
161
+ put('experimental-inspector', row => { row.config = { ...row.config, port: this.inspectorPort } })
162
+ await writeFile(patchPath, yaml.dump(rewritten, { noRefs: true, lineWidth: 4096 }))
163
+ const rootRows = yaml.load(await readFile(join(source, 'cordis.yml'), 'utf8')) ?? []
164
+ if (!Array.isArray(rootRows) || rootRows.length) throw new Error('Nonempty Profile root requires an explicit runner; built-in Verify will not guess its isolation')
165
+ await writeFile(join(this.profile, 'cordis.yml'), '[]\n')
166
+ for (const name of ['cordis.patch.yml', 'cordis.yml']) this.originalHashes[name] = createHash('sha256').update(await readFile(join(source, name))).digest('hex')
167
+ const homePatch = join(sourceHome, 'cordis.patch.yml')
168
+ try {
169
+ if ((await lstat(homePatch)).isSymbolicLink()) throw new Error('Home patch reparse link refused')
170
+ const data = yaml.load(await readFile(homePatch, 'utf8'))
171
+ if (Array.isArray(data) ? data.length : data && Object.keys(data).length) throw new Error('Nonempty Home overlay requires an explicit runner; built-in Verify will not guess its isolation')
172
+ } catch (error) { if (error.code !== 'ENOENT') throw error }
173
+ const preparation = { sourceProfile, runtimeProfile: 'desktop', copied, sourceHashes: this.originalHashes,
174
+ credentialsFilesCopied: false, modelRequests: 0, requiredPluginsRemoved: false, home: this.home, documents: this.documents }
175
+ await writeFile(join(job, 'preparation.json'), JSON.stringify(preparation, null, 2))
176
+ this.prepared = true
177
+ return preparation
178
+ }
179
+ async boot() {
180
+ if (this.generation >= this.config.maximumBoots) throw new Error('Verify desktop boot limit reached')
181
+ if (!this.prepared) await this.prepare()
182
+ this.generation += 1; this.cdpPort = await freePort()
183
+ const env = isolatedEnvironment(process.env, this.config.job, this.home)
184
+ if (this.config.observe) {
185
+ this.trace = join(this.config.job, 'trace-' + this.generation)
186
+ await mkdir(this.trace)
187
+ Object.assign(env, { CRATE_TEST_ROOT: this.config.job, CRATE_TEST_OBSERVATION_DIR: this.trace, CRATE_TEST_RUN_ID: randomUUID() })
188
+ }
189
+ const stdout = await open(join(this.config.job, `desktop-${this.generation}.stdout.log`), 'wx')
190
+ const stderr = await open(join(this.config.job, `desktop-${this.generation}.stderr.log`), 'wx')
191
+ try {
192
+ this.child = spawn(this.config.desktopExecutable, [`--user-data-dir=${join(this.config.job, 'electron')}`,
193
+ '--remote-debugging-address=127.0.0.1', `--remote-debugging-port=${this.cdpPort}`], {
194
+ cwd: this.config.job, env, windowsHide: true, stdio: ['ignore', stdout.fd, stderr.fd] })
195
+ } finally { await stdout.close(); await stderr.close() }
196
+ this.childError = null
197
+ this.child.once('error', error => { this.childError = error })
198
+ const state = { pid: this.child.pid, cdpPort: this.cdpPort, inspectorPort: this.inspectorPort, trace: this.trace,
199
+ home: this.home, job: this.config.job, generation: this.generation }
200
+ await writeFile(join(this.config.job, 'state.json'), JSON.stringify(state, null, 2))
201
+ send({ event: 'desktop.spawned', ...state })
202
+ await this.event('desktop.spawned', state)
203
+ const deadline = Date.now() + 45000
204
+ let version
205
+ while (Date.now() < deadline) {
206
+ if (this.childError) throw new Error('Verification desktop spawn failed: ' + this.childError.code)
207
+ if (this.child.exitCode !== null) throw new Error('Verification desktop exited before ready: ' + this.child.exitCode)
208
+ try {
209
+ const response = await fetch(`http://127.0.0.1:${this.cdpPort}/json/version`, { signal: AbortSignal.timeout(1500) })
210
+ if (response.ok) { version = await response.json(); break }
211
+ } catch { /* Passive startup wait; no launch retry. */ }
212
+ await sleep(200)
213
+ }
214
+ if (!version) throw new Error('Verification desktop CDP ready deadline')
215
+ this.browser = new Cdp(version.webSocketDebuggerUrl); await this.browser.connect()
216
+ let target
217
+ while (!target && Date.now() < deadline) {
218
+ const targets = await this.browser.call('Target.getTargets')
219
+ target = targets.targetInfos.find(item => item.type === 'page' && !item.url.startsWith('devtools:'))
220
+ if (!target) await sleep(150)
221
+ }
222
+ if (!target) throw new Error('Owned desktop page missing')
223
+ this.browser.pageSession = (await this.browser.call('Target.attachToTarget', { targetId: target.targetId, flatten: true })).sessionId
224
+ let openedKey = false
225
+ while (Date.now() < deadline) {
226
+ const ready = await this.browser.evaluate(`(() => {
227
+ const labels = element => (element.getAttribute('aria-label') || element.innerText || '').trim();
228
+ const buttons = [...document.querySelectorAll('button')];
229
+ if (buttons.some(button => /^(Account menu|账户菜单|账号菜单)$/i.test(labels(button)))) return 'ready';
230
+ const skip = document.querySelector('#skip-key'); if (skip) { skip.click(); return 'skip'; }
231
+ ${openedKey ? '' : "const add = buttons.find(button => /^(Add API Key|添加 API Key)$/i.test(labels(button))); if (add) { add.click(); return 'opened-key'; }"}
232
+ return 'waiting';
233
+ })()`)
234
+ if (ready === 'ready') break
235
+ if (ready === 'opened-key') openedKey = true
236
+ await sleep(150)
237
+ }
238
+ const surface = await this.surface()
239
+ await this.event('desktop.ready', { ...state, surface })
240
+ return { ...state, surface }
241
+ }
242
+ async surface() {
243
+ const value = await this.browser.evaluate(`(async () => {
244
+ const accountMenu = [...document.querySelectorAll('button')].some(button => /^(Account menu|账户菜单|账号菜单)$/i.test((button.getAttribute('aria-label') || button.innerText || '').trim()));
245
+ const response = await fetch('/', { credentials: 'same-origin', signal: AbortSignal.timeout(7000) });
246
+ const backend = await fetch('/dsh-crate/api', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'profiles' }), signal: AbortSignal.timeout(7000) });
247
+ let result; try { result = await backend.json() } catch { result = {} }
248
+ return { accountMenu, httpStatus: response.status, origin: location.origin, protocol: location.protocol, hostname: location.hostname,
249
+ backendHttpStatus: backend.status, backendStatus: result.status, runtimePid: result.runtime?.pid,
250
+ runtimeHome: result.runtime?.dshHome, runtimeProfile: result.runtime?.currentProfile };
251
+ })()`)
252
+ const log = join(this.config.job, `desktop-${this.generation}.stdout.log`)
253
+ if ((await lstat(log)).size > 1024 * 1024) throw new Error('Owned desktop startup log exceeded observation limit')
254
+ const ports = [...(await readFile(log, 'utf8')).matchAll(/dsh web: http:\/\/127\.0\.0\.1:(\d+)\//g)]
255
+ const webPort = ports.length ? Number(ports.at(-1)[1]) : undefined
256
+ await this.event('surface.observation', { ...value, webPort })
257
+ return validateSurface(value, this.home, webPort)
258
+ }
259
+ async newSession() {
260
+ const selected = () => this.browser.evaluate(`([...document.querySelectorAll('[data-row-key^="session:"][aria-selected="true"]')].map(element => element.getAttribute('data-row-key').slice(8)))`)
261
+ const selectedBefore = await selected()
262
+ const clicked = await this.browser.evaluate(`(() => {
263
+ const button = [...document.querySelectorAll('button')].find(element => /^New session$/i.test((element.getAttribute('aria-label') || element.innerText || '').trim()) && !element.disabled && element.getClientRects().length);
264
+ if (!button) return false; button.click(); return true;
265
+ })()`)
266
+ if (!clicked) throw new Error('Official New session button unavailable')
267
+ await this.event('new-session.clicked')
268
+ let selectedAfter = []
269
+ const deadline = Date.now() + 10000
270
+ while (Date.now() < deadline) {
271
+ selectedAfter = await selected()
272
+ if (selectedAfter.length === 1) break
273
+ await sleep(100)
274
+ }
275
+ if (selectedAfter.length !== 1) throw new Error('New session did not select exactly one session')
276
+ return { officialButtonClicks: 1, selectedBefore, selectedAfter }
277
+ }
278
+ async close() {
279
+ if (!this.child) return { requested: false, exitCode: null }
280
+ let requested = false
281
+ if (this.child.exitCode === null && this.browser) {
282
+ requested = true
283
+ try { await this.browser.call('Browser.close') } catch { /* Browser may close its socket before acknowledging. */ }
284
+ }
285
+ const deadline = Date.now() + 20000
286
+ while (this.child.exitCode === null && Date.now() < deadline) await sleep(100)
287
+ this.browser?.disconnect()
288
+ const value = { requested, pid: this.child.pid, exitCode: this.child.exitCode, generation: this.generation }
289
+ await this.event('desktop.closed', value)
290
+ if (value.exitCode === null) throw new Error('Owned desktop did not exit after close request')
291
+ return value
292
+ }
293
+ }
294
+
295
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
296
+ const config = JSON.parse(await readFile(process.argv[2], 'utf8'))
297
+ if (config.modelRequests !== 0 || config.automaticRetries !== 0) throw new Error('Built-in desktop verifier only accepts zero-model, no-retry scope')
298
+ const controller = new DesktopController(config)
299
+ try {
300
+ for await (const line of createInterface({ input: process.stdin })) {
301
+ let response
302
+ try {
303
+ const request = JSON.parse(line)
304
+ const value = request.operation === 'boot' ? await controller.boot()
305
+ : request.operation === 'surface' ? await controller.surface()
306
+ : request.operation === 'new_session' ? await controller.newSession()
307
+ : request.operation === 'close' ? await controller.close()
308
+ : (() => { throw new Error('Unsupported desktop controller operation') })()
309
+ response = { value }
310
+ } catch (error) { response = { error: { type: error.name, message: error.message } } }
311
+ send(response)
312
+ }
313
+ } finally { await controller.close().catch(() => undefined) }
314
+ }