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.
@@ -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
- constructor(workspacePath) {
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 child = spawn('bash', ['-lc', command], {
41
+ const launchPlan = buildHostShellLaunchPlan(this.strategy, command, this.workspacePath);
42
+ const child = spawn(launchPlan.executable, launchPlan.args, {
19
43
  cwd: this.workspacePath,
20
- env: process.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
- ? 'Command completed successfully'
105
- : (timedOut ? `Command timed out after ${idleTimeoutMs}ms without output` : (stderr || `Command failed with exit code ${resolvedExitCode}${signalSuffix}`)),
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