@exulu/backend 1.61.1 → 1.61.3
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-PFKAWGB6.js} +101 -22
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-CULC37U6.js → convert-exulu-tools-to-ai-sdk-tools-VRZ45OGI.js} +1 -1
- package/dist/index.cjs +100 -23
- package/dist/index.js +1 -2
- 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-VRZ45OGI.js");
|
|
748
748
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
749
749
|
[this],
|
|
750
750
|
[],
|
|
@@ -1767,22 +1767,45 @@ var createUppyRoutes = async (app, config) => {
|
|
|
1767
1767
|
} else {
|
|
1768
1768
|
prefix += "global";
|
|
1769
1769
|
}
|
|
1770
|
+
console.log("[EXULU] bucket", config.fileUploads.s3Bucket);
|
|
1770
1771
|
console.log("[EXULU] prefix", prefix);
|
|
1771
|
-
|
|
1772
|
-
Bucket: config.fileUploads.s3Bucket,
|
|
1773
|
-
Prefix: prefix,
|
|
1774
|
-
MaxKeys: 9,
|
|
1775
|
-
...req.query.continuationToken && {
|
|
1776
|
-
ContinuationToken: req.query.continuationToken
|
|
1777
|
-
}
|
|
1778
|
-
});
|
|
1779
|
-
const response = await client.send(command);
|
|
1772
|
+
let response;
|
|
1780
1773
|
if (req.query.search) {
|
|
1781
|
-
const search = req.query.search;
|
|
1774
|
+
const search = req.query.search.toLowerCase();
|
|
1782
1775
|
console.log("[EXULU] Filtering files by search query", req.query.search);
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1776
|
+
const matched = [];
|
|
1777
|
+
let token;
|
|
1778
|
+
do {
|
|
1779
|
+
const page = await client.send(
|
|
1780
|
+
new ListObjectsV2Command({
|
|
1781
|
+
Bucket: config.fileUploads.s3Bucket,
|
|
1782
|
+
Prefix: prefix,
|
|
1783
|
+
...token && { ContinuationToken: token }
|
|
1784
|
+
})
|
|
1785
|
+
);
|
|
1786
|
+
for (const obj of page.Contents ?? []) {
|
|
1787
|
+
if (obj.Key?.toLowerCase().includes(search)) {
|
|
1788
|
+
matched.push(obj);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
token = page.IsTruncated ? page.NextContinuationToken : void 0;
|
|
1792
|
+
} while (token);
|
|
1793
|
+
response = {
|
|
1794
|
+
$metadata: {},
|
|
1795
|
+
Contents: matched,
|
|
1796
|
+
KeyCount: matched.length,
|
|
1797
|
+
IsTruncated: false
|
|
1798
|
+
};
|
|
1799
|
+
} else {
|
|
1800
|
+
const command = new ListObjectsV2Command({
|
|
1801
|
+
Bucket: config.fileUploads.s3Bucket,
|
|
1802
|
+
Prefix: prefix,
|
|
1803
|
+
MaxKeys: 9,
|
|
1804
|
+
...req.query.continuationToken && {
|
|
1805
|
+
ContinuationToken: req.query.continuationToken
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
response = await client.send(command);
|
|
1786
1809
|
}
|
|
1787
1810
|
res.json({
|
|
1788
1811
|
...response,
|
|
@@ -6442,6 +6465,42 @@ var getAllExuluVariables = async () => {
|
|
|
6442
6465
|
};
|
|
6443
6466
|
var execAsync2 = promisify2(exec2);
|
|
6444
6467
|
var EXEC_MAX_BUFFER = 32 * 1024 * 1024;
|
|
6468
|
+
var sandboxProbePromise;
|
|
6469
|
+
function probeSandboxSupport() {
|
|
6470
|
+
if (sandboxProbePromise) return sandboxProbePromise;
|
|
6471
|
+
sandboxProbePromise = (async () => {
|
|
6472
|
+
if (process.platform === "darwin") return { canSandbox: true };
|
|
6473
|
+
if (process.platform !== "linux") {
|
|
6474
|
+
return { canSandbox: false, reason: `Unsupported platform: ${process.platform}` };
|
|
6475
|
+
}
|
|
6476
|
+
return await new Promise((resolve3) => {
|
|
6477
|
+
const child = spawn2("bwrap", ["--dev-bind", "/", "/", "--", "/bin/true"]);
|
|
6478
|
+
let stderr = "";
|
|
6479
|
+
child.stderr.on("data", (chunk) => {
|
|
6480
|
+
stderr += chunk.toString();
|
|
6481
|
+
});
|
|
6482
|
+
child.on("error", (err) => {
|
|
6483
|
+
resolve3({ canSandbox: false, reason: `bwrap not executable: ${err.message}` });
|
|
6484
|
+
});
|
|
6485
|
+
child.on("exit", (code) => {
|
|
6486
|
+
if (code === 0) resolve3({ canSandbox: true });
|
|
6487
|
+
else resolve3({ canSandbox: false, reason: stderr.trim() || `bwrap exited ${code}` });
|
|
6488
|
+
});
|
|
6489
|
+
});
|
|
6490
|
+
})();
|
|
6491
|
+
return sandboxProbePromise;
|
|
6492
|
+
}
|
|
6493
|
+
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.';
|
|
6494
|
+
var degradedModeLogged = false;
|
|
6495
|
+
function logDegradedModeOnce(reason) {
|
|
6496
|
+
if (degradedModeLogged) return;
|
|
6497
|
+
degradedModeLogged = true;
|
|
6498
|
+
console.warn(
|
|
6499
|
+
`[SKILLS] ${SANDBOX_FALLBACK_INSTRUCTIONS}
|
|
6500
|
+
|
|
6501
|
+
Probe error: ${reason ?? "(no detail)"}`
|
|
6502
|
+
);
|
|
6503
|
+
}
|
|
6445
6504
|
var sandboxCache = /* @__PURE__ */ new Map();
|
|
6446
6505
|
async function downloadSkill(skill, skillsDirectory, config) {
|
|
6447
6506
|
const version = skill.current_version ?? 1;
|
|
@@ -6581,6 +6640,20 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6581
6640
|
if (userId && config.fileUploads && !dirExisted) {
|
|
6582
6641
|
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config);
|
|
6583
6642
|
}
|
|
6643
|
+
const probe = await probeSandboxSupport();
|
|
6644
|
+
const useDirectExec = !probe.canSandbox;
|
|
6645
|
+
if (useDirectExec) {
|
|
6646
|
+
if (process.env.EXULU_REQUIRE_SANDBOX === "1") {
|
|
6647
|
+
throw new Error(
|
|
6648
|
+
`[SKILLS] Sandbox required (EXULU_REQUIRE_SANDBOX=1) but unavailable.
|
|
6649
|
+
|
|
6650
|
+
${SANDBOX_FALLBACK_INSTRUCTIONS}
|
|
6651
|
+
|
|
6652
|
+
Probe error: ${probe.reason ?? "(no detail)"}`
|
|
6653
|
+
);
|
|
6654
|
+
}
|
|
6655
|
+
logDegradedModeOnce(probe.reason);
|
|
6656
|
+
}
|
|
6584
6657
|
const baselineSandboxConfig = {
|
|
6585
6658
|
network: {
|
|
6586
6659
|
allowedDomains: [],
|
|
@@ -6596,7 +6669,9 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6596
6669
|
denyWrite: []
|
|
6597
6670
|
}
|
|
6598
6671
|
};
|
|
6599
|
-
|
|
6672
|
+
if (!useDirectExec) {
|
|
6673
|
+
await SandboxManager.initialize(baselineSandboxConfig);
|
|
6674
|
+
}
|
|
6600
6675
|
const npmGlobalRoot = await getNpmGlobalRoot();
|
|
6601
6676
|
const sessionSandboxConfig = {
|
|
6602
6677
|
network: {
|
|
@@ -6632,8 +6707,12 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6632
6707
|
...process.env,
|
|
6633
6708
|
...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}
|
|
6634
6709
|
};
|
|
6710
|
+
const wrapIfNeeded = async (command) => {
|
|
6711
|
+
if (useDirectExec) return command;
|
|
6712
|
+
return SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig);
|
|
6713
|
+
};
|
|
6635
6714
|
const runWrapped = async (command) => {
|
|
6636
|
-
const wrapped = await
|
|
6715
|
+
const wrapped = await wrapIfNeeded(command);
|
|
6637
6716
|
try {
|
|
6638
6717
|
const { stdout, stderr } = await execAsync2(wrapped, {
|
|
6639
6718
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
@@ -6710,10 +6789,8 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6710
6789
|
async function writeFilesInternal(files) {
|
|
6711
6790
|
const results = [];
|
|
6712
6791
|
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
|
|
6792
|
+
const wrapped = await wrapIfNeeded(
|
|
6793
|
+
`mkdir -p ${shellQuote(dirname(file.path))} && cat > ${shellQuote(file.path)}`
|
|
6717
6794
|
);
|
|
6718
6795
|
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
6719
6796
|
const child = spawn2("/bin/bash", ["-c", wrapped]);
|
|
@@ -6868,10 +6945,12 @@ ${lines.join("\n")}`;
|
|
|
6868
6945
|
const handle = {
|
|
6869
6946
|
sessionDir,
|
|
6870
6947
|
tools: wrappedTools,
|
|
6871
|
-
wrapCommand: (command) =>
|
|
6948
|
+
wrapCommand: (command) => wrapIfNeeded(command),
|
|
6872
6949
|
cleanup: async () => {
|
|
6873
6950
|
sandboxCache.delete(sessionId);
|
|
6874
|
-
|
|
6951
|
+
if (!useDirectExec) {
|
|
6952
|
+
await SandboxManager.reset();
|
|
6953
|
+
}
|
|
6875
6954
|
await rm(sessionDir, { recursive: true, force: true });
|
|
6876
6955
|
}
|
|
6877
6956
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -1847,22 +1847,45 @@ var init_uppy = __esm({
|
|
|
1847
1847
|
} else {
|
|
1848
1848
|
prefix += "global";
|
|
1849
1849
|
}
|
|
1850
|
+
console.log("[EXULU] bucket", config.fileUploads.s3Bucket);
|
|
1850
1851
|
console.log("[EXULU] prefix", prefix);
|
|
1851
|
-
|
|
1852
|
-
Bucket: config.fileUploads.s3Bucket,
|
|
1853
|
-
Prefix: prefix,
|
|
1854
|
-
MaxKeys: 9,
|
|
1855
|
-
...req.query.continuationToken && {
|
|
1856
|
-
ContinuationToken: req.query.continuationToken
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1859
|
-
const response = await client2.send(command);
|
|
1852
|
+
let response;
|
|
1860
1853
|
if (req.query.search) {
|
|
1861
|
-
const search = req.query.search;
|
|
1854
|
+
const search = req.query.search.toLowerCase();
|
|
1862
1855
|
console.log("[EXULU] Filtering files by search query", req.query.search);
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1856
|
+
const matched = [];
|
|
1857
|
+
let token;
|
|
1858
|
+
do {
|
|
1859
|
+
const page = await client2.send(
|
|
1860
|
+
new import_client_s3.ListObjectsV2Command({
|
|
1861
|
+
Bucket: config.fileUploads.s3Bucket,
|
|
1862
|
+
Prefix: prefix,
|
|
1863
|
+
...token && { ContinuationToken: token }
|
|
1864
|
+
})
|
|
1865
|
+
);
|
|
1866
|
+
for (const obj of page.Contents ?? []) {
|
|
1867
|
+
if (obj.Key?.toLowerCase().includes(search)) {
|
|
1868
|
+
matched.push(obj);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
token = page.IsTruncated ? page.NextContinuationToken : void 0;
|
|
1872
|
+
} while (token);
|
|
1873
|
+
response = {
|
|
1874
|
+
$metadata: {},
|
|
1875
|
+
Contents: matched,
|
|
1876
|
+
KeyCount: matched.length,
|
|
1877
|
+
IsTruncated: false
|
|
1878
|
+
};
|
|
1879
|
+
} else {
|
|
1880
|
+
const command = new import_client_s3.ListObjectsV2Command({
|
|
1881
|
+
Bucket: config.fileUploads.s3Bucket,
|
|
1882
|
+
Prefix: prefix,
|
|
1883
|
+
MaxKeys: 9,
|
|
1884
|
+
...req.query.continuationToken && {
|
|
1885
|
+
ContinuationToken: req.query.continuationToken
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
response = await client2.send(command);
|
|
1866
1889
|
}
|
|
1867
1890
|
res.json({
|
|
1868
1891
|
...response,
|
|
@@ -2294,6 +2317,39 @@ var init_variable = __esm({
|
|
|
2294
2317
|
});
|
|
2295
2318
|
|
|
2296
2319
|
// ee/invoke-skills/create-sandbox.ts
|
|
2320
|
+
function probeSandboxSupport() {
|
|
2321
|
+
if (sandboxProbePromise) return sandboxProbePromise;
|
|
2322
|
+
sandboxProbePromise = (async () => {
|
|
2323
|
+
if (process.platform === "darwin") return { canSandbox: true };
|
|
2324
|
+
if (process.platform !== "linux") {
|
|
2325
|
+
return { canSandbox: false, reason: `Unsupported platform: ${process.platform}` };
|
|
2326
|
+
}
|
|
2327
|
+
return await new Promise((resolve7) => {
|
|
2328
|
+
const child = (0, import_node_child_process3.spawn)("bwrap", ["--dev-bind", "/", "/", "--", "/bin/true"]);
|
|
2329
|
+
let stderr = "";
|
|
2330
|
+
child.stderr.on("data", (chunk) => {
|
|
2331
|
+
stderr += chunk.toString();
|
|
2332
|
+
});
|
|
2333
|
+
child.on("error", (err) => {
|
|
2334
|
+
resolve7({ canSandbox: false, reason: `bwrap not executable: ${err.message}` });
|
|
2335
|
+
});
|
|
2336
|
+
child.on("exit", (code) => {
|
|
2337
|
+
if (code === 0) resolve7({ canSandbox: true });
|
|
2338
|
+
else resolve7({ canSandbox: false, reason: stderr.trim() || `bwrap exited ${code}` });
|
|
2339
|
+
});
|
|
2340
|
+
});
|
|
2341
|
+
})();
|
|
2342
|
+
return sandboxProbePromise;
|
|
2343
|
+
}
|
|
2344
|
+
function logDegradedModeOnce(reason) {
|
|
2345
|
+
if (degradedModeLogged) return;
|
|
2346
|
+
degradedModeLogged = true;
|
|
2347
|
+
console.warn(
|
|
2348
|
+
`[SKILLS] ${SANDBOX_FALLBACK_INSTRUCTIONS}
|
|
2349
|
+
|
|
2350
|
+
Probe error: ${reason ?? "(no detail)"}`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2297
2353
|
async function downloadSkill(skill, skillsDirectory, config) {
|
|
2298
2354
|
const version = skill.current_version ?? 1;
|
|
2299
2355
|
if (!skill.current_version) {
|
|
@@ -2432,6 +2488,20 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
2432
2488
|
if (userId && config.fileUploads && !dirExisted) {
|
|
2433
2489
|
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config);
|
|
2434
2490
|
}
|
|
2491
|
+
const probe = await probeSandboxSupport();
|
|
2492
|
+
const useDirectExec = !probe.canSandbox;
|
|
2493
|
+
if (useDirectExec) {
|
|
2494
|
+
if (process.env.EXULU_REQUIRE_SANDBOX === "1") {
|
|
2495
|
+
throw new Error(
|
|
2496
|
+
`[SKILLS] Sandbox required (EXULU_REQUIRE_SANDBOX=1) but unavailable.
|
|
2497
|
+
|
|
2498
|
+
${SANDBOX_FALLBACK_INSTRUCTIONS}
|
|
2499
|
+
|
|
2500
|
+
Probe error: ${probe.reason ?? "(no detail)"}`
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
logDegradedModeOnce(probe.reason);
|
|
2504
|
+
}
|
|
2435
2505
|
const baselineSandboxConfig = {
|
|
2436
2506
|
network: {
|
|
2437
2507
|
allowedDomains: [],
|
|
@@ -2447,7 +2517,9 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
2447
2517
|
denyWrite: []
|
|
2448
2518
|
}
|
|
2449
2519
|
};
|
|
2450
|
-
|
|
2520
|
+
if (!useDirectExec) {
|
|
2521
|
+
await import_sandbox_runtime.SandboxManager.initialize(baselineSandboxConfig);
|
|
2522
|
+
}
|
|
2451
2523
|
const npmGlobalRoot = await getNpmGlobalRoot();
|
|
2452
2524
|
const sessionSandboxConfig = {
|
|
2453
2525
|
network: {
|
|
@@ -2483,8 +2555,12 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
2483
2555
|
...process.env,
|
|
2484
2556
|
...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}
|
|
2485
2557
|
};
|
|
2558
|
+
const wrapIfNeeded = async (command) => {
|
|
2559
|
+
if (useDirectExec) return command;
|
|
2560
|
+
return import_sandbox_runtime.SandboxManager.wrapWithSandbox(command, void 0, sessionSandboxConfig);
|
|
2561
|
+
};
|
|
2486
2562
|
const runWrapped = async (command) => {
|
|
2487
|
-
const wrapped = await
|
|
2563
|
+
const wrapped = await wrapIfNeeded(command);
|
|
2488
2564
|
try {
|
|
2489
2565
|
const { stdout, stderr } = await execAsync2(wrapped, {
|
|
2490
2566
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
@@ -2561,10 +2637,8 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
2561
2637
|
async function writeFilesInternal(files) {
|
|
2562
2638
|
const results = [];
|
|
2563
2639
|
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
|
|
2640
|
+
const wrapped = await wrapIfNeeded(
|
|
2641
|
+
`mkdir -p ${shellQuote((0, import_node_path3.dirname)(file.path))} && cat > ${shellQuote(file.path)}`
|
|
2568
2642
|
);
|
|
2569
2643
|
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
2570
2644
|
const child = (0, import_node_child_process3.spawn)("/bin/bash", ["-c", wrapped]);
|
|
@@ -2719,17 +2793,19 @@ ${lines.join("\n")}`;
|
|
|
2719
2793
|
const handle = {
|
|
2720
2794
|
sessionDir,
|
|
2721
2795
|
tools: wrappedTools,
|
|
2722
|
-
wrapCommand: (command) =>
|
|
2796
|
+
wrapCommand: (command) => wrapIfNeeded(command),
|
|
2723
2797
|
cleanup: async () => {
|
|
2724
2798
|
sandboxCache.delete(sessionId);
|
|
2725
|
-
|
|
2799
|
+
if (!useDirectExec) {
|
|
2800
|
+
await import_sandbox_runtime.SandboxManager.reset();
|
|
2801
|
+
}
|
|
2726
2802
|
await (0, import_promises.rm)(sessionDir, { recursive: true, force: true });
|
|
2727
2803
|
}
|
|
2728
2804
|
};
|
|
2729
2805
|
sandboxCache.set(sessionId, { handle, installedSkills });
|
|
2730
2806
|
return handle;
|
|
2731
2807
|
}
|
|
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;
|
|
2808
|
+
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
2809
|
var init_create_sandbox = __esm({
|
|
2734
2810
|
"ee/invoke-skills/create-sandbox.ts"() {
|
|
2735
2811
|
"use strict";
|
|
@@ -2776,6 +2852,8 @@ var init_create_sandbox = __esm({
|
|
|
2776
2852
|
};
|
|
2777
2853
|
execAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.exec);
|
|
2778
2854
|
EXEC_MAX_BUFFER = 32 * 1024 * 1024;
|
|
2855
|
+
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.';
|
|
2856
|
+
degradedModeLogged = false;
|
|
2779
2857
|
sandboxCache = /* @__PURE__ */ new Map();
|
|
2780
2858
|
}
|
|
2781
2859
|
});
|
|
@@ -14290,7 +14368,6 @@ ${skillsList}
|
|
|
14290
14368
|
|
|
14291
14369
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
14292
14370
|
'Inform the user that the action was not performed.`;
|
|
14293
|
-
import_fs2.default.writeFileSync("system-prompt.txt", system);
|
|
14294
14371
|
console.log("[EXULU] Tools", currentTools?.map((x) => x.name));
|
|
14295
14372
|
console.log("[EXULU] Skills", currentSkills?.map((x) => x.name));
|
|
14296
14373
|
const tools = await convertExuluToolsToAiSdkTools(
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
vectorSearch,
|
|
54
54
|
waitForLiteLLMReady,
|
|
55
55
|
withRetry
|
|
56
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-PFKAWGB6.js";
|
|
57
57
|
import {
|
|
58
58
|
findLiteLLMModel
|
|
59
59
|
} from "./chunk-ILAHW4UT.js";
|
|
@@ -6497,7 +6497,6 @@ ${skillsList}
|
|
|
6497
6497
|
|
|
6498
6498
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
6499
6499
|
'Inform the user that the action was not performed.`;
|
|
6500
|
-
fs2.writeFileSync("system-prompt.txt", system);
|
|
6501
6500
|
console.log("[EXULU] Tools", currentTools?.map((x) => x.name));
|
|
6502
6501
|
console.log("[EXULU] Skills", currentSkills?.map((x) => x.name));
|
|
6503
6502
|
const tools = await convertExuluToolsToAiSdkTools(
|
|
@@ -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
|
}
|