@trim21/personal-pi-extensions 0.1.445 → 0.1.448
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/package.json +3 -3
- package/src/bwrap/holder.js +5 -9
- package/src/bwrap/holder.ts +9 -11
- package/src/bwrap/network-stack.ts +22 -26
- package/src/lib/proc.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.448",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"node": ">=24"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@cortexkit/aft-bridge": "0.
|
|
97
|
+
"@cortexkit/aft-bridge": "0.54.0",
|
|
98
98
|
"@mozilla/readability": "^0.6.0",
|
|
99
99
|
"@vscode/tree-sitter-wasm": "^0.3.1",
|
|
100
100
|
"jsonc-parser": "^3.3.1",
|
|
@@ -107,6 +107,6 @@
|
|
|
107
107
|
"web-tree-sitter": "^0.26.12"
|
|
108
108
|
},
|
|
109
109
|
"optionalDependencies": {
|
|
110
|
-
"@cortexkit/aft-linux-x64": "0.
|
|
110
|
+
"@cortexkit/aft-linux-x64": "0.54.0"
|
|
111
111
|
}
|
|
112
112
|
}
|
package/src/bwrap/holder.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { rmSync } from "node:fs";
|
|
3
2
|
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { dirname } from "node:path";
|
|
5
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
4
|
const args = process.argv.slice(2);
|
|
7
|
-
const
|
|
5
|
+
const configBase64 = args[0];
|
|
8
6
|
const mihomoPath = args[1];
|
|
9
7
|
const slirp4netnsPath = args[2];
|
|
10
8
|
const tunMtu = Number(args[3]);
|
|
11
|
-
if (!
|
|
9
|
+
if (!configBase64 || !mihomoPath || !slirp4netnsPath || !tunMtu) {
|
|
12
10
|
process.exit(2);
|
|
13
11
|
}
|
|
12
|
+
process.chdir("/");
|
|
14
13
|
function tap0Exists() {
|
|
15
14
|
return new Promise((resolve) => {
|
|
16
15
|
const child = spawn("ip", ["link", "show", "tap0"], { stdio: "ignore" });
|
|
@@ -27,10 +26,7 @@ async function waitFor(attempts, delayMs, predicate) {
|
|
|
27
26
|
}
|
|
28
27
|
async function main() {
|
|
29
28
|
process.stdin.resume();
|
|
30
|
-
process.stdin.on("end", () =>
|
|
31
|
-
rmSync(dirname(configPath), { recursive: true, force: true });
|
|
32
|
-
process.exit(0);
|
|
33
|
-
});
|
|
29
|
+
process.stdin.on("end", () => process.exit(0));
|
|
34
30
|
const status = await readFile("/proc/self/status", "utf8");
|
|
35
31
|
const hostPid = /^NSpid:\s+(.+)$/m.exec(status)?.[1].split(/\s+/, 1)[0];
|
|
36
32
|
if (!hostPid) {
|
|
@@ -40,7 +36,7 @@ async function main() {
|
|
|
40
36
|
stdio: ["ignore", "inherit", "inherit"]
|
|
41
37
|
});
|
|
42
38
|
await waitFor(100, 100, tap0Exists);
|
|
43
|
-
const mihomo = spawn(mihomoPath, ["-
|
|
39
|
+
const mihomo = spawn(mihomoPath, ["-config", configBase64], {
|
|
44
40
|
stdio: ["ignore", "inherit", "inherit"]
|
|
45
41
|
});
|
|
46
42
|
const stop = () => {
|
package/src/bwrap/holder.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
/* eslint-disable no-console, unicorn/no-process-exit -- 独立 CLI 脚本,由 unshare 直接执行 */
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { rmSync } from "node:fs";
|
|
4
3
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { dirname } from "node:path";
|
|
6
4
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
5
|
|
|
8
|
-
// 用法:unshare -Urnp --fork --kill-child node holder.ts <config> <mihomo> <slirp4netns> <mtu>
|
|
6
|
+
// 用法:unshare -Urnp --fork --kill-child node holder.ts <config-base64> <mihomo> <slirp4netns> <mtu>
|
|
9
7
|
// 本进程是 pid namespace 的 init:无论以何种方式退出(含 SIGKILL),内核都会清理该 pid ns 内的
|
|
10
8
|
// slirp4netns / mihomo,userns/netns 引用随之归零。
|
|
11
9
|
const args = process.argv.slice(2);
|
|
12
|
-
const
|
|
10
|
+
const configBase64 = args[0];
|
|
13
11
|
const mihomoPath = args[1];
|
|
14
12
|
const slirp4netnsPath = args[2];
|
|
15
13
|
const tunMtu = Number(args[3]);
|
|
16
|
-
if (!
|
|
14
|
+
if (!configBase64 || !mihomoPath || !slirp4netnsPath || !tunMtu) {
|
|
17
15
|
process.exit(2);
|
|
18
16
|
}
|
|
19
17
|
|
|
18
|
+
// mihomo 不写工作目录(配置经 -config 直传),切到根目录防止在宿主 cwd 意外落盘
|
|
19
|
+
process.chdir("/");
|
|
20
|
+
|
|
20
21
|
function tap0Exists(): Promise<boolean> {
|
|
21
22
|
return new Promise((resolve) => {
|
|
22
23
|
const child = spawn("ip", ["link", "show", "tap0"], { stdio: "ignore" });
|
|
@@ -39,12 +40,9 @@ async function waitFor(
|
|
|
39
40
|
|
|
40
41
|
async function main(): Promise<void> {
|
|
41
42
|
// 宿主(pi)进程死亡检测:宿主以任何方式退出(含 SIGKILL)都会关闭它持有的 stdin 写端,
|
|
42
|
-
// 这里读到 EOF 即自杀(init 退出即触发内核清理整个 pid ns
|
|
43
|
+
// 这里读到 EOF 即自杀(init 退出即触发内核清理整个 pid ns)。
|
|
43
44
|
process.stdin.resume();
|
|
44
|
-
process.stdin.on("end", () =>
|
|
45
|
-
rmSync(dirname(configPath), { recursive: true, force: true });
|
|
46
|
-
process.exit(0);
|
|
47
|
-
});
|
|
45
|
+
process.stdin.on("end", () => process.exit(0));
|
|
48
46
|
|
|
49
47
|
// 宿主视角 pid(NSpid 第一项):/proc 挂载是宿主的(-Urnp 不含 -m,宿主视角下
|
|
50
48
|
// /proc/1 是 systemd 而非本 pid ns 的 init),slirp4netns 的 setns 必须走宿主 /proc
|
|
@@ -63,7 +61,7 @@ async function main(): Promise<void> {
|
|
|
63
61
|
// mihomo 日志透传到 holder 的 stdout/stderr,宿主侧监听 "Tun adapter listening" 判定就绪
|
|
64
62
|
await waitFor(100, 100, tap0Exists);
|
|
65
63
|
|
|
66
|
-
const mihomo = spawn(mihomoPath, ["-
|
|
64
|
+
const mihomo = spawn(mihomoPath, ["-config", configBase64], {
|
|
67
65
|
stdio: ["ignore", "inherit", "inherit"],
|
|
68
66
|
});
|
|
69
67
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { readFile, readlink } from "node:fs/promises";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import { forEachLine } from "../lib/proc.js";
|
|
8
6
|
import { generateMihomoConfig, TUN_MTU } from "./mihomo-config.js";
|
|
9
7
|
|
|
10
8
|
/** 命令超时错误:name=TimeoutError(对齐标准错误分类),message 保留 timeout:N 格式。 */
|
|
@@ -120,16 +118,23 @@ function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<
|
|
|
120
118
|
settled = true;
|
|
121
119
|
reject(new Error("Timed out waiting for mihomo to start"));
|
|
122
120
|
}, timeoutMs);
|
|
123
|
-
const
|
|
121
|
+
const onReady = (): void => {
|
|
124
122
|
if (settled) return;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
resolve();
|
|
129
|
-
}
|
|
123
|
+
settled = true;
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
resolve();
|
|
130
126
|
};
|
|
131
|
-
|
|
132
|
-
holder.
|
|
127
|
+
// 按行扫描:mihomo 日志行可能跨 data chunk,由 forEachLine 负责拼接
|
|
128
|
+
for (const stream of [holder.stdout, holder.stderr]) {
|
|
129
|
+
if (stream) {
|
|
130
|
+
// 不提前停止:holder 生命周期内持续消费输出,避免流无消费者触发背压
|
|
131
|
+
void forEachLine(stream, (line) => {
|
|
132
|
+
if (line.includes("Tun adapter listening")) onReady();
|
|
133
|
+
}).catch(() => {
|
|
134
|
+
// 就绪判定由超时与 holder exit 兜底,流的 error 忽略
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
133
138
|
holder.once("exit", (code) => {
|
|
134
139
|
if (settled) return;
|
|
135
140
|
settled = true;
|
|
@@ -169,20 +174,16 @@ export interface NetworkStack {
|
|
|
169
174
|
stop(): Promise<void>;
|
|
170
175
|
/** holder pid:可 `nsenter -U -n --preserve-credentials -t <pid>` 手动进入该 netns 排查。 */
|
|
171
176
|
readonly holderPid: number;
|
|
172
|
-
/** 生成的 mihomo 配置路径(stop() 后随临时目录一起删除)。 */
|
|
173
|
-
readonly configPath: string;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
interface NetworkStackState {
|
|
177
180
|
holderPid: number;
|
|
178
|
-
directory: string;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
/** 兜底:调用方忘记 stop() 时,对象被 GC 回收后 kill
|
|
183
|
+
/** 兜底:调用方忘记 stop() 时,对象被 GC 回收后 kill 残留进程。 */
|
|
182
184
|
const stackFinalizer = new FinalizationRegistry<NetworkStackState>((state) => {
|
|
183
185
|
// SIGTERM 经 unshare --kill-child 转发给 init,内核清理 pid ns 内全部进程
|
|
184
186
|
killProcess(state.holderPid);
|
|
185
|
-
void rm(state.directory, { recursive: true, force: true }).catch(() => false);
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
/**
|
|
@@ -198,11 +199,9 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
198
199
|
slirp4netnsPath,
|
|
199
200
|
onHolderOutput: holderOutput,
|
|
200
201
|
} = options;
|
|
201
|
-
//
|
|
202
|
-
const directory = await mkdtemp(join(getAgentDir(), "tmp", "pi-netns-"));
|
|
203
|
-
const configPath = join(directory, "mihomo.json");
|
|
202
|
+
// mihomo 支持 -config 直接接收 base64 配置,无需写配置文件中转
|
|
204
203
|
const config = generateMihomoConfig({ allowlist, dnsServers });
|
|
205
|
-
|
|
204
|
+
const configBase64 = Buffer.from(JSON.stringify(config)).toString("base64");
|
|
206
205
|
|
|
207
206
|
let holder: ChildProcess | undefined;
|
|
208
207
|
try {
|
|
@@ -218,7 +217,7 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
218
217
|
"--",
|
|
219
218
|
"node",
|
|
220
219
|
HOLDER_PATH,
|
|
221
|
-
|
|
220
|
+
configBase64,
|
|
222
221
|
mihomoPath,
|
|
223
222
|
slirp4netnsPath,
|
|
224
223
|
String(TUN_MTU),
|
|
@@ -242,7 +241,7 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
242
241
|
|
|
243
242
|
await waitForMihomoStarted(holder);
|
|
244
243
|
|
|
245
|
-
const state: NetworkStackState = { holderPid
|
|
244
|
+
const state: NetworkStackState = { holderPid };
|
|
246
245
|
const stack: NetworkStack = {
|
|
247
246
|
exec: async (execOptions: NetworkStackExecOptions) => {
|
|
248
247
|
const child = spawn(
|
|
@@ -320,10 +319,8 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
320
319
|
for (const pid of children) {
|
|
321
320
|
killProcess(pid, "SIGKILL");
|
|
322
321
|
}
|
|
323
|
-
await rm(state.directory, { recursive: true, force: true }).catch(() => false);
|
|
324
322
|
},
|
|
325
323
|
holderPid,
|
|
326
|
-
configPath,
|
|
327
324
|
};
|
|
328
325
|
stackFinalizer.register(stack, state);
|
|
329
326
|
return stack;
|
|
@@ -338,7 +335,6 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
338
335
|
killProcess(pid, "SIGKILL");
|
|
339
336
|
}
|
|
340
337
|
}
|
|
341
|
-
await rm(directory, { recursive: true, force: true }).catch(() => false);
|
|
342
338
|
throw error;
|
|
343
339
|
}
|
|
344
340
|
}
|
package/src/lib/proc.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按行消费输出流:data 事件按 buffer 边界切分,行可能跨多个 chunk,
|
|
3
|
+
* 这里拼接缓冲后按 \n 逐行回调(回调不含换行符)。
|
|
4
|
+
* 回调返回 false 可提前停止消费;流的 error 会以 rejection 形式抛出。
|
|
5
|
+
*/
|
|
6
|
+
export async function forEachLine(
|
|
7
|
+
stream: NodeJS.ReadableStream,
|
|
8
|
+
callback: (line: string) => boolean | void,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
let buffer = "";
|
|
11
|
+
for await (const chunk of stream) {
|
|
12
|
+
buffer += chunk.toString();
|
|
13
|
+
let newline: number;
|
|
14
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
15
|
+
const line = buffer.slice(0, newline);
|
|
16
|
+
buffer = buffer.slice(newline + 1);
|
|
17
|
+
if (callback(line) === false) return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|