@trim21/personal-pi-extensions 0.0.429 → 0.0.431
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 +3 -1
- package/src/bwrap/core.ts +1 -1
- package/src/bwrap/network-stack.ts +2 -2
- package/src/bwrap/runtime.ts +37 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.431",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@earendil-works/pi-ai": "^0.84.1",
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
39
39
|
"@eslint/js": "10.0.1",
|
|
40
|
+
"@types/lodash-es": "^4.17.12",
|
|
40
41
|
"@types/node": "^24.13.3",
|
|
41
42
|
"@types/turndown": "^5.0.6",
|
|
42
43
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"@vscode/tree-sitter-wasm": "^0.3.1",
|
|
95
96
|
"jsonc-parser": "^3.3.1",
|
|
96
97
|
"linkedom": "^0.18.0",
|
|
98
|
+
"lodash-es": "^4.18.1",
|
|
97
99
|
"minimatch": "^10.2.6",
|
|
98
100
|
"turndown": "^7.2.0",
|
|
99
101
|
"vscode-jsonrpc": "^9.0.1",
|
package/src/bwrap/core.ts
CHANGED
|
@@ -370,7 +370,7 @@ function killChild(child: ChildProcess): void {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/** 为 net-allowlist
|
|
373
|
+
/** 为 net-allowlist 模式创建网络栈;非该模式返回 undefined。每次命令现建现停。 */
|
|
374
374
|
export async function createNetworkStack(
|
|
375
375
|
resolved: ResolvedBwrap,
|
|
376
376
|
): Promise<NetworkStack | undefined> {
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { generateSingboxConfig } from "./singbox-config.js";
|
|
8
8
|
|
|
9
9
|
export interface NetworkStackOptions {
|
|
10
|
-
/** 允许直连的域名 / IP:port
|
|
10
|
+
/** 允许直连的域名 / IP:port 列表(每次命令从配置重新读取)。 */
|
|
11
11
|
readonly allowlist: readonly string[];
|
|
12
12
|
/** 真实 DNS 服务器列表,按顺序 fallback。 */
|
|
13
13
|
readonly dnsServers: readonly string[];
|
|
@@ -132,7 +132,7 @@ const stackFinalizer = new FinalizationRegistry<NetworkStackState>((state) => {
|
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
135
|
+
* 启动网络栈:holder 进程持有 netns(内部跑 sing-box 的
|
|
136
136
|
* TUN + fakeip + deny-by-default),slirp4netns 提供 egress。返回前
|
|
137
137
|
* netns/sing-box/slirp4netns 均已就绪,命令通过 nsenter 进入该 netns 执行。
|
|
138
138
|
*/
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
truncateTail,
|
|
14
14
|
type TruncationResult,
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { throttle } from "lodash-es";
|
|
16
17
|
import { type TObject, Type } from "typebox";
|
|
17
18
|
|
|
18
19
|
import { type CommandSpec, parseCommand } from "../lib/cli.js";
|
|
@@ -38,7 +39,6 @@ import {
|
|
|
38
39
|
resolveHeadlessBwrap,
|
|
39
40
|
} from "./core.js";
|
|
40
41
|
import { dcgSuggestion } from "./dcg-scan.js";
|
|
41
|
-
import { type NetworkStack } from "./network-stack.js";
|
|
42
42
|
|
|
43
43
|
export type EscalationDecision = { kind: "dialog" } | { kind: "deny"; reason: string };
|
|
44
44
|
|
|
@@ -197,6 +197,11 @@ class BashOutput {
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// eslint-disable-next-line unicorn/no-nonstandard-builtin-properties -- ES2024 标准(Node 24 原生支持)
|
|
201
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
202
|
+
await this.close();
|
|
203
|
+
}
|
|
204
|
+
|
|
200
205
|
/** 尾部文本(截断结果的候选,未截断时即完整输出)。 */
|
|
201
206
|
tailText(): string {
|
|
202
207
|
return Buffer.concat(this.tail, this.tailBytes).toString("utf8");
|
|
@@ -242,7 +247,6 @@ export class BwrapRuntime {
|
|
|
242
247
|
private resolved: ResolvedBwrap | undefined;
|
|
243
248
|
private sandboxDisabled = false;
|
|
244
249
|
private bwrapUnavailable = false;
|
|
245
|
-
private networkStack: NetworkStack | undefined;
|
|
246
250
|
|
|
247
251
|
setup(pi: ExtensionAPI): void {
|
|
248
252
|
pi.registerFlag("no-bwrap", {
|
|
@@ -315,8 +319,6 @@ export class BwrapRuntime {
|
|
|
315
319
|
setMode(cwd: string, mode: BwrapMode): ResolvedBwrap {
|
|
316
320
|
this.resolved = resolveBwrap({ ...loadBwrapConfig(cwd), mode });
|
|
317
321
|
this.sandboxDisabled = false;
|
|
318
|
-
void this.networkStack?.stop();
|
|
319
|
-
this.networkStack = undefined;
|
|
320
322
|
return this.resolved;
|
|
321
323
|
}
|
|
322
324
|
|
|
@@ -324,8 +326,6 @@ export class BwrapRuntime {
|
|
|
324
326
|
this.resolved = undefined;
|
|
325
327
|
this.sandboxDisabled = false;
|
|
326
328
|
this.bwrapUnavailable = false;
|
|
327
|
-
void this.networkStack?.stop();
|
|
328
|
-
this.networkStack = undefined;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
async execute(request: BwrapExecutionRequest): Promise<BwrapExecutionResult> {
|
|
@@ -361,49 +361,43 @@ export class BwrapRuntime {
|
|
|
361
361
|
await this.approveFullAccess(request.ctx, request.command, request.description, execCwd);
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
364
|
+
const local = isWindows || needsApproval || !runtime.bwrapEnabled;
|
|
365
|
+
// 每次命令现建网络栈(启动约 140ms),作用域结束自动停栈;allowlist 变化即时生效
|
|
366
|
+
const networkStack = local ? undefined : await createNetworkStack(runtime);
|
|
367
|
+
await using _stack = {
|
|
368
|
+
// eslint-disable-next-line unicorn/no-nonstandard-builtin-properties -- ES2024 标准(Node 24 原生支持)
|
|
369
|
+
async [Symbol.asyncDispose]() {
|
|
370
|
+
try {
|
|
371
|
+
await networkStack?.stop();
|
|
372
|
+
} catch {
|
|
373
|
+
// best-effort:停栈失败(进程已退出/目录删除失败)不掩盖命令结果
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
const operations = local
|
|
378
|
+
? createLocalBashOperations()
|
|
379
|
+
: createBwrapBashOperations(runtime, workspace, networkStack);
|
|
380
|
+
await using output = new BashOutput(request.ctx.sessionManager.getSessionId());
|
|
369
381
|
const { onUpdate } = request;
|
|
370
382
|
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
383
|
-
};
|
|
384
|
-
const scheduleUpdate = () => {
|
|
385
|
-
if (!onUpdate) return;
|
|
386
|
-
dirty = true;
|
|
387
|
-
const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
|
|
388
|
-
if (delay <= 0) {
|
|
389
|
-
if (updateTimer) clearTimeout(updateTimer);
|
|
390
|
-
updateTimer = undefined;
|
|
391
|
-
emitUpdate();
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (updateTimer) return;
|
|
395
|
-
updateTimer = setTimeout(() => {
|
|
396
|
-
updateTimer = undefined;
|
|
397
|
-
emitUpdate();
|
|
398
|
-
}, delay);
|
|
399
|
-
};
|
|
383
|
+
// 流式进度:限流推送尾部快照(对齐 pi 内置 bash 的实时输出体验)
|
|
384
|
+
const emitUpdate = throttle(
|
|
385
|
+
() => {
|
|
386
|
+
onUpdate?.({
|
|
387
|
+
content: [{ type: "text", text: output.tailSnapshot() }],
|
|
388
|
+
details: undefined,
|
|
389
|
+
});
|
|
390
|
+
},
|
|
391
|
+
BASH_UPDATE_THROTTLE_MS,
|
|
392
|
+
{ trailing: true },
|
|
393
|
+
);
|
|
400
394
|
|
|
401
395
|
try {
|
|
402
396
|
if (onUpdate) onUpdate({ content: [], details: undefined });
|
|
403
397
|
const { exitCode } = await operations.exec(request.command, execCwd, {
|
|
404
398
|
onData: (data) => {
|
|
405
399
|
output.append(data);
|
|
406
|
-
|
|
400
|
+
emitUpdate();
|
|
407
401
|
},
|
|
408
402
|
signal: request.signal,
|
|
409
403
|
timeout: request.timeout,
|
|
@@ -440,9 +434,8 @@ export class BwrapRuntime {
|
|
|
440
434
|
}
|
|
441
435
|
throw error;
|
|
442
436
|
} finally {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
await output.close();
|
|
437
|
+
emitUpdate.flush();
|
|
438
|
+
emitUpdate.cancel();
|
|
446
439
|
}
|
|
447
440
|
}
|
|
448
441
|
|
|
@@ -454,21 +447,6 @@ export class BwrapRuntime {
|
|
|
454
447
|
return this.resolved;
|
|
455
448
|
}
|
|
456
449
|
|
|
457
|
-
/** net-allowlist 模式的常驻网络栈:惰性创建,session 期间复用(allowlist 固定)。 */
|
|
458
|
-
private async getNetworkStack(runtime: ResolvedBwrap): Promise<NetworkStack | undefined> {
|
|
459
|
-
if (runtime.mode !== "net-allowlist" || runtime.networkAllowlist.length === 0) {
|
|
460
|
-
if (this.networkStack) {
|
|
461
|
-
void this.networkStack.stop();
|
|
462
|
-
this.networkStack = undefined;
|
|
463
|
-
}
|
|
464
|
-
return undefined;
|
|
465
|
-
}
|
|
466
|
-
if (!this.networkStack) {
|
|
467
|
-
this.networkStack = await createNetworkStack(runtime);
|
|
468
|
-
}
|
|
469
|
-
return this.networkStack;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
450
|
private async approveFullAccess(
|
|
473
451
|
ctx: ExtensionContext,
|
|
474
452
|
command: string,
|
|
@@ -744,8 +722,6 @@ export class BwrapRuntime {
|
|
|
744
722
|
private reload(ctx: ExtensionCommandContext): void {
|
|
745
723
|
this.resolved = undefined;
|
|
746
724
|
this.bwrapUnavailable = false;
|
|
747
|
-
void this.networkStack?.stop();
|
|
748
|
-
this.networkStack = undefined;
|
|
749
725
|
const runtime = this.resolve(ctx);
|
|
750
726
|
ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", `bwrap: ${runtime.mode}`));
|
|
751
727
|
ctx.ui.notify(`bwrap config reloaded (mode: ${runtime.mode})`, "info");
|