@exulu/backend 1.61.1 → 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.
- package/dist/{chunk-MPV7HBV6.js → chunk-KKJF3NAY.js} +65 -9
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-CULC37U6.js → convert-exulu-tools-to-ai-sdk-tools-GU3BGHDW.js} +1 -1
- package/dist/index.cjs +64 -9
- package/dist/index.js +1 -1
- package/ee/invoke-skills/create-sandbox.ts +93 -8
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
6925
|
+
wrapCommand: (command) => wrapIfNeeded(command),
|
|
6872
6926
|
cleanup: async () => {
|
|
6873
6927
|
sandboxCache.delete(sessionId);
|
|
6874
|
-
|
|
6928
|
+
if (!useDirectExec) {
|
|
6929
|
+
await SandboxManager.reset();
|
|
6930
|
+
}
|
|
6875
6931
|
await rm(sessionDir, { recursive: true, force: true });
|
|
6876
6932
|
}
|
|
6877
6933
|
};
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
2773
|
+
wrapCommand: (command) => wrapIfNeeded(command),
|
|
2723
2774
|
cleanup: async () => {
|
|
2724
2775
|
sandboxCache.delete(sessionId);
|
|
2725
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
945
|
+
if (!useDirectExec) {
|
|
946
|
+
await SandboxManager.reset()
|
|
947
|
+
}
|
|
863
948
|
await rm(sessionDir, { recursive: true, force: true })
|
|
864
949
|
},
|
|
865
950
|
}
|