@zhushanwen/pi-permission 1.5.0 → 1.6.0
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 +11 -11
- package/src/__tests__/config.test.ts +2 -2
- package/src/__tests__/pipeline.test.ts +20 -9
- package/src/__tests__/types.test.ts +1 -1
- package/src/classifier/classifier.ts +3 -1
- package/src/pipeline.ts +9 -14
- package/src/types.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-permission",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Pi permission extension — four permission modes (yolo/auto/approve/strict) with three-layer pipeline (AST + rules + AI classifier).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"classifier": {
|
|
16
16
|
"enabled": true,
|
|
17
17
|
"model": "auto",
|
|
18
|
-
"timeout":
|
|
18
|
+
"timeout": 0,
|
|
19
19
|
"autoApproveLowRisk": true,
|
|
20
20
|
"autoDenyHighRisk": true,
|
|
21
21
|
"thinkingLevel": "off"
|
|
@@ -50,14 +50,14 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"tree-sitter-bash": "^0.25.1",
|
|
52
52
|
"web-tree-sitter": "^0.26.11",
|
|
53
|
-
"@zhushanwen/pi-ext-guards": "0.4.
|
|
54
|
-
"@zhushanwen/pi-extension-logger": "0.6.
|
|
55
|
-
"@zhushanwen/pi-llm-shared": "0.11.
|
|
53
|
+
"@zhushanwen/pi-ext-guards": "0.4.3",
|
|
54
|
+
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
55
|
+
"@zhushanwen/pi-llm-shared": "0.11.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@earendil-works/pi-ai": "^0.
|
|
59
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
60
|
-
"@earendil-works/pi-tui": "^0.
|
|
58
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
59
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
60
|
+
"@earendil-works/pi-tui": "^1.0.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@earendil-works/pi-ai": {
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@earendil-works/pi-ai": "^0.
|
|
75
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
76
|
-
"@earendil-works/pi-tui": "^0.
|
|
74
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
75
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
76
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
77
77
|
"@vitest/coverage-v8": "^4.1.9",
|
|
78
78
|
"vitest": "^4.1.8"
|
|
79
79
|
},
|
|
@@ -54,7 +54,7 @@ describe("WT1: 默认配置生成(首次无配置文件)", () => {
|
|
|
54
54
|
expect(config.enabled).toBe(true);
|
|
55
55
|
expect(config.classifier.enabled).toBe(true);
|
|
56
56
|
expect(config.classifier.model).toBe("auto");
|
|
57
|
-
expect(config.classifier.timeout).toBe(
|
|
57
|
+
expect(config.classifier.timeout).toBe(0);
|
|
58
58
|
expect(config.classifier.autoApproveLowRisk).toBe(true);
|
|
59
59
|
expect(config.classifier.autoDenyHighRisk).toBe(true);
|
|
60
60
|
expect(config.userRules).toEqual([]);
|
|
@@ -130,7 +130,7 @@ describe("WT3: 配置解析容错(malformed JSON)", () => {
|
|
|
130
130
|
const config = loadAndWatchConfig();
|
|
131
131
|
expect(config.mode).toBe("strict");
|
|
132
132
|
expect(config.classifier.model).toBe("auto"); // 默认
|
|
133
|
-
expect(config.classifier.timeout).toBe(
|
|
133
|
+
expect(config.classifier.timeout).toBe(0); // 默认
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
it("TC6 (C3b): classifier.model 传对象形式 → console.warn + 回落默认 auto", () => {
|
|
@@ -844,16 +844,19 @@ describe("m3: bash 工具收到非字符串 command 时 logger.warn(不静默
|
|
|
844
844
|
});
|
|
845
845
|
});
|
|
846
846
|
|
|
847
|
-
// ──────────────────────── M3:
|
|
847
|
+
// ──────────────────────── M3: AI-ask 分支等待用户决策(无包内超时) ────────────────────────
|
|
848
848
|
|
|
849
|
-
describe("M3: runLayer3WithRacing AI-ask
|
|
850
|
-
it("AI ask
|
|
851
|
-
// 用 fake timer 模拟 5 分钟流逝,避免真实等待
|
|
849
|
+
describe("M3: runLayer3WithRacing AI-ask 分支(ADR-0122:无包内超时兜底)", () => {
|
|
850
|
+
it("AI ask → 无超时兜底等待;远超原 5min 后用户决策照常生效", async () => {
|
|
852
851
|
vi.useFakeTimers();
|
|
853
852
|
try {
|
|
853
|
+
let resolveApproval: (d: UserDecision) => void = () => {};
|
|
854
854
|
const deps = makeDeps({
|
|
855
855
|
classify: () => Promise.resolve(fallbackClassifier()), // outcome=ask
|
|
856
|
-
approve: () =>
|
|
856
|
+
approve: () =>
|
|
857
|
+
new Promise<UserDecision>((resolve) => {
|
|
858
|
+
resolveApproval = resolve;
|
|
859
|
+
}),
|
|
857
860
|
});
|
|
858
861
|
const { runLayer3WithRacing } = await import("../pipeline.js");
|
|
859
862
|
const decisionPromise = runLayer3WithRacing(
|
|
@@ -862,11 +865,19 @@ describe("M3: runLayer3WithRacing AI-ask 分支超时兜底(防永久挂起)
|
|
|
862
865
|
DEFAULT_CFG,
|
|
863
866
|
undefined,
|
|
864
867
|
);
|
|
865
|
-
// 推进
|
|
866
|
-
await vi.advanceTimersByTimeAsync(
|
|
868
|
+
// 推进 10 分钟(远超原 APPROVAL_TIMEOUT_MS=300_000):决策未出(无超时兜底,等待用户是交互语义)
|
|
869
|
+
await vi.advanceTimersByTimeAsync(600_000);
|
|
870
|
+
let settled = false;
|
|
871
|
+
void decisionPromise.then(() => {
|
|
872
|
+
settled = true;
|
|
873
|
+
});
|
|
874
|
+
await Promise.resolve();
|
|
875
|
+
expect(settled).toBe(false);
|
|
876
|
+
// 用户最终决策 → 按决策返回(用户晚到不被超时截杀)
|
|
877
|
+
resolveApproval({ approved: true, reason: "late approval" });
|
|
867
878
|
const decision = await decisionPromise;
|
|
868
|
-
expect(decision.action).toBe("
|
|
869
|
-
expect(decision.
|
|
879
|
+
expect(decision.action).toBe("allow");
|
|
880
|
+
expect(decision.source).toBe("user");
|
|
870
881
|
} finally {
|
|
871
882
|
vi.useRealTimers();
|
|
872
883
|
}
|
|
@@ -51,7 +51,7 @@ describe("WT6: PermissionMode 枚举完整性", () => {
|
|
|
51
51
|
expect(DEFAULT_CONFIG.enabled).toBe(true);
|
|
52
52
|
expect(DEFAULT_CONFIG.classifier.enabled).toBe(true);
|
|
53
53
|
expect(DEFAULT_CONFIG.classifier.model).toBe("auto");
|
|
54
|
-
expect(DEFAULT_CONFIG.classifier.timeout).toBe(
|
|
54
|
+
expect(DEFAULT_CONFIG.classifier.timeout).toBe(0);
|
|
55
55
|
expect(DEFAULT_CONFIG.classifier.autoApproveLowRisk).toBe(true);
|
|
56
56
|
expect(DEFAULT_CONFIG.classifier.autoDenyHighRisk).toBe(true);
|
|
57
57
|
expect(DEFAULT_CONFIG.userRules).toEqual([]);
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* - G3 修正(由 callLLM 承接):completeSimple 的 EventStream.result() 只 resolve 不 reject,
|
|
14
14
|
* error/aborted 也 resolve(带 stopReason)。callLLM 已把 error/aborted 归一为 ok:false +
|
|
15
15
|
* stopReason 独立透传;classifier 消费透传字段保留日志区分(abort 与 error 分开记)
|
|
16
|
-
* -
|
|
16
|
+
* - 外层超时/中止兜底:仅在用户显式配置 timeout > 0 时武装(默认 0 = 不限时,ADR-0122:
|
|
17
|
+
* 无包内挂死兜底);abort 竞速为事件驱动恒在。武装时防御 provider 不支持 timeoutMs
|
|
18
|
+
* 时 result() 永挂
|
|
17
19
|
* - fail-closed:timeout / 抛错 / 解析失败 / 无可用模型 → 一律 ask
|
|
18
20
|
*/
|
|
19
21
|
|
package/src/pipeline.ts
CHANGED
|
@@ -301,24 +301,19 @@ function settleAiWin(
|
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
303
|
* AI ask → 转 human(等用户最终决策,不关闭对话框)。
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
304
|
+
*
|
|
305
|
+
* 无超时(ADR-0122):等待用户审批是交互语义(审批对话框无上界等待 = 权限系统
|
|
306
|
+
* fail-closed 本职:用户不动 = 不放行也不拒绝),非挂死保护。promise 落定由结构
|
|
307
|
+
* 保证——pi 实装 showExtensionCustom(1.0.0 dist/modes/interactive/interactive-mode.js)
|
|
308
|
+
* 中 close 即 resolve 函数、调用必 resolve;abort 路径经 comp.cancel→done / rpc select signal 短路。
|
|
309
|
+
* 原 M3 的 5min APPROVAL_TIMEOUT_MS 兜底已删:其登记的根因(同 tick 操作致
|
|
310
|
+
* _resolved 置位但 done 未调 → promise 永不 resolve)经现行代码 + pi 实装核实
|
|
311
|
+
* 不存在——ApprovalComponent 的 approve/deny/cancel 三个 settle 点均同步紧跟 done。
|
|
308
312
|
*/
|
|
309
313
|
async function awaitUserAfterAiAsk(
|
|
310
314
|
realUserPromise: Promise<UserDecision>,
|
|
311
315
|
): Promise<PermissionDecision> {
|
|
312
|
-
const
|
|
313
|
-
const userFinal = await Promise.race<UserDecision>([
|
|
314
|
-
realUserPromise,
|
|
315
|
-
new Promise<UserDecision>((resolve) =>
|
|
316
|
-
setTimeout(
|
|
317
|
-
() => resolve({ approved: false, reason: "approval dialog timeout (fail-closed)" }),
|
|
318
|
-
APPROVAL_TIMEOUT_MS,
|
|
319
|
-
),
|
|
320
|
-
),
|
|
321
|
-
]);
|
|
316
|
+
const userFinal = await realUserPromise;
|
|
322
317
|
const action: PermissionAction = userFinal.approved ? "allow" : "deny";
|
|
323
318
|
return {
|
|
324
319
|
action,
|
package/src/types.ts
CHANGED
|
@@ -100,7 +100,10 @@ export interface ClassifierConfig {
|
|
|
100
100
|
enabled: boolean;
|
|
101
101
|
/** 模型:'auto'(取 modelRegistry.getAvailable() 首个可用)或精确 'provider/model-id'(如 'zhipu/glm-4-flash') */
|
|
102
102
|
model: string;
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* 超时秒数(仅用户显式配置时生效的 C2 业务超时;0 = 不限时)。
|
|
105
|
+
* 包内默认 0(ADR-0122:无包内挂死兜底,LLM 挂死由上层失败直报,不设时间窗猜测)。
|
|
106
|
+
*/
|
|
104
107
|
timeout: number;
|
|
105
108
|
/** 低风险是否自动放行 */
|
|
106
109
|
autoApproveLowRisk: boolean;
|
|
@@ -133,7 +136,7 @@ export interface PermissionConfig {
|
|
|
133
136
|
export const DEFAULT_CLASSIFIER_CONFIG: ClassifierConfig = {
|
|
134
137
|
enabled: true,
|
|
135
138
|
model: "auto",
|
|
136
|
-
timeout:
|
|
139
|
+
timeout: 0,
|
|
137
140
|
autoApproveLowRisk: true,
|
|
138
141
|
autoDenyHighRisk: true,
|
|
139
142
|
thinkingLevel: "off",
|