@trim21/personal-pi-extensions 0.0.361 → 0.0.363
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 +1 -1
- package/src/bwrap/core.ts +9 -2
- package/src/bwrap/runtime.ts +15 -11
- package/src/claude-code/shell.ts +2 -2
- package/src/lib/lsp/client.ts +26 -47
- package/src/opencode/bash.ts +2 -2
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -237,7 +237,14 @@ function killChild(child: ChildProcess): void {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
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,
|
|
260
|
+
...buildBwrapArgs(resolved, workspace),
|
|
254
261
|
"--dev",
|
|
255
262
|
"/dev",
|
|
256
263
|
"--proc",
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -87,8 +87,8 @@ export interface BwrapExecutionRequest {
|
|
|
87
87
|
timeout?: number;
|
|
88
88
|
requestFullAccess?: boolean;
|
|
89
89
|
requestFullAccessReason?: string;
|
|
90
|
-
/**
|
|
91
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
533
|
-
if (
|
|
534
|
-
lines.push(`Workdir: ${escapeHtml(
|
|
536
|
+
// 执行目录与工作区不同时,提示实际执行目录(execCwd 是解析后的绝对路径)
|
|
537
|
+
if (execCwd !== ctx.cwd) {
|
|
538
|
+
lines.push(`Workdir: ${escapeHtml(execCwd)}`);
|
|
535
539
|
}
|
|
536
540
|
const description = lines.join("\n");
|
|
537
541
|
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -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
|
|
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
|
});
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -259,11 +259,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
259
259
|
(params: { uri: string; version?: number; diagnostics: Diagnostic[] }) => {
|
|
260
260
|
const filePath = getFilePath(params.uri);
|
|
261
261
|
if (!filePath) return;
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
if (supportsPullDiagnostics()) return;
|
|
265
|
-
// 纯 push 服务器:服务器版本滞后于已发送版本时,该 push 对应的是旧内容
|
|
266
|
-
// (重算未完成时的迟到结果),同样忽略。
|
|
262
|
+
// 服务器版本滞后于已发送版本时,该 push 对应的是旧内容(异步重算未完成
|
|
263
|
+
// 时的迟到结果)。忽略,避免与当前版本结果混淆。
|
|
267
264
|
const currentVersion = documentVersions.get(filePath);
|
|
268
265
|
const isStalePush =
|
|
269
266
|
typeof params.version === "number" &&
|
|
@@ -461,15 +458,6 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
461
458
|
return { handled: true, matched, byFile, timedOut };
|
|
462
459
|
}
|
|
463
460
|
|
|
464
|
-
/** 是否支持文档级 pull 诊断:静态 diagnosticProvider 或动态注册的 document 诊断。 */
|
|
465
|
-
function supportsPullDiagnostics(): boolean {
|
|
466
|
-
if (hasStaticPullDiagnostics) return true;
|
|
467
|
-
for (const registration of diagnosticRegistrations.values()) {
|
|
468
|
-
if (registration.registerOptions?.workspaceDiagnostics !== true) return true;
|
|
469
|
-
}
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
461
|
function documentPullState() {
|
|
474
462
|
const documentRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
475
463
|
(registration) => registration.registerOptions?.workspaceDiagnostics !== true,
|
|
@@ -478,7 +466,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
478
466
|
documentIdentifiers: [
|
|
479
467
|
...new Set(documentRegistrations.flatMap((r) => r.registerOptions?.identifier ?? [])),
|
|
480
468
|
],
|
|
481
|
-
supported:
|
|
469
|
+
supported: hasStaticPullDiagnostics || documentRegistrations.length > 0,
|
|
482
470
|
};
|
|
483
471
|
}
|
|
484
472
|
|
|
@@ -634,18 +622,9 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
634
622
|
signal?: AbortSignal;
|
|
635
623
|
}): Promise<void> {
|
|
636
624
|
const startedAt = request.after ?? Date.now();
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
while (!connectionClosed && !request.signal?.aborted) {
|
|
641
|
-
const result = await requestDocumentDiagnostics(request.path);
|
|
642
|
-
if (result.matched) return;
|
|
643
|
-
if (result.timedOut) return;
|
|
644
|
-
await sleep(PULL_RETRY_INTERVAL_MS);
|
|
645
|
-
}
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
|
|
625
|
+
// pull 与 push 语义相同:都是等「当前文档版本」的诊断结果,统一一个循环。
|
|
626
|
+
// 先 pull(拿到即返回);pull 超时说明服务器未响应,不再重试 pull,只等
|
|
627
|
+
// 版本匹配的 push 兜底;版本不匹配的 push 一律忽略(防迟到旧结果)。
|
|
649
628
|
const pushWait = waitForFreshPush({
|
|
650
629
|
path: request.path,
|
|
651
630
|
version: request.version,
|
|
@@ -653,18 +632,23 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
653
632
|
timeout: diagnosticsDocumentWaitTimeoutMs,
|
|
654
633
|
});
|
|
655
634
|
|
|
656
|
-
while (
|
|
657
|
-
const result = await requestDocumentDiagnostics(request.path);
|
|
658
|
-
if (result.matched) return;
|
|
635
|
+
while (!connectionClosed && !request.signal?.aborted) {
|
|
659
636
|
const remaining = diagnosticsDocumentWaitTimeoutMs - (Date.now() - startedAt);
|
|
660
637
|
if (remaining <= 0) return;
|
|
638
|
+
const result = await requestDocumentDiagnostics(request.path);
|
|
639
|
+
if (result.matched) return;
|
|
640
|
+
if (result.timedOut) {
|
|
641
|
+
await pushWait;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
661
644
|
const next = await Promise.race([
|
|
662
|
-
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
|
|
645
|
+
pushWait.then((ready) => (ready ? ("push" as const) : ("timeout" as const))),
|
|
663
646
|
waitForRegistrationChange(remaining).then((changed) =>
|
|
664
647
|
changed ? ("registration" as const) : ("timeout" as const),
|
|
665
648
|
),
|
|
649
|
+
sleep(Math.min(remaining, PULL_RETRY_INTERVAL_MS)).then(() => "interval" as const),
|
|
666
650
|
]);
|
|
667
|
-
if (next
|
|
651
|
+
if (next === "push") return;
|
|
668
652
|
}
|
|
669
653
|
}
|
|
670
654
|
|
|
@@ -675,16 +659,6 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
675
659
|
signal?: AbortSignal;
|
|
676
660
|
}): Promise<void> {
|
|
677
661
|
const startedAt = request.after ?? Date.now();
|
|
678
|
-
if (supportsPullDiagnostics()) {
|
|
679
|
-
while (!connectionClosed && !request.signal?.aborted) {
|
|
680
|
-
const result = await requestFullDiagnostics(request.path);
|
|
681
|
-
if (result.handled || result.matched) return;
|
|
682
|
-
if (result.timedOut) return;
|
|
683
|
-
await sleep(PULL_RETRY_INTERVAL_MS);
|
|
684
|
-
}
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
662
|
const pushWait = waitForFreshPush({
|
|
689
663
|
path: request.path,
|
|
690
664
|
version: request.version,
|
|
@@ -692,18 +666,23 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
692
666
|
timeout: diagnosticsFullWaitTimeoutMs,
|
|
693
667
|
});
|
|
694
668
|
|
|
695
|
-
while (
|
|
696
|
-
const result = await requestFullDiagnostics(request.path);
|
|
697
|
-
if (result.handled || result.matched) return;
|
|
669
|
+
while (!connectionClosed && !request.signal?.aborted) {
|
|
698
670
|
const remaining = diagnosticsFullWaitTimeoutMs - (Date.now() - startedAt);
|
|
699
671
|
if (remaining <= 0) return;
|
|
672
|
+
const result = await requestFullDiagnostics(request.path);
|
|
673
|
+
if (result.handled || result.matched) return;
|
|
674
|
+
if (result.timedOut) {
|
|
675
|
+
await pushWait;
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
700
678
|
const next = await Promise.race([
|
|
701
|
-
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
|
|
679
|
+
pushWait.then((ready) => (ready ? ("push" as const) : ("timeout" as const))),
|
|
702
680
|
waitForRegistrationChange(remaining).then((changed) =>
|
|
703
681
|
changed ? ("registration" as const) : ("timeout" as const),
|
|
704
682
|
),
|
|
683
|
+
sleep(Math.min(remaining, PULL_RETRY_INTERVAL_MS)).then(() => "interval" as const),
|
|
705
684
|
]);
|
|
706
|
-
if (next
|
|
685
|
+
if (next === "push") return;
|
|
707
686
|
}
|
|
708
687
|
}
|
|
709
688
|
|
package/src/opencode/bash.ts
CHANGED
|
@@ -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
|
|
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
|
});
|