@downcity/agent 1.1.91 → 1.1.96
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/bin/sandbox/SandboxPreflight.d.ts +73 -0
- package/bin/sandbox/SandboxPreflight.d.ts.map +1 -0
- package/bin/sandbox/SandboxPreflight.js +122 -0
- package/bin/sandbox/SandboxPreflight.js.map +1 -0
- package/package.json +2 -2
- package/scripts/shell-sandbox-preflight.test.mjs +88 -0
- package/src/sandbox/SandboxPreflight.ts +205 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SandboxPreflight:本机 shell sandbox 依赖预检。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - shell 命令必须进入 sandbox;这里提前检查 backend 依赖,避免启动后首次 shell 执行才失败。
|
|
6
|
+
* - Linux backend 基于 bubblewrap,本质使用 Linux namespaces / bind mount 等内核能力。
|
|
7
|
+
* - 本模块只诊断并给出修复建议,不自动安装软件,也不修改宿主机 sysctl。
|
|
8
|
+
*/
|
|
9
|
+
import type { SandboxBackend } from "../sandbox/types/SandboxRuntime.js";
|
|
10
|
+
/**
|
|
11
|
+
* sandbox 预检失败原因。
|
|
12
|
+
*/
|
|
13
|
+
export type SandboxPreflightIssueCode = "unsupported-platform" | "missing-command" | "userns-disabled";
|
|
14
|
+
/**
|
|
15
|
+
* 单条 sandbox 预检失败。
|
|
16
|
+
*/
|
|
17
|
+
export interface SandboxPreflightIssue {
|
|
18
|
+
/**
|
|
19
|
+
* 机器可读的失败原因。
|
|
20
|
+
*/
|
|
21
|
+
code: SandboxPreflightIssueCode;
|
|
22
|
+
/**
|
|
23
|
+
* 人类可读的失败说明。
|
|
24
|
+
*/
|
|
25
|
+
message: string;
|
|
26
|
+
/**
|
|
27
|
+
* 可复制的修复建议列表。
|
|
28
|
+
*/
|
|
29
|
+
fixes: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* sandbox 预检结果。
|
|
33
|
+
*/
|
|
34
|
+
export interface SandboxPreflightResult {
|
|
35
|
+
/**
|
|
36
|
+
* 当前平台是否满足 shell sandbox 启动要求。
|
|
37
|
+
*/
|
|
38
|
+
ok: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 当前宿主平台。
|
|
41
|
+
*/
|
|
42
|
+
platform: NodeJS.Platform;
|
|
43
|
+
/**
|
|
44
|
+
* 当前平台对应的 sandbox backend。
|
|
45
|
+
*/
|
|
46
|
+
backend?: SandboxBackend;
|
|
47
|
+
/**
|
|
48
|
+
* 失败原因集合。
|
|
49
|
+
*/
|
|
50
|
+
issues: SandboxPreflightIssue[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* sandbox 预检宿主探测依赖。
|
|
54
|
+
*/
|
|
55
|
+
export interface ShellSandboxPreflightProbe {
|
|
56
|
+
/**
|
|
57
|
+
* 判断命令是否存在于 PATH 中。
|
|
58
|
+
*/
|
|
59
|
+
commandExists(command: string): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* 读取 `/proc` 下整数配置。
|
|
62
|
+
*/
|
|
63
|
+
readProcInt(filePath: string): Promise<number | null>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 检查当前宿主是否满足 shell sandbox 运行要求。
|
|
67
|
+
*/
|
|
68
|
+
export declare function checkShellSandboxPreflight(): Promise<SandboxPreflightResult>;
|
|
69
|
+
/**
|
|
70
|
+
* 使用注入探针检查当前宿主是否满足 shell sandbox 运行要求。
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkShellSandboxPreflightWithProbe(probe: ShellSandboxPreflightProbe): Promise<SandboxPreflightResult>;
|
|
73
|
+
//# sourceMappingURL=SandboxPreflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SandboxPreflight.d.ts","sourceRoot":"","sources":["../../src/sandbox/SandboxPreflight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GACjC,sBAAsB,GACtB,iBAAiB,GACjB,iBAAiB,CAAC;AAEtB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,IAAI,EAAE,yBAAyB,CAAC;IAEhC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IAEZ;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAE1B;;OAEG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;OAEG;IACH,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEjD;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACvD;AAyCD;;GAEG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAKlF;AAED;;GAEG;AACH,wBAAsB,mCAAmC,CACvD,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,sBAAsB,CAAC,CAoEjC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SandboxPreflight:本机 shell sandbox 依赖预检。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - shell 命令必须进入 sandbox;这里提前检查 backend 依赖,避免启动后首次 shell 执行才失败。
|
|
6
|
+
* - Linux backend 基于 bubblewrap,本质使用 Linux namespaces / bind mount 等内核能力。
|
|
7
|
+
* - 本模块只诊断并给出修复建议,不自动安装软件,也不修改宿主机 sysctl。
|
|
8
|
+
*/
|
|
9
|
+
import { access, readFile } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { delimiter } from "node:path";
|
|
12
|
+
async function commandExists(command) {
|
|
13
|
+
const pathValue = String(process.env.PATH || "").trim();
|
|
14
|
+
const dirs = pathValue ? pathValue.split(delimiter) : [];
|
|
15
|
+
for (const dir of dirs) {
|
|
16
|
+
const candidate = path.join(dir, command);
|
|
17
|
+
try {
|
|
18
|
+
await access(candidate);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// continue
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
async function readProcInt(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(filePath, "utf-8");
|
|
30
|
+
const value = Number.parseInt(raw.trim(), 10);
|
|
31
|
+
return Number.isFinite(value) && !Number.isNaN(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function isLinuxUserNamespaceEnabled(probe) {
|
|
38
|
+
const unprivilegedUsernsClone = await probe.readProcInt("/proc/sys/kernel/unprivileged_userns_clone");
|
|
39
|
+
if (unprivilegedUsernsClone === 0)
|
|
40
|
+
return false;
|
|
41
|
+
const maxUserNamespaces = await probe.readProcInt("/proc/sys/user/max_user_namespaces");
|
|
42
|
+
if (maxUserNamespaces === 0)
|
|
43
|
+
return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 检查当前宿主是否满足 shell sandbox 运行要求。
|
|
48
|
+
*/
|
|
49
|
+
export async function checkShellSandboxPreflight() {
|
|
50
|
+
return await checkShellSandboxPreflightWithProbe({
|
|
51
|
+
commandExists,
|
|
52
|
+
readProcInt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 使用注入探针检查当前宿主是否满足 shell sandbox 运行要求。
|
|
57
|
+
*/
|
|
58
|
+
export async function checkShellSandboxPreflightWithProbe(probe) {
|
|
59
|
+
const platform = process.platform;
|
|
60
|
+
const issues = [];
|
|
61
|
+
if (platform === "darwin") {
|
|
62
|
+
if (!(await probe.commandExists("sandbox-exec"))) {
|
|
63
|
+
issues.push({
|
|
64
|
+
code: "missing-command",
|
|
65
|
+
message: "macOS shell sandbox requires sandbox-exec, but it was not found.",
|
|
66
|
+
fixes: [
|
|
67
|
+
"Use a macOS system that includes /usr/bin/sandbox-exec.",
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: issues.length === 0,
|
|
73
|
+
platform,
|
|
74
|
+
backend: "macos-seatbelt",
|
|
75
|
+
issues,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (platform === "linux") {
|
|
79
|
+
if (!(await probe.commandExists("bwrap"))) {
|
|
80
|
+
issues.push({
|
|
81
|
+
code: "missing-command",
|
|
82
|
+
message: "Linux shell sandbox requires bubblewrap (bwrap), but it was not found.",
|
|
83
|
+
fixes: [
|
|
84
|
+
"Debian / Ubuntu: sudo apt install bubblewrap",
|
|
85
|
+
"Fedora: sudo dnf install bubblewrap",
|
|
86
|
+
"Arch: sudo pacman -S bubblewrap",
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!(await isLinuxUserNamespaceEnabled(probe))) {
|
|
91
|
+
issues.push({
|
|
92
|
+
code: "userns-disabled",
|
|
93
|
+
message: "Linux user namespaces are disabled, so bubblewrap cannot create the sandbox.",
|
|
94
|
+
fixes: [
|
|
95
|
+
"Check: cat /proc/sys/kernel/unprivileged_userns_clone",
|
|
96
|
+
"Check: cat /proc/sys/user/max_user_namespaces",
|
|
97
|
+
"Debian / Ubuntu: sudo sysctl kernel.unprivileged_userns_clone=1",
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ok: issues.length === 0,
|
|
103
|
+
platform,
|
|
104
|
+
backend: "linux-bubblewrap",
|
|
105
|
+
issues,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
platform,
|
|
111
|
+
issues: [
|
|
112
|
+
{
|
|
113
|
+
code: "unsupported-platform",
|
|
114
|
+
message: `Shell sandbox is not supported on this platform: ${platform}.`,
|
|
115
|
+
fixes: [
|
|
116
|
+
"Use macOS or Linux for local shell execution.",
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=SandboxPreflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SandboxPreflight.js","sourceRoot":"","sources":["../../src/sandbox/SandboxPreflight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAuEtC,KAAK,UAAU,aAAa,CAAC,OAAe;IAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,WAAW;QACb,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,QAAgB;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAC9C,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,2BAA2B,CACxC,KAAiC;IAEjC,MAAM,uBAAuB,GAAG,MAAM,KAAK,CAAC,WAAW,CACrD,4CAA4C,CAC7C,CAAC;IACF,IAAI,uBAAuB,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEhD,MAAM,iBAAiB,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,oCAAoC,CAAC,CAAC;IACxF,IAAI,iBAAiB,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE1C,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B;IAC9C,OAAO,MAAM,mCAAmC,CAAC;QAC/C,aAAa;QACb,WAAW;KACZ,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mCAAmC,CACvD,KAAiC;IAEjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,kEAAkE;gBAC3E,KAAK,EAAE;oBACL,yDAAyD;iBAC1D;aACF,CAAC,CAAC;QACL,CAAC;QACD,OAAO;YACL,EAAE,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;YACvB,QAAQ;YACR,OAAO,EAAE,gBAAgB;YACzB,MAAM;SACP,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,wEAAwE;gBACjF,KAAK,EAAE;oBACL,8CAA8C;oBAC9C,qCAAqC;oBACrC,iCAAiC;iBAClC;aACF,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,CAAC,MAAM,2BAA2B,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAChD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,8EAA8E;gBACvF,KAAK,EAAE;oBACL,uDAAuD;oBACvD,+CAA+C;oBAC/C,iEAAiE;iBAClE;aACF,CAAC,CAAC;QACL,CAAC;QAED,OAAO;YACL,EAAE,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;YACvB,QAAQ;YACR,OAAO,EAAE,kBAAkB;YAC3B,MAAM;SACP,CAAC;IACJ,CAAC;IAED,OAAO;QACL,EAAE,EAAE,KAAK;QACT,QAAQ;QACR,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,sBAAsB;gBAC5B,OAAO,EAAE,oDAAoD,QAAQ,GAAG;gBACxE,KAAK,EAAE;oBACL,+CAA+C;iBAChD;aACF;SACF;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@downcity/agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.96",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Downcity Agent 运行时 — 单 Agent 执行壳与本机 RPC 能力",
|
|
6
6
|
"main": "./bin/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node-cron": "^4.2.1",
|
|
30
30
|
"ws": "^8.21.0",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
|
-
"@downcity/type": "0.1.
|
|
32
|
+
"@downcity/type": "0.1.30"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证 shell sandbox 启动前依赖诊断。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 测试编译后的 bin 输出,避免测试文件进入 package 源码导出面。
|
|
6
|
+
* - 通过注入探针模拟 Linux 依赖状态,不要求当前测试机安装 bwrap。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
checkShellSandboxPreflightWithProbe,
|
|
14
|
+
} from "../bin/sandbox/SandboxPreflight.js";
|
|
15
|
+
|
|
16
|
+
async function withPlatform(platform, callback) {
|
|
17
|
+
const previous = Object.getOwnPropertyDescriptor(process, "platform");
|
|
18
|
+
Object.defineProperty(process, "platform", {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value: platform,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return await callback();
|
|
24
|
+
} finally {
|
|
25
|
+
if (previous) {
|
|
26
|
+
Object.defineProperty(process, "platform", previous);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createProbe(params) {
|
|
32
|
+
return {
|
|
33
|
+
commandExists: async (command) => params.commands?.has(command) === true,
|
|
34
|
+
readProcInt: async (filePath) => params.proc?.get(filePath) ?? null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("Linux shell sandbox preflight reports missing bwrap and disabled userns", async () => {
|
|
39
|
+
await withPlatform("linux", async () => {
|
|
40
|
+
const result = await checkShellSandboxPreflightWithProbe(createProbe({
|
|
41
|
+
commands: new Set(),
|
|
42
|
+
proc: new Map([
|
|
43
|
+
["/proc/sys/kernel/unprivileged_userns_clone", 0],
|
|
44
|
+
["/proc/sys/user/max_user_namespaces", 0],
|
|
45
|
+
]),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
assert.equal(result.ok, false);
|
|
49
|
+
assert.equal(result.backend, "linux-bubblewrap");
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
result.issues.map((issue) => issue.code),
|
|
52
|
+
["missing-command", "userns-disabled"],
|
|
53
|
+
);
|
|
54
|
+
assert.match(result.issues[0].message, /bubblewrap/);
|
|
55
|
+
assert.match(result.issues[1].message, /user namespaces/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("Linux shell sandbox preflight accepts bwrap with enabled userns", async () => {
|
|
60
|
+
await withPlatform("linux", async () => {
|
|
61
|
+
const result = await checkShellSandboxPreflightWithProbe(createProbe({
|
|
62
|
+
commands: new Set(["bwrap"]),
|
|
63
|
+
proc: new Map([
|
|
64
|
+
["/proc/sys/kernel/unprivileged_userns_clone", 1],
|
|
65
|
+
["/proc/sys/user/max_user_namespaces", 1024],
|
|
66
|
+
]),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
assert.equal(result.ok, true);
|
|
70
|
+
assert.equal(result.backend, "linux-bubblewrap");
|
|
71
|
+
assert.deepEqual(result.issues, []);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("Unsupported platforms fail shell sandbox preflight", async () => {
|
|
76
|
+
await withPlatform("win32", async () => {
|
|
77
|
+
const result = await checkShellSandboxPreflightWithProbe(createProbe({
|
|
78
|
+
commands: new Set(),
|
|
79
|
+
proc: new Map(),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
assert.equal(result.ok, false);
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
result.issues.map((issue) => issue.code),
|
|
85
|
+
["unsupported-platform"],
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SandboxPreflight:本机 shell sandbox 依赖预检。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - shell 命令必须进入 sandbox;这里提前检查 backend 依赖,避免启动后首次 shell 执行才失败。
|
|
6
|
+
* - Linux backend 基于 bubblewrap,本质使用 Linux namespaces / bind mount 等内核能力。
|
|
7
|
+
* - 本模块只诊断并给出修复建议,不自动安装软件,也不修改宿主机 sysctl。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { access, readFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { delimiter } from "node:path";
|
|
13
|
+
import type { SandboxBackend } from "@/sandbox/types/SandboxRuntime.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* sandbox 预检失败原因。
|
|
17
|
+
*/
|
|
18
|
+
export type SandboxPreflightIssueCode =
|
|
19
|
+
| "unsupported-platform"
|
|
20
|
+
| "missing-command"
|
|
21
|
+
| "userns-disabled";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 单条 sandbox 预检失败。
|
|
25
|
+
*/
|
|
26
|
+
export interface SandboxPreflightIssue {
|
|
27
|
+
/**
|
|
28
|
+
* 机器可读的失败原因。
|
|
29
|
+
*/
|
|
30
|
+
code: SandboxPreflightIssueCode;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 人类可读的失败说明。
|
|
34
|
+
*/
|
|
35
|
+
message: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 可复制的修复建议列表。
|
|
39
|
+
*/
|
|
40
|
+
fixes: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* sandbox 预检结果。
|
|
45
|
+
*/
|
|
46
|
+
export interface SandboxPreflightResult {
|
|
47
|
+
/**
|
|
48
|
+
* 当前平台是否满足 shell sandbox 启动要求。
|
|
49
|
+
*/
|
|
50
|
+
ok: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 当前宿主平台。
|
|
54
|
+
*/
|
|
55
|
+
platform: NodeJS.Platform;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 当前平台对应的 sandbox backend。
|
|
59
|
+
*/
|
|
60
|
+
backend?: SandboxBackend;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 失败原因集合。
|
|
64
|
+
*/
|
|
65
|
+
issues: SandboxPreflightIssue[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* sandbox 预检宿主探测依赖。
|
|
70
|
+
*/
|
|
71
|
+
export interface ShellSandboxPreflightProbe {
|
|
72
|
+
/**
|
|
73
|
+
* 判断命令是否存在于 PATH 中。
|
|
74
|
+
*/
|
|
75
|
+
commandExists(command: string): Promise<boolean>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 读取 `/proc` 下整数配置。
|
|
79
|
+
*/
|
|
80
|
+
readProcInt(filePath: string): Promise<number | null>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
84
|
+
const pathValue = String(process.env.PATH || "").trim();
|
|
85
|
+
const dirs = pathValue ? pathValue.split(delimiter) : [];
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
const candidate = path.join(dir, command);
|
|
88
|
+
try {
|
|
89
|
+
await access(candidate);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// continue
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readProcInt(filePath: string): Promise<number | null> {
|
|
99
|
+
try {
|
|
100
|
+
const raw = await readFile(filePath, "utf-8");
|
|
101
|
+
const value = Number.parseInt(raw.trim(), 10);
|
|
102
|
+
return Number.isFinite(value) && !Number.isNaN(value) ? value : null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function isLinuxUserNamespaceEnabled(
|
|
109
|
+
probe: ShellSandboxPreflightProbe,
|
|
110
|
+
): Promise<boolean> {
|
|
111
|
+
const unprivilegedUsernsClone = await probe.readProcInt(
|
|
112
|
+
"/proc/sys/kernel/unprivileged_userns_clone",
|
|
113
|
+
);
|
|
114
|
+
if (unprivilegedUsernsClone === 0) return false;
|
|
115
|
+
|
|
116
|
+
const maxUserNamespaces = await probe.readProcInt("/proc/sys/user/max_user_namespaces");
|
|
117
|
+
if (maxUserNamespaces === 0) return false;
|
|
118
|
+
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 检查当前宿主是否满足 shell sandbox 运行要求。
|
|
124
|
+
*/
|
|
125
|
+
export async function checkShellSandboxPreflight(): Promise<SandboxPreflightResult> {
|
|
126
|
+
return await checkShellSandboxPreflightWithProbe({
|
|
127
|
+
commandExists,
|
|
128
|
+
readProcInt,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 使用注入探针检查当前宿主是否满足 shell sandbox 运行要求。
|
|
134
|
+
*/
|
|
135
|
+
export async function checkShellSandboxPreflightWithProbe(
|
|
136
|
+
probe: ShellSandboxPreflightProbe,
|
|
137
|
+
): Promise<SandboxPreflightResult> {
|
|
138
|
+
const platform = process.platform;
|
|
139
|
+
const issues: SandboxPreflightIssue[] = [];
|
|
140
|
+
|
|
141
|
+
if (platform === "darwin") {
|
|
142
|
+
if (!(await probe.commandExists("sandbox-exec"))) {
|
|
143
|
+
issues.push({
|
|
144
|
+
code: "missing-command",
|
|
145
|
+
message: "macOS shell sandbox requires sandbox-exec, but it was not found.",
|
|
146
|
+
fixes: [
|
|
147
|
+
"Use a macOS system that includes /usr/bin/sandbox-exec.",
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: issues.length === 0,
|
|
153
|
+
platform,
|
|
154
|
+
backend: "macos-seatbelt",
|
|
155
|
+
issues,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (platform === "linux") {
|
|
160
|
+
if (!(await probe.commandExists("bwrap"))) {
|
|
161
|
+
issues.push({
|
|
162
|
+
code: "missing-command",
|
|
163
|
+
message: "Linux shell sandbox requires bubblewrap (bwrap), but it was not found.",
|
|
164
|
+
fixes: [
|
|
165
|
+
"Debian / Ubuntu: sudo apt install bubblewrap",
|
|
166
|
+
"Fedora: sudo dnf install bubblewrap",
|
|
167
|
+
"Arch: sudo pacman -S bubblewrap",
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!(await isLinuxUserNamespaceEnabled(probe))) {
|
|
173
|
+
issues.push({
|
|
174
|
+
code: "userns-disabled",
|
|
175
|
+
message: "Linux user namespaces are disabled, so bubblewrap cannot create the sandbox.",
|
|
176
|
+
fixes: [
|
|
177
|
+
"Check: cat /proc/sys/kernel/unprivileged_userns_clone",
|
|
178
|
+
"Check: cat /proc/sys/user/max_user_namespaces",
|
|
179
|
+
"Debian / Ubuntu: sudo sysctl kernel.unprivileged_userns_clone=1",
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: issues.length === 0,
|
|
186
|
+
platform,
|
|
187
|
+
backend: "linux-bubblewrap",
|
|
188
|
+
issues,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
platform,
|
|
195
|
+
issues: [
|
|
196
|
+
{
|
|
197
|
+
code: "unsupported-platform",
|
|
198
|
+
message: `Shell sandbox is not supported on this platform: ${platform}.`,
|
|
199
|
+
fixes: [
|
|
200
|
+
"Use macOS or Linux for local shell execution.",
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|