@trim21/personal-pi-extensions 0.0.362 → 0.0.366

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.362",
3
+ "version": "0.0.366",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -99,6 +99,6 @@
99
99
  "web-tree-sitter": "^0.26.12"
100
100
  },
101
101
  "optionalDependencies": {
102
- "@cortexkit/aft-linux-x64": "0.51.2"
102
+ "@cortexkit/aft-linux-x64": "0.51.3"
103
103
  }
104
104
  }
package/src/bwrap/core.ts CHANGED
@@ -237,7 +237,14 @@ function killChild(child: ChildProcess): void {
237
237
  }
238
238
  }
239
239
 
240
- export function createBwrapBashOperations(resolved: ResolvedBwrap): BashOperations {
240
+ /**
241
+ * @param workspace session 工作区:writablePaths 的 "." 与 PROTECTED_DIRS 都基于它解析,
242
+ * 与当次命令的 cwd(仅作为进程执行目录)解耦,避免 workdir 参数漂移可写边界。
243
+ */
244
+ export function createBwrapBashOperations(
245
+ resolved: ResolvedBwrap,
246
+ workspace: string,
247
+ ): BashOperations {
241
248
  return {
242
249
  async exec(command, cwd, { onData, signal, timeout }) {
243
250
  await fsAccess(cwd, constants.F_OK).catch(() => {
@@ -250,7 +257,7 @@ export function createBwrapBashOperations(resolved: ResolvedBwrap): BashOperatio
250
257
  "--ro-bind",
251
258
  "/",
252
259
  "/",
253
- ...buildBwrapArgs(resolved, cwd),
260
+ ...buildBwrapArgs(resolved, workspace),
254
261
  "--dev",
255
262
  "/dev",
256
263
  "--proc",
@@ -87,8 +87,8 @@ export interface BwrapExecutionRequest {
87
87
  timeout?: number;
88
88
  requestFullAccess?: boolean;
89
89
  requestFullAccessReason?: string;
90
- /** bash 工具显式传入的 workdir 参数;存在时审批对话框显示实际执行目录。 */
91
- workdir?: string;
90
+ /** 解析后的实际执行目录;缺省时与 ctx.cwd 相同。ctx.cwd 始终是 session 工作区。 */
91
+ cwd?: string;
92
92
  signal?: AbortSignal;
93
93
  onUpdate?: AgentToolUpdateCallback;
94
94
  ctx: ExtensionContext;
@@ -334,6 +334,10 @@ export class BwrapRuntime {
334
334
  "Install bubblewrap and restart the session, or pass --no-bwrap to disable the sandbox explicitly.",
335
335
  );
336
336
  }
337
+ // workspace 恒为 session 工作区;cwd 只是本次命令的进程执行目录,
338
+ // 二者解耦后 workdir 参数无法把沙箱可写边界带出工作区。
339
+ const workspace = request.ctx.cwd;
340
+ const execCwd = request.cwd ?? workspace;
337
341
  // 需要人工审批:非 Windows 仅 requestFullAccess;Windows 上默认所有命令
338
342
  // (allow-all 模式是显式 opt-out,仍直接执行)。
339
343
  const needsApproval = request.requestFullAccess === true || (isWindows && runtime.bwrapEnabled);
@@ -348,14 +352,14 @@ export class BwrapRuntime {
348
352
  request.ctx,
349
353
  request.command,
350
354
  request.requestFullAccessReason,
351
- request.workdir,
355
+ execCwd,
352
356
  );
353
357
  }
354
358
  }
355
359
  const operations =
356
360
  isWindows || needsApproval || !runtime.bwrapEnabled
357
361
  ? createLocalBashOperations()
358
- : createBwrapBashOperations(runtime);
362
+ : createBwrapBashOperations(runtime, workspace);
359
363
  const output = new BashOutput();
360
364
  const { onUpdate } = request;
361
365
 
@@ -391,7 +395,7 @@ export class BwrapRuntime {
391
395
 
392
396
  try {
393
397
  if (onUpdate) onUpdate({ content: [], details: undefined });
394
- const { exitCode } = await operations.exec(request.command, request.ctx.cwd, {
398
+ const { exitCode } = await operations.exec(request.command, execCwd, {
395
399
  onData: (data) => {
396
400
  output.append(data);
397
401
  scheduleUpdate();
@@ -439,11 +443,11 @@ export class BwrapRuntime {
439
443
  ctx: ExtensionContext,
440
444
  command: string,
441
445
  reason: string | undefined,
442
- workdir?: string,
446
+ execCwd: string,
443
447
  ): Promise<void> {
444
448
  const policy = resolveEscalation({ hasUI: ctx.hasUI });
445
449
  if (policy.kind === "deny") throw new Error(policy.reason);
446
- const decision = await this.approveFullAccessUI(ctx, command, reason, workdir);
450
+ const decision = await this.approveFullAccessUI(ctx, command, reason, execCwd);
447
451
  // 关闭对话框 = 中断并拒绝,不循环重问
448
452
  if (decision === undefined) {
449
453
  ctx.abort();
@@ -490,7 +494,7 @@ export class BwrapRuntime {
490
494
  ctx: ExtensionContext,
491
495
  command: string,
492
496
  reason: string | undefined,
493
- workdir?: string,
497
+ execCwd: string,
494
498
  ): Promise<FullAccessUIDecision | undefined> {
495
499
  // 弹框前解析命令的持久化规则(勾选的 pattern 会写入),在弹框里以
496
500
  // checkbox 列出:`echo 1 | head` → `echo *`、`head *`,逐项决定是否
@@ -529,9 +533,9 @@ export class BwrapRuntime {
529
533
  );
530
534
  }
531
535
  lines.push(fenceCodeBlock(command));
532
- // workdir 存在时展示实际执行目录(ctx.cwd 已被 bash 工具解析为绝对路径)
533
- if (workdir) {
534
- lines.push(`Workdir: ${escapeHtml(ctx.cwd)}`);
536
+ // 执行目录与工作区不同时,提示实际执行目录(execCwd 是解析后的绝对路径)
537
+ if (execCwd !== ctx.cwd) {
538
+ lines.push(`Workdir: ${escapeHtml(execCwd)}`);
535
539
  }
536
540
  const description = lines.join("\n");
537
541
 
@@ -121,13 +121,13 @@ export function registerShellTools(
121
121
  let result: Awaited<ReturnType<BwrapRuntime["execute"]>>;
122
122
  try {
123
123
  result = await runtime.execute({
124
- ctx: { ...ctx, cwd },
124
+ ctx,
125
+ cwd,
125
126
  toolCallId: id,
126
127
  command: params.command,
127
128
  timeout: timeout / 1000,
128
129
  requestFullAccess: params.dangerouslyDisableSandbox,
129
130
  requestFullAccessReason: params.description,
130
- workdir: params.workdir,
131
131
  signal,
132
132
  onUpdate,
133
133
  });
@@ -67,12 +67,12 @@ export default function opencodeBash(
67
67
  let result: Awaited<ReturnType<BwrapRuntime["execute"]>>;
68
68
  try {
69
69
  result = await runtime.execute({
70
- ctx: { ...ctx, cwd },
70
+ ctx,
71
+ cwd,
71
72
  toolCallId: id,
72
73
  command: params.command,
73
74
  timeout: timeout / 1000,
74
75
  requestFullAccess: params.dangerouslyDisableSandbox,
75
- workdir: params.workdir,
76
76
  signal,
77
77
  onUpdate,
78
78
  });