@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-permission",
3
- "version": "1.5.0",
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": 90,
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.2",
54
- "@zhushanwen/pi-extension-logger": "0.6.1",
55
- "@zhushanwen/pi-llm-shared": "0.11.0"
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.84.4",
59
- "@earendil-works/pi-coding-agent": "^0.84.4",
60
- "@earendil-works/pi-tui": "^0.84.4"
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.84.4",
75
- "@earendil-works/pi-coding-agent": "^0.84.4",
76
- "@earendil-works/pi-tui": "^0.84.4",
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(90);
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(90); // 默认
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: realUserPromise 超时兜底 ────────────────────────
847
+ // ──────────────────────── M3: AI-ask 分支等待用户决策(无包内超时) ────────────────────────
848
848
 
849
- describe("M3: runLayer3WithRacing AI-ask 分支超时兜底(防永久挂起)", () => {
850
- it("AI ask + requestUserApproval 永挂 → 5 分钟超时 fail-closed deny(不永久阻塞)", async () => {
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: () => new Promise<UserDecision>(() => undefined), // 永不 resolve(模拟挂起)
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
- // 推进 5 分钟(超过 APPROVAL_TIMEOUT_MS=300_000)
866
- await vi.advanceTimersByTimeAsync(300_001);
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("deny"); // fail-closed
869
- expect(decision.reason).toContain("timeout");
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(90);
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
- * - 外层超时/中止兜底保留:防御 provider 不支持 timeoutMs 时 result() 永挂
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
- * M3:超时兜底防止永久挂起。若 AI 返回 ask 后用户恰好与 AI 在同一 tick 操作,
305
- * comp.cancel() 因 _resolved 守卫不调 done → requestUserApproval promise 永不
306
- * resolve → realUserPromise pending → 永久挂起(G5 串行化放大为全链卡死)。
307
- * 5 分钟超时后 fail-closed 拒绝(不静默放行)。
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 APPROVAL_TIMEOUT_MS = 300_000;
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: 90,
139
+ timeout: 0,
137
140
  autoApproveLowRisk: true,
138
141
  autoDenyHighRisk: true,
139
142
  thinkingLevel: "off",