@trim21/personal-pi-extensions 0.0.425 → 0.0.427
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/README.md +2 -2
- package/package.json +1 -1
- package/src/bwrap/core.ts +24 -10
- package/src/bwrap/holder.js +0 -1
- package/src/bwrap/holder.ts +1 -3
- package/src/bwrap/network-stack.ts +1 -1
- package/src/bwrap/runtime.ts +4 -4
- package/src/bwrap/singbox-config.ts +89 -2
package/README.md
CHANGED
|
@@ -80,8 +80,8 @@ bash 工具(opencode 风格 `bash`、Claude Code 风格 `Bash`)注册了 `da
|
|
|
80
80
|
"writablePaths": [".", "/tmp", "~/my-projects"],
|
|
81
81
|
// 额外可写路径,与默认值合并(ro-bind)
|
|
82
82
|
"extraWritablePaths": ["~/.config"],
|
|
83
|
-
// tmpfs
|
|
84
|
-
"
|
|
83
|
+
// 沙箱内隐藏的路径:以 / 结尾的视为目录(挂空 tmpfs),否则按文件处理(--ro-bind-try /dev/null)
|
|
84
|
+
"denyPaths": [],
|
|
85
85
|
// 额外 bwrap 参数
|
|
86
86
|
"extraArgs": ["--die-with-parent"],
|
|
87
87
|
// 全权限执行的自动审批规则:命中规则的命令不弹确认框
|
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -31,9 +31,16 @@ const bwrapConfigProperties = {
|
|
|
31
31
|
bwrapPath: Type.Optional(Type.String()),
|
|
32
32
|
writablePaths: Type.Array(Type.String()),
|
|
33
33
|
extraWritablePaths: Type.Array(Type.String()),
|
|
34
|
-
|
|
34
|
+
denyPaths: Type.Array(
|
|
35
|
+
Type.String({
|
|
36
|
+
description:
|
|
37
|
+
"沙箱内隐藏的路径:以 / 结尾为目录(挂空 tmpfs),否则为文件(--ro-bind-try /dev/null)",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
35
40
|
extraArgs: Type.Array(Type.String()),
|
|
36
|
-
networkAllowlist: Type.Array(
|
|
41
|
+
networkAllowlist: Type.Array(
|
|
42
|
+
Type.String({ description: "允许直连的域名 / IP / CIDR,可带 :port" }),
|
|
43
|
+
),
|
|
37
44
|
singBoxPath: Type.Optional(Type.String()),
|
|
38
45
|
slirp4netnsPath: Type.Optional(Type.String()),
|
|
39
46
|
approvalRules: Type.Optional(
|
|
@@ -69,9 +76,10 @@ export interface ResolvedBwrap {
|
|
|
69
76
|
bwrapPath?: string;
|
|
70
77
|
writablePaths: string[];
|
|
71
78
|
extraWritablePaths: string[];
|
|
72
|
-
|
|
79
|
+
/** 沙箱内隐藏的路径:以 / 结尾为目录(挂空 tmpfs),否则为文件(--ro-bind-try /dev/null)。 */
|
|
80
|
+
denyPaths: string[];
|
|
73
81
|
extraArgs: string[];
|
|
74
|
-
/**
|
|
82
|
+
/** 允许直连的域名 / IP / IP:port 白名单(非空 = 启用 sing-box 网络过滤)。 */
|
|
75
83
|
networkAllowlist: string[];
|
|
76
84
|
singBoxPath?: string;
|
|
77
85
|
slirp4netnsPath?: string;
|
|
@@ -83,7 +91,7 @@ const DEFAULT_CONFIG: BwrapConfig = {
|
|
|
83
91
|
mode: "workspace-write",
|
|
84
92
|
writablePaths: [".", "/tmp"],
|
|
85
93
|
extraWritablePaths: [],
|
|
86
|
-
|
|
94
|
+
denyPaths: [],
|
|
87
95
|
extraArgs: [],
|
|
88
96
|
networkAllowlist: [],
|
|
89
97
|
};
|
|
@@ -94,7 +102,7 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
94
102
|
bwrapPath: config.bwrapPath,
|
|
95
103
|
writablePaths: config.writablePaths ?? [".", "/tmp"],
|
|
96
104
|
extraWritablePaths: config.extraWritablePaths,
|
|
97
|
-
|
|
105
|
+
denyPaths: config.denyPaths ?? [],
|
|
98
106
|
extraArgs: config.extraArgs ?? [],
|
|
99
107
|
networkAllowlist: config.networkAllowlist ?? [],
|
|
100
108
|
singBoxPath: config.singBoxPath,
|
|
@@ -126,7 +134,7 @@ export function resolveHeadlessBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
126
134
|
mode: "readonly",
|
|
127
135
|
writablePaths: [],
|
|
128
136
|
extraWritablePaths: [],
|
|
129
|
-
|
|
137
|
+
denyPaths: [],
|
|
130
138
|
extraArgs: [],
|
|
131
139
|
});
|
|
132
140
|
}
|
|
@@ -137,7 +145,7 @@ function deepMerge(base: BwrapConfig, overrides: Partial<BwrapConfig>): BwrapCon
|
|
|
137
145
|
bwrapPath: overrides.bwrapPath ?? base.bwrapPath,
|
|
138
146
|
writablePaths: overrides.writablePaths ?? base.writablePaths,
|
|
139
147
|
extraWritablePaths: [...base.extraWritablePaths, ...(overrides.extraWritablePaths ?? [])],
|
|
140
|
-
|
|
148
|
+
denyPaths: overrides.denyPaths ?? base.denyPaths,
|
|
141
149
|
extraArgs: overrides.extraArgs ?? base.extraArgs,
|
|
142
150
|
networkAllowlist: overrides.networkAllowlist ?? base.networkAllowlist,
|
|
143
151
|
singBoxPath: overrides.singBoxPath ?? base.singBoxPath,
|
|
@@ -321,8 +329,14 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
|
|
|
321
329
|
const absolutePath = resolveBwrapPath(path, cwd);
|
|
322
330
|
args.push("--bind-try", absolutePath, absolutePath);
|
|
323
331
|
}
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
// denyPaths:以 / 结尾的条目视为目录(挂空 tmpfs),否则视为文件(--ro-bind-try /dev/null 覆盖)
|
|
333
|
+
for (const path of resolved.denyPaths) {
|
|
334
|
+
const target = resolveBwrapPath(path, cwd);
|
|
335
|
+
if (path.endsWith("/")) {
|
|
336
|
+
args.push("--tmpfs", target);
|
|
337
|
+
} else {
|
|
338
|
+
args.push("--ro-bind-try", "/dev/null", target);
|
|
339
|
+
}
|
|
326
340
|
}
|
|
327
341
|
if (!resolved.network) args.push("--unshare-net");
|
|
328
342
|
// --ro-bind-try:目录不存在(或已被删除)时自动忽略
|
package/src/bwrap/holder.js
CHANGED
package/src/bwrap/holder.ts
CHANGED
|
@@ -55,8 +55,7 @@ async function main(): Promise<void> {
|
|
|
55
55
|
process.exit(0);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
// 以 "sing-box started" 日志作为进程就绪标志,再等待栈初始化完成
|
|
58
|
+
// 以 "sing-box started" 日志作为就绪标志(TUN 与 gvisor 数据面此时已可用)
|
|
60
59
|
await waitFor(200, 100, async () => {
|
|
61
60
|
try {
|
|
62
61
|
const log = await readFile(logPath, "utf8");
|
|
@@ -65,7 +64,6 @@ async function main(): Promise<void> {
|
|
|
65
64
|
return false;
|
|
66
65
|
}
|
|
67
66
|
});
|
|
68
|
-
await sleep(4000);
|
|
69
67
|
await writeFile(readyFile, "");
|
|
70
68
|
|
|
71
69
|
await new Promise<void>((resolve) => singbox.once("exit", () => resolve()));
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { generateSingboxConfig } from "./singbox-config.js";
|
|
8
8
|
|
|
9
9
|
export interface NetworkStackOptions {
|
|
10
|
-
/**
|
|
10
|
+
/** 允许直连的域名 / IP:port 列表(session 启动时固定,reload 才会重新加载)。 */
|
|
11
11
|
readonly allowlist: readonly string[];
|
|
12
12
|
/** 真实 DNS 服务器列表,按顺序 fallback。 */
|
|
13
13
|
readonly dnsServers: readonly string[];
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -232,7 +232,7 @@ function notifyMode(
|
|
|
232
232
|
"allow-all": "allow-all: sandbox off, network on",
|
|
233
233
|
"workspace-write": "workspace-write: sandbox on, network off",
|
|
234
234
|
"allow-net": "allow-net: sandbox on, network on, workspace writable",
|
|
235
|
-
"net-allowlist": "net-allowlist: sandbox on, network filtered by
|
|
235
|
+
"net-allowlist": "net-allowlist: sandbox on, network filtered by allowlist",
|
|
236
236
|
readonly: "readonly: sandbox on, network off, read-only fs",
|
|
237
237
|
};
|
|
238
238
|
ctx.ui.notify(labels[mode], "info");
|
|
@@ -672,7 +672,7 @@ export class BwrapRuntime {
|
|
|
672
672
|
"bwrap-net-allowlist": {
|
|
673
673
|
name: "bwrap-net-allowlist",
|
|
674
674
|
usage: "",
|
|
675
|
-
description: "Sandbox on, network filtered by
|
|
675
|
+
description: "Sandbox on, network filtered by allowlist, workspace writable",
|
|
676
676
|
flags: Type.Object({}),
|
|
677
677
|
},
|
|
678
678
|
"bwrap-readonly": {
|
|
@@ -708,9 +708,9 @@ export class BwrapRuntime {
|
|
|
708
708
|
const writable = runtime.writablePaths.map((path) =>
|
|
709
709
|
resolveBwrapPath(path, commandCtx.cwd),
|
|
710
710
|
);
|
|
711
|
-
const
|
|
711
|
+
const deny = runtime.denyPaths.map((path) => resolveBwrapPath(path, commandCtx.cwd));
|
|
712
712
|
commandCtx.ui.notify(
|
|
713
|
-
`bwrap ${runtime.mode} ${runtime.network ? "net" : "no-net"} write:[${writable.join(", ")}]
|
|
713
|
+
`bwrap ${runtime.mode} ${runtime.network ? "net" : "no-net"} write:[${writable.join(", ")}] deny:[${deny.join(", ") || "-"}]`,
|
|
714
714
|
"info",
|
|
715
715
|
);
|
|
716
716
|
}),
|
|
@@ -3,12 +3,99 @@ const TUN_ADDRESS = "198.18.0.1/16";
|
|
|
3
3
|
const EGRESS_INTERFACE = "tap0";
|
|
4
4
|
|
|
5
5
|
export interface SingboxConfigOptions {
|
|
6
|
-
/**
|
|
6
|
+
/** 允许直连的条目列表(域名 / IP / CIDR,可带 :port;空列表 = 默认拒绝一切出网)。 */
|
|
7
7
|
readonly allowlist: readonly string[];
|
|
8
8
|
/** 真实 DNS 服务器地址列表(IPv4,UDP 53),按顺序 fallback,至少一个。 */
|
|
9
9
|
readonly dnsServers: readonly string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface AllowlistEntry {
|
|
13
|
+
readonly host: string;
|
|
14
|
+
readonly port?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AllowlistRule {
|
|
18
|
+
readonly domain?: readonly string[];
|
|
19
|
+
readonly ip_cidr?: readonly string[];
|
|
20
|
+
readonly port?: readonly number[];
|
|
21
|
+
readonly outbound: "direct";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}(?:\/\d{1,2})?$/;
|
|
25
|
+
|
|
26
|
+
function isIp(host: string): boolean {
|
|
27
|
+
return IPV4_PATTERN.test(host) || host.includes(":");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 单 IP 补掩码为 CIDR(IPv4 /32、IPv6 /128),带掩码原样返回。 */
|
|
31
|
+
function toCidr(host: string): string {
|
|
32
|
+
if (host.includes("/")) return host;
|
|
33
|
+
return host.includes(":") ? `${host}/128` : `${host}/32`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parsePort(value: string): number {
|
|
37
|
+
const port = Number(value);
|
|
38
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
39
|
+
throw new Error(`Invalid port "${value}"`);
|
|
40
|
+
}
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 解析 allowlist 条目:域名 / IPv4 / CIDR,可带 :port;IPv6 必须用 [] 包裹
|
|
46
|
+
* (如 `[::1]:80`),裸 IPv6 会报错提示补方括号。
|
|
47
|
+
*/
|
|
48
|
+
function parseAllowlistEntry(entry: string): AllowlistEntry {
|
|
49
|
+
if (entry.startsWith("[")) {
|
|
50
|
+
const match = /^\[(.+)\](?::(\d+))?$/.exec(entry);
|
|
51
|
+
if (match?.[1] === undefined) throw new Error(`Invalid allowlist entry "${entry}"`);
|
|
52
|
+
return { host: match[1], port: match[2] === undefined ? undefined : parsePort(match[2]) };
|
|
53
|
+
}
|
|
54
|
+
const colon = entry.lastIndexOf(":");
|
|
55
|
+
if (colon === -1) {
|
|
56
|
+
if (entry.length === 0) throw new Error(`Invalid allowlist entry ""`);
|
|
57
|
+
return { host: entry };
|
|
58
|
+
}
|
|
59
|
+
const portPart = entry.slice(colon + 1);
|
|
60
|
+
if (!/^\d+$/.test(portPart)) throw new Error(`Invalid allowlist entry "${entry}"`);
|
|
61
|
+
const host = entry.slice(0, colon);
|
|
62
|
+
if (host.length === 0) throw new Error(`Invalid allowlist entry "${entry}"`);
|
|
63
|
+
if (host.includes(":")) {
|
|
64
|
+
throw new Error(`IPv6 addresses must be wrapped in brackets, e.g. "[${host}]:${portPart}"`);
|
|
65
|
+
}
|
|
66
|
+
return { host, port: parsePort(portPart) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildAllowlistRules(allowlist: readonly string[]): AllowlistRule[] {
|
|
70
|
+
const noPortDomains: string[] = [];
|
|
71
|
+
const noPortIps: string[] = [];
|
|
72
|
+
const byPort = new Map<number, { domains: string[]; ips: string[] }>();
|
|
73
|
+
for (const entry of allowlist) {
|
|
74
|
+
const { host, port } = parseAllowlistEntry(entry);
|
|
75
|
+
if (port === undefined) {
|
|
76
|
+
(isIp(host) ? noPortIps : noPortDomains).push(host);
|
|
77
|
+
} else {
|
|
78
|
+
const bucket = byPort.get(port) ?? { domains: [], ips: [] };
|
|
79
|
+
(isIp(host) ? bucket.ips : bucket.domains).push(host);
|
|
80
|
+
byPort.set(port, bucket);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const rules: AllowlistRule[] = [];
|
|
84
|
+
if (noPortDomains.length > 0) rules.push({ domain: noPortDomains, outbound: "direct" });
|
|
85
|
+
if (noPortIps.length > 0) {
|
|
86
|
+
rules.push({ ip_cidr: noPortIps.map((host) => toCidr(host)), outbound: "direct" });
|
|
87
|
+
}
|
|
88
|
+
for (const [port, bucket] of byPort) {
|
|
89
|
+
rules.push({
|
|
90
|
+
outbound: "direct",
|
|
91
|
+
port: [port],
|
|
92
|
+
...(bucket.domains.length > 0 && { domain: bucket.domains }),
|
|
93
|
+
...(bucket.ips.length > 0 && { ip_cidr: bucket.ips.map((host) => toCidr(host)) }),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return rules;
|
|
97
|
+
}
|
|
98
|
+
|
|
12
99
|
/**
|
|
13
100
|
* 生成 sing-box 1.13 配置:TUN + fakeip + deny-by-default allowlist。
|
|
14
101
|
*
|
|
@@ -69,7 +156,7 @@ export function generateSingboxConfig(options: SingboxConfigOptions): string {
|
|
|
69
156
|
rules: [
|
|
70
157
|
{ action: "sniff" },
|
|
71
158
|
{ protocol: "dns", action: "hijack-dns" },
|
|
72
|
-
...(allowlist
|
|
159
|
+
...buildAllowlistRules(allowlist),
|
|
73
160
|
],
|
|
74
161
|
},
|
|
75
162
|
};
|