@exulu/backend 1.61.0 → 1.61.2

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.
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dispatcher for `npx @exulu/backend <subcommand>`.
4
+ *
5
+ * npx refuses to pick a bin when a package has multiple, unless one matches
6
+ * the package's unscoped name. This dispatcher claims that slot and forwards
7
+ * to the real subcommand scripts.
8
+ */
9
+
10
+ const { spawn } = require('child_process');
11
+ const { existsSync } = require('fs');
12
+ const path = require('path');
13
+
14
+ const subcommands = {
15
+ 'setup-python': path.join(__dirname, 'setup-python.cjs'),
16
+ 'exulu-start-whisper': path.join(__dirname, '..', 'dist', 'cli', 'start-whisper.cjs'),
17
+ 'start-whisper': path.join(__dirname, '..', 'dist', 'cli', 'start-whisper.cjs'),
18
+ };
19
+
20
+ function printUsage(stream) {
21
+ stream.write('Usage: npx @exulu/backend <subcommand> [args...]\n\n');
22
+ stream.write('Available subcommands:\n');
23
+ for (const name of Object.keys(subcommands)) {
24
+ stream.write(` ${name}\n`);
25
+ }
26
+ stream.write('\n');
27
+ }
28
+
29
+ const [, , subcommand, ...args] = process.argv;
30
+
31
+ if (!subcommand || subcommand === '--help' || subcommand === '-h') {
32
+ printUsage(process.stdout);
33
+ process.exit(subcommand ? 0 : 1);
34
+ }
35
+
36
+ const target = subcommands[subcommand];
37
+ if (!target) {
38
+ process.stderr.write(`Unknown subcommand: ${subcommand}\n\n`);
39
+ printUsage(process.stderr);
40
+ process.exit(1);
41
+ }
42
+
43
+ if (!existsSync(target)) {
44
+ process.stderr.write(`Subcommand script missing: ${target}\n`);
45
+ process.stderr.write('The @exulu/backend package may not be fully built or installed.\n');
46
+ process.exit(1);
47
+ }
48
+
49
+ const child = spawn(process.execPath, [target, ...args], { stdio: 'inherit' });
50
+ child.on('exit', (code, signal) => {
51
+ if (signal) {
52
+ process.kill(process.pid, signal);
53
+ return;
54
+ }
55
+ process.exit(code ?? 0);
56
+ });
57
+ child.on('error', (err) => {
58
+ process.stderr.write(`Failed to run subcommand: ${err.message}\n`);
59
+ process.exit(1);
60
+ });
@@ -744,7 +744,7 @@ var ExuluTool = class {
744
744
  });
745
745
  providerapikey = resolved.apiKey;
746
746
  }
747
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-CULC37U6.js");
747
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-GU3BGHDW.js");
748
748
  const tools = await convertExuluToolsToAiSdkTools2(
749
749
  [this],
750
750
  [],
@@ -6442,6 +6442,42 @@ var getAllExuluVariables = async () => {
6442
6442
  };
6443
6443
  var execAsync2 = promisify2(exec2);
6444
6444
  var EXEC_MAX_BUFFER = 32 * 1024 * 1024;
6445
+ var sandboxProbePromise;
6446
+ function probeSandboxSupport() {
6447
+ if (sandboxProbePromise) return sandboxProbePromise;
6448
+ sandboxProbePromise = (async () => {
6449
+ if (process.platform === "darwin") return { canSandbox: true };
6450
+ if (process.platform !== "linux") {
6451
+ return { canSandbox: false, reason: `Unsupported platform: ${process.platform}` };
6452
+ }
6453
+ return await new Promise((resolve3) => {
6454
+ const child = spawn2("bwrap", ["--dev-bind", "/", "/", "--", "/bin/true"]);
6455
+ let stderr = "";
6456
+ child.stderr.on("data", (chunk) => {
6457
+ stderr += chunk.toString();
6458
+ });
6459
+ child.on("error", (err) => {
6460
+ resolve3({ canSandbox: false, reason: `bwrap not executable: ${err.message}` });
6461
+ });
6462
+ child.on("exit", (code) => {
6463
+ if (code === 0) resolve3({ canSandbox: true });
6464
+ else resolve3({ canSandbox: false, reason: stderr.trim() || `bwrap exited ${code}` });
6465
+ });
6466
+ });
6467
+ })();
6468
+ return sandboxProbePromise;
6469
+ }
6470
+ var SANDBOX_FALLBACK_INSTRUCTIONS = 'Skill sandboxing is running in DEGRADED mode: bwrap cannot create user namespaces on this host, so commands\nexecute directly. The container remains the isolation boundary and resolveSessionPath still scopes\nreadFile/writeFile to the session directory at the JS layer, but bash commands are NOT kernel-sandboxed.\n\nTo restore full sandboxing on Ubuntu 23.10+ / 24.04+ hosts (kernel 6.5+):\n sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n echo "kernel.apparmor_restrict_unprivileged_userns=0" | sudo tee /etc/sysctl.d/60-userns.conf\n sudo sysctl --system\n\nOn Debian hosts where the same symptom appears:\n sudo sysctl -w kernel.unprivileged_userns_clone=1\n\nSet EXULU_REQUIRE_SANDBOX=1 to fail startup instead of degrading.';
6471
+ var degradedModeLogged = false;
6472
+ function logDegradedModeOnce(reason) {
6473
+ if (degradedModeLogged) return;
6474
+ degradedModeLogged = true;
6475
+ console.warn(
6476
+ `[SKILLS] ${SANDBOX_FALLBACK_INSTRUCTIONS}
6477
+
6478
+ Probe error: ${reason ?? "(no detail)"}`
6479
+ );
6480
+ }
6445
6481
  var sandboxCache = /* @__PURE__ */ new Map();
6446
6482
  async function downloadSkill(skill, skillsDirectory, config) {
6447
6483
  const version = skill.current_version ?? 1;
@@ -6581,6 +6617,20 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
6581
6617
  if (userId && config.fileUploads && !dirExisted) {
6582
6618
  await restoreArtifactsFromS3(sessionDir, sessionId, userId, config);
6583
6619
  }
6620
+ const probe = await probeSandboxSupport();
6621
+ const useDirectExec = !probe.canSandbox;
6622
+ if (useDirectExec) {
6623
+ if (process.env.EXULU_REQUIRE_SANDBOX === "1") {
6624
+ throw new Error(
6625
+ `[SKILLS] Sandbox required (EXULU_REQUIRE_SANDBOX=1) but unavailable.
6626
+
6627
+ ${SANDBOX_FALLBACK_INSTRUCTIONS}
6628
+
6629
+ Probe error: ${probe.reason ?? "(no detail)"}`
6630
+ );
6631
+ }
6632
+ logDegradedModeOnce(probe.reason);
6633
+ }
6584
6634
  const baselineSandboxConfig = {
6585
6635
  network: {
6586
6636
  allowedDomains: [],
@@ -6596,7 +6646,9 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
6596
6646
  denyWrite: []
6597
6647
  }
6598
6648
  };
6599
- await SandboxManager.initialize(baselineSandboxConfig);
6649
+ if (!useDirectExec) {
6650
+ await SandboxManager.initialize(baselineSandboxConfig);
6651
+ }
6600
6652
  const npmGlobalRoot = await getNpmGlobalRoot();
6601
6653
  const sessionSandboxConfig = {
6602
6654
  network: {
@@ -6632,8 +6684,12 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
6632
6684
  ...process.env,
6633
6685
  ...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}
6634
6686
  };
6687
+ const wrapIfNeeded = async (command) => {
6688
+ if (useDirectExec) return command;
6689
+ return SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig);
6690
+ };
6635
6691
  const runWrapped = async (command) => {
6636
- const wrapped = await SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig);
6692
+ const wrapped = await wrapIfNeeded(command);
6637
6693
  try {
6638
6694
  const { stdout, stderr } = await execAsync2(wrapped, {
6639
6695
  maxBuffer: EXEC_MAX_BUFFER,
@@ -6710,10 +6766,8 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
6710
6766
  async function writeFilesInternal(files) {
6711
6767
  const results = [];
6712
6768
  for (const file of files) {
6713
- const wrapped = await SandboxManager.wrapWithSandbox(
6714
- `mkdir -p ${shellQuote(dirname(file.path))} && cat > ${shellQuote(file.path)}`,
6715
- void 0,
6716
- sessionSandboxConfig
6769
+ const wrapped = await wrapIfNeeded(
6770
+ `mkdir -p ${shellQuote(dirname(file.path))} && cat > ${shellQuote(file.path)}`
6717
6771
  );
6718
6772
  await new Promise((resolveSpawn, rejectSpawn) => {
6719
6773
  const child = spawn2("/bin/bash", ["-c", wrapped]);
@@ -6868,10 +6922,12 @@ ${lines.join("\n")}`;
6868
6922
  const handle = {
6869
6923
  sessionDir,
6870
6924
  tools: wrappedTools,
6871
- wrapCommand: (command) => SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig),
6925
+ wrapCommand: (command) => wrapIfNeeded(command),
6872
6926
  cleanup: async () => {
6873
6927
  sandboxCache.delete(sessionId);
6874
- await SandboxManager.reset();
6928
+ if (!useDirectExec) {
6929
+ await SandboxManager.reset();
6930
+ }
6875
6931
  await rm(sessionDir, { recursive: true, force: true });
6876
6932
  }
6877
6933
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertExuluToolsToAiSdkTools
3
- } from "./chunk-MPV7HBV6.js";
3
+ } from "./chunk-KKJF3NAY.js";
4
4
  export {
5
5
  convertExuluToolsToAiSdkTools
6
6
  };
package/dist/index.cjs CHANGED
@@ -2294,6 +2294,39 @@ var init_variable = __esm({
2294
2294
  });
2295
2295
 
2296
2296
  // ee/invoke-skills/create-sandbox.ts
2297
+ function probeSandboxSupport() {
2298
+ if (sandboxProbePromise) return sandboxProbePromise;
2299
+ sandboxProbePromise = (async () => {
2300
+ if (process.platform === "darwin") return { canSandbox: true };
2301
+ if (process.platform !== "linux") {
2302
+ return { canSandbox: false, reason: `Unsupported platform: ${process.platform}` };
2303
+ }
2304
+ return await new Promise((resolve7) => {
2305
+ const child = (0, import_node_child_process3.spawn)("bwrap", ["--dev-bind", "/", "/", "--", "/bin/true"]);
2306
+ let stderr = "";
2307
+ child.stderr.on("data", (chunk) => {
2308
+ stderr += chunk.toString();
2309
+ });
2310
+ child.on("error", (err) => {
2311
+ resolve7({ canSandbox: false, reason: `bwrap not executable: ${err.message}` });
2312
+ });
2313
+ child.on("exit", (code) => {
2314
+ if (code === 0) resolve7({ canSandbox: true });
2315
+ else resolve7({ canSandbox: false, reason: stderr.trim() || `bwrap exited ${code}` });
2316
+ });
2317
+ });
2318
+ })();
2319
+ return sandboxProbePromise;
2320
+ }
2321
+ function logDegradedModeOnce(reason) {
2322
+ if (degradedModeLogged) return;
2323
+ degradedModeLogged = true;
2324
+ console.warn(
2325
+ `[SKILLS] ${SANDBOX_FALLBACK_INSTRUCTIONS}
2326
+
2327
+ Probe error: ${reason ?? "(no detail)"}`
2328
+ );
2329
+ }
2297
2330
  async function downloadSkill(skill, skillsDirectory, config) {
2298
2331
  const version = skill.current_version ?? 1;
2299
2332
  if (!skill.current_version) {
@@ -2432,6 +2465,20 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
2432
2465
  if (userId && config.fileUploads && !dirExisted) {
2433
2466
  await restoreArtifactsFromS3(sessionDir, sessionId, userId, config);
2434
2467
  }
2468
+ const probe = await probeSandboxSupport();
2469
+ const useDirectExec = !probe.canSandbox;
2470
+ if (useDirectExec) {
2471
+ if (process.env.EXULU_REQUIRE_SANDBOX === "1") {
2472
+ throw new Error(
2473
+ `[SKILLS] Sandbox required (EXULU_REQUIRE_SANDBOX=1) but unavailable.
2474
+
2475
+ ${SANDBOX_FALLBACK_INSTRUCTIONS}
2476
+
2477
+ Probe error: ${probe.reason ?? "(no detail)"}`
2478
+ );
2479
+ }
2480
+ logDegradedModeOnce(probe.reason);
2481
+ }
2435
2482
  const baselineSandboxConfig = {
2436
2483
  network: {
2437
2484
  allowedDomains: [],
@@ -2447,7 +2494,9 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
2447
2494
  denyWrite: []
2448
2495
  }
2449
2496
  };
2450
- await import_sandbox_runtime.SandboxManager.initialize(baselineSandboxConfig);
2497
+ if (!useDirectExec) {
2498
+ await import_sandbox_runtime.SandboxManager.initialize(baselineSandboxConfig);
2499
+ }
2451
2500
  const npmGlobalRoot = await getNpmGlobalRoot();
2452
2501
  const sessionSandboxConfig = {
2453
2502
  network: {
@@ -2483,8 +2532,12 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
2483
2532
  ...process.env,
2484
2533
  ...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}
2485
2534
  };
2535
+ const wrapIfNeeded = async (command) => {
2536
+ if (useDirectExec) return command;
2537
+ return import_sandbox_runtime.SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig);
2538
+ };
2486
2539
  const runWrapped = async (command) => {
2487
- const wrapped = await import_sandbox_runtime.SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig);
2540
+ const wrapped = await wrapIfNeeded(command);
2488
2541
  try {
2489
2542
  const { stdout, stderr } = await execAsync2(wrapped, {
2490
2543
  maxBuffer: EXEC_MAX_BUFFER,
@@ -2561,10 +2614,8 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
2561
2614
  async function writeFilesInternal(files) {
2562
2615
  const results = [];
2563
2616
  for (const file of files) {
2564
- const wrapped = await import_sandbox_runtime.SandboxManager.wrapWithSandbox(
2565
- `mkdir -p ${shellQuote((0, import_node_path3.dirname)(file.path))} && cat > ${shellQuote(file.path)}`,
2566
- void 0,
2567
- sessionSandboxConfig
2617
+ const wrapped = await wrapIfNeeded(
2618
+ `mkdir -p ${shellQuote((0, import_node_path3.dirname)(file.path))} && cat > ${shellQuote(file.path)}`
2568
2619
  );
2569
2620
  await new Promise((resolveSpawn, rejectSpawn) => {
2570
2621
  const child = (0, import_node_child_process3.spawn)("/bin/bash", ["-c", wrapped]);
@@ -2719,17 +2770,19 @@ ${lines.join("\n")}`;
2719
2770
  const handle = {
2720
2771
  sessionDir,
2721
2772
  tools: wrappedTools,
2722
- wrapCommand: (command) => import_sandbox_runtime.SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig),
2773
+ wrapCommand: (command) => wrapIfNeeded(command),
2723
2774
  cleanup: async () => {
2724
2775
  sandboxCache.delete(sessionId);
2725
- await import_sandbox_runtime.SandboxManager.reset();
2776
+ if (!useDirectExec) {
2777
+ await import_sandbox_runtime.SandboxManager.reset();
2778
+ }
2726
2779
  await (0, import_promises.rm)(sessionDir, { recursive: true, force: true });
2727
2780
  }
2728
2781
  };
2729
2782
  sandboxCache.set(sessionId, { handle, installedSkills });
2730
2783
  return handle;
2731
2784
  }
2732
- var import_sandbox_runtime, import_promises, import_node_fs3, import_node_path3, import_node_child_process3, import_node_util2, import_bash_tool, import_ai, import_zod4, import_crypto_js2, getAllExuluVariables, execAsync2, EXEC_MAX_BUFFER, sandboxCache;
2785
+ var import_sandbox_runtime, import_promises, import_node_fs3, import_node_path3, import_node_child_process3, import_node_util2, import_bash_tool, import_ai, import_zod4, import_crypto_js2, getAllExuluVariables, execAsync2, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
2733
2786
  var init_create_sandbox = __esm({
2734
2787
  "ee/invoke-skills/create-sandbox.ts"() {
2735
2788
  "use strict";
@@ -2776,6 +2829,8 @@ var init_create_sandbox = __esm({
2776
2829
  };
2777
2830
  execAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.exec);
2778
2831
  EXEC_MAX_BUFFER = 32 * 1024 * 1024;
2832
+ SANDBOX_FALLBACK_INSTRUCTIONS = 'Skill sandboxing is running in DEGRADED mode: bwrap cannot create user namespaces on this host, so commands\nexecute directly. The container remains the isolation boundary and resolveSessionPath still scopes\nreadFile/writeFile to the session directory at the JS layer, but bash commands are NOT kernel-sandboxed.\n\nTo restore full sandboxing on Ubuntu 23.10+ / 24.04+ hosts (kernel 6.5+):\n sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n echo "kernel.apparmor_restrict_unprivileged_userns=0" | sudo tee /etc/sysctl.d/60-userns.conf\n sudo sysctl --system\n\nOn Debian hosts where the same symptom appears:\n sudo sysctl -w kernel.unprivileged_userns_clone=1\n\nSet EXULU_REQUIRE_SANDBOX=1 to fail startup instead of degrading.';
2833
+ degradedModeLogged = false;
2779
2834
  sandboxCache = /* @__PURE__ */ new Map();
2780
2835
  }
2781
2836
  });
package/dist/index.js CHANGED
@@ -53,7 +53,7 @@ import {
53
53
  vectorSearch,
54
54
  waitForLiteLLMReady,
55
55
  withRetry
56
- } from "./chunk-MPV7HBV6.js";
56
+ } from "./chunk-KKJF3NAY.js";
57
57
  import {
58
58
  findLiteLLMModel
59
59
  } from "./chunk-ILAHW4UT.js";
@@ -60,6 +60,69 @@ const execAsync = promisify(exec);
60
60
  // Sandbox commands can be very long (long deny lists) — bump default buffer.
61
61
  const EXEC_MAX_BUFFER = 32 * 1024 * 1024;
62
62
 
63
+ /**
64
+ * Probe whether bwrap can actually create a user namespace on this host.
65
+ * SRT's own dependency check only verifies bwrap is installed, not that the
66
+ * kernel will let it run — and the latter is what Ubuntu 24.04 (kernel 6.8+
67
+ * with `kernel.apparmor_restrict_unprivileged_userns=1`) and Debian hosts with
68
+ * `kernel.unprivileged_userns_clone=0` deny.
69
+ *
70
+ * Memoized for the process lifetime: the answer is host-level state that
71
+ * doesn't change without a sysctl flip + restart.
72
+ */
73
+ type SandboxProbeResult = { canSandbox: boolean; reason?: string };
74
+ let sandboxProbePromise: Promise<SandboxProbeResult> | undefined;
75
+
76
+ function probeSandboxSupport(): Promise<SandboxProbeResult> {
77
+ if (sandboxProbePromise) return sandboxProbePromise;
78
+ sandboxProbePromise = (async () => {
79
+ if (process.platform === 'darwin') return { canSandbox: true };
80
+ if (process.platform !== 'linux') {
81
+ return { canSandbox: false, reason: `Unsupported platform: ${process.platform}` };
82
+ }
83
+ return await new Promise<SandboxProbeResult>((resolve) => {
84
+ // Minimal bwrap invocation: bind / read-only and exit. If the host
85
+ // refuses unprivileged user namespaces, this returns
86
+ // "Operation not permitted" — same path SRT would hit on every command.
87
+ const child = spawn('bwrap', ['--dev-bind', '/', '/', '--', '/bin/true']);
88
+ let stderr = '';
89
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
90
+ child.on('error', (err) => {
91
+ resolve({ canSandbox: false, reason: `bwrap not executable: ${err.message}` });
92
+ });
93
+ child.on('exit', (code) => {
94
+ if (code === 0) resolve({ canSandbox: true });
95
+ else resolve({ canSandbox: false, reason: stderr.trim() || `bwrap exited ${code}` });
96
+ });
97
+ });
98
+ })();
99
+ return sandboxProbePromise;
100
+ }
101
+
102
+ const SANDBOX_FALLBACK_INSTRUCTIONS =
103
+ 'Skill sandboxing is running in DEGRADED mode: bwrap cannot create user namespaces on this host, so commands\n' +
104
+ 'execute directly. The container remains the isolation boundary and resolveSessionPath still scopes\n' +
105
+ 'readFile/writeFile to the session directory at the JS layer, but bash commands are NOT kernel-sandboxed.\n' +
106
+ '\n' +
107
+ 'To restore full sandboxing on Ubuntu 23.10+ / 24.04+ hosts (kernel 6.5+):\n' +
108
+ ' sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n' +
109
+ ' echo "kernel.apparmor_restrict_unprivileged_userns=0" | sudo tee /etc/sysctl.d/60-userns.conf\n' +
110
+ ' sudo sysctl --system\n' +
111
+ '\n' +
112
+ 'On Debian hosts where the same symptom appears:\n' +
113
+ ' sudo sysctl -w kernel.unprivileged_userns_clone=1\n' +
114
+ '\n' +
115
+ 'Set EXULU_REQUIRE_SANDBOX=1 to fail startup instead of degrading.';
116
+
117
+ let degradedModeLogged = false;
118
+ function logDegradedModeOnce(reason: string | undefined): void {
119
+ if (degradedModeLogged) return;
120
+ degradedModeLogged = true;
121
+ console.warn(
122
+ `[SKILLS] ${SANDBOX_FALLBACK_INSTRUCTIONS}\n\nProbe error: ${reason ?? '(no detail)'}`,
123
+ );
124
+ }
125
+
63
126
  // This is called on every session where a skill is enabled
64
127
  // each sandbox setup includes the skill files from the enabled
65
128
  // skills, and uses the Anthropic Sandbox Runtime (SRT) to
@@ -380,6 +443,19 @@ export async function createSessionSandbox(
380
443
  await restoreArtifactsFromS3(sessionDir, sessionId, userId, config)
381
444
  }
382
445
 
446
+ // Probe whether bwrap can actually create namespaces on this host. On a
447
+ // failure, either throw (strict mode) or degrade to direct exec.
448
+ const probe = await probeSandboxSupport();
449
+ const useDirectExec = !probe.canSandbox;
450
+ if (useDirectExec) {
451
+ if (process.env.EXULU_REQUIRE_SANDBOX === '1') {
452
+ throw new Error(
453
+ `[SKILLS] Sandbox required (EXULU_REQUIRE_SANDBOX=1) but unavailable.\n\n${SANDBOX_FALLBACK_INSTRUCTIONS}\n\nProbe error: ${probe.reason ?? '(no detail)'}`,
454
+ );
455
+ }
456
+ logDegradedModeOnce(probe.reason);
457
+ }
458
+
383
459
  // SRT's `SandboxManager.initialize()` is a one-shot singleton (see
384
460
  // node_modules/@anthropic-ai/sandbox-runtime/.../sandbox-manager.js:187-191);
385
461
  // the first config wins and later calls are no-ops. That's incompatible
@@ -402,7 +478,9 @@ export async function createSessionSandbox(
402
478
  },
403
479
  }
404
480
 
405
- await SandboxManager.initialize(baselineSandboxConfig)
481
+ if (!useDirectExec) {
482
+ await SandboxManager.initialize(baselineSandboxConfig)
483
+ }
406
484
 
407
485
  // Resolve the global node_modules directory so skill-generated scripts
408
486
  // can `require()` packages installed via `npm install -g <pkg>` (e.g. the
@@ -487,13 +565,21 @@ export async function createSessionSandbox(
487
565
  ...(npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}),
488
566
  }
489
567
 
568
+ // Either wrap a command with bwrap/sandbox-exec or return it unchanged when
569
+ // the host can't sandbox. In degraded mode the container itself is the
570
+ // isolation boundary; resolveSessionPath still scopes readFile/writeFile.
571
+ const wrapIfNeeded = async (command: string): Promise<string> => {
572
+ if (useDirectExec) return command;
573
+ return SandboxManager.wrapWithSandbox(command, undefined, sessionSandboxConfig);
574
+ };
575
+
490
576
  // wrapWithSandbox only constructs the sandbox-exec invocation string —
491
577
  // it does NOT run it. We have to shell out ourselves and capture the
492
578
  // real stdout/stderr/exitCode. The third arg passes the per-session
493
579
  // policy so the kernel only allows this session's dir, not whatever
494
580
  // baseline the singleton was initialized with.
495
581
  const runWrapped = async (command: string): Promise<{ stdout: string; stderr: string; exitCode: number }> => {
496
- const wrapped = await SandboxManager.wrapWithSandbox(command, undefined, sessionSandboxConfig);
582
+ const wrapped = await wrapIfNeeded(command);
497
583
  try {
498
584
  const { stdout, stderr } = await execAsync(wrapped, {
499
585
  maxBuffer: EXEC_MAX_BUFFER,
@@ -618,10 +704,8 @@ export async function createSessionSandbox(
618
704
  for (const file of files) {
619
705
  // Pipe content via stdin so arbitrary file content (quotes, $, etc.)
620
706
  // doesn't need to be escaped into the shell command.
621
- const wrapped = await SandboxManager.wrapWithSandbox(
707
+ const wrapped = await wrapIfNeeded(
622
708
  `mkdir -p ${shellQuote(dirname(file.path))} && cat > ${shellQuote(file.path)}`,
623
- undefined,
624
- sessionSandboxConfig,
625
709
  )
626
710
  await new Promise<void>((resolveSpawn, rejectSpawn) => {
627
711
  const child = spawn('/bin/bash', ['-c', wrapped])
@@ -855,11 +939,12 @@ export async function createSessionSandbox(
855
939
  const handle: SessionSandboxHandle = {
856
940
  sessionDir,
857
941
  tools: wrappedTools,
858
- wrapCommand: (command: string) =>
859
- SandboxManager.wrapWithSandbox(command, undefined, sessionSandboxConfig),
942
+ wrapCommand: (command: string) => wrapIfNeeded(command),
860
943
  cleanup: async () => {
861
944
  sandboxCache.delete(sessionId)
862
- await SandboxManager.reset()
945
+ if (!useDirectExec) {
946
+ await SandboxManager.reset()
947
+ }
863
948
  await rm(sessionDir, { recursive: true, force: true })
864
949
  },
865
950
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.61.0",
4
+ "version": "1.61.2",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -10,6 +10,7 @@
10
10
  "module": "./dist/index.mjs",
11
11
  "types": "./dist/index.d.ts",
12
12
  "bin": {
13
+ "backend": "./bin/backend.cjs",
13
14
  "setup-python": "./bin/setup-python.cjs",
14
15
  "exulu-start-whisper": "./dist/cli/start-whisper.cjs"
15
16
  },