@trim21/personal-pi-extensions 0.1.523 → 0.1.525

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.1.523",
3
+ "version": "0.1.525",
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,6 +1,6 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
2
  import { constants, type Dirent, existsSync, readFileSync } from "node:fs";
3
- import { access as fsAccess, readdir, stat } from "node:fs/promises";
3
+ import { access as fsAccess, readdir, realpath, stat } from "node:fs/promises";
4
4
  import { delimiter, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
@@ -337,20 +337,34 @@ export async function findGitDirs(root: string): Promise<string[]> {
337
337
  return found;
338
338
  }
339
339
 
340
+ /**
341
+ * bwrap 创建挂载点时不跟随目标路径中的 symlink 组件(防 symlink 逃逸),
342
+ * 含绝对 symlink 的配置路径会以 "Can't mkdir parents ... No such file or directory" 失败。
343
+ * 已存在的路径先解析成真实路径(沙箱内 ro-bind 的 / 下同样可见,symlink 语义不变);
344
+ * 不存在的路径保持原样,交给 --*-try 的跳过语义处理。
345
+ */
346
+ async function realpathOrSelf(path: string): Promise<string> {
347
+ try {
348
+ return await realpath(path);
349
+ } catch {
350
+ return path;
351
+ }
352
+ }
353
+
340
354
  export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Promise<string[]> {
341
355
  const args = ["--new-session", "--die-with-parent", "--unshare-user", "--unshare-pid"];
342
356
  // --*-bind-try:配置的路径不存在时忽略该项而不是让整条命令失败
343
357
  for (const path of resolved.writablePaths) {
344
- const absolutePath = resolveBwrapPath(path, cwd);
358
+ const absolutePath = await realpathOrSelf(resolveBwrapPath(path, cwd));
345
359
  args.push("--bind-try", absolutePath, absolutePath);
346
360
  }
347
361
  for (const path of resolved.extraWritablePaths) {
348
- const absolutePath = resolveBwrapPath(path, cwd);
362
+ const absolutePath = await realpathOrSelf(resolveBwrapPath(path, cwd));
349
363
  args.push("--bind-try", absolutePath, absolutePath);
350
364
  }
351
365
  // denyPaths:以 / 结尾的条目视为目录(挂空 tmpfs),否则视为文件(--ro-bind-try /dev/null 覆盖)
352
366
  for (const path of resolved.denyPaths) {
353
- const target = resolveBwrapPath(path, cwd);
367
+ const target = await realpathOrSelf(resolveBwrapPath(path, cwd));
354
368
  if (path.endsWith("/")) {
355
369
  args.push("--tmpfs", target);
356
370
  } else {
@@ -360,13 +374,13 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
360
374
  if (!resolved.network) args.push("--unshare-net");
361
375
  // --ro-bind-try:目录不存在(或已被删除)时自动忽略
362
376
  for (const name of PROTECTED_DIRS) {
363
- const absolutePath = join(cwd, name);
377
+ const absolutePath = await realpathOrSelf(join(cwd, name));
364
378
  args.push("--ro-bind-try", absolutePath, absolutePath);
365
379
  }
366
380
  // 工作区下所有 .git 一律只读:可写 bind 之上的覆盖绑定,防止命令篡改仓库元数据。
367
381
  // 根目录本身是 git 仓库时只保护根 .git(递归扫描有成本,绝大多数情况根即唯一仓库);
368
382
  // 根不是 git 仓库时才递归扫描嵌套仓库(如 monorepo 子仓库)。
369
- const rootGit = join(cwd, ".git");
383
+ const rootGit = await realpathOrSelf(join(cwd, ".git"));
370
384
  let gitDirs: string[];
371
385
  try {
372
386
  await stat(rootGit);
@@ -375,7 +389,8 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
375
389
  gitDirs = await findGitDirs(cwd);
376
390
  }
377
391
  for (const gitDir of gitDirs) {
378
- args.push("--ro-bind-try", gitDir, gitDir);
392
+ const realGitDir = await realpathOrSelf(gitDir);
393
+ args.push("--ro-bind-try", realGitDir, realGitDir);
379
394
  }
380
395
  args.push(...resolved.extraArgs);
381
396
  return args;
@@ -18,7 +18,7 @@ IMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `
18
18
  - Communication: Output text directly (NOT echo/printf)
19
19
  While the Bash tool can do similar things, it's better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.
20
20
 
21
- - You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 120000ms (2 minutes).
21
+ - You may specify an optional timeout in milliseconds (up to 7200000ms / 2 hours). By default, your command will timeout after 120000ms (2 minutes).
22
22
  - Prever to use workdir argument over `cd ...`
23
23
  - When issuing multiple commands:
24
24
  - If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message. Example: if you need to run "git status" and "git diff", send a single message with two Bash tool calls in parallel.
@@ -14,7 +14,7 @@ import { BashInterruptedError, type BwrapRuntime, createBwrapRuntime } from "../
14
14
  import { resolveWorkdir } from "../lib/path.js";
15
15
 
16
16
  const DEFAULT_TIMEOUT_MS = 120_000;
17
- const MAX_TIMEOUT_MS = 600_000;
17
+ const MAX_TIMEOUT_MS = 7_200_000;
18
18
 
19
19
  /** 对齐 Claude Code formatError:错误文本超过该长度时头尾各保留一半。 */
20
20
  const MAX_ERROR_CHARS = 10_000;
@@ -89,14 +89,14 @@ export function registerShellTools(
89
89
  label: "Bash",
90
90
  description: [
91
91
  "Executes a given bash command synchronously and returns its output.",
92
- "timeout is in milliseconds, defaults to 120000, and may not exceed 600000.",
92
+ "timeout is in milliseconds, defaults to 120000, and may not exceed 7200000.",
93
93
  "Every command runs in the foreground. Background command execution is not supported; shell jobs are waited for before the tool returns.",
94
94
  ].join("\n"),
95
95
  parameters: Type.Object(
96
96
  {
97
97
  command: Type.String({ description: "The command to execute" }),
98
98
  timeout: Type.Optional(
99
- Type.Number({ description: "Optional timeout in milliseconds (max 600000)" }),
99
+ Type.Number({ description: "Optional timeout in milliseconds (max 7200000)" }),
100
100
  ),
101
101
  description: Type.Optional(
102
102
  Type.String({ description: "Clear, concise description of the command" }),
@@ -8,7 +8,7 @@ import { BashInterruptedError, type BwrapRuntime, createBwrapRuntime } from "../
8
8
  import { resolveWorkdir } from "../lib/path.js";
9
9
 
10
10
  const DEFAULT_TIMEOUT_MS = 120_000;
11
- const MAX_TIMEOUT_MS = 600_000;
11
+ const MAX_TIMEOUT_MS = 7_200_000;
12
12
 
13
13
  /** 对齐上游 opencode 的截断提示文案(tools/BashTool/bash.ts)。 */
14
14
  const CAPTURE_TRUNCATED_NOTICE = "[output capture truncated at the in-memory safety limit]";
@@ -46,7 +46,7 @@ export default function opencodeBash(
46
46
  description: [
47
47
  "Executes a given bash command synchronously and returns its output.",
48
48
  "The default working directory is the current directory; use workdir to run elsewhere.",
49
- "timeout is in milliseconds, defaults to 120000, and may not exceed 600000.",
49
+ "timeout is in milliseconds, defaults to 120000, and may not exceed 7200000.",
50
50
  "Every command runs in the foreground. Background command execution is not supported; shell jobs are waited for before the tool returns.",
51
51
  ].join("\n"),
52
52
  promptSnippet: "execute bash command",
@@ -65,8 +65,7 @@ export default function opencodeBash(
65
65
  ),
66
66
  timeout: Type.Optional(
67
67
  Type.Number({
68
- description: "Optional timeout in milliseconds (max 600000)",
69
- default: 600,
68
+ description: "Optional timeout in milliseconds (max 7200000)",
70
69
  }),
71
70
  ),
72
71
  dangerouslyDisableSandbox: Type.Optional(