deepclause-sdk 0.0.67 → 0.0.68
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.md +64 -0
- package/dist/cli/config.d.ts +18 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +19 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +18 -7
- package/dist/cli/tui.js.map +1 -1
- package/dist/system/assets/compactors/default-loop-compactor.dml +1 -1
- package/dist/system/assets/compactors/default-session-compactor.dml +1 -1
- package/dist/system/runtime/agentvm-manager.d.ts +4 -0
- package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
- package/dist/system/runtime/agentvm-manager.js +5 -3
- package/dist/system/runtime/agentvm-manager.js.map +1 -1
- package/dist/system/runtime/dml-executor.js +1 -0
- package/dist/system/runtime/dml-executor.js.map +1 -1
- package/dist/system/runtime/shell-manager.d.ts +40 -1
- package/dist/system/runtime/shell-manager.d.ts.map +1 -1
- package/dist/system/runtime/shell-manager.js +567 -7
- package/dist/system/runtime/shell-manager.js.map +1 -1
- package/dist/system/runtime/skill-creator.d.ts.map +1 -1
- package/dist/system/runtime/skill-creator.js +1 -0
- package/dist/system/runtime/skill-creator.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, rmSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
2
5
|
import { AgentVMManager, } from './agentvm-manager.js';
|
|
3
6
|
const ABORT_FORCE_KILL_MS = 500;
|
|
4
7
|
const EXIT_STDIO_GRACE_MS = 25;
|
|
5
8
|
const DEFAULT_HOST_SHELL_IDLE_TIMEOUT_MS = 180_000;
|
|
9
|
+
const HOST_SHELL_WRAPPER_OVERRIDE_ENV = 'DC_HOST_SHELL_WRAPPER';
|
|
10
|
+
const HOST_SHELL_DEBUG_ENV = 'DC_HOST_SHELL_DEBUG';
|
|
11
|
+
const DEFAULT_HOST_EXECUTABLE_LOOKUP_DIRS = process.platform === 'darwin'
|
|
12
|
+
? ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin', '/opt/homebrew/bin']
|
|
13
|
+
: ['/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/bin'];
|
|
14
|
+
const DEFAULT_BWRAP_READ_ONLY_PATHS = [
|
|
15
|
+
'/bin',
|
|
16
|
+
'/usr',
|
|
17
|
+
'/sbin',
|
|
18
|
+
'/lib',
|
|
19
|
+
'/lib64',
|
|
20
|
+
'/etc',
|
|
21
|
+
'/usr/local',
|
|
22
|
+
'/opt',
|
|
23
|
+
'/nix',
|
|
24
|
+
'/run/current-system',
|
|
25
|
+
];
|
|
26
|
+
const DEFAULT_BWRAP_REQUIRED_HOST_FILES = ['/etc/resolv.conf'];
|
|
6
27
|
export class HostShellManager {
|
|
7
28
|
workspacePath;
|
|
8
29
|
kind = 'host';
|
|
9
|
-
|
|
30
|
+
strategy;
|
|
31
|
+
constructor(workspacePath, hostConfig = {}) {
|
|
10
32
|
this.workspacePath = workspacePath;
|
|
33
|
+
this.strategy = resolveHostShellStrategy({ hostConfig });
|
|
11
34
|
}
|
|
12
35
|
async exec(command, signal, observer) {
|
|
13
36
|
if (signal?.aborted) {
|
|
@@ -15,15 +38,17 @@ export class HostShellManager {
|
|
|
15
38
|
}
|
|
16
39
|
return new Promise((resolve, reject) => {
|
|
17
40
|
const idleTimeoutMs = getHostShellIdleTimeoutMs();
|
|
18
|
-
const
|
|
41
|
+
const launchPlan = buildHostShellLaunchPlan(this.strategy, command, this.workspacePath);
|
|
42
|
+
const child = spawn(launchPlan.executable, launchPlan.args, {
|
|
19
43
|
cwd: this.workspacePath,
|
|
20
|
-
env:
|
|
44
|
+
env: launchPlan.env,
|
|
21
45
|
detached: process.platform !== 'win32',
|
|
22
46
|
});
|
|
23
47
|
observer?.onStart?.({
|
|
24
48
|
command,
|
|
25
49
|
pid: child.pid,
|
|
26
50
|
backend: this.kind,
|
|
51
|
+
backendLabel: this.strategy.backendLabel,
|
|
27
52
|
});
|
|
28
53
|
let stdout = '';
|
|
29
54
|
let stderr = '';
|
|
@@ -69,6 +94,7 @@ export class HostShellManager {
|
|
|
69
94
|
chunk,
|
|
70
95
|
pid: child.pid,
|
|
71
96
|
backend: this.kind,
|
|
97
|
+
backendLabel: this.strategy.backendLabel,
|
|
72
98
|
});
|
|
73
99
|
});
|
|
74
100
|
child.stderr.on('data', (chunk) => {
|
|
@@ -79,6 +105,7 @@ export class HostShellManager {
|
|
|
79
105
|
chunk,
|
|
80
106
|
pid: child.pid,
|
|
81
107
|
backend: this.kind,
|
|
108
|
+
backendLabel: this.strategy.backendLabel,
|
|
82
109
|
});
|
|
83
110
|
});
|
|
84
111
|
let forceKillTimer = null;
|
|
@@ -100,9 +127,14 @@ export class HostShellManager {
|
|
|
100
127
|
exitCode: resolvedExitCode,
|
|
101
128
|
pid: child.pid,
|
|
102
129
|
backend: this.kind,
|
|
130
|
+
backendLabel: launchPlan.strategy.backendLabel,
|
|
103
131
|
summary: resolvedExitCode === 0
|
|
104
|
-
?
|
|
105
|
-
: (timedOut
|
|
132
|
+
? `Command completed successfully via ${launchPlan.strategy.backendLabel}`
|
|
133
|
+
: (timedOut
|
|
134
|
+
? `Command timed out after ${idleTimeoutMs}ms without output via ${launchPlan.strategy.backendLabel}`
|
|
135
|
+
: (stderr
|
|
136
|
+
? `Command failed via ${launchPlan.strategy.backendLabel}: ${stderr}`
|
|
137
|
+
: `Command failed via ${launchPlan.strategy.backendLabel} with exit code ${resolvedExitCode}${signalSuffix}`)),
|
|
106
138
|
};
|
|
107
139
|
};
|
|
108
140
|
const finalizeExit = (detachStreams = false) => {
|
|
@@ -114,6 +146,7 @@ export class HostShellManager {
|
|
|
114
146
|
command,
|
|
115
147
|
pid: child.pid,
|
|
116
148
|
backend: this.kind,
|
|
149
|
+
backendLabel: result.backendLabel,
|
|
117
150
|
success: result.success,
|
|
118
151
|
exitCode: result.exitCode,
|
|
119
152
|
summary: result.summary,
|
|
@@ -125,6 +158,7 @@ export class HostShellManager {
|
|
|
125
158
|
return;
|
|
126
159
|
}
|
|
127
160
|
settled = true;
|
|
161
|
+
cleanupScratchDir(launchPlan.scratchDir);
|
|
128
162
|
cleanup();
|
|
129
163
|
reject(error);
|
|
130
164
|
};
|
|
@@ -133,6 +167,7 @@ export class HostShellManager {
|
|
|
133
167
|
return;
|
|
134
168
|
}
|
|
135
169
|
settled = true;
|
|
170
|
+
cleanupScratchDir(launchPlan.scratchDir);
|
|
136
171
|
cleanup();
|
|
137
172
|
resolve(result);
|
|
138
173
|
};
|
|
@@ -204,7 +239,439 @@ export function createShellManager(options) {
|
|
|
204
239
|
if (options.sandbox) {
|
|
205
240
|
return new AgentVMManager(options.workspacePath, options.network ?? false);
|
|
206
241
|
}
|
|
207
|
-
return new HostShellManager(options.workspacePath);
|
|
242
|
+
return new HostShellManager(options.workspacePath, options.hostConfig);
|
|
243
|
+
}
|
|
244
|
+
export function describeShellExecutionBackend(sandbox = false, hostConfig = {}) {
|
|
245
|
+
if (sandbox) {
|
|
246
|
+
return {
|
|
247
|
+
backendLabel: 'sandbox[agentvm]',
|
|
248
|
+
description: 'AgentVM sandbox',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const strategy = resolveHostShellStrategy({ hostConfig });
|
|
252
|
+
return {
|
|
253
|
+
backendLabel: strategy.backendLabel,
|
|
254
|
+
description: strategy.description,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export function resolveHostShellStrategy(options = {}) {
|
|
258
|
+
const { hostConfig = {}, platform = process.platform, probes = defaultHostShellWrapperProbes, envOverride = process.env[HOST_SHELL_WRAPPER_OVERRIDE_ENV], } = options;
|
|
259
|
+
const strictIsolationRequested = hostConfig.strictIsolation ?? false;
|
|
260
|
+
const normalizedOverride = normalizeHostShellWrapperPreference(envOverride);
|
|
261
|
+
const normalizedPreference = normalizeHostShellWrapperPreference(hostConfig.wrapper);
|
|
262
|
+
const shouldResolveBwrapExecWrapperMode = platform === 'linux'
|
|
263
|
+
&& typeof probes.resolveBwrapExecWrapperMode === 'function'
|
|
264
|
+
&& normalizedOverride !== 'clean-room'
|
|
265
|
+
&& normalizedOverride !== 'sandbox-exec'
|
|
266
|
+
&& !(normalizedOverride == null && normalizedPreference === 'clean-room')
|
|
267
|
+
&& !(normalizedOverride == null && normalizedPreference === 'sandbox-exec');
|
|
268
|
+
const resolvedBwrapExecWrapperMode = shouldResolveBwrapExecWrapperMode
|
|
269
|
+
? probes.resolveBwrapExecWrapperMode?.(strictIsolationRequested)
|
|
270
|
+
: undefined;
|
|
271
|
+
const strategyProbes = resolvedBwrapExecWrapperMode !== undefined
|
|
272
|
+
? {
|
|
273
|
+
...probes,
|
|
274
|
+
isBwrapUsable: () => resolvedBwrapExecWrapperMode !== null,
|
|
275
|
+
}
|
|
276
|
+
: probes;
|
|
277
|
+
const wrapperKind = resolveHostShellWrapperKind({
|
|
278
|
+
platform,
|
|
279
|
+
probes: strategyProbes,
|
|
280
|
+
wrapperPreference: hostConfig.wrapper,
|
|
281
|
+
envOverride,
|
|
282
|
+
strictIsolation: strictIsolationRequested,
|
|
283
|
+
});
|
|
284
|
+
const strictIsolation = wrapperKind === 'bwrap' && !!hostConfig.strictIsolation;
|
|
285
|
+
const backendLabel = `host[${wrapperKind}${strictIsolation ? ' strict' : ''}]`;
|
|
286
|
+
const bwrapExecWrapperMode = wrapperKind === 'bwrap'
|
|
287
|
+
? (resolvedBwrapExecWrapperMode ?? 'direct')
|
|
288
|
+
: undefined;
|
|
289
|
+
if (wrapperKind === 'bwrap') {
|
|
290
|
+
return {
|
|
291
|
+
wrapperKind,
|
|
292
|
+
strictIsolation,
|
|
293
|
+
backendLabel,
|
|
294
|
+
description: strictIsolation
|
|
295
|
+
? 'bubblewrap sandbox with network disabled'
|
|
296
|
+
: 'bubblewrap sandbox',
|
|
297
|
+
bwrapExecWrapperMode,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (wrapperKind === 'sandbox-exec') {
|
|
301
|
+
return {
|
|
302
|
+
wrapperKind,
|
|
303
|
+
strictIsolation: false,
|
|
304
|
+
backendLabel,
|
|
305
|
+
description: 'sandbox-exec host wrapper',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
wrapperKind,
|
|
310
|
+
strictIsolation: false,
|
|
311
|
+
backendLabel,
|
|
312
|
+
description: hostConfig.strictIsolation
|
|
313
|
+
? 'clean-room host shell (strict isolation requested but unavailable)'
|
|
314
|
+
: 'clean-room host shell',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
export function resolveHostShellWrapperKind(options = {}) {
|
|
318
|
+
const { platform = process.platform, probes = defaultHostShellWrapperProbes, wrapperPreference = 'auto', envOverride = process.env[HOST_SHELL_WRAPPER_OVERRIDE_ENV], strictIsolation = false, } = options;
|
|
319
|
+
const normalizedOverride = normalizeHostShellWrapperPreference(envOverride);
|
|
320
|
+
if (normalizedOverride) {
|
|
321
|
+
logHostShellDebug('wrapper override selected', {
|
|
322
|
+
platform,
|
|
323
|
+
envOverride,
|
|
324
|
+
wrapperPreference,
|
|
325
|
+
strictIsolation,
|
|
326
|
+
});
|
|
327
|
+
return normalizedOverride;
|
|
328
|
+
}
|
|
329
|
+
const normalizedPreference = normalizeHostShellWrapperPreference(wrapperPreference);
|
|
330
|
+
if (normalizedPreference) {
|
|
331
|
+
logHostShellDebug('wrapper preference selected', {
|
|
332
|
+
platform,
|
|
333
|
+
wrapperPreference,
|
|
334
|
+
strictIsolation,
|
|
335
|
+
});
|
|
336
|
+
return normalizedPreference;
|
|
337
|
+
}
|
|
338
|
+
if (platform === 'linux') {
|
|
339
|
+
const bwrapUsable = probes.isBwrapUsable(strictIsolation);
|
|
340
|
+
logHostShellDebug('bwrap probe completed', {
|
|
341
|
+
platform,
|
|
342
|
+
strictIsolation,
|
|
343
|
+
usable: bwrapUsable,
|
|
344
|
+
});
|
|
345
|
+
if (bwrapUsable) {
|
|
346
|
+
return 'bwrap';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (platform === 'darwin') {
|
|
350
|
+
const sandboxExecUsable = probes.isSandboxExecUsable();
|
|
351
|
+
logHostShellDebug('sandbox-exec probe completed', {
|
|
352
|
+
platform,
|
|
353
|
+
usable: sandboxExecUsable,
|
|
354
|
+
});
|
|
355
|
+
if (sandboxExecUsable) {
|
|
356
|
+
return 'sandbox-exec';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
logHostShellDebug('wrapper fallback selected', {
|
|
360
|
+
platform,
|
|
361
|
+
strictIsolation,
|
|
362
|
+
wrapperKind: 'clean-room',
|
|
363
|
+
});
|
|
364
|
+
return 'clean-room';
|
|
365
|
+
}
|
|
366
|
+
function normalizeHostShellWrapperPreference(value) {
|
|
367
|
+
const normalized = value?.trim().toLowerCase();
|
|
368
|
+
if (!normalized || normalized === 'auto') {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
if (normalized === 'clean-room' || normalized === 'bwrap' || normalized === 'sandbox-exec') {
|
|
372
|
+
return normalized;
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const defaultHostShellWrapperProbes = {
|
|
377
|
+
isBwrapUsable: (strictIsolation) => resolveBwrapExecWrapperMode(strictIsolation) !== null,
|
|
378
|
+
resolveBwrapExecWrapperMode,
|
|
379
|
+
isSandboxExecUsable: () => commandSucceeds('sandbox-exec', ['-p', '(version 1) (allow default)', '/usr/bin/true']),
|
|
380
|
+
};
|
|
381
|
+
function commandSucceeds(command, args) {
|
|
382
|
+
return probeCommand(command, args).success;
|
|
383
|
+
}
|
|
384
|
+
function resolveBwrapExecWrapperMode(strictIsolation) {
|
|
385
|
+
const probeArgs = buildBwrapProbeArgs(strictIsolation);
|
|
386
|
+
const directProbe = probeCommand('bwrap', probeArgs);
|
|
387
|
+
if (directProbe.success) {
|
|
388
|
+
return 'direct';
|
|
389
|
+
}
|
|
390
|
+
if (!hasUidMapPermissionDenied(directProbe) || !canUseSetprivWrapper()) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const wrappedProbe = probeCommand('setpriv', ['--no-new-privs', resolveExecutable('bwrap'), ...probeArgs]);
|
|
394
|
+
logHostShellDebug('bwrap setpriv fallback probe completed', {
|
|
395
|
+
strictIsolation,
|
|
396
|
+
usable: wrappedProbe.success,
|
|
397
|
+
});
|
|
398
|
+
return wrappedProbe.success ? 'setpriv-no-new-privs' : null;
|
|
399
|
+
}
|
|
400
|
+
function buildBwrapProbeArgs(strictIsolation) {
|
|
401
|
+
return [
|
|
402
|
+
'--unshare-user-try',
|
|
403
|
+
...(strictIsolation ? ['--unshare-net'] : []),
|
|
404
|
+
'--ro-bind', '/', '/',
|
|
405
|
+
'/bin/true',
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
function canUseSetprivWrapper() {
|
|
409
|
+
const executable = resolveExecutable('setpriv');
|
|
410
|
+
return executable.includes('/') && pathExists(executable);
|
|
411
|
+
}
|
|
412
|
+
function hasUidMapPermissionDenied(result) {
|
|
413
|
+
return [result.sanitized, result.inherited].some((probeResult) => {
|
|
414
|
+
const stderr = typeof probeResult.stderr === 'string' ? probeResult.stderr : '';
|
|
415
|
+
return stderr.includes('setting up uid map: Permission denied');
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function probeCommand(command, args) {
|
|
419
|
+
const executable = resolveExecutable(command);
|
|
420
|
+
const sanitized = spawnSync(executable, args, {
|
|
421
|
+
cwd: '/',
|
|
422
|
+
env: buildHostProbeEnv(),
|
|
423
|
+
stdio: isHostShellDebugEnabled() ? 'pipe' : 'ignore',
|
|
424
|
+
encoding: 'utf8',
|
|
425
|
+
});
|
|
426
|
+
logHostShellDebug('probe attempt', {
|
|
427
|
+
command,
|
|
428
|
+
executable,
|
|
429
|
+
args,
|
|
430
|
+
envKind: 'sanitized',
|
|
431
|
+
result: summarizeProbeResult(sanitized),
|
|
432
|
+
});
|
|
433
|
+
logHostShellProbeFailureDiagnostics(command, sanitized, 'sanitized');
|
|
434
|
+
if (sanitized.status === 0 && !sanitized.error) {
|
|
435
|
+
return {
|
|
436
|
+
success: true,
|
|
437
|
+
sanitized,
|
|
438
|
+
inherited: sanitized,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const inherited = spawnSync(executable, args, {
|
|
442
|
+
cwd: '/',
|
|
443
|
+
env: process.env,
|
|
444
|
+
stdio: isHostShellDebugEnabled() ? 'pipe' : 'ignore',
|
|
445
|
+
encoding: 'utf8',
|
|
446
|
+
});
|
|
447
|
+
logHostShellDebug('probe attempt', {
|
|
448
|
+
command,
|
|
449
|
+
executable,
|
|
450
|
+
args,
|
|
451
|
+
envKind: 'inherited',
|
|
452
|
+
result: summarizeProbeResult(inherited),
|
|
453
|
+
});
|
|
454
|
+
logHostShellProbeFailureDiagnostics(command, inherited, 'inherited');
|
|
455
|
+
return {
|
|
456
|
+
success: inherited.status === 0 && !inherited.error,
|
|
457
|
+
sanitized,
|
|
458
|
+
inherited,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function buildHostShellLaunchPlan(strategy, command, workspacePath) {
|
|
462
|
+
const scratchDir = createScratchDir();
|
|
463
|
+
const env = buildRestrictedEnv(scratchDir, workspacePath);
|
|
464
|
+
switch (strategy.wrapperKind) {
|
|
465
|
+
case 'bwrap':
|
|
466
|
+
if (strategy.bwrapExecWrapperMode === 'setpriv-no-new-privs') {
|
|
467
|
+
return {
|
|
468
|
+
executable: resolveExecutable('setpriv'),
|
|
469
|
+
args: ['--no-new-privs', resolveExecutable('bwrap'), ...buildBwrapArgs(command, workspacePath, scratchDir, env, strategy)],
|
|
470
|
+
env,
|
|
471
|
+
strategy,
|
|
472
|
+
scratchDir,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
executable: resolveExecutable('bwrap'),
|
|
477
|
+
args: buildBwrapArgs(command, workspacePath, scratchDir, env, strategy),
|
|
478
|
+
env,
|
|
479
|
+
strategy,
|
|
480
|
+
scratchDir,
|
|
481
|
+
};
|
|
482
|
+
case 'sandbox-exec':
|
|
483
|
+
return {
|
|
484
|
+
executable: resolveExecutable('sandbox-exec'),
|
|
485
|
+
args: ['-p', buildSandboxExecProfile(workspacePath, scratchDir), '/bin/bash', '--noprofile', '--norc', '-c', command],
|
|
486
|
+
env,
|
|
487
|
+
strategy,
|
|
488
|
+
scratchDir,
|
|
489
|
+
};
|
|
490
|
+
case 'clean-room':
|
|
491
|
+
default:
|
|
492
|
+
return {
|
|
493
|
+
executable: resolveExecutable('bash'),
|
|
494
|
+
args: ['--noprofile', '--norc', '-c', command],
|
|
495
|
+
env,
|
|
496
|
+
strategy,
|
|
497
|
+
scratchDir,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function createScratchDir() {
|
|
502
|
+
const scratchDir = mkdtempSync(join(tmpdir(), 'deepclause-shell-'));
|
|
503
|
+
mkdirSync(join(scratchDir, 'tmp'), { recursive: true });
|
|
504
|
+
mkdirSync(join(scratchDir, '.config'), { recursive: true });
|
|
505
|
+
mkdirSync(join(scratchDir, '.cache'), { recursive: true });
|
|
506
|
+
mkdirSync(join(scratchDir, '.local', 'share'), { recursive: true });
|
|
507
|
+
return scratchDir;
|
|
508
|
+
}
|
|
509
|
+
function cleanupScratchDir(scratchDir) {
|
|
510
|
+
try {
|
|
511
|
+
rmSync(scratchDir, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Best-effort cleanup only.
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function buildRestrictedEnv(scratchDir, workspacePath) {
|
|
518
|
+
const pathValue = getDefaultHostPathValue();
|
|
519
|
+
return {
|
|
520
|
+
PATH: pathValue,
|
|
521
|
+
HOME: scratchDir,
|
|
522
|
+
TMPDIR: join(scratchDir, 'tmp'),
|
|
523
|
+
XDG_CONFIG_HOME: join(scratchDir, '.config'),
|
|
524
|
+
XDG_CACHE_HOME: join(scratchDir, '.cache'),
|
|
525
|
+
XDG_DATA_HOME: join(scratchDir, '.local', 'share'),
|
|
526
|
+
LANG: 'C.UTF-8',
|
|
527
|
+
LC_ALL: 'C.UTF-8',
|
|
528
|
+
PWD: workspacePath,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function buildHostProbeEnv() {
|
|
532
|
+
return {
|
|
533
|
+
PATH: getDefaultHostPathValue(),
|
|
534
|
+
HOME: process.env.HOME ?? tmpdir(),
|
|
535
|
+
LANG: 'C.UTF-8',
|
|
536
|
+
LC_ALL: 'C.UTF-8',
|
|
537
|
+
...(process.env.XDG_RUNTIME_DIR ? { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR } : {}),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function buildBwrapArgs(command, workspacePath, scratchDir, env, strategy) {
|
|
541
|
+
const createdMountDirs = new Set();
|
|
542
|
+
const args = [
|
|
543
|
+
'--die-with-parent',
|
|
544
|
+
'--new-session',
|
|
545
|
+
'--unshare-user-try',
|
|
546
|
+
'--clearenv',
|
|
547
|
+
'--proc', '/proc',
|
|
548
|
+
'--dev', '/dev',
|
|
549
|
+
];
|
|
550
|
+
if (strategy.strictIsolation) {
|
|
551
|
+
args.push('--unshare-net');
|
|
552
|
+
}
|
|
553
|
+
for (const systemPath of DEFAULT_BWRAP_READ_ONLY_PATHS) {
|
|
554
|
+
if (pathExists(systemPath)) {
|
|
555
|
+
args.push('--ro-bind', systemPath, systemPath);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const requiredHostPath of getAdditionalBwrapReadOnlyPaths()) {
|
|
559
|
+
for (const directoryPath of buildMountParentDirs(requiredHostPath)) {
|
|
560
|
+
if (createdMountDirs.has(directoryPath)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
args.push('--dir', directoryPath);
|
|
564
|
+
createdMountDirs.add(directoryPath);
|
|
565
|
+
}
|
|
566
|
+
args.push('--ro-bind', requiredHostPath, requiredHostPath);
|
|
567
|
+
}
|
|
568
|
+
for (const directoryPath of buildMountParentDirs(workspacePath)) {
|
|
569
|
+
if (createdMountDirs.has(directoryPath)) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
args.push('--dir', directoryPath);
|
|
573
|
+
createdMountDirs.add(directoryPath);
|
|
574
|
+
}
|
|
575
|
+
for (const directoryPath of buildMountParentDirs(scratchDir)) {
|
|
576
|
+
if (createdMountDirs.has(directoryPath)) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
args.push('--dir', directoryPath);
|
|
580
|
+
createdMountDirs.add(directoryPath);
|
|
581
|
+
}
|
|
582
|
+
args.push('--bind', workspacePath, workspacePath);
|
|
583
|
+
args.push('--bind', scratchDir, scratchDir);
|
|
584
|
+
args.push('--tmpfs', '/tmp');
|
|
585
|
+
args.push('--chdir', workspacePath);
|
|
586
|
+
for (const [key, value] of Object.entries(env)) {
|
|
587
|
+
if (typeof value === 'string') {
|
|
588
|
+
args.push('--setenv', key, value);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
args.push('/bin/bash', '--noprofile', '--norc', '-c', command);
|
|
592
|
+
return args;
|
|
593
|
+
}
|
|
594
|
+
function buildMountParentDirs(targetPath) {
|
|
595
|
+
const segments = targetPath.split('/').filter(Boolean);
|
|
596
|
+
const directories = [];
|
|
597
|
+
let current = '';
|
|
598
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
599
|
+
current += `/${segments[index]}`;
|
|
600
|
+
directories.push(current);
|
|
601
|
+
}
|
|
602
|
+
return directories;
|
|
603
|
+
}
|
|
604
|
+
function buildSandboxExecProfile(workspacePath, scratchDir) {
|
|
605
|
+
const allowedWritablePaths = [workspacePath, scratchDir].map(escapeSandboxPath);
|
|
606
|
+
return [
|
|
607
|
+
'(version 1)',
|
|
608
|
+
'(deny default)',
|
|
609
|
+
'(import "system.sb")',
|
|
610
|
+
'(allow process*)',
|
|
611
|
+
'(allow network-outbound)',
|
|
612
|
+
'(allow file-read* (subpath "/bin") (subpath "/usr") (subpath "/System") (subpath "/Library") (subpath "/dev"))',
|
|
613
|
+
`(allow file-read* file-write* ${allowedWritablePaths.map((value) => `(subpath "${value}")`).join(' ')})`,
|
|
614
|
+
].join(' ');
|
|
615
|
+
}
|
|
616
|
+
function escapeSandboxPath(value) {
|
|
617
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
618
|
+
}
|
|
619
|
+
function pathExists(value) {
|
|
620
|
+
return existsSync(value);
|
|
621
|
+
}
|
|
622
|
+
function getAdditionalBwrapReadOnlyPaths() {
|
|
623
|
+
const requiredPaths = new Set();
|
|
624
|
+
for (const requiredHostFile of DEFAULT_BWRAP_REQUIRED_HOST_FILES) {
|
|
625
|
+
const resolvedPath = resolveHostRealPathOutsideBwrapRoots(requiredHostFile);
|
|
626
|
+
if (!resolvedPath) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
requiredPaths.add(resolvedPath);
|
|
630
|
+
}
|
|
631
|
+
return [...requiredPaths];
|
|
632
|
+
}
|
|
633
|
+
function resolveHostRealPathOutsideBwrapRoots(filePath) {
|
|
634
|
+
if (!pathExists(filePath)) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const resolvedPath = realpathSync(filePath);
|
|
639
|
+
if (resolvedPath === filePath || isPathCoveredByBwrapReadOnlyRoots(resolvedPath)) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return resolvedPath;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function isPathCoveredByBwrapReadOnlyRoots(targetPath) {
|
|
649
|
+
return DEFAULT_BWRAP_READ_ONLY_PATHS.some((rootPath) => targetPath === rootPath || targetPath.startsWith(`${rootPath}/`));
|
|
650
|
+
}
|
|
651
|
+
function resolveExecutable(command) {
|
|
652
|
+
if (command.includes('/')) {
|
|
653
|
+
return command;
|
|
654
|
+
}
|
|
655
|
+
for (const directoryPath of getHostExecutableLookupDirs()) {
|
|
656
|
+
const candidate = join(directoryPath, command);
|
|
657
|
+
if (pathExists(candidate)) {
|
|
658
|
+
return candidate;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return command;
|
|
662
|
+
}
|
|
663
|
+
function getHostExecutableLookupDirs() {
|
|
664
|
+
const envPath = process.env.PATH ?? '';
|
|
665
|
+
const envDirectories = envPath
|
|
666
|
+
.split(':')
|
|
667
|
+
.map((value) => value.trim())
|
|
668
|
+
.filter(Boolean);
|
|
669
|
+
return [...new Set([...envDirectories, ...DEFAULT_HOST_EXECUTABLE_LOOKUP_DIRS])];
|
|
670
|
+
}
|
|
671
|
+
function getDefaultHostPathValue() {
|
|
672
|
+
return process.platform === 'darwin'
|
|
673
|
+
? '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin'
|
|
674
|
+
: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin';
|
|
208
675
|
}
|
|
209
676
|
function getHostShellIdleTimeoutMs() {
|
|
210
677
|
const raw = process.env.DC_HOST_SHELL_IDLE_TIMEOUT_MS;
|
|
@@ -216,6 +683,99 @@ function getHostShellIdleTimeoutMs() {
|
|
|
216
683
|
}
|
|
217
684
|
return DEFAULT_HOST_SHELL_IDLE_TIMEOUT_MS;
|
|
218
685
|
}
|
|
686
|
+
function isHostShellDebugEnabled() {
|
|
687
|
+
const value = process.env[HOST_SHELL_DEBUG_ENV]?.trim().toLowerCase();
|
|
688
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
689
|
+
}
|
|
690
|
+
function logHostShellDebug(message, details) {
|
|
691
|
+
if (!isHostShellDebugEnabled()) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
process.stderr.write(`[deepclause:shell] ${message} ${JSON.stringify(details)}\n`);
|
|
695
|
+
}
|
|
696
|
+
function summarizeProbeResult(result) {
|
|
697
|
+
return {
|
|
698
|
+
status: result.status,
|
|
699
|
+
signal: result.signal,
|
|
700
|
+
error: result.error
|
|
701
|
+
? {
|
|
702
|
+
name: result.error.name,
|
|
703
|
+
message: result.error.message,
|
|
704
|
+
...(typeof result.error.code === 'string'
|
|
705
|
+
? { code: result.error.code }
|
|
706
|
+
: {}),
|
|
707
|
+
}
|
|
708
|
+
: null,
|
|
709
|
+
stdout: typeof result.stdout === 'string' ? result.stdout.trim() : '',
|
|
710
|
+
stderr: typeof result.stderr === 'string' ? result.stderr.trim() : '',
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function logHostShellProbeFailureDiagnostics(command, result, envKind) {
|
|
714
|
+
if (!isHostShellDebugEnabled() || command !== 'bwrap' || (result.status === 0 && !result.error)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
logHostShellDebug('probe process diagnostics', {
|
|
718
|
+
command,
|
|
719
|
+
envKind,
|
|
720
|
+
process: getCurrentProcessDiagnostics(),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function getCurrentProcessDiagnostics() {
|
|
724
|
+
const status = readProcStatusFields([
|
|
725
|
+
'NoNewPrivs',
|
|
726
|
+
'Seccomp',
|
|
727
|
+
'Seccomp_filters',
|
|
728
|
+
'Uid',
|
|
729
|
+
'Gid',
|
|
730
|
+
'Groups',
|
|
731
|
+
'CapInh',
|
|
732
|
+
'CapPrm',
|
|
733
|
+
'CapEff',
|
|
734
|
+
'CapBnd',
|
|
735
|
+
'CapAmb',
|
|
736
|
+
]);
|
|
737
|
+
return {
|
|
738
|
+
pid: process.pid,
|
|
739
|
+
ppid: process.ppid,
|
|
740
|
+
cwd: process.cwd(),
|
|
741
|
+
status,
|
|
742
|
+
namespaces: {
|
|
743
|
+
user: readNamespaceLink('user'),
|
|
744
|
+
pid: readNamespaceLink('pid'),
|
|
745
|
+
mnt: readNamespaceLink('mnt'),
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function readProcStatusFields(fieldNames) {
|
|
750
|
+
try {
|
|
751
|
+
const requestedFields = new Set(fieldNames);
|
|
752
|
+
const lines = readFileSync('/proc/self/status', 'utf8').split('\n');
|
|
753
|
+
const values = Object.fromEntries(fieldNames.map((name) => [name, null]));
|
|
754
|
+
for (const line of lines) {
|
|
755
|
+
const separatorIndex = line.indexOf(':');
|
|
756
|
+
if (separatorIndex === -1) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const name = line.slice(0, separatorIndex);
|
|
760
|
+
if (!requestedFields.has(name)) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
values[name] = line.slice(separatorIndex + 1).trim();
|
|
764
|
+
}
|
|
765
|
+
return values;
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return Object.fromEntries(fieldNames.map((name) => [name, null]));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function readNamespaceLink(name) {
|
|
772
|
+
try {
|
|
773
|
+
return readlinkSync(`/proc/self/ns/${name}`);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
219
779
|
function abortError(reason) {
|
|
220
780
|
return reason instanceof Error
|
|
221
781
|
? reason
|