@trim21/personal-pi-extensions 0.0.427 → 0.0.429

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.427",
3
+ "version": "0.0.429",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/bwrap/core.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
- import { closeSync, constants, type Dirent, existsSync, openSync, readFileSync } from "node:fs";
2
+ import { constants, type Dirent, existsSync, readFileSync } from "node:fs";
3
3
  import { access as fsAccess, readdir, stat } from "node:fs/promises";
4
4
  import { delimiter, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
5
 
7
6
  import { StringEnum } from "@earendil-works/pi-ai";
8
7
  import { type BashOperations, getAgentDir, getShellConfig } from "@earendil-works/pi-coding-agent";
@@ -362,22 +361,6 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
362
361
  return args;
363
362
  }
364
363
 
365
- const SECCOMP_BPF_FILE = (() => {
366
- const directory = fileURLToPath(new URL(".", import.meta.url));
367
- if (process.arch === "x64") return join(directory, "seccomp-x86_64.bpf");
368
- if (process.arch === "arm64") return join(directory, "seccomp-aarch64.bpf");
369
- return "";
370
- })();
371
-
372
- function getSeccompFd(): number | undefined {
373
- if (!SECCOMP_BPF_FILE) return undefined;
374
- try {
375
- return openSync(SECCOMP_BPF_FILE, "r");
376
- } catch {
377
- return undefined;
378
- }
379
- }
380
-
381
364
  function killChild(child: ChildProcess): void {
382
365
  if (!child.pid) return;
383
366
  try {
@@ -460,19 +443,13 @@ export function createBwrapBashOperations(
460
443
  });
461
444
  }
462
445
 
463
- const seccompFd = resolved.network ? undefined : getSeccompFd();
464
446
  const child = spawn(
465
447
  findBwrap(resolved.bwrapPath),
466
- seccompFd === undefined
467
- ? [...baseArgs, "--", shell, "-lc", command]
468
- : [...baseArgs, "--seccomp", "3", "--", shell, "-lc", command],
448
+ [...baseArgs, "--", shell, "-lc", command],
469
449
  {
470
450
  cwd,
471
451
  detached: true,
472
- stdio:
473
- seccompFd === undefined
474
- ? ["ignore", "pipe", "pipe"]
475
- : ["ignore", "pipe", "pipe", seccompFd],
452
+ stdio: ["ignore", "pipe", "pipe"],
476
453
  env,
477
454
  },
478
455
  );
@@ -493,7 +470,6 @@ export function createBwrapBashOperations(
493
470
  child.once("close", (exitCode) => {
494
471
  if (timeoutHandle) clearTimeout(timeoutHandle);
495
472
  signal?.removeEventListener("abort", onAbort);
496
- if (seccompFd !== undefined) closeSync(seccompFd);
497
473
  if (signal?.aborted) reject(new Error("aborted"));
498
474
  else if (timedOut) reject(new Error(`timeout:${timeout}`));
499
475
  else resolve({ exitCode });
@@ -1,11 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
- import { open, readFile, writeFile } from "node:fs/promises";
3
2
  import { setTimeout as sleep } from "node:timers/promises";
4
3
  const args = process.argv.slice(2);
5
4
  const configPath = args[0];
6
5
  const singBoxPath = args[1];
7
- const readyFile = args[2];
8
- if (!configPath || !singBoxPath || !readyFile) {
6
+ if (!configPath || !singBoxPath) {
9
7
  process.exit(2);
10
8
  }
11
9
  function tap0Exists() {
@@ -24,11 +22,9 @@ async function waitFor(attempts, delayMs, predicate) {
24
22
  }
25
23
  async function main() {
26
24
  await waitFor(100, 100, tap0Exists);
27
- const logPath = `${configPath}.log`;
28
- const logFd = await open(logPath, "w");
29
25
  const singbox = spawn(singBoxPath, ["run", "-c", configPath], {
30
26
  env: { ...process.env, ENABLE_DEPRECATED_MISSING_DOMAIN_RESOLVER: "true" },
31
- stdio: ["ignore", logFd.fd, logFd.fd]
27
+ stdio: ["ignore", "inherit", "inherit"]
32
28
  });
33
29
  const stop = () => {
34
30
  singbox.kill("SIGTERM");
@@ -41,15 +37,6 @@ async function main() {
41
37
  stop();
42
38
  process.exit(0);
43
39
  });
44
- await waitFor(200, 100, async () => {
45
- try {
46
- const log = await readFile(logPath, "utf8");
47
- return log.includes("sing-box started");
48
- } catch {
49
- return false;
50
- }
51
- });
52
- await writeFile(readyFile, "");
53
40
  await new Promise((resolve) => singbox.once("exit", () => resolve()));
54
41
  }
55
42
  try {
@@ -1,14 +1,12 @@
1
1
  /* eslint-disable no-console, unicorn/no-process-exit -- 独立 CLI 脚本,由 unshare 直接执行 */
2
2
  import { spawn } from "node:child_process";
3
- import { open, readFile, writeFile } from "node:fs/promises";
4
3
  import { setTimeout as sleep } from "node:timers/promises";
5
4
 
6
- // 用法:unshare -Urn node holder.ts <config> <singbox> <ready-file>
5
+ // 用法:unshare -Urn node holder.ts <config> <singbox>
7
6
  const args = process.argv.slice(2);
8
7
  const configPath = args[0];
9
8
  const singBoxPath = args[1];
10
- const readyFile = args[2];
11
- if (!configPath || !singBoxPath || !readyFile) {
9
+ if (!configPath || !singBoxPath) {
12
10
  process.exit(2);
13
11
  }
14
12
 
@@ -36,11 +34,10 @@ async function main(): Promise<void> {
36
34
  // slirp4netns 由宿主侧 spawn,这里等它建好 tap0
37
35
  await waitFor(100, 100, tap0Exists);
38
36
 
39
- const logPath = `${configPath}.log`;
40
- const logFd = await open(logPath, "w");
37
+ // sing-box 日志透传到 holder 的 stdout/stderr,宿主侧监听 "sing-box started" 判定就绪
41
38
  const singbox = spawn(singBoxPath, ["run", "-c", configPath], {
42
39
  env: { ...process.env, ENABLE_DEPRECATED_MISSING_DOMAIN_RESOLVER: "true" },
43
- stdio: ["ignore", logFd.fd, logFd.fd],
40
+ stdio: ["ignore", "inherit", "inherit"],
44
41
  });
45
42
 
46
43
  const stop = (): void => {
@@ -55,17 +52,6 @@ async function main(): Promise<void> {
55
52
  process.exit(0);
56
53
  });
57
54
 
58
- // 以 "sing-box started" 日志作为就绪标志(TUN 与 gvisor 数据面此时已可用)
59
- await waitFor(200, 100, async () => {
60
- try {
61
- const log = await readFile(logPath, "utf8");
62
- return log.includes("sing-box started");
63
- } catch {
64
- return false;
65
- }
66
- });
67
- await writeFile(readyFile, "");
68
-
69
55
  await new Promise<void>((resolve) => singbox.once("exit", () => resolve()));
70
56
  }
71
57
 
@@ -1,5 +1,5 @@
1
- import { spawn } from "node:child_process";
2
- import { access, mkdtemp, readFile, readlink, rm, writeFile } from "node:fs/promises";
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { mkdtemp, readFile, readlink, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -72,17 +72,32 @@ async function waitForNewUserns(pid: number, timeoutMs = 5000): Promise<void> {
72
72
  throw new Error("Timed out waiting for sandbox network namespace");
73
73
  }
74
74
 
75
- async function waitForFile(path: string, timeoutMs = 15000): Promise<void> {
76
- const deadline = Date.now() + timeoutMs;
77
- while (Date.now() < deadline) {
78
- try {
79
- await access(path);
80
- return;
81
- } catch {
82
- await new Promise((resolve) => setTimeout(resolve, 100));
83
- }
84
- }
85
- throw new Error("Timed out waiting for sandbox network stack to become ready");
75
+ /** 监听 holder 的 stdout/stderr(sing-box 日志透传),以 "sing-box started" 作为就绪标志。 */
76
+ function waitForSingboxStarted(holder: ChildProcess, timeoutMs = 20000): Promise<void> {
77
+ return new Promise((resolve, reject) => {
78
+ let settled = false;
79
+ const timer = setTimeout(() => {
80
+ if (settled) return;
81
+ settled = true;
82
+ reject(new Error("Timed out waiting for sing-box to start"));
83
+ }, timeoutMs);
84
+ const onData = (data: Buffer): void => {
85
+ if (settled) return;
86
+ if (data.toString().includes("sing-box started")) {
87
+ settled = true;
88
+ clearTimeout(timer);
89
+ resolve();
90
+ }
91
+ };
92
+ holder.stdout?.on("data", onData);
93
+ holder.stderr?.on("data", onData);
94
+ holder.once("exit", (code) => {
95
+ if (settled) return;
96
+ settled = true;
97
+ clearTimeout(timer);
98
+ reject(new Error(`Sandbox holder exited before sing-box started (code ${code})`));
99
+ });
100
+ });
86
101
  }
87
102
 
88
103
  function killChild(pid: number | undefined): void {
@@ -125,20 +140,16 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
125
140
  const { allowlist, dnsServers, singBoxPath, slirp4netnsPath } = options;
126
141
  const directory = await mkdtemp(join(tmpdir(), "pi-netns-"));
127
142
  const configPath = join(directory, "singbox.json");
128
- const readyFile = join(directory, "ready");
129
143
  await writeFile(configPath, generateSingboxConfig({ allowlist, dnsServers }));
130
144
 
131
- const holder = spawn(
132
- "unshare",
133
- ["-Urn", "--", "node", HOLDER_PATH, configPath, singBoxPath, readyFile],
134
- {
135
- env: {
136
- HOME: process.env.HOME ?? "",
137
- PATH: `${process.env.PATH ?? ""}:${SBIN_PATH_SUFFIX}`,
138
- },
139
- stdio: "ignore",
145
+ const holder = spawn("unshare", ["-Urn", "--", "node", HOLDER_PATH, configPath, singBoxPath], {
146
+ env: {
147
+ HOME: process.env.HOME ?? "",
148
+ PATH: `${process.env.PATH ?? ""}:${SBIN_PATH_SUFFIX}`,
140
149
  },
141
- );
150
+ // sing-box 日志经 holder 透传到这里的 stdout/stderr,用于判定就绪
151
+ stdio: ["ignore", "pipe", "pipe"],
152
+ });
142
153
  if (holder.pid === undefined) {
143
154
  throw new Error("Failed to start network namespace holder");
144
155
  }
@@ -157,7 +168,7 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
157
168
  { stdio: "ignore" },
158
169
  );
159
170
  const slirpPid = slirp.pid;
160
- await waitForFile(readyFile);
171
+ await waitForSingboxStarted(holder);
161
172
 
162
173
  const state: NetworkStackState = { holderPid, slirpPid, directory };
163
174
  const stack: NetworkStack = {
@@ -17,6 +17,7 @@ import { Type } from "typebox";
17
17
  import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
18
18
  import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
19
19
  import { formatSubtitlePath } from "../lib/path.js";
20
+ import type { ToolPendant } from "../lib/pendant.ts";
20
21
  import { guardWriteAccess } from "../lib/write-guard.js";
21
22
  import {
22
23
  type ClaudeCodeState,
@@ -302,7 +303,10 @@ export function registerFileTools(
302
303
  content,
303
304
  details: {
304
305
  reads: { [key]: snapshot },
305
- pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath) },
306
+ pendant: {
307
+ subtitle: formatSubtitlePath(ctx.cwd, filePath),
308
+ title: "Read",
309
+ } satisfies ToolPendant,
306
310
  },
307
311
  };
308
312
  }
@@ -354,7 +358,10 @@ export function registerFileTools(
354
358
  content: [{ type: "text", text: formatted.text }],
355
359
  details: {
356
360
  reads: { [key]: snapshot },
357
- pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath) },
361
+ pendant: {
362
+ subtitle: formatSubtitlePath(ctx.cwd, filePath),
363
+ title: "Read",
364
+ } satisfies ToolPendant,
358
365
  },
359
366
  };
360
367
  },
@@ -566,7 +573,10 @@ export function registerFileTools(
566
573
  content: [{ type: "text" as const, text }],
567
574
  details: {
568
575
  ...details,
569
- pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath, errorCount, warningCount) },
576
+ pendant: {
577
+ subtitle: formatSubtitlePath(ctx.cwd, filePath, errorCount, warningCount),
578
+ title: "Edit",
579
+ } satisfies ToolPendant,
570
580
  },
571
581
  };
572
582
  },
Binary file
Binary file