@hope_phenom/dsh-plugin-remote-use 0.0.0-stage → 0.2.24
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/LICENSE +21 -0
- package/README.md +207 -2
- package/README.zh.md +195 -0
- package/assets/icon.svg +47 -0
- package/cordis.patch.yml +41 -0
- package/lib/bridge/acp-bridge.js +905 -0
- package/lib/bridge/peer-connection.js +730 -0
- package/lib/client.js +2842 -0
- package/lib/core/acp-model-overlay.js +121 -0
- package/lib/core/approval-queue.js +216 -0
- package/lib/core/audit-log.js +322 -0
- package/lib/core/desktop-notify.js +173 -0
- package/lib/core/environment-prompt.js +76 -0
- package/lib/core/relay-supervisor.js +278 -0
- package/lib/core/review-engine.js +679 -0
- package/lib/core/sandbox-diagnosis.js +258 -0
- package/lib/core/sandbox-probe.js +310 -0
- package/lib/core/self-acp-spec.js +117 -0
- package/lib/core/selfcheck-runner.js +150 -0
- package/lib/core/settings-store.js +226 -0
- package/lib/core/shot-gc.js +334 -0
- package/lib/core/task-cwd.js +50 -0
- package/lib/core/task-memory.js +163 -0
- package/lib/core/task-registry.js +238 -0
- package/lib/core/toolcall-cache.js +206 -0
- package/lib/core/transfer-queue.js +58 -0
- package/lib/core/transfer.js +1144 -0
- package/lib/core/worker-arming.js +334 -0
- package/lib/index.js +2207 -0
- package/lib/provider/out-of-process-helpers.js +76 -0
- package/lib/provider/remote-provider.js +261 -0
- package/lib/tools/mediator-shim.mjs +180 -0
- package/lib/tools/prepare-sandbox-mediator.mjs +529 -0
- package/lib/tools/remote-tools.js +680 -0
- package/lib/tools/selfcheck-fix-prompt.md +206 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +72 -4
- package/slave/acp-profile-approval.patch.yml +66 -0
- package/slave/file-transfer.mjs +552 -0
- package/slave/integrity-probe.ps1 +99 -0
- package/slave/make-launch-spec.mjs +184 -0
- package/slave/net-address.mjs +138 -0
- package/slave/node_modules/.package-lock.json +26 -0
- package/slave/node_modules/ws/LICENSE +20 -0
- package/slave/node_modules/ws/README.md +548 -0
- package/slave/node_modules/ws/browser.js +8 -0
- package/slave/node_modules/ws/index.js +22 -0
- package/slave/node_modules/ws/lib/buffer-util.js +131 -0
- package/slave/node_modules/ws/lib/constants.js +19 -0
- package/slave/node_modules/ws/lib/event-target.js +292 -0
- package/slave/node_modules/ws/lib/extension.js +203 -0
- package/slave/node_modules/ws/lib/limiter.js +55 -0
- package/slave/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/slave/node_modules/ws/lib/receiver.js +743 -0
- package/slave/node_modules/ws/lib/sender.js +607 -0
- package/slave/node_modules/ws/lib/stream.js +161 -0
- package/slave/node_modules/ws/lib/subprotocol.js +62 -0
- package/slave/node_modules/ws/lib/validation.js +152 -0
- package/slave/node_modules/ws/lib/websocket-server.js +562 -0
- package/slave/node_modules/ws/lib/websocket.js +1421 -0
- package/slave/node_modules/ws/package.json +74 -0
- package/slave/node_modules/ws/wrapper.mjs +21 -0
- package/slave/pairing.mjs +175 -0
- package/slave/permission-pairing.mjs +240 -0
- package/slave/prune-shots.mjs +170 -0
- package/slave/relay.mjs +1351 -0
- package/slave/repair-workspace-integrity.ps1 +75 -0
- package/slave/screenshot.ps1 +114 -0
- package/slave/selfcheck.ps1 +350 -0
- package/slave/stage-app.ps1 +116 -0
- package/slave/workspace-integrity.mjs +157 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prepare-sandbox-mediator.mjs -- the one command the worker-side sandbox gate prescribes.
|
|
4
|
+
*
|
|
5
|
+
* WHO RUNS THIS, AND WHY IT IS NOT AUTOMATIC. The settings page diagnoses what is missing and
|
|
6
|
+
* prints this script's invocation with every path filled in (lib/core/sandbox-diagnosis.js). It
|
|
7
|
+
* does NOT run it: materializing a mediator writes ~1300 files to a machine path and (when the
|
|
8
|
+
* profile is not wired yet) appends a row to `%DSH_HOME%\profiles\acp\cordis.patch.yml`, which is
|
|
9
|
+
* the OPERATOR's file, not the plugin's own state. A page visit must not do that on its own; a
|
|
10
|
+
* person or an agent they point at the printed command can. The probe is still the thing that
|
|
11
|
+
* decides whether arming is allowed.
|
|
12
|
+
*
|
|
13
|
+
* WHAT IT DOES, IN ORDER
|
|
14
|
+
* 1. read the profile; if it already names a shim, that directory is the target (the profile is
|
|
15
|
+
* the source of truth -- see sandbox-probe.js), so the operator's existing wiring is honored
|
|
16
|
+
* and nothing is rewritten;
|
|
17
|
+
* 2. materialize the mediator there if `shim.mjs` + the runner closure are missing. The closure
|
|
18
|
+
* is read OUT of the running installation's `app.asar`, because plain node.exe cannot read
|
|
19
|
+
* inside it: this script re-runs itself under `DeepSeek Harness.exe` with
|
|
20
|
+
* `ELECTRON_RUN_AS_NODE=1` for that step;
|
|
21
|
+
* 3. drop the shipped `mediator-shim.mjs` in as `shim.mjs` (the console-subsystem mediator that
|
|
22
|
+
* translates bwrap arguments -- see that file's header);
|
|
23
|
+
* 4. ONLY when the profile has no `sandbox` entry at all, append one naming this node.exe and
|
|
24
|
+
* this shim. An existing entry is never rewritten: a second entry for the same id would be a
|
|
25
|
+
* merge nobody can predict from reading the file;
|
|
26
|
+
* 5. run the six-check probe and report. Exit code: 0 ready, 1 not ready, 2 could not run.
|
|
27
|
+
*
|
|
28
|
+
* IDEMPOTENT. A second run over a healthy machine touches no files (`--dry-run` proves it).
|
|
29
|
+
*
|
|
30
|
+
* USAGE
|
|
31
|
+
* node prepare-sandbox-mediator.mjs [--dsh-home <dir>] [--profile acp]
|
|
32
|
+
* [--install <dir>] [--target <dir>] [--dry-run]
|
|
33
|
+
*/
|
|
34
|
+
import { spawnSync } from 'node:child_process'
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
36
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
37
|
+
import { fileURLToPath } from 'node:url'
|
|
38
|
+
|
|
39
|
+
import { inspectSandbox } from '../core/sandbox-diagnosis.js'
|
|
40
|
+
import { probeSandbox } from '../core/sandbox-probe.js'
|
|
41
|
+
import { resolveDshHome } from '../core/settings-store.js'
|
|
42
|
+
|
|
43
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
44
|
+
const SHIM_SOURCE = join(HERE, 'mediator-shim.mjs')
|
|
45
|
+
const SELF = fileURLToPath(import.meta.url)
|
|
46
|
+
|
|
47
|
+
/** Packages the runner needs at run time. A hand-written list was tried and failed twice. */
|
|
48
|
+
const SEEDS = [
|
|
49
|
+
'@deepseek-ai/dsh-sandbox-windows-acl',
|
|
50
|
+
'@deepseek-ai/dsh-subprocess',
|
|
51
|
+
'@deepseek-ai/dsh-win32-process',
|
|
52
|
+
'@deepseek-ai/dsh-lazy-require',
|
|
53
|
+
'@deepseek-ai/dsh-skill',
|
|
54
|
+
'@deepseek-ai/cordis',
|
|
55
|
+
'koffi',
|
|
56
|
+
'@koromix/koffi-win32-x64',
|
|
57
|
+
'yaml',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/** The managed block this script appends when the profile has no `sandbox` row at all. */
|
|
61
|
+
export const MANAGED_HEADER = '# --- dsh-plugin-remote-use managed (auto-generated; delete this block to let the plugin prescribe again) ---'
|
|
62
|
+
export const MANAGED_FOOTER = '# --- end dsh-plugin-remote-use managed ---'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {{ nodeExe: string, shim: string }} opts
|
|
66
|
+
* @returns {string} a YAML list of one `sandbox` entry
|
|
67
|
+
*/
|
|
68
|
+
export function sandboxEntry({ nodeExe, shim }) {
|
|
69
|
+
const quoted = (p) => String(p).replace(/\\/gu, '\\\\')
|
|
70
|
+
return [
|
|
71
|
+
MANAGED_HEADER,
|
|
72
|
+
'# The Windows sandbox build its mediator from `process.execPath`, which is',
|
|
73
|
+
'# "DeepSeek Harness.exe" -- a GUI-subsystem image. Windows never allocates a console to one,',
|
|
74
|
+
'# so the restricted child dies during DLL initialization and EVERY command fails silently.',
|
|
75
|
+
'# node.exe is a console-subsystem image, and shim.mjs translates bwrap arguments to the',
|
|
76
|
+
'# Windows runner. Re-run prepare-sandbox-mediator.mjs if the mediator directory is removed.',
|
|
77
|
+
'- id: sandbox',
|
|
78
|
+
" name: '@deepseek-ai/dsh-sandbox-local'",
|
|
79
|
+
' config:',
|
|
80
|
+
' runnerCommand:',
|
|
81
|
+
` - "${quoted(nodeExe)}"`,
|
|
82
|
+
` - "${quoted(shim)}"`,
|
|
83
|
+
' runnerFailureSignatures:',
|
|
84
|
+
' - "windows-acl-run: "',
|
|
85
|
+
MANAGED_FOOTER,
|
|
86
|
+
].join('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Does the profile already contain a sandbox row (wired or not)? */
|
|
90
|
+
export function hasSandboxEntry(text) {
|
|
91
|
+
return /(?:^|\n)\s*-\s*id:\s*sandbox\s*(?:\n|$)/u.test(String(text ?? ''))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove a standalone `[]` line.
|
|
96
|
+
*
|
|
97
|
+
* DSH's profile template IS the empty flow array: `[]` is a complete YAML document, and appending
|
|
98
|
+
* entries after it produces TWO documents with no `---` separator. The ACP child then refuses the
|
|
99
|
+
* whole profile ("end of the stream or a document separator is expected") while the arming probe,
|
|
100
|
+
* which reads the file structurally, is happy. Measured 2026-11-05: the worker armed, and every
|
|
101
|
+
* ACP connection afterwards closed -- the master saw only `S0 worker reachable: fail` (PITFALLS F45).
|
|
102
|
+
*
|
|
103
|
+
* A file that is ONLY `[]` is valid and is left alone by the caller.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} text
|
|
106
|
+
* @returns {{ text: string, removed: boolean }}
|
|
107
|
+
*/
|
|
108
|
+
export function stripEmptyListMarker(text) {
|
|
109
|
+
const lines = String(text ?? '').split(/\r?\n/)
|
|
110
|
+
const kept = lines.filter((line) => !/^[ \t]*\[\][ \t]*$/u.test(line))
|
|
111
|
+
return { text: kept.join('\n'), removed: kept.length !== lines.length }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Ask the REAL consumer whether this profile parses.
|
|
116
|
+
*
|
|
117
|
+
* `--dump-config` composes the profile (including this overlay) and exits non-zero on a parse or
|
|
118
|
+
* schema error. It is the same loader the ACP child uses, which is the point: the arming probe
|
|
119
|
+
* cannot answer this question, and a profile that parses in one place and not the other is how a
|
|
120
|
+
* worker arms and then cannot serve a single connection.
|
|
121
|
+
*
|
|
122
|
+
* @param {{ installDir: string, profile: string }} opts
|
|
123
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
124
|
+
*/
|
|
125
|
+
export function validateProfile(opts) {
|
|
126
|
+
const exe = join(opts.installDir, 'DeepSeek Harness.exe')
|
|
127
|
+
const cli = join(opts.installDir, 'resources', 'app.asar', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js')
|
|
128
|
+
const run = spawnSync(exe, ['--expose-internals', cli, '--profile', opts.profile, '--dump-config'], {
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
timeout: 60_000,
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
|
133
|
+
})
|
|
134
|
+
if (run.error !== undefined) return { ok: false, detail: run.error.message }
|
|
135
|
+
if (run.status !== 0) {
|
|
136
|
+
return { ok: false, detail: String(run.stderr ?? '').trim().split(/\r?\n/u).slice(-4).join(' / ') }
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, detail: '' }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Append the managed block, preserving the file's own text byte for byte.
|
|
143
|
+
*
|
|
144
|
+
* NO BOM: a BOM written here would be the third time this project made a YAML file unreadable
|
|
145
|
+
* (PITFALLS E8). `writeFileSync` with 'utf8' writes none.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} text
|
|
148
|
+
* @param {string} entry
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
export function appendEntry(text, entry) {
|
|
152
|
+
// `[]` is the profile template's EMPTY LIST -- the patch list itself. Appending after it makes a
|
|
153
|
+
// second YAML document; see stripEmptyListMarker.
|
|
154
|
+
const { text: stripped } = stripEmptyListMarker(text)
|
|
155
|
+
const base = stripped.replace(/\s+$/u, '')
|
|
156
|
+
if (base === '') return `${entry}\n`
|
|
157
|
+
return `${base}\n\n${entry}\n`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @param {string} file */
|
|
161
|
+
function readText(file) {
|
|
162
|
+
try {
|
|
163
|
+
return readFileSync(file, 'utf8').replace(/^\uFEFF/u, '')
|
|
164
|
+
} catch {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// extraction from app.asar
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Copy the runner's dependency closure out of `app.asar`.
|
|
175
|
+
*
|
|
176
|
+
* WHY A CLOSURE AND NOT A LIST: adding packages one at a time produced two consecutive invalid
|
|
177
|
+
* experiments, each reported as evidence about the sandbox. Resolving `dependencies` from each
|
|
178
|
+
* copied manifest makes that class of failure impossible.
|
|
179
|
+
*
|
|
180
|
+
* MUST RUN WHERE `fs` CAN READ app.asar -- i.e. under Electron. That is ensured by the caller.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} asarRoot
|
|
183
|
+
* @param {string} outDir
|
|
184
|
+
* @returns {{ ok: boolean, copied: number, missing: string[], natives: number }}
|
|
185
|
+
*/
|
|
186
|
+
export function extractAsar(asarRoot, outDir) {
|
|
187
|
+
const unpacked = asarRoot.replace(/app\.asar$/u, 'app.asar.unpacked')
|
|
188
|
+
const roots = [asarRoot]
|
|
189
|
+
if (existsSync(unpacked)) roots.push(unpacked)
|
|
190
|
+
mkdirSync(join(outDir, 'node_modules'), { recursive: true })
|
|
191
|
+
|
|
192
|
+
const locateCache = new Map()
|
|
193
|
+
/** @param {string} base */
|
|
194
|
+
const locate = (base) => {
|
|
195
|
+
if (locateCache.has(base)) return locateCache.get(base)
|
|
196
|
+
const hits = []
|
|
197
|
+
const count = (dir) => {
|
|
198
|
+
let n = 0
|
|
199
|
+
let entries
|
|
200
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return 0 }
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (entry.isDirectory()) n += count(join(dir, entry.name))
|
|
203
|
+
else n += 1
|
|
204
|
+
}
|
|
205
|
+
return n
|
|
206
|
+
}
|
|
207
|
+
const walk = (dir, depth) => {
|
|
208
|
+
if (depth > 7) return
|
|
209
|
+
let entries
|
|
210
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (!entry.isDirectory()) continue
|
|
213
|
+
const p = join(dir, entry.name)
|
|
214
|
+
if (entry.name === base) {
|
|
215
|
+
try {
|
|
216
|
+
if (statSync(join(p, 'package.json')).isFile()) hits.push({ path: p, files: count(p) })
|
|
217
|
+
} catch { /* no manifest */ }
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
walk(p, depth + 1)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const root of roots) walk(root, 0)
|
|
224
|
+
hits.sort((a, b) => b.files - a.files)
|
|
225
|
+
locateCache.set(base, hits)
|
|
226
|
+
return hits
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const copyTree = (src, dest) => {
|
|
230
|
+
mkdirSync(dest, { recursive: true })
|
|
231
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
232
|
+
const s = join(src, entry.name)
|
|
233
|
+
const d = join(dest, entry.name)
|
|
234
|
+
if (entry.isDirectory()) copyTree(s, d)
|
|
235
|
+
else writeFileSync(d, readFileSync(s))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const copied = new Set()
|
|
240
|
+
const missing = []
|
|
241
|
+
const queue = [...SEEDS]
|
|
242
|
+
while (queue.length > 0) {
|
|
243
|
+
const name = queue.shift()
|
|
244
|
+
if (copied.has(name) || missing.includes(name)) continue
|
|
245
|
+
if (name.startsWith('node:') || name.startsWith('.')) continue
|
|
246
|
+
const base = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name
|
|
247
|
+
const chosen = locate(base)[0]
|
|
248
|
+
if (chosen === undefined) {
|
|
249
|
+
missing.push(name)
|
|
250
|
+
process.stdout.write(` MISSING ${name}\n`)
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
const dest = join(outDir, 'node_modules', ...name.split('/'))
|
|
254
|
+
try {
|
|
255
|
+
copyTree(chosen.path, dest)
|
|
256
|
+
copied.add(name)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
missing.push(name)
|
|
259
|
+
process.stdout.write(` FAILED ${name}: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
let manifest = {}
|
|
263
|
+
try { manifest = JSON.parse(readFileSync(join(chosen.path, 'package.json'), 'utf8')) } catch { /* unreadable */ }
|
|
264
|
+
for (const field of ['dependencies', 'peerDependencies']) {
|
|
265
|
+
for (const dep of Object.keys(manifest[field] ?? {})) {
|
|
266
|
+
if (!copied.has(dep) && !missing.includes(dep) && !queue.includes(dep)) queue.push(dep)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const dep of Object.keys(manifest.optionalDependencies ?? {})) {
|
|
270
|
+
if (!copied.has(dep) && !queue.includes(dep)) queue.push(dep)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const natives = []
|
|
275
|
+
const findNative = (dir) => {
|
|
276
|
+
let entries
|
|
277
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const p = join(dir, entry.name)
|
|
280
|
+
if (entry.isDirectory()) findNative(p)
|
|
281
|
+
else if (entry.name.endsWith('.node')) natives.push(p)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
findNative(join(outDir, 'node_modules'))
|
|
285
|
+
|
|
286
|
+
const runner = join(outDir, 'node_modules', '@deepseek-ai', 'dsh-sandbox-windows-acl', 'lib', 'runner.js')
|
|
287
|
+
// `missing` includes optional dependencies for OTHER platforms, which is normal and must not fail
|
|
288
|
+
// the run: koffi lists every OS/arch variant as optional, and only this machine's is present.
|
|
289
|
+
// Only a seed that is absent is a real failure -- the seeds are the entry points this mediator
|
|
290
|
+
// cannot start without.
|
|
291
|
+
const missingRequired = missing.filter((name) => SEEDS.includes(name))
|
|
292
|
+
return {
|
|
293
|
+
ok: existsSync(runner) && natives.length > 0 && missingRequired.length === 0,
|
|
294
|
+
copied: copied.size,
|
|
295
|
+
missing,
|
|
296
|
+
missingRequired,
|
|
297
|
+
natives: natives.length,
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// main
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
function argOf(argv, name, fallback) {
|
|
306
|
+
const i = argv.indexOf(`--${name}`)
|
|
307
|
+
return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : fallback
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {string[]} [argv]
|
|
312
|
+
* @param {object} [deps] injectable for tests: env, exists, spawn, probe, write
|
|
313
|
+
*/
|
|
314
|
+
export function main(argv = process.argv.slice(2), deps = {}) {
|
|
315
|
+
const env = deps.env ?? process.env
|
|
316
|
+
const exists = deps.exists ?? existsSync
|
|
317
|
+
const say = deps.say ?? ((line) => process.stdout.write(`${line}\n`))
|
|
318
|
+
const dryRun = argv.includes('--dry-run')
|
|
319
|
+
|
|
320
|
+
// The internal mode: only reachable when this file re-runs itself under Electron.
|
|
321
|
+
const extractIndex = argv.indexOf('--extract-asar')
|
|
322
|
+
if (extractIndex >= 0) {
|
|
323
|
+
const asarRoot = argv[extractIndex + 1] ?? ''
|
|
324
|
+
const outDir = argv[extractIndex + 2] ?? ''
|
|
325
|
+
if (asarRoot === '' || outDir === '') {
|
|
326
|
+
say('--extract-asar needs <app.asar> <outDir>')
|
|
327
|
+
return 2
|
|
328
|
+
}
|
|
329
|
+
say(`extracting into ${outDir}`)
|
|
330
|
+
const result = extractAsar(asarRoot, outDir)
|
|
331
|
+
say(`copied ${result.copied} packages, ${result.natives} native binaries`
|
|
332
|
+
+ (result.missingRequired.length > 0 ? `, MISSING REQUIRED: ${result.missingRequired.join(', ')}` : '')
|
|
333
|
+
+ (result.missing.length > result.missingRequired.length
|
|
334
|
+
? `, absent optional (other platforms): ${result.missing.length - result.missingRequired.length}`
|
|
335
|
+
: ''))
|
|
336
|
+
return result.ok ? 0 : 1
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const profile = argOf(argv, 'profile', 'acp')
|
|
340
|
+
if (!/^[\w.-]+$/u.test(profile)) {
|
|
341
|
+
say(`refusing profile name ${JSON.stringify(profile)}: expected a plain profile name`)
|
|
342
|
+
return 2
|
|
343
|
+
}
|
|
344
|
+
const installArg = argOf(argv, 'install', undefined)
|
|
345
|
+
const targetArg = argOf(argv, 'target', undefined)
|
|
346
|
+
|
|
347
|
+
const home = argOf(argv, 'dsh-home', undefined) ?? resolveDshHome(env)
|
|
348
|
+
const profilePath = join(home, 'profiles', profile, 'cordis.patch.yml')
|
|
349
|
+
|
|
350
|
+
// REPAIR BEFORE DIAGNOSING: the `[]` + entries shape is invalid YAML, and leaving it in place
|
|
351
|
+
// would make the diagnosis report "unwired" (the guard in sandbox-probe.js) and this script
|
|
352
|
+
// refuse to touch a file that it is the one able to fix.
|
|
353
|
+
let profileText = readText(profilePath)
|
|
354
|
+
const marker = profileText === undefined ? { text: undefined, removed: false } : stripEmptyListMarker(profileText)
|
|
355
|
+
if (marker.removed && /(?:^|\n)[ \t]*-[ \t]/u.test(String(profileText))) {
|
|
356
|
+
if (dryRun) {
|
|
357
|
+
say(' profile : would remove the invalid `[]` marker (two YAML documents -> the ACP child refuses to start)')
|
|
358
|
+
} else {
|
|
359
|
+
try { writeFileSync(`${profilePath}.bak-bracefix-${Date.now()}`, String(profileText), 'utf8') } catch { /* best effort */ }
|
|
360
|
+
writeFileSync(profilePath, marker.text, 'utf8')
|
|
361
|
+
profileText = marker.text
|
|
362
|
+
say(' profile : removed the invalid `[]` line (two YAML documents -> the ACP child refused to start)')
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const diagnosis = inspectSandbox({
|
|
367
|
+
env,
|
|
368
|
+
home,
|
|
369
|
+
profilePath,
|
|
370
|
+
installDir: installArg === undefined ? undefined : installArg,
|
|
371
|
+
exists,
|
|
372
|
+
// Injected so a test can describe a machine without a real PE image to inspect.
|
|
373
|
+
subsystem: deps.subsystem,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
say('sandbox mediator preparation')
|
|
377
|
+
say(` dsh home : ${diagnosis.home}`)
|
|
378
|
+
say(` profile : ${profilePath}`)
|
|
379
|
+
say(` install : ${diagnosis.installDir === '' ? '(not found)' : diagnosis.installDir}`)
|
|
380
|
+
say(` node.exe : ${diagnosis.nodeExe === '' ? '(not found)' : diagnosis.nodeExe}`)
|
|
381
|
+
say(` state now : ${diagnosis.state}${diagnosis.reason === '' ? '' : ` -- ${diagnosis.reason}`}`)
|
|
382
|
+
say('')
|
|
383
|
+
|
|
384
|
+
if (diagnosis.platform !== 'win32') {
|
|
385
|
+
say('this platform has no Windows sandbox mediator to prepare')
|
|
386
|
+
return 2
|
|
387
|
+
}
|
|
388
|
+
if (diagnosis.installDir === '' || diagnosis.nodeExe === '') {
|
|
389
|
+
say('cannot prepare: the installation or a console-subsystem node.exe was not found')
|
|
390
|
+
say('pass --install <dir> and/or set DSH_NODE_EXE to a node.exe')
|
|
391
|
+
return 2
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 1. target = what the profile already names (the profile is the source of truth).
|
|
395
|
+
const alreadyHasEntry = profileText !== undefined && hasSandboxEntry(profileText)
|
|
396
|
+
const target = diagnosis.profileNamesShim
|
|
397
|
+
? diagnosis.mediatorDir
|
|
398
|
+
: (targetArg ?? join(diagnosis.home, 'sandbox-mediator'))
|
|
399
|
+
say(` target : ${target}`)
|
|
400
|
+
|
|
401
|
+
if (diagnosis.profileNamesShim && targetArg !== undefined && targetArg.toLowerCase() !== target.toLowerCase()) {
|
|
402
|
+
say(` note : ignoring --target; the profile already names ${diagnosis.shim}`)
|
|
403
|
+
}
|
|
404
|
+
if (!diagnosis.profileNamesShim && alreadyHasEntry) {
|
|
405
|
+
say('')
|
|
406
|
+
say('the profile already has a `sandbox` entry this script will not rewrite:')
|
|
407
|
+
say(` ${diagnosis.profileReason}`)
|
|
408
|
+
say('fix or remove that block by hand, then run this again')
|
|
409
|
+
return 1
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 2. materialize the runner closure if it is not there.
|
|
413
|
+
const shim = join(target, 'shim.mjs')
|
|
414
|
+
const runner = join(target, 'node_modules', '@deepseek-ai', 'dsh-sandbox-windows-acl', 'lib', 'runner.js')
|
|
415
|
+
const haveRunner = exists(runner)
|
|
416
|
+
const haveShim = exists(shim)
|
|
417
|
+
let materialized = haveRunner && haveShim
|
|
418
|
+
if (materialized) {
|
|
419
|
+
say(' mediator : already materialized -- leaving it alone')
|
|
420
|
+
} else if (dryRun) {
|
|
421
|
+
say(` mediator : ${haveRunner ? 'shim.mjs is missing' : 'MISSING'} (would materialize from app.asar)`)
|
|
422
|
+
} else if (haveRunner) {
|
|
423
|
+
// A previous run that stopped after extraction, or someone deleted the shim. Re-installing the
|
|
424
|
+
// shipped shim is cheap and cannot regress the runner, so there is no reason to re-extract here.
|
|
425
|
+
writeFileSync(shim, readFileSync(SHIM_SOURCE))
|
|
426
|
+
materialized = true
|
|
427
|
+
say(` shim : installed at ${shim} (the runner was already present)`)
|
|
428
|
+
} else {
|
|
429
|
+
const asarRoot = join(diagnosis.installDir, 'resources', 'app.asar')
|
|
430
|
+
if (!exists(asarRoot)) {
|
|
431
|
+
say(`cannot prepare: ${asarRoot} does not exist`)
|
|
432
|
+
return 2
|
|
433
|
+
}
|
|
434
|
+
mkdirSync(target, { recursive: true })
|
|
435
|
+
const spawn = deps.spawn ?? spawnSync
|
|
436
|
+
const exe = join(diagnosis.installDir, 'DeepSeek Harness.exe')
|
|
437
|
+
say(` extracting : ${relative(diagnosis.installDir, asarRoot)} -> ${target}`)
|
|
438
|
+
// Re-run THIS file under Electron so `fs` can read app.asar. stdio is inherited: the
|
|
439
|
+
// operator sees the package-by-package trace, and a failure is visible where it happened.
|
|
440
|
+
const run = spawn(exe, [SELF, '--extract-asar', asarRoot, target], {
|
|
441
|
+
stdio: 'inherit',
|
|
442
|
+
windowsHide: true,
|
|
443
|
+
timeout: 300_000,
|
|
444
|
+
env: { ...env, ELECTRON_RUN_AS_NODE: '1' },
|
|
445
|
+
})
|
|
446
|
+
if (run.error !== undefined || (run.status ?? 1) !== 0) {
|
|
447
|
+
say(` extraction failed (${run.error?.message ?? `exit ${run.status}`})`)
|
|
448
|
+
return 2
|
|
449
|
+
}
|
|
450
|
+
if (!exists(runner)) {
|
|
451
|
+
say(` extraction finished but ${runner} is missing`)
|
|
452
|
+
return 2
|
|
453
|
+
}
|
|
454
|
+
writeFileSync(shim, readFileSync(SHIM_SOURCE))
|
|
455
|
+
materialized = true
|
|
456
|
+
say(` shim : ${shim}`)
|
|
457
|
+
say(` runner : ${runner}`)
|
|
458
|
+
try {
|
|
459
|
+
writeFileSync(join(target, 'mediator.json'), JSON.stringify({
|
|
460
|
+
preparedAt: new Date().toISOString(),
|
|
461
|
+
installDir: diagnosis.installDir,
|
|
462
|
+
asar: asarRoot,
|
|
463
|
+
nodeExe: diagnosis.nodeExe,
|
|
464
|
+
shim,
|
|
465
|
+
preparedBy: 'prepare-sandbox-mediator.mjs',
|
|
466
|
+
}, null, 2), 'utf8')
|
|
467
|
+
} catch { /* provenance is a convenience, not a requirement */ }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 3. wire the profile ONLY when it has no sandbox row at all.
|
|
471
|
+
if (!diagnosis.profileNamesShim && !alreadyHasEntry) {
|
|
472
|
+
const entry = sandboxEntry({ nodeExe: diagnosis.nodeExe, shim })
|
|
473
|
+
if (dryRun) {
|
|
474
|
+
say(' profile : would append the managed sandbox block')
|
|
475
|
+
} else {
|
|
476
|
+
mkdirSync(dirname(profilePath), { recursive: true })
|
|
477
|
+
const before = readText(profilePath)
|
|
478
|
+
if (before !== undefined) {
|
|
479
|
+
try {
|
|
480
|
+
writeFileSync(`${profilePath}.bak-sandbox-${Date.now()}`, before, 'utf8')
|
|
481
|
+
} catch { /* a backup is best effort; refusing to write over it is not */ }
|
|
482
|
+
}
|
|
483
|
+
writeFileSync(profilePath, appendEntry(before ?? '', entry), 'utf8')
|
|
484
|
+
say(` profile : appended the managed sandbox block to ${profilePath}`)
|
|
485
|
+
}
|
|
486
|
+
} else if (diagnosis.profileNamesShim) {
|
|
487
|
+
say(' profile : already names this shim -- not rewritten')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (dryRun) {
|
|
491
|
+
say('')
|
|
492
|
+
say('dry run: nothing was written')
|
|
493
|
+
return 0
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 3b. PROVE THE PROFILE PARSES, with the loader the ACP child actually uses.
|
|
497
|
+
//
|
|
498
|
+
// The sandbox probe below cannot answer this: it reads the file structurally, so a profile with
|
|
499
|
+
// two YAML documents passes it and fails at ACP startup (PITFALLS F45). `--dump-config` is the
|
|
500
|
+
// real consumer, it exits non-zero on a parse error, and it costs about a second.
|
|
501
|
+
const validation = validateProfile({ installDir: diagnosis.installDir, profile })
|
|
502
|
+
if (!validation.ok) {
|
|
503
|
+
say('')
|
|
504
|
+
say(`the profile does not parse: ${validation.detail}`)
|
|
505
|
+
say('the ACP child would refuse to start; do not arm this machine until this is fixed')
|
|
506
|
+
return 1
|
|
507
|
+
}
|
|
508
|
+
say(' profile : parses (--dump-config OK)')
|
|
509
|
+
|
|
510
|
+
// 4. verify with the same probe the gate uses. Nothing less than that counts.
|
|
511
|
+
say('')
|
|
512
|
+
say('verifying with the arming gate\'s own probe:')
|
|
513
|
+
const probe = deps.probe ?? ((opts) => probeSandbox(opts))
|
|
514
|
+
const result = probe({ profile: profilePath })
|
|
515
|
+
for (const check of result.checks ?? []) {
|
|
516
|
+
say(` [${check.ok ? 'PASS' : 'FAIL'}] ${check.name}${check.detail === '' ? '' : ` -- ${check.detail}`}`)
|
|
517
|
+
}
|
|
518
|
+
say('')
|
|
519
|
+
if (result.ok === true) {
|
|
520
|
+
say('RESULT: READY -- arm this machine from 设置 → 远程工作节点 → 本机角色')
|
|
521
|
+
return 0
|
|
522
|
+
}
|
|
523
|
+
say(`RESULT: NOT READY -- ${result.reason}`)
|
|
524
|
+
return 1
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const invokedDirectly = process.argv[1] !== undefined
|
|
528
|
+
&& resolve(SELF).toLowerCase() === resolve(process.argv[1]).toLowerCase()
|
|
529
|
+
if (invokedDirectly) process.exit(main())
|