@zhushanwen/pi-permission 1.3.2 → 1.3.3

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.3.2",
3
+ "version": "1.3.3",
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",
@@ -32,7 +32,8 @@
32
32
  "dependencies": {
33
33
  "tree-sitter-bash": "^0.25.1",
34
34
  "web-tree-sitter": "^0.26.11",
35
- "@zhushanwen/pi-llm-shared": "0.4.0"
35
+ "@zhushanwen/pi-extension-logger": "0.3.0",
36
+ "@zhushanwen/pi-llm-shared": "0.4.1"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@earendil-works/pi-ai": "^0.84.1",
@@ -9,6 +9,16 @@ import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
11
 
12
+ // Mock 共享 logger,让 logger.warn 可被 spy(源码已从 console.warn 改为 logger.warn)
13
+ const { loggerMock } = vi.hoisted(() => ({
14
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
15
+ }));
16
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
17
+ getLogger: () => loggerMock,
18
+ createLogger: () => loggerMock,
19
+ setPiHandle: vi.fn(),
20
+ }));
21
+
12
22
  import { DEFAULT_CONFIG } from "../types.js";
13
23
  import {
14
24
  clearConfigCache,
@@ -127,42 +137,36 @@ describe("WT3: 配置解析容错(malformed JSON)", () => {
127
137
  // 用户照旧设计文档配对象形式 selector(不受支持)
128
138
  writeFileSync(configPath, JSON.stringify({ classifier: { model: { type: "available" } } }), "utf-8");
129
139
 
130
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
131
- try {
132
- const config = loadAndWatchConfig();
133
- // 回落默认 auto(不再静默)
134
- expect(config.classifier.model).toBe("auto");
135
- // warn 提示忽略无效 classifier.model
136
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Ignoring invalid classifier.model"));
137
- } finally {
138
- warnSpy.mockRestore();
139
- }
140
+ loggerMock.warn.mockClear();
141
+ const config = loadAndWatchConfig();
142
+ // 回落默认 auto(不再静默)
143
+ expect(config.classifier.model).toBe("auto");
144
+ // warn 提示忽略无效 classifier.model
145
+ expect(loggerMock.warn).toHaveBeenCalledWith(
146
+ expect.stringContaining("Ignoring invalid classifier.model"),
147
+ );
140
148
  });
141
149
 
142
- it("classifier.model 传数字 → console.warn + 回落默认 auto", () => {
150
+ it("classifier.model 传数字 → logger.warn + 回落默认 auto", () => {
143
151
  writeFileSync(configPath, JSON.stringify({ classifier: { model: 42 } }), "utf-8");
144
152
 
145
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
146
- try {
147
- const config = loadAndWatchConfig();
148
- expect(config.classifier.model).toBe("auto");
149
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Ignoring invalid classifier.model"));
150
- } finally {
151
- warnSpy.mockRestore();
152
- }
153
+ loggerMock.warn.mockClear();
154
+ const config = loadAndWatchConfig();
155
+ expect(config.classifier.model).toBe("auto");
156
+ expect(loggerMock.warn).toHaveBeenCalledWith(
157
+ expect.stringContaining("Ignoring invalid classifier.model"),
158
+ );
153
159
  });
154
160
 
155
161
  it("classifier.model 合法 string('auto' / 'provider/model-id')→ 不 warn", () => {
156
162
  writeFileSync(configPath, JSON.stringify({ classifier: { model: "zhipu/glm-4-flash" } }), "utf-8");
157
163
 
158
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
159
- try {
160
- const config = loadAndWatchConfig();
161
- expect(config.classifier.model).toBe("zhipu/glm-4-flash");
162
- expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("Ignoring invalid classifier.model"));
163
- } finally {
164
- warnSpy.mockRestore();
165
- }
164
+ loggerMock.warn.mockClear();
165
+ const config = loadAndWatchConfig();
166
+ expect(config.classifier.model).toBe("zhipu/glm-4-flash");
167
+ expect(loggerMock.warn).not.toHaveBeenCalledWith(
168
+ expect.stringContaining("Ignoring invalid classifier.model"),
169
+ );
166
170
  });
167
171
 
168
172
  it("userRules 含非法条目时过滤掉", () => {
@@ -275,23 +279,21 @@ describe("[MIGRATION] legacy config 降级告警(ensureConfigFile 兜底)",
275
279
  const legacyPath = join(tempDir, "permission-config.json");
276
280
  writeFileSync(legacyPath, JSON.stringify({ mode: "strict" }), "utf-8");
277
281
 
278
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
279
- try {
280
- loadAndWatchConfig();
281
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Legacy config detected"));
282
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("downgrade"));
283
- } finally {
284
- warnSpy.mockRestore();
285
- }
282
+ loggerMock.warn.mockClear();
283
+ loadAndWatchConfig();
284
+ expect(loggerMock.warn).toHaveBeenCalledWith(
285
+ expect.stringContaining("Legacy config detected"),
286
+ );
287
+ expect(loggerMock.warn).toHaveBeenCalledWith(
288
+ expect.stringContaining("downgrade"),
289
+ );
286
290
  });
287
291
 
288
292
  it("旧路径不存在 → 不触发 legacy 告警", () => {
289
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
290
- try {
291
- loadAndWatchConfig();
292
- expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("Legacy config detected"));
293
- } finally {
294
- warnSpy.mockRestore();
295
- }
293
+ loggerMock.warn.mockClear();
294
+ loadAndWatchConfig();
295
+ expect(loggerMock.warn).not.toHaveBeenCalledWith(
296
+ expect.stringContaining("Legacy config detected"),
297
+ );
296
298
  });
297
299
  });
@@ -16,6 +16,16 @@
16
16
  */
17
17
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
18
 
19
+ // Mock 共享 logger,让 logger.warn 可被 spy(源码已从 console.warn 改为 logger.warn)
20
+ const { loggerMock } = vi.hoisted(() => ({
21
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
22
+ }));
23
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
24
+ getLogger: () => loggerMock,
25
+ createLogger: () => loggerMock,
26
+ setPiHandle: vi.fn(),
27
+ }));
28
+
19
29
  import {
20
30
  FOOTER_HANDSHAKE_KEY,
21
31
  type FooterLineRenderer,
@@ -211,8 +221,8 @@ describe("TC6/TC7:requestFooterRender", () => {
211
221
  // ──────────────────────── TC8:version mismatch → 重建 slot ────────────────────────
212
222
 
213
223
  describe("TC8:version mismatch → readSlot 丢弃 + ensureSlot 重建", () => {
214
- it("slot.version !== 1 → 注册时建新 slot(丢弃旧的),并 console.warn", () => {
215
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
224
+ it("slot.version !== 1 → 注册时建新 slot(丢弃旧的),并 logger.warn", () => {
225
+ loggerMock.warn.mockClear();
216
226
  // 污染槽位:version=99
217
227
  Reflect.set(globalThis, FOOTER_HANDSHAKE_KEY, {
218
228
  version: 99,
@@ -222,10 +232,10 @@ describe("TC8:version mismatch → readSlot 丢弃 + ensureSlot 重建", () =>
222
232
  const slot = getRawSlot() as { version: number; pending: unknown[] };
223
233
  expect(slot.version).toBe(1); // 重建为 version=1
224
234
  expect(slot.pending).toHaveLength(1);
225
- expect(warnSpy).toHaveBeenCalledWith(
235
+ expect(loggerMock.warn).toHaveBeenCalledWith(
226
236
  expect.stringContaining("version mismatch"),
237
+ expect.any(Object),
227
238
  );
228
- warnSpy.mockRestore();
229
239
  });
230
240
 
231
241
  it("pending 非 Array → 视为无 slot,重建", () => {
@@ -7,6 +7,16 @@
7
7
  */
8
8
  import { describe, expect, it, vi } from "vitest";
9
9
 
10
+ // Mock 共享 logger,让 logger.warn 可被 spy(源码已从 console.warn 改为 logger.warn)
11
+ const { loggerMock } = vi.hoisted(() => ({
12
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
13
+ }));
14
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
15
+ getLogger: () => loggerMock,
16
+ createLogger: () => loggerMock,
17
+ setPiHandle: vi.fn(),
18
+ }));
19
+
10
20
  import {
11
21
  applyAutoApproveOverrides,
12
22
  buildApprovalRequest,
@@ -708,9 +718,9 @@ describe("M5b: runLayer3WithRacing headless 模式(纯等 AI,不启动 user
708
718
 
709
719
  // ──────────────────────── m3: extractCommand 非字符串 bash command 告警 ────────────────────────
710
720
 
711
- describe("m3: bash 工具收到非字符串 command 时 console.warn(不静默)", () => {
721
+ describe("m3: bash 工具收到非字符串 command 时 logger.warn(不静默)", () => {
712
722
  it("command 为数字 → warn 被调用 + fail-closed(不 throw,送下游)", async () => {
713
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
723
+ loggerMock.warn.mockClear();
714
724
  const deps = makeDeps({
715
725
  classify: () => Promise.resolve(allowClassifier("low")),
716
726
  approve: () => new Promise<UserDecision>(() => undefined),
@@ -718,21 +728,19 @@ describe("m3: bash 工具收到非字符串 command 时 console.warn(不静默
718
728
  // command 为数字(异常输入)
719
729
  const decision = await checkPermission("bash", { command: 123 }, "auto", DEFAULT_CFG, [], deps, ctxBase);
720
730
  // warn 被调用(记录异常)
721
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("non-string command"));
722
- warnSpy.mockRestore();
731
+ expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining("non-string command"), expect.any(Object));
723
732
  // fail-closed:不 throw,正常返回决策(AI allow low → allow)
724
733
  expect(decision.action).toBe("allow");
725
734
  });
726
735
 
727
736
  it("command 为合法字符串 → 不 warn", async () => {
728
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
737
+ loggerMock.warn.mockClear();
729
738
  const deps = makeDeps({
730
739
  analyze: () => Promise.resolve(cleanAnalysis([["ls"]])),
731
740
  matchArgv: () => ({ action: "allow", matchedRule: undefined }),
732
741
  });
733
742
  await checkPermission("bash", { command: "ls" }, "approve", DEFAULT_CFG, [], deps, ctxBase);
734
- expect(warnSpy).not.toHaveBeenCalled();
735
- warnSpy.mockRestore();
743
+ expect(loggerMock.warn).not.toHaveBeenCalled();
736
744
  });
737
745
  });
738
746
 
@@ -21,6 +21,16 @@ import type { ApprovalContext } from "../approval.js";
21
21
  import type { CheckPermissionDeps } from "../pipeline.js";
22
22
  import { createPipelineDeps, createProductionClassifier, toSelector } from "../production.js";
23
23
 
24
+ // Mock 共享 logger,让 logger.warn 可被 spy(源码已从 console.warn 改为 logger.warn)
25
+ const { loggerMock } = vi.hoisted(() => ({
26
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
27
+ }));
28
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
29
+ getLogger: () => loggerMock,
30
+ createLogger: () => loggerMock,
31
+ setPiHandle: vi.fn(),
32
+ }));
33
+
24
34
  // vi.mock 必须在 import 之前(vitest hoisting)。替换 llm-shared 的 resolveModel + callLLM
25
35
  // 为可控 mock,默认透传 actual(真实实现),单测用 mockImplementation/mockResolvedValue 精确控制。
26
36
  vi.mock("@zhushanwen/pi-llm-shared", async (importOriginal) => {
@@ -174,14 +184,10 @@ describe("resolveModel 装配(resolveModel + callLLM)", () => {
174
184
  vi.mocked(resolveModelShared).mockReturnValue(MOCK_MODEL_A);
175
185
  vi.mocked(callLLMShared).mockResolvedValue({ ok: true, content: "x" });
176
186
  const classifier = createProductionClassifier(makeMockCtx());
177
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
178
- try {
179
- await classifier.classifyRisk(CTX, CFG_REF);
180
- // onLog 实装为 console.warn(production.ts),成功路径日志在 LLM 调用前输出
181
- expect(warnSpy).toHaveBeenCalledWith("[pi-permission] classifier: using model model-a");
182
- } finally {
183
- warnSpy.mockRestore();
184
- }
187
+ loggerMock.warn.mockClear();
188
+ await classifier.classifyRisk(CTX, CFG_REF);
189
+ // onLog 实装为 logger.warn(production.ts),成功路径日志在 LLM 调用前输出
190
+ expect(loggerMock.warn).toHaveBeenCalledWith("[pi-permission] classifier: using model model-a");
185
191
  });
186
192
 
187
193
  it("TC5: callLLM 返回 ok:false(LLM 调用失败)→ classifier fail-closed fallback(不 throw)", async () => {
@@ -19,10 +19,13 @@
19
19
  */
20
20
 
21
21
  import type { Node, Tree } from "web-tree-sitter";
22
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
22
23
 
23
24
  import type { BashAnalysis } from "../types.js";
24
25
  import { getBashParser } from "./loader.js";
25
26
 
27
+ const logger = getLogger("pi-permission/ast");
28
+
26
29
  /** 超过此长度的输入直接拒(防止 wasm 解析巨型 payload DoS)。 */
27
30
  const MAX_COMMAND_LENGTH = 65536;
28
31
 
@@ -230,7 +233,7 @@ export async function analyzeBashStructure(command: string): Promise<BashAnalysi
230
233
  } catch (err) {
231
234
  // 任何意外异常 → fail-closed
232
235
  const msg = err instanceof Error ? err.message : String(err);
233
- console.warn(`[pi-permission/ast] analyzeBashStructure exception: ${msg}`);
236
+ logger.warn("analyzeBashStructure exception", { error: msg });
234
237
  return failClosed(msg || "EXCEPTION");
235
238
  } finally {
236
239
  // G1 critical: Tree 是 wasm 堆对象,不受 V8 GC 管,必须显式 delete 释放。
package/src/ast/loader.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * - 初始化 Parser 运行时(locateFile 指向 wasm 目录)
7
7
  * - 加载 bash grammar,构造并缓存 Parser 单例(promise memoization,天然并发互斥)
8
8
  *
9
- * 失败策略(fail-closed):任一步抛错 → console.warn + 返回 null,永不 throw。
9
+ * 失败策略(fail-closed):任一步抛错 → logger.warn + 返回 null,永不 throw。
10
10
  * analyzer 拿到 null 时降级为 { clean:false, parseError:true }。
11
11
  *
12
12
  * 注意(实测 web-tree-sitter 0.26.11 的事实,与 pi-lens 用的 0.24.5 有差异):
@@ -25,6 +25,9 @@ import { dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
 
27
27
  import { Language, Parser } from "web-tree-sitter";
28
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
29
+
30
+ const logger = getLogger("pi-permission/ast");
28
31
 
29
32
  const nodeRequire = createRequire(import.meta.url);
30
33
 
@@ -119,7 +122,7 @@ export function getBashParser(): Promise<Parser | null> {
119
122
  // fail-closed:清空缓存让下次调用重试,warn 后返回 null。
120
123
  parserPromise = null;
121
124
  const msg = err instanceof Error ? err.message : String(err);
122
- console.warn(`[pi-permission/ast] getBashParser init failed: ${msg}`);
125
+ logger.warn("getBashParser init failed", { error: msg });
123
126
  return null;
124
127
  }
125
128
  })();
package/src/config.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  loadConfig,
26
26
  saveConfig as saveLlmConfig,
27
27
  } from "@zhushanwen/pi-llm-shared";
28
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
28
29
 
29
30
  import {
30
31
  DEFAULT_CLASSIFIER_CONFIG,
@@ -38,6 +39,8 @@ import {
38
39
  /** llm-shared 泛型 config 的包名(决定文件名 permission-ext-config.json,llm-shared getConfigPath 追加 -ext-config.json 后缀)。 */
39
40
  const CONFIG_PKG = "permission";
40
41
 
42
+ const logger = getLogger("pi-permission");
43
+
41
44
  // ──────────────────────── 路径解析 ────────────────────────
42
45
 
43
46
  /** 配置文件完整路径:<agentDir>/config/permission-ext-config.json(llm-shared 推导)。 */
@@ -71,7 +74,7 @@ function normalizeClassifierConfig(raw: unknown): ClassifierConfig {
71
74
  // C3b:classifier.model 只接受 string('auto' 或 'provider/model-id')。对象形式(如
72
75
  // `{ "type": "available" }`)不受支持,此前会被静默忽略回落默认——现在显式 warn 消除静默。
73
76
  if (record.model !== undefined && !(typeof record.model === "string" && record.model.length > 0)) {
74
- console.warn("[pi-permission] Ignoring invalid classifier.model (expected string 'auto' or 'provider/model-id'), using default auto");
77
+ logger.warn("Ignoring invalid classifier.model (expected string 'auto' or 'provider/model-id'), using default auto");
75
78
  }
76
79
  // thinkingLevel 校验:合法值为 'off'|'minimal'|'low'|'medium'|'high'|'xhigh'|'max'
77
80
  const thinkingLevel = isModelThinkingLevel(record.thinkingLevel)
@@ -140,8 +143,8 @@ function warnLegacyConfigIfExists(): void {
140
143
  const legacyPath = join(getAgentDir(), "permission-config.json");
141
144
  if (!existsSync(legacyPath)) return;
142
145
  const newPath = join(getAgentDir(), "config", "permission-ext-config.json");
143
- console.warn(
144
- `[pi-permission] Legacy config detected at '${legacyPath}' but new config '${newPath}' is missing. ` +
146
+ logger.warn(
147
+ `Legacy config detected at '${legacyPath}' but new config '${newPath}' is missing. ` +
145
148
  `Migration did not run — defaulting to yolo mode, which may downgrade your previous strict/auto setting. ` +
146
149
  `Remove the legacy file or move it to the new path after migrating.`,
147
150
  );
@@ -16,9 +16,12 @@
16
16
  // 纯 globalThis 反射实现:不静态 import pi-statusline(permission 的可选 peerDep,
17
17
  // 未安装时静态 import 会致整个 permission 加载失败),运行时结构兼容即可。
18
18
 
19
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
19
20
  import { MODE_LABELS, type PermissionMode } from "./types.js";
20
21
  import type { PermissionPalette } from "./statusline-palette.js";
21
22
 
23
+ const logger = getLogger("pi-permission");
24
+
22
25
  // ── 协议契约字面量(必须与 pi-statusline 的 footer-handshake-access.ts 完全一致)──
23
26
  // Symbol.for 字符串是跨包稳定契约:pi-statusline(owner) 与 permission(consumer) 通过同一
24
27
  // Symbol 读/写 globalThis slot。字符串不一致 → 握手静默失败(双方各持独立 slot,永不对齐)。
@@ -81,9 +84,10 @@ function readSlot(): FooterHandshakeSlot | undefined {
81
84
  if (!("version" in slot) || !("pending" in slot)) return undefined;
82
85
  const loose = slot as { version: unknown; pending: unknown };
83
86
  if (loose.version !== HANDSHAKE_VERSION) {
84
- console.warn(
85
- `[pi-permission] footer handshake version mismatch (got ${String(loose.version)}, expected ${HANDSHAKE_VERSION}); discarding and recreating.`,
86
- );
87
+ logger.warn("footer handshake version mismatch, discarding and recreating", {
88
+ got: String(loose.version),
89
+ expected: HANDSHAKE_VERSION,
90
+ });
87
91
  return undefined;
88
92
  }
89
93
  if (!Array.isArray(loose.pending)) return undefined;
package/src/index.ts CHANGED
@@ -18,9 +18,11 @@ import {
18
18
  type ExtensionContext,
19
19
  getAgentDir,
20
20
  } from "@earendil-works/pi-coding-agent";
21
-
21
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
22
22
  import { migrateLegacyConfig } from "@zhushanwen/pi-llm-shared";
23
23
 
24
+ const logger = getLogger("pi-permission");
25
+
24
26
  import { listAvailableModels } from "./classifier/model-resolver.js";
25
27
  import { handlePermissionCommand, handlePermissionModelCommand, handlePermissionRuleCommand } from "./commands.js";
26
28
  import { loadAndWatchConfig, saveConfig } from "./config.js";
@@ -45,8 +47,8 @@ import { paletteFromTheme, type PermissionPalette } from "./statusline-palette.j
45
47
  // 模块级 once flag 防同进程重复触发;agentDir 由 getAgentDir() 推导(尊重 PI_CODING_AGENT_DIR)。
46
48
  let configMigrationChecked = false;
47
49
 
48
- /** 默认配置 warning 回调(loadAndWatchConfig 共用,透传 console.warn)。 */
49
- const defaultConfigWarn = (msg: string): void => console.warn(msg);
50
+ /** 默认配置 warning 回调(loadAndWatchConfig 共用,透传 logger.warn)。 */
51
+ const defaultConfigWarn = (msg: string): void => logger.warn(msg);
50
52
 
51
53
  // ──────────────────────── tool_call event 最小子集 ────────────────────────
52
54
 
@@ -185,7 +187,7 @@ export default function permissionExtension(pi: ExtensionAPI): void {
185
187
  },
186
188
  config,
187
189
  {
188
- listModels: (pickerCtx) => listAvailableModels(pickerCtx, (m) => console.warn(m)),
190
+ listModels: (pickerCtx) => listAvailableModels(pickerCtx, (m) => logger.warn(m)),
189
191
  save: (newConfig) => {
190
192
  const r = saveConfig(newConfig);
191
193
  if (r.success) requestFooterRender();
@@ -352,7 +354,7 @@ async function processToolCall(
352
354
  } catch (error) {
353
355
  // fail-closed:异常 → block + reason(绝不放行)
354
356
  const msg = error instanceof Error ? error.message : String(error);
355
- console.warn(`[pi-permission] tool_call handler exception for ${toolName}: ${msg}`);
357
+ logger.warn("tool_call handler exception", { toolName, error: msg });
356
358
  return {
357
359
  block: true,
358
360
  reason: `[pi-permission] internal error (fail-closed): ${msg}`,
package/src/pipeline.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  * checkPermission 永不 throw(caller index.ts 的 tool_call handler 依赖此契约)。
17
17
  */
18
18
 
19
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
19
20
  import { matchRules } from "./rules/matcher.js";
20
21
  import { wildcardToRegExp } from "./rules/wildcard.js";
21
22
  import type {
@@ -39,6 +40,8 @@ import type {
39
40
  // pipeline.js import 这些类型,不破坏现有 import 路径)。
40
41
  export type { ApprovalRequest, CheckPermissionDeps } from "./types.js";
41
42
 
43
+ const logger = getLogger("pi-permission");
44
+
42
45
  // ──────────────────────── buildApprovalRequest ────────────────────────
43
46
 
44
47
  /**
@@ -533,9 +536,9 @@ function extractCommand(toolName: string, input: Record<string, unknown>): strin
533
536
  if (typeof cmd === "string") return cmd;
534
537
  // m3:bash + command 非字符串(异常输入)→ 记录警告,fail-closed 送下游
535
538
  // (不静默跳过 AST,避免危险命令因类型异常绕过检查)。
536
- console.warn(
537
- `[pi-permission] bash tool received non-string command (type=${typeof cmd}); ` +
538
- `skipping AST analysis (fail-closed: forwarded to downstream layers)`,
539
+ logger.warn(
540
+ "bash tool received non-string command (skipping AST analysis, fail-closed: forwarded to downstream layers)",
541
+ { type: typeof cmd },
539
542
  );
540
543
  return undefined;
541
544
  }
package/src/production.ts CHANGED
@@ -19,9 +19,12 @@
19
19
  */
20
20
 
21
21
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
22
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
22
23
  import { callLLM, resolveModel as resolveModelShared, type ModelSelector } from "@zhushanwen/pi-llm-shared";
23
24
 
24
25
  import { type ApprovalContext, requestUserApproval } from "./approval.js";
26
+
27
+ const logger = getLogger("pi-permission");
25
28
  import { analyzeBashStructure } from "./ast/index.js";
26
29
  import { createClassifier } from "./classifier/index.js";
27
30
  import type { CheckPermissionDeps } from "./pipeline.js";
@@ -65,7 +68,7 @@ export function createProductionClassifier(ctx: ExtensionContext): {
65
68
  return resolveModelShared(ctx, toSelector(config.model));
66
69
  },
67
70
  callLLM: (opts) => callLLM(ctx, opts),
68
- onLog: (msg: string) => console.warn(msg),
71
+ onLog: (msg: string) => logger.warn(msg),
69
72
  });
70
73
  }
71
74