@trim21/personal-pi-extensions 0.0.426 → 0.0.428

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 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
- "tmpfsPaths": [],
83
+ // 沙箱内隐藏的路径:以 / 结尾的视为目录(挂空 tmpfs),否则按文件处理(--ro-bind-try /dev/null)
84
+ "denyPaths": [],
85
85
  // 额外 bwrap 参数
86
86
  "extraArgs": ["--die-with-parent"],
87
87
  // 全权限执行的自动审批规则:命中规则的命令不弹确认框
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.426",
3
+ "version": "0.0.428",
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";
@@ -31,7 +30,12 @@ const bwrapConfigProperties = {
31
30
  bwrapPath: Type.Optional(Type.String()),
32
31
  writablePaths: Type.Array(Type.String()),
33
32
  extraWritablePaths: Type.Array(Type.String()),
34
- tmpfsPaths: Type.Array(Type.String()),
33
+ denyPaths: Type.Array(
34
+ Type.String({
35
+ description:
36
+ "沙箱内隐藏的路径:以 / 结尾为目录(挂空 tmpfs),否则为文件(--ro-bind-try /dev/null)",
37
+ }),
38
+ ),
35
39
  extraArgs: Type.Array(Type.String()),
36
40
  networkAllowlist: Type.Array(
37
41
  Type.String({ description: "允许直连的域名 / IP / CIDR,可带 :port" }),
@@ -71,7 +75,8 @@ export interface ResolvedBwrap {
71
75
  bwrapPath?: string;
72
76
  writablePaths: string[];
73
77
  extraWritablePaths: string[];
74
- tmpfsPaths: string[];
78
+ /** 沙箱内隐藏的路径:以 / 结尾为目录(挂空 tmpfs),否则为文件(--ro-bind-try /dev/null)。 */
79
+ denyPaths: string[];
75
80
  extraArgs: string[];
76
81
  /** 允许直连的域名 / IP / IP:port 白名单(非空 = 启用 sing-box 网络过滤)。 */
77
82
  networkAllowlist: string[];
@@ -85,7 +90,7 @@ const DEFAULT_CONFIG: BwrapConfig = {
85
90
  mode: "workspace-write",
86
91
  writablePaths: [".", "/tmp"],
87
92
  extraWritablePaths: [],
88
- tmpfsPaths: [],
93
+ denyPaths: [],
89
94
  extraArgs: [],
90
95
  networkAllowlist: [],
91
96
  };
@@ -96,7 +101,7 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
96
101
  bwrapPath: config.bwrapPath,
97
102
  writablePaths: config.writablePaths ?? [".", "/tmp"],
98
103
  extraWritablePaths: config.extraWritablePaths,
99
- tmpfsPaths: config.tmpfsPaths ?? [],
104
+ denyPaths: config.denyPaths ?? [],
100
105
  extraArgs: config.extraArgs ?? [],
101
106
  networkAllowlist: config.networkAllowlist ?? [],
102
107
  singBoxPath: config.singBoxPath,
@@ -128,7 +133,7 @@ export function resolveHeadlessBwrap(config: BwrapConfig): ResolvedBwrap {
128
133
  mode: "readonly",
129
134
  writablePaths: [],
130
135
  extraWritablePaths: [],
131
- tmpfsPaths: [],
136
+ denyPaths: [],
132
137
  extraArgs: [],
133
138
  });
134
139
  }
@@ -139,7 +144,7 @@ function deepMerge(base: BwrapConfig, overrides: Partial<BwrapConfig>): BwrapCon
139
144
  bwrapPath: overrides.bwrapPath ?? base.bwrapPath,
140
145
  writablePaths: overrides.writablePaths ?? base.writablePaths,
141
146
  extraWritablePaths: [...base.extraWritablePaths, ...(overrides.extraWritablePaths ?? [])],
142
- tmpfsPaths: overrides.tmpfsPaths ?? base.tmpfsPaths,
147
+ denyPaths: overrides.denyPaths ?? base.denyPaths,
143
148
  extraArgs: overrides.extraArgs ?? base.extraArgs,
144
149
  networkAllowlist: overrides.networkAllowlist ?? base.networkAllowlist,
145
150
  singBoxPath: overrides.singBoxPath ?? base.singBoxPath,
@@ -323,8 +328,14 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
323
328
  const absolutePath = resolveBwrapPath(path, cwd);
324
329
  args.push("--bind-try", absolutePath, absolutePath);
325
330
  }
326
- for (const path of resolved.tmpfsPaths) {
327
- args.push("--tmpfs", resolveBwrapPath(path, cwd));
331
+ // denyPaths:以 / 结尾的条目视为目录(挂空 tmpfs),否则视为文件(--ro-bind-try /dev/null 覆盖)
332
+ for (const path of resolved.denyPaths) {
333
+ const target = resolveBwrapPath(path, cwd);
334
+ if (path.endsWith("/")) {
335
+ args.push("--tmpfs", target);
336
+ } else {
337
+ args.push("--ro-bind-try", "/dev/null", target);
338
+ }
328
339
  }
329
340
  if (!resolved.network) args.push("--unshare-net");
330
341
  // --ro-bind-try:目录不存在(或已被删除)时自动忽略
@@ -350,22 +361,6 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
350
361
  return args;
351
362
  }
352
363
 
353
- const SECCOMP_BPF_FILE = (() => {
354
- const directory = fileURLToPath(new URL(".", import.meta.url));
355
- if (process.arch === "x64") return join(directory, "seccomp-x86_64.bpf");
356
- if (process.arch === "arm64") return join(directory, "seccomp-aarch64.bpf");
357
- return "";
358
- })();
359
-
360
- function getSeccompFd(): number | undefined {
361
- if (!SECCOMP_BPF_FILE) return undefined;
362
- try {
363
- return openSync(SECCOMP_BPF_FILE, "r");
364
- } catch {
365
- return undefined;
366
- }
367
- }
368
-
369
364
  function killChild(child: ChildProcess): void {
370
365
  if (!child.pid) return;
371
366
  try {
@@ -448,19 +443,13 @@ export function createBwrapBashOperations(
448
443
  });
449
444
  }
450
445
 
451
- const seccompFd = resolved.network ? undefined : getSeccompFd();
452
446
  const child = spawn(
453
447
  findBwrap(resolved.bwrapPath),
454
- seccompFd === undefined
455
- ? [...baseArgs, "--", shell, "-lc", command]
456
- : [...baseArgs, "--seccomp", "3", "--", shell, "-lc", command],
448
+ [...baseArgs, "--", shell, "-lc", command],
457
449
  {
458
450
  cwd,
459
451
  detached: true,
460
- stdio:
461
- seccompFd === undefined
462
- ? ["ignore", "pipe", "pipe"]
463
- : ["ignore", "pipe", "pipe", seccompFd],
452
+ stdio: ["ignore", "pipe", "pipe"],
464
453
  env,
465
454
  },
466
455
  );
@@ -481,7 +470,6 @@ export function createBwrapBashOperations(
481
470
  child.once("close", (exitCode) => {
482
471
  if (timeoutHandle) clearTimeout(timeoutHandle);
483
472
  signal?.removeEventListener("abort", onAbort);
484
- if (seccompFd !== undefined) closeSync(seccompFd);
485
473
  if (signal?.aborted) reject(new Error("aborted"));
486
474
  else if (timedOut) reject(new Error(`timeout:${timeout}`));
487
475
  else resolve({ exitCode });
@@ -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 tmpfs = runtime.tmpfsPaths.map((path) => resolveBwrapPath(path, commandCtx.cwd));
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(", ")}] tmpfs:[${tmpfs.join(", ") || "-"}]`,
713
+ `bwrap ${runtime.mode} ${runtime.network ? "net" : "no-net"} write:[${writable.join(", ")}] deny:[${deny.join(", ") || "-"}]`,
714
714
  "info",
715
715
  );
716
716
  }),
@@ -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