pi-information-protecter 0.12.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog / 更新日志
2
2
 
3
+ ## 0.14.0 — Ask before passing attachments / 附件放行前先询问
4
+
5
+ ### Changed / 变更
6
+
7
+ - Ask before sending opaque image, audio, video or file content (default `attachments: "ask"`). An explicit per-request choice can allow the unchanged attachment or cancel it; `"allow"` and `"deny"` configure a permanent policy. Attachment-free requests continue through normal redaction.
8
+ 发送不透明的图片、音频、视频或文件内容前先询问(默认 `attachments: "ask"`)。用户可明确选择原样发送附件或取消请求;也可用 `"allow"` 和 `"deny"` 配置固定策略。没有附件的请求仍按原有流程脱敏。
9
+ - Merge attachment settings conservatively across configuration files: deny takes precedence over ask, which takes precedence over allow.
10
+ 多份配置合并附件策略时从严处理:拒绝优先于询问,询问优先于允许。
11
+
12
+ ## 0.13.0 — Capabilities, not a version allowlist / 以能力为准,而非版本白名单
13
+
14
+ ### Changed / 变更
15
+
16
+ - Replace the Pi version allowlist with a capability probe. At session start the extension checks what this Pi build actually exposes — `pi.on()`, `pi.registerCommand()`, `pi.registerMarkdownTransformer()`, `ctx.abort()`, `ctx.ui.notify()`, `ctx.ui.setStatus()` and `ctx.modelRegistry` — and only a missing capability stops it. The block names the missing capability and points at `https://github.com/taills/pi-information-protecter/issues/new`, because a user cannot fix Pi internals and a report is the only useful action. A Pi upgrade therefore keeps working instead of disabling protection on the day Pi releases a new minor.
17
+ 将 Pi 版本白名单改为能力探测。会话启动时检查当前 Pi 构建实际暴露的能力——`pi.on()`、`pi.registerCommand()`、`pi.registerMarkdownTransformer()`、`ctx.abort()`、`ctx.ui.notify()`、`ctx.ui.setStatus()` 与 `ctx.modelRegistry`——仅在能力缺失时停止运行。拦截时会列出缺失的能力并给出 `https://github.com/taills/pi-information-protecter/issues/new`,因为用户无法修改 Pi 内部实现,反馈是唯一有用的处理。因此 Pi 升级后扩展仍可用,不再一发新次版本就停止保护。
18
+ - Only capabilities whose absence would send plaintext can block. `waitForIdle`, `reload`, `ctx.ui.select`, `confirm` and `editor` belong to command interaction: missing ones degrade `/protecter`, so they no longer stop redaction.
19
+ 只有缺失会导致发送明文的能力才会拦截。`waitForIdle`、`reload`、`ctx.ui.select`、`confirm` 与 `editor` 属于命令交互,缺失只会削弱 `/protecter`,不再停止脱敏。
20
+ - Widen the declared peer range to `>=0.84.0` (excluding 0.85.0, 1.0.x and 1.1.0, which fail to load Pi itself). The diagnostic code `UNSUPPORTED_PI` becomes `PI_CAPABILITY`.
21
+ 将声明的 peer 范围扩宽为 `>=0.84.0`(排除自身无法加载的 0.85.0、1.0.x 与 1.1.0)。诊断码 `UNSUPPORTED_PI` 改为 `PI_CAPABILITY`。
22
+
23
+ ### Added / 新增
24
+
25
+ - Verify the request hook per call. Payload-carrying calls are passed through because `before_provider_request` redacts them and `stream`/`streamSimple` must stay synchronous — an assumption no version check could confirm. The runtime guard now wraps Pi's awaited `onPayload` and counts this extension's own hook runs around it; if the hook did not run, the guard redacts that payload itself and reports it once. A renamed, reordered or removed hook can no longer become a silent plaintext leak, which is what makes dropping the allowlist safe.
26
+ 逐次校验请求钩子。带 payload 钩子的调用之前直通,是因为 `before_provider_request` 会对其脱敏且 `stream`/`streamSimple` 必须保持同步——这是任何版本检查都无法证实的假设。现在运行时闸门会包装 Pi 所 await 的 `onPayload`,并在其前后统计本扩展钩子的执行次数;钩子未执行时由闸门自行脱敏并提示一次。钩子被改名、调序或移除不再可能变成静默的明文泄露,这正是可以去掉白名单的前提。
27
+ - Verify Pi 1.4.0 with the full suite and announce, once per session, any release beyond the verified set (`0.84.0-0.84.x, 0.85.1-0.87.x, 0.99.x, 1.2.0-1.4.x`) instead of refusing it.
28
+ 对 Pi 1.4.0 运行完整测试验证;超出已验证集合(`0.84.0-0.84.x、0.85.1-0.87.x、0.99.x、1.2.0-1.4.x`)的版本不再被拒绝,每会话提示一次。
29
+
3
30
  ## 0.12.0 — Offline rule testing without a model / 无需模型的离线规则测试
4
31
 
5
32
  ### Added / 新增
package/README.md CHANGED
@@ -41,11 +41,15 @@ If redaction itself fails for any reason, the request is **blocked**, never sent
41
41
 
42
42
  **Treat `/bug`, `/share` and `/export` as publishing plaintext.** To report a bug from a session that touched real secrets, use the local zip/export option, open it, and check the contents before sending it anywhere.
43
43
 
44
- Two more limits worth knowing: only text matching your rules is protected — not paraphrases, split characters or values the model infers — and requests containing images, audio, video or file blocks are rejected rather than sent, because they cannot be inspected.
44
+ Two more limits worth knowing: only text matching your rules is protected — not paraphrases, split characters or values the model infers — and attachments cannot be inspected or redacted. By default, the extension asks before sending them; allowing passes them through unchanged.
45
45
 
46
46
  ## Installation
47
47
 
48
- Requires Node.js 22+ and Pi (`@earendil-works/pi-coding-agent`) **0.84.0–0.84.x, 0.85.1–0.87.x, 0.99.x or 1.2.0–1.3.x**. Every version in that list was verified by running the full test suite against it; others are refused on purpose, because a wrong guess here means sending plaintext.
48
+ Requires Node.js 22+ and Pi (`@earendil-works/pi-coding-agent`) **0.84.0 or newer**. There is no version allowlist: at session start the extension probes the capabilities it protects with — `pi.on()`, `pi.registerCommand()`, `pi.registerMarkdownTransformer()`, `ctx.abort()`, `ctx.ui.notify()`, `ctx.ui.setStatus()` and `ctx.modelRegistry` — and refuses to run only when one is missing, naming it and pointing at [the issue tracker](https://github.com/taills/pi-information-protecter/issues/new). So a Pi upgrade keeps working, and a Pi that changed something this extension needs blocks requests instead of sending plaintext.
49
+
50
+ On top of that, every model call Pi routes through its own request hook is verified per call: the runtime guard counts this extension's hook runs around Pi's `onPayload`, and redacts the payload itself if that hook did not run. A renamed or removed hook therefore cannot turn into a silent leak.
51
+
52
+ **0.84.0–0.84.x, 0.85.1–0.87.x, 0.99.x and 1.2.0–1.4.x** have been verified by running the full test suite against them. A newer Pi is not refused; it reports once that it is beyond the verified set. Pi 0.85.0, 1.0.x and 1.1.0 fail to load Pi itself (an unresolvable internal import), so they are excluded from the declared peer range.
49
53
 
50
54
  ```bash
51
55
  # Install the latest release.
@@ -145,6 +149,14 @@ Never type a secret as a command argument — `test` is the one exception, becau
145
149
  /protecter test @./demo.txt
146
150
  ```
147
151
 
152
+ ### Attachments
153
+
154
+ The default `attachments: "ask"` prompts when a request contains opaque image, audio, video or file content. Allowing sends the request unchanged, without attachment inspection or redaction; declining cancels it. Choose `"allow"` to always pass attachments through, or `"deny"` to always cancel them. Text in requests without attachments continues through normal redaction.
155
+
156
+ ```json
157
+ { "version": 1, "attachments": "ask", "sensitiveWords": [] }
158
+ ```
159
+
148
160
  ### Context compaction
149
161
 
150
162
  When the context fills up, Pi summarizes the conversation. That request is built inside Pi and does **not** pass through the normal hook, so this extension redacts the conversation Pi is about to summarize and lets Pi summarize it with your current model. No second model is needed, and the saved summary keeps the fake values, so no plaintext summary is written into the session file.
@@ -243,7 +255,7 @@ Write fictional rules into `$PI_CODING_AGENT_DIR/protecter.json`, start Pi, and
243
255
  | `src/config.ts`, `src/migration.ts` | Parsing, validation, machine-hash configuration migration |
244
256
  | `src/guard.ts` | Blocks tool access to the configuration and audit log |
245
257
  | `src/diagnostics.ts`, `src/locale.ts` | Error codes, stages and bilingual messages |
246
- | `src/compat.ts` | The verified Pi version gate |
258
+ | `src/compat.ts` | The capability probe and the verified-release list |
247
259
 
248
260
  ### Debugging workflow
249
261
 
@@ -255,14 +267,14 @@ Write fictional rules into `$PI_CODING_AGENT_DIR/protecter.json`, start Pi, and
255
267
 
256
268
  ### Widening Pi compatibility
257
269
 
258
- `src/compat.ts` refuses any Pi release that has not been verified, because a wrong guess means sending plaintext rather than breaking a feature. To add a release, install it and run the full suite against it:
270
+ `src/compat.ts` holds the capability probe and the suite-verified list. The probe is the gate; the list only decides whether the "newer than verified" notice appears. To verify a new release, install it and run the full suite against it:
259
271
 
260
272
  ```bash
261
273
  npm i -D @earendil-works/pi-coding-agent@<version>
262
274
  npm run check
263
275
  ```
264
276
 
265
- Only then update `supportsPi`, `SUPPORTED_PI` and `peerDependencies`, keeping the runtime gate and the declared range identical.
277
+ Only then widen `isVerified` and `VERIFIED_PI`. Add a capability to `missingCapabilities` only when its absence would send plaintext: anything that merely degrades `/protecter` must not block protection.
266
278
 
267
279
  More detail: [`CONTRIBUTING.md`](CONTRIBUTING.md) for the bilingual and review rules, [`docs/BUILDING.md`](docs/BUILDING.md) for packaging, [`docs/RELEASING.md`](docs/RELEASING.md) for publishing, [`SECURITY.md`](SECURITY.md) for the threat model, [`CHANGELOG.md`](CHANGELOG.md) for version history.
268
280
 
@@ -313,11 +325,15 @@ Pi 没有为后三者提供任何钩子,因此本扩展包装会话共享的
313
325
 
314
326
  **请把 `/bug`、`/share`、`/export` 视为公开明文。** 若需要为接触过真实秘密的会话提交错误报告,请使用本地 zip/导出选项,打开它,确认内容之后再决定发给谁。
315
327
 
316
- 另外两点同样要紧:只有命中你的规则的文本会被保护——改写、片段值以及模型推断出的内容都不会;包含图片、音频、视频或文件块的请求会被拒绝而不是发送,因为它们无法被检查。
328
+ 另外两点同样要紧:只有命中你的规则的文本会被保护——改写、片段值以及模型推断出的内容都不会;附件无法被检查或脱敏,默认在发送前询问,允许后会原样放行。
317
329
 
318
330
  ## 安装
319
331
 
320
- 要求 Node.js 22+ 与 Pi(`@earendil-works/pi-coding-agent`)**0.84.0–0.84.x、0.85.1–0.87.x、0.99.x 或 1.2.0–1.3.x**。清单中的每个版本都经过完整测试验证;其余版本会被有意拒绝——此处判断失误意味着发送明文。
332
+ 要求 Node.js 22+ 与 Pi(`@earendil-works/pi-coding-agent`)**0.84.0 或更高**。不再使用版本白名单:会话启动时扩展会探测所依赖的能力——`pi.on()`、`pi.registerCommand()`、`pi.registerMarkdownTransformer()`、`ctx.abort()`、`ctx.ui.notify()`、`ctx.ui.setStatus()` 与 `ctx.modelRegistry`——仅在某项缺失时拖绝运行,并明确列出该能力与[问题反馈地址](https://github.com/taills/pi-information-protecter/issues/new)。因此 Pi 升级后仍可正常使用,而一旦 Pi 改动了本扩展所需的东西,会拦截请求而不是发送明文。
333
+
334
+ 在此之上,所有由 Pi 自身请求钩子转发的模型调用都会逐次校验:运行时闸门在 Pi 的 `onPayload` 前后统计本扩展钩子的执行次数,若未执行则由闸门自行脱敏。因此钩子被改名或移除也不会变成静默泄露。
335
+
336
+ **0.84.0–0.84.x、0.85.1–0.87.x、0.99.x 与 1.2.0–1.4.x** 已通过完整测试验证。更高版本不会被拒绝,只会提示一次已超出已验证范围。Pi 0.85.0、1.0.x 与 1.1.0 会因无法解析的内部引用而导致 Pi 自身加载失败,因此被排除在声明的 peer 范围外。
321
337
 
322
338
  ```bash
323
339
  # 安装最新版本。
@@ -419,6 +435,14 @@ pi update npm:pi-information-protecter
419
435
 
420
436
  不要把秘密作为命令参数输入。
421
437
 
438
+ ### 附件
439
+
440
+ 默认 `attachments: "ask"`:请求包含不透明的图片、音频、视频或文件内容时会询问。允许后请求将未经附件检查或脱敏而原样发送;拒绝则取消请求。设为 `"allow"` 可始终放行附件,设为 `"deny"` 则始终取消。没有附件的请求仍正常脱敏。
441
+
442
+ ```json
443
+ { "version": 1, "attachments": "ask", "sensitiveWords": [] }
444
+ ```
445
+
422
446
  ### 上下文压缩
423
447
 
424
448
  上下文写满时,Pi 会对对话生成摘要。该请求由 Pi 内部构造,**不经过**常规钩子,因此本扩展会先对 Pi 即将摘要的对话脱敏,再由 Pi 用你当前的模型生成摘要。不需要第二个模型;保存下来的摘要保留假值,因此会话文件中不会写入明文摘要。
@@ -517,7 +541,7 @@ pi -e ./src/index.ts
517
541
  | `src/config.ts`、`src/migration.ts` | 解析、校验、机器哈希配置迁移 |
518
542
  | `src/guard.ts` | 阻止工具访问配置与审计日志 |
519
543
  | `src/diagnostics.ts`、`src/locale.ts` | 错误码、阶段与双语文案 |
520
- | `src/compat.ts` | 已验证的 Pi 版本门禁 |
544
+ | `src/compat.ts` | 能力探测与已验证版本清单 |
521
545
 
522
546
  ### 调试流程
523
547
 
@@ -529,14 +553,14 @@ pi -e ./src/index.ts
529
553
 
530
554
  ### 扩大 Pi 兼容范围
531
555
 
532
- `src/compat.ts` 会拒绝任何未经验证的 Pi 版本,因为此处判断失误意味着发送明文,而不仅是功能失效。要新增一个版本,请安装它并在其上运行完整测试:
556
+ `src/compat.ts` 存放能力探测与已验证版本清单。门禁是能力探测,清单只决定是否出现“超出已验证范围”的提示。要验证一个新版本,请安装它并在其上运行完整测试:
533
557
 
534
558
  ```bash
535
559
  npm i -D @earendil-works/pi-coding-agent@<版本>
536
560
  npm run check
537
561
  ```
538
562
 
539
- 通过之后再更新 `supportsPi`、`SUPPORTED_PI` 与 `peerDependencies`,并保持运行时门禁与声明范围完全一致。
563
+ 通过之后再扩宽 `isVerified` 与 `VERIFIED_PI`。只有当某项能力缺失会导致发送明文时,才把它加入 `missingCapabilities`;仅削弱 `/protecter` 的能力不得拦截保护。
540
564
 
541
565
  更多资料:双语与评审规范见 [`CONTRIBUTING.md`](CONTRIBUTING.md),打包见 [`docs/BUILDING.md`](docs/BUILDING.md),发布见 [`docs/RELEASING.md`](docs/RELEASING.md),威胁模型见 [`SECURITY.md`](SECURITY.md),版本变更见 [`CHANGELOG.md`](CHANGELOG.md)。
542
566
 
package/SECURITY.md CHANGED
@@ -33,8 +33,8 @@ Prevent configured personal information from accidentally entering Pi LLM reques
33
33
  拦截诊断仅包含目录错误码、阶段、非负整数定位和白名单系统错误码。规则序号和节点数只是位置,不是内容。不输出原始异常消息、请求文本、命中原文、替换值、配置内容和文件路径,worker 边界同样适用,因此诊断可安全分享;错误码有意保持粗粒度,精确定位规则仍可能需要本地查看。
34
34
  - Limit worker regex scans to 2 seconds, with resource and input limits. Errors exclude original configuration, rules and request text.
35
35
  worker 正则扫描限制为 2 秒,并限制资源和输入;错误不包含原始配置、规则或请求文本。
36
- - Every supported Pi release, 0.84.0–0.87.x, 0.99.x and 1.2.0–1.3.x, catches `before_provider_request` errors and continues with the current payload. Error paths therefore return `{}` and attempt cancellation. This does not guarantee zero HTTP requests; it prevents this handler from falling back to the original payload.
37
- 所有受支持的 Pi 版本(0.84.0–0.87.x、0.99.x 与 1.2.0–1.3.x)都会捕获该钩子异常并继续使用当前请求体,因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求,只保证本处理器不回退原始请求体。
36
+ - Every verified Pi release, 0.84.0–0.87.x, 0.99.x and 1.2.0–1.4.x, catches `before_provider_request` errors and continues with the current payload. Error paths therefore return `{}` and attempt cancellation. This does not guarantee zero HTTP requests; it prevents this handler from falling back to the original payload.
37
+ 所有已验证的 Pi 版本(0.84.0–0.87.x、0.99.x 与 1.2.0–1.4.x)都会捕获该钩子异常并继续使用当前请求体,因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求,只保证本处理器不回退原始请求体。
38
38
  - Only the agent's own model requests carry the `onPayload` callback that fires `before_provider_request`. Every other provider call reaches the one `ModelRuntime` the session shares, so `session_start` wraps its `stream`, `streamSimple`, `classify` and `generateImages` methods: calls carrying the payload hook pass through untouched, because they are already redacted and their callers need the synchronous lazy stream, and every other call is redacted first, with a redaction failure cancelling it. This covers Pi's `/bug` summary, which has no extension event and whose command cannot be shadowed, plus nested calls from `codemode` scripts and other extensions. It wraps methods that are internal to Pi, so it is verified at startup and a runtime that cannot be wrapped fails closed with `GUARD_UNAVAILABLE` instead of running unprotected. MCP servers are external programs that receive tool arguments directly, which this does not change.
39
39
  只有智能体自身的模型请求会携带本扩展挂接的 `onPayload` 回调。循环之外的嵌套模型调用(如 `codemode` 脚本的 `models.classify()`、`models.generateImages()`、其他扩展的 `modelRegistry` 调用或任何直接 SDK 调用)不经脖敏即发出。Pi 0.99.0 将 `codemode`、MCP 与工具检索内置为扩展;`codemode` 仅在 `defaultTools` 或 `--tools` 启用时生效,MCP 服务器则是直接接收工具参数的外部程序。高风险环境下应将二者保持关闭。
40
40
  - Create configuration separately with Unix mode 0600. Reject links, non-regular files, multiple links and files owned by others. Users remain responsible for directories and OS permissions; TOCTOU races are not prevented.
@@ -49,8 +49,8 @@ Prevent configured personal information from accidentally entering Pi LLM reques
49
49
  压缩、split-turn 前缀摘要与 `/tree` 分支摘要都由 Pi 构造,且不传 `onPayload` 回调,`before_provider_request` 对其不会触发。但 Pi 交给钩子的 `preparation` 正是它随后用于摘要的同一对象,因此对其原地脱敏,再由 Pi 用会话自身的模型对脱敏文本生成摘要;路由、凭证、重试与 token 预算仍由 Pi 负责。保存的摘要保留替换值,因此不会向会话写入明文摘要。任何失败都取消压缩,而不让 Pi 对已存明文生成摘要。当 `/compact <指令>` 的指令含受保护值时取消,因为 Pi 从自身状态读取该指令,脱敏副本不会被采用。
50
50
  - Transfers that are not model calls are outside this boundary: `/bug`'s uploaded bundle, `/share` and `/export` send the stored transcript over their own HTTP paths, which no extension API can intercept, and that transcript holds restored plaintext. Only the `/bug` summarization request is covered. Review what these produce before sending them.
51
51
  非模型调用的传输不在此边界内:`/bug` 上传的报告包、`/share` 与 `/export` 通过各自的 HTTP 路径发送已保存的会话记录,任何扩展 API 都无法拦截,而该记录中保存的是已还原的明文。仅 `/bug` 的摘要请求在覆盖范围内。发送前请先检查这些产物的内容。
52
- - Summarized `/tree` navigation takes the same internal path and is redacted the same way; that event does read its instructions back, so they are returned redacted rather than cancelling. Do not widen Pi compatibility without verification: refusing a working release blocks every request, and accepting a broken one can send plaintext. Each supported version is established by running the full suite against it, and the runtime gate and declared peer range are kept identical.
53
- 带摘要的 `/tree` 导航走同一条内部路径,以同样方式脱敏;该事件会读回指令,因此返回脱敏后的指令而非取消。未经验证不扩大 Pi 兼容范围:误拒可用版本会拦截所有请求,误受损坏版本则可能发送明文。每个受支持版本均以完整测试验证,并保持运行时门禁与声明的 peer 范围一致。
52
+ - Summarized `/tree` navigation takes the same internal path and is redacted the same way; that event does read its instructions back, so they are returned redacted rather than cancelling. Pi compatibility is decided by a capability probe at session start, not a version allowlist: a missing capability is named and blocks every request, while a Pi release this extension has not been tested against still runs and reports once that it is beyond the verified set. Passthrough of payload-hook calls is verified per call by counting this extension's hook runs around Pi's awaited `onPayload`, and the runtime guard redacts the payload itself when that hook did not run, so a renamed or removed hook cannot become a silent leak.
53
+ 带摘要的 `/tree` 导航走同一条内部路径,以同样方式脱敏;该事件会读回指令,因此返回脱敏后的指令而非取消。Pi 兼容性由会话启动时的能力探测判定,而非版本白名单:能力缺失会被明确列出并拦截所有请求;尚未测试过的 Pi 版本仍可运行,只提示一次已超出已验证范围。带 payload 钩子的调用则逐次校验:在 Pi 所 await 的 `onPayload` 前后统计本扩展钩子的执行次数,钩子未执行时由运行时闸门自行脱敏,因此钩子被改名或移除不会变成静默泄露。
54
54
 
55
55
  ## Fixed aliases and caching / 固定别名与缓存
56
56
 
package/dist/index.mjs CHANGED
@@ -1,29 +1,31 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{getAgentDir as _t,VERSION as Rt}from"@earendil-works/pi-coding-agent";import{Worker as gt}from"node:worker_threads";import{fileURLToPath as ht}from"node:url";import*as I from"node:fs";import{join as D,resolve as ot}from"node:path";import{createHash as it,randomUUID as st}from"node:crypto";import{Worker as at}from"node:worker_threads";import{fileURLToPath as ct}from"node:url";var M="0.84.0-0.84.x, 0.85.1-0.87.x, 0.99.x, 1.2.0-1.3.x";function ce(t){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(t);if(!e)return!1;let[r,n,o]=e.slice(1).map(Number);return r===1?n===2||n===3:r!==0?!1:n===84?!0:n===85?o>=1:n===86||n===87||n===99}var K={INTERNAL:["Unexpected local failure","Reload; check the installed package integrity"],NOT_READY:["Protection is not initialized","Run /protecter status for the initialization error, fix it, then /reload"],UNSUPPORTED_PI:["Unsupported Pi version",`Use Pi ${M}`],GUARD_UNAVAILABLE:["Cannot cover the model calls that have no extension hook","This Pi build exposes no model runtime to wrap; use Pi "+M+" or set a trusted Pi version"],MACHINE_ID:["Machine identifier unavailable","Check local OS machine-ID access; no random fallback is used"],CONFIG_JSON:["Invalid configuration JSON","Fix JSON syntax locally, then /reload"],CONFIG_SCHEMA:["Invalid configuration structure or rule","Use version 1, a sensitiveWords array and documented fields; rule shows the 1-based index"],CONFIG_REGEX:["Invalid regex or flags","Fix the numbered rule; flags support gimsu only"],CONFIG_TARGET:["Invalid fixed replacement target","Use a non-blank string under 8192 characters without reserved __PIP_ names"],CONFIG_CONFLICT:["Conflicting rule definitions","Give the same matcher one replacement policy across all configs"],CONFIG_SIZE:["Configuration exceeds size or rule limit","Reduce configuration size or rule count; rules are never truncated"],CONFIG_IO:["Cannot access configuration","Check local permissions, ownership and free space"],CONFIG_UNSAFE:["Unsafe configuration file","Use an owned regular file without symbolic or hard links"],CONFIG_EMPTY_MATCH:["Regex matches an empty string","Require a non-empty match in the numbered rule"],CONFIG_SENSITIVE_TARGET:["Fixed target overlaps a sensitive rule","Choose a target unrelated to every sensitive matcher"],CONFIG_ALIAS_CONFLICT:["Fixed aliases overlap each other","Use distinct aliases that are not substrings of one another"],CONFIG_TIMEOUT:["Rule validation timed out","Simplify regex rules, then /reload"],MIGRATION_LOCKED:["Configuration migration lock is busy","Wait for other Pi processes; inspect a stale lock only after stopping them"],MIGRATION_CHANGED:["Configuration changed during migration","Stop concurrent edits and /reload; source files are preserved"],MIGRATION_LIMIT:["Too many configuration candidates","Consolidate old configuration files locally"],MIGRATION_IO:["Migration file operation failed","Check permissions, free space and leftover files before retrying"],PAYLOAD_JSON:["Request is not serializable JSON","Inspect the provider or another payload-modifying extension; no payload is logged"],PAYLOAD_SIZE:["Request exceeds the size limit","Reduce history or tool output size, or start a new session"],SCAN_TIMEOUT:["Redaction worker timed out","Simplify regex rules or reduce input size"],WORKER_FAILED:["Worker failed to start or exited early","Verify all dist files and the Node version; reinstall if needed"],SCAN_INTERNAL:["Unexpected scan failure","Review recent rule changes and retry with smaller input"],SCAN_NUMBER_UNSAFE:["Replacement would turn a numeric JSON value into an invalid number","Narrow the rule so it does not match inside numeric fields such as timestamps or schema limits"],SCAN_UNIQUE_FAILED:["Could not generate a unique shape-preserving replacement","The matched value is too short; match a longer value or set an explicit replacement"],SCAN_ZERO_WIDTH:["Regex produced a zero-width match","Require a non-empty match in the numbered rule"],SCAN_MATCH_LIMIT:["Too many matches in one request","Narrow overly broad rules or reduce input"],SCAN_MAPPING_LIMIT:["Mapping limit reached","Start a new session or /reload; in-memory mappings reset"],FIXED_MAPPING_CONFLICT:["One original has conflicting replacements","Use one replacement for overlapping matchers, then /reload"],FIXED_ALIAS_REUSED:["Fixed alias already maps to another original","A fixed regex represents one original per instance; use random replacement or split literal rules"],FIXED_SENSITIVE_TARGET:["Replacement contains the matched original","Choose a target without sensitive content"],FIXED_OVERLAP:["Fixed replacement partially overlaps another match","Remove the partial overlap or use random rules; rule and otherRule show both rules"],FIXED_ALIAS_CROSSING:["Regex crosses a learned fixed alias","Narrow the regex or choose a different alias"],SCAN_COMPLEXITY:["Request exceeds depth or node limits","Reduce nested data and tool schema size"],SCAN_JSON_REWRITE:["Replacement breaks embedded JSON syntax","Avoid rules spanning JSON syntax; match field values instead"],SCAN_ATTACHMENT:["Unsupported image, audio, video or file content","Remove attachments or send reviewed text only"],SCAN_KEY_COLLISION:["Replacement creates duplicate object keys","Use distinct targets or avoid matching schema keys"],RESTORE_COMPLEXITY:["Response nesting is too deep to restore","Reduce nested structures or start a new session with /new"],RESTORE_KEY_COLLISION:["Restoring produced duplicate object keys","Use more specific rules or fixed targets that cannot collide with schema keys"],COMPACT_DENIED:["Compaction refused",'Allow it when asked, or set "compaction" to "protected" in the local config'],COMPACT_FAILED:["Could not redact what Pi is about to summarize",'Retry; if it persists, set "compaction" to "off" and use /new'],COMPACT_INSTRUCTIONS:["The compaction instruction contains a protected value","Pi reads that instruction from its own state, so it cannot be redacted: run /compact without it"],CONFIG_COMPACTION:["Invalid compaction setting",'Use "ask", "protected" or "off", or an object with mode'],AUDIT_BATCH_LIMIT:["Audit batch exceeds the size limit","Reduce matched content per request"],AUDIT_FULL:["Audit log would exceed the size limit","Review records, then run /protecter logs clear or archive locally"],AUDIT_PARTIAL:["Audit log has an incomplete last line","Inspect locally or run /protecter logs clear; never upload the log"],AUDIT_LOCKED:["Audit lock stayed busy","Wait for writers; stop all Pi processes before inspecting a stale lock"],AUDIT_UNSAFE:["Unsafe audit file","Use an owned regular file without links; checks are not bypassed"],AUDIT_IO:["Audit file operation failed","Check errno, permissions and free space; no path or content is exposed"],TEST_INPUT:["Cannot read the offline test input","Pass text, or @<path> pointing to a readable regular text file"],TEST_SIZE:["Offline test input exceeds the size limit","Test a smaller file or a shorter excerpt"]},Je={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["Pi 版本不受支持",`请使用 Pi ${M}`],GUARD_UNAVAILABLE:["无法覆盖没有扩展钩子的模型调用","该 Pi 构建未暴露可包装的模型运行时:请使用 Pi "+M+" 或改用可信的 Pi 版本"],MACHINE_ID:["无法获取机器标识","检查系统机器标识访问权限,不使用随机回退"],CONFIG_JSON:["配置 JSON 无效","本地修复 JSON 语法后重载"],CONFIG_SCHEMA:["配置结构或规则无效","使用版本1、敏感词数组及文档字段,rule 为规则序号"],CONFIG_REGEX:["正则或标志无效","修复指定规则,标志仅支持 gimsu"],CONFIG_TARGET:["固定替换目标无效","使用非空且少于8192字符的目标,不含保留名称"],CONFIG_CONFLICT:["规则定义冲突","为同一匹配器设置一致的替换策略"],CONFIG_SIZE:["配置超过体积或规则数上限","减少配置体积或规则数,规则不会被截断"],CONFIG_IO:["无法访问配置","检查本地权限、所有者和磁盘空间"],CONFIG_UNSAFE:["配置文件不安全","使用本人所有的普通文件,不含符号或硬链接"],CONFIG_EMPTY_MATCH:["正则匹配空字符串","修改指定规则,要求非空匹配"],CONFIG_SENSITIVE_TARGET:["固定目标与敏感规则重叠","选择与所有敏感匹配器无关的目标"],CONFIG_ALIAS_CONFLICT:["固定别名相互重叠","使用互不包含的不同别名"],CONFIG_TIMEOUT:["规则验证超时","简化正则规则后重载"],MIGRATION_LOCKED:["配置迁移锁被占用","等待其他进程,全部停止后再检查残留锁"],MIGRATION_CHANGED:["迁移期间配置发生变化","停止并发编辑后重载,源文件会保留"],MIGRATION_LIMIT:["候选配置过多","在本地整理旧配置文件"],MIGRATION_IO:["迁移文件操作失败","检查权限、空间和残留文件后重试"],PAYLOAD_JSON:["请求无法序列化为 JSON","检查提供商或其他请求改写扩展,不记录请求内容"],PAYLOAD_SIZE:["请求超过体积上限","减少历史或工具输出,或新建会话"],SCAN_TIMEOUT:["脱敏 worker 超时","简化正则规则或缩小输入"],WORKER_FAILED:["worker 启动失败或提前退出","检查 dist 文件完整性和 Node 版本,必要时重装"],SCAN_INTERNAL:["未预期的扫描错误","检查最近的规则改动并缩小输入"],SCAN_NUMBER_UNSAFE:["替换会把 JSON 数值变成无效数字","缩小规则范围,避免匹配时间戳或结构上限等数值字段"],SCAN_UNIQUE_FAILED:["无法生成唯一的同形替换值","命中内容过短或可选空间过小,请匹配更长内容或设置固定 replacement"],SCAN_ZERO_WIDTH:["正则产生零宽匹配","修改指定规则,要求非空匹配"],SCAN_MATCH_LIMIT:["单次请求匹配次数过多","缩小过宽规则或减少输入"],SCAN_MAPPING_LIMIT:["映射数量达到上限","新建会话或重载,内存映射将重置"],FIXED_MAPPING_CONFLICT:["同一原文存在冲突替换","为重叠匹配器使用同一目标后重载"],FIXED_ALIAS_REUSED:["固定别名已映射其他原文","固定正则在同一实例仅代表一种原文,改用随机替换或拆分字面规则"],FIXED_SENSITIVE_TARGET:["替换目标包含命中原文","选择不含敏感内容的目标"],FIXED_OVERLAP:["固定替换与其他匹配部分重叠","消除部分重叠或改用随机规则,rule 与 otherRule 指出两条规则"],FIXED_ALIAS_CROSSING:["正则跨越已学习的固定别名","缩小正则范围或更换别名"],SCAN_COMPLEXITY:["请求超过层级或节点上限","减少嵌套数据和工具结构体积"],SCAN_JSON_REWRITE:["替换破坏内嵌 JSON 语法","避免跨 JSON 语法的规则,改为匹配字段值"],SCAN_ATTACHMENT:["不支持图片、音频、视频或文件内容","移除附件或仅发送已审查的文本"],SCAN_KEY_COLLISION:["替换后对象键重复","使用不同目标或避免匹配结构键"],RESTORE_COMPLEXITY:["还原响应时层级过深","减少嵌套结构,或使用 /new 开始新会话"],RESTORE_KEY_COLLISION:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],COMPACT_DENIED:["压缩被拒绝",'询问时选择允许,或在本地配置中将 "compaction" 设为 "protected"'],COMPACT_FAILED:["无法对 Pi 即将摘要的内容脱敏",'请重试;若持续失败,将 "compaction" 设为 "off" 并使用 /new'],COMPACT_INSTRUCTIONS:["压缩指令中包含受保护值","Pi 从自身状态读取该指令,无法脱敏:请不带该指令执行 /compact"],CONFIG_COMPACTION:["compaction 配置值无效",'请使用 "ask"、"protected"、"off",或含 mode 的对象'],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"],TEST_INPUT:["无法读取离线测试输入","请直接传入文本,或用 @<路径> 指向可读的普通文本文件"],TEST_SIZE:["离线测试输入超出大小上限","改用更小的文件或更短的片段"]},le={blockedHeader:"SPI Protecter blocked this request",reasonLabel:"Reason",actionLabel:"Action",detailsLabel:"Details",statusReady:"ready",statusNotReady:"not ready",statusLine:"SPI Protecter: {state}; {rules}; {mappings}. Memory snapshot; /reload to refresh.",statusLastBlock:"Last block @ {at}:",statusNoBlock:"No recorded block in this session.",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · not ready",emptyRules:"SPI Protecter: no rules configured; edit the local machine config, then /reload.",commandDescription:"View local SPI protection records",usage:"Usage: /protecter [logs [1-100|clear]|status|reload|test <text>|test @<path>]. Only `test` takes content; never type secrets as other arguments.",testPreviewTitle:"Offline redaction test; local only, edits are discarded",testRedacted:"SPI Protecter: redacted locally ({chars}); nothing was sent to a model. Replacements are now learned for this session.",testUnchanged:"SPI Protecter: no rule matched ({chars}); the text below is unchanged.",testFileLabel:"Input: local file",testTextLabel:"Input: inline text",logsTuiOnly:"Open the local TUI to view protection records.",logsNone:"No readable records.",logsSelectTitle:"Protection records",logsTruncated:" (limited tail)",logsRevealTitle:"Sensitive plaintext",logsRevealBody:"Reveal locally? Never share this view.",logsPreviewTitle:"Local preview; edits are discarded",logsPreviewTruncated:"[Preview truncated]",clearTitle:"Clear the current audit log?",clearBody:"Irreversible. Only this machine's log is cleared; configuration and in-memory mappings stay. Later requests may add new records.",cleared:"Audit log cleared: {bytes}.",toolBlocked:"SPI Protecter blocked tool access to the protected configuration or audit log; edit it locally.",compactAskTitle:"Compact this conversation?",compactAskBody:`Pi builds the summarization request internally and never routes it through this extension's redaction, so letting Pi summarize as-is would send the conversation, including protected values, as plaintext.
2
+ import{getAgentDir as $t,VERSION as et}from"@earendil-works/pi-coding-agent";import{Worker as kt}from"node:worker_threads";import{fileURLToPath as Dt}from"node:url";import*as O from"node:fs";import{join as x,resolve as At}from"node:path";import{createHash as Ot,randomUUID as wt}from"node:crypto";import{Worker as Nt}from"node:worker_threads";import{fileURLToPath as Ct}from"node:url";var _="https://github.com/taills/pi-information-protecter/issues/new",he="0.84.0-0.84.x, 0.85.1-0.87.x, 0.99.x, 1.2.0-1.4.x",nt=["on","registerCommand","registerMarkdownTransformer"],ot=["abort"],rt=["notify","setStatus"];function ee(t,e){return!t||typeof t!="object"&&typeof t!="function"?!1:typeof t[e]=="function"}function te(t,e){let o=[];for(let r of nt)ee(t,r)||o.push(`pi.${r}()`);if(e===void 0)return o;let n=e;for(let r of ot)ee(n,r)||o.push(`ctx.${r}()`);for(let r of rt)ee(n.ui,r)||o.push(`ctx.ui.${r}()`);return(!n.modelRegistry||typeof n.modelRegistry!="object")&&o.push("ctx.modelRegistry"),o}function ye(t){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(t);if(!e)return!1;let[o,n,r]=e.slice(1).map(Number);return o===1?n>=2&&n<=4:o!==0?!1:n===84?!0:n===85?r>=1:n===86||n===87||n===99}var ne={INTERNAL:["Unexpected local failure","Reload; check the installed package integrity"],NOT_READY:["Protection is not initialized","Run /protecter status for the initialization error, fix it, then /reload"],PI_CAPABILITY:["This Pi build is missing a capability this extension requires",`Update Pi; if the newest Pi still fails, report the listed capability at ${_}`],GUARD_UNAVAILABLE:["Cannot cover the model calls that have no extension hook","This Pi build exposes no model runtime to wrap; update Pi, or report this build at "+_],MACHINE_ID:["Machine identifier unavailable","Check local OS machine-ID access; no random fallback is used"],CONFIG_JSON:["Invalid configuration JSON","Fix JSON syntax locally, then /reload"],CONFIG_SCHEMA:["Invalid configuration structure or rule","Use version 1, a sensitiveWords array and documented fields; rule shows the 1-based index"],CONFIG_REGEX:["Invalid regex or flags","Fix the numbered rule; flags support gimsu only"],CONFIG_TARGET:["Invalid fixed replacement target","Use a non-blank string under 8192 characters without reserved __PIP_ names"],CONFIG_CONFLICT:["Conflicting rule definitions","Give the same matcher one replacement policy across all configs"],CONFIG_SIZE:["Configuration exceeds size or rule limit","Reduce configuration size or rule count; rules are never truncated"],CONFIG_IO:["Cannot access configuration","Check local permissions, ownership and free space"],CONFIG_UNSAFE:["Unsafe configuration file","Use an owned regular file without symbolic or hard links"],CONFIG_EMPTY_MATCH:["Regex matches an empty string","Require a non-empty match in the numbered rule"],CONFIG_SENSITIVE_TARGET:["Fixed target overlaps a sensitive rule","Choose a target unrelated to every sensitive matcher"],CONFIG_ALIAS_CONFLICT:["Fixed aliases overlap each other","Use distinct aliases that are not substrings of one another"],CONFIG_TIMEOUT:["Rule validation timed out","Simplify regex rules, then /reload"],MIGRATION_LOCKED:["Configuration migration lock is busy","Wait for other Pi processes; inspect a stale lock only after stopping them"],MIGRATION_CHANGED:["Configuration changed during migration","Stop concurrent edits and /reload; source files are preserved"],MIGRATION_LIMIT:["Too many configuration candidates","Consolidate old configuration files locally"],MIGRATION_IO:["Migration file operation failed","Check permissions, free space and leftover files before retrying"],PAYLOAD_JSON:["Request is not serializable JSON","Inspect the provider or another payload-modifying extension; no payload is logged"],PAYLOAD_SIZE:["Request exceeds the size limit","Reduce history or tool output size, or start a new session"],SCAN_TIMEOUT:["Redaction worker timed out","Simplify regex rules or reduce input size"],WORKER_FAILED:["Worker failed to start or exited early","Verify all dist files and the Node version; reinstall if needed"],SCAN_INTERNAL:["Unexpected scan failure","Review recent rule changes and retry with smaller input"],SCAN_NUMBER_UNSAFE:["Replacement would turn a numeric JSON value into an invalid number","Narrow the rule so it does not match inside numeric fields such as timestamps or schema limits"],SCAN_UNIQUE_FAILED:["Could not generate a unique shape-preserving replacement","The matched value is too short; match a longer value or set an explicit replacement"],SCAN_ZERO_WIDTH:["Regex produced a zero-width match","Require a non-empty match in the numbered rule"],SCAN_MATCH_LIMIT:["Too many matches in one request","Narrow overly broad rules or reduce input"],SCAN_MAPPING_LIMIT:["Mapping limit reached","Start a new session or /reload; in-memory mappings reset"],FIXED_MAPPING_CONFLICT:["One original has conflicting replacements","Use one replacement for overlapping matchers, then /reload"],FIXED_ALIAS_REUSED:["Fixed alias already maps to another original","A fixed regex represents one original per instance; use random replacement or split literal rules"],FIXED_SENSITIVE_TARGET:["Replacement contains the matched original","Choose a target without sensitive content"],FIXED_OVERLAP:["Fixed replacement partially overlaps another match","Remove the partial overlap or use random rules; rule and otherRule show both rules"],FIXED_ALIAS_CROSSING:["Regex crosses a learned fixed alias","Narrow the regex or choose a different alias"],SCAN_COMPLEXITY:["Request exceeds depth or node limits","Reduce nested data and tool schema size"],SCAN_JSON_REWRITE:["Replacement breaks embedded JSON syntax","Avoid rules spanning JSON syntax; match field values instead"],SCAN_ATTACHMENT:["Unsupported image, audio, video or file content","Remove attachments or send reviewed text only"],SCAN_KEY_COLLISION:["Replacement creates duplicate object keys","Use distinct targets or avoid matching schema keys"],RESTORE_COMPLEXITY:["Response nesting is too deep to restore","Reduce nested structures or start a new session with /new"],RESTORE_KEY_COLLISION:["Restoring produced duplicate object keys","Use more specific rules or fixed targets that cannot collide with schema keys"],COMPACT_DENIED:["Compaction refused",'Allow it when asked, or set "compaction" to "protected" in the local config'],COMPACT_FAILED:["Could not redact what Pi is about to summarize",'Retry; if it persists, set "compaction" to "off" and use /new'],COMPACT_INSTRUCTIONS:["The compaction instruction contains a protected value","Pi reads that instruction from its own state, so it cannot be redacted: run /compact without it"],CONFIG_COMPACTION:["Invalid compaction setting",'Use "ask", "protected" or "off", or an object with mode'],CONFIG_ATTACHMENT:["Invalid attachment setting",'Use "ask", "allow" or "deny"'],AUDIT_BATCH_LIMIT:["Audit batch exceeds the size limit","Reduce matched content per request"],AUDIT_FULL:["Audit log would exceed the size limit","Review records, then run /protecter logs clear or archive locally"],AUDIT_PARTIAL:["Audit log has an incomplete last line","Inspect locally or run /protecter logs clear; never upload the log"],AUDIT_LOCKED:["Audit lock stayed busy","Wait for writers; stop all Pi processes before inspecting a stale lock"],AUDIT_UNSAFE:["Unsafe audit file","Use an owned regular file without links; checks are not bypassed"],AUDIT_IO:["Audit file operation failed","Check errno, permissions and free space; no path or content is exposed"],TEST_INPUT:["Cannot read the offline test input","Pass text, or @<path> pointing to a readable regular text file"],TEST_SIZE:["Offline test input exceeds the size limit","Test a smaller file or a shorter excerpt"]},it={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],PI_CAPABILITY:["当前 Pi 构建缺少本扩展所需的能力",`请升级 Pi;若最新版 Pi 仍然失败,请将所列能力反馈至 ${_}`],GUARD_UNAVAILABLE:["无法覆盖没有扩展钩子的模型调用","该 Pi 构建未暴露可包装的模型运行时:请升级 Pi,或将该构建反馈至 "+_],MACHINE_ID:["无法获取机器标识","检查系统机器标识访问权限,不使用随机回退"],CONFIG_JSON:["配置 JSON 无效","本地修复 JSON 语法后重载"],CONFIG_SCHEMA:["配置结构或规则无效","使用版本1、敏感词数组及文档字段,rule 为规则序号"],CONFIG_REGEX:["正则或标志无效","修复指定规则,标志仅支持 gimsu"],CONFIG_TARGET:["固定替换目标无效","使用非空且少于8192字符的目标,不含保留名称"],CONFIG_CONFLICT:["规则定义冲突","为同一匹配器设置一致的替换策略"],CONFIG_SIZE:["配置超过体积或规则数上限","减少配置体积或规则数,规则不会被截断"],CONFIG_IO:["无法访问配置","检查本地权限、所有者和磁盘空间"],CONFIG_UNSAFE:["配置文件不安全","使用本人所有的普通文件,不含符号或硬链接"],CONFIG_EMPTY_MATCH:["正则匹配空字符串","修改指定规则,要求非空匹配"],CONFIG_SENSITIVE_TARGET:["固定目标与敏感规则重叠","选择与所有敏感匹配器无关的目标"],CONFIG_ALIAS_CONFLICT:["固定别名相互重叠","使用互不包含的不同别名"],CONFIG_TIMEOUT:["规则验证超时","简化正则规则后重载"],MIGRATION_LOCKED:["配置迁移锁被占用","等待其他进程,全部停止后再检查残留锁"],MIGRATION_CHANGED:["迁移期间配置发生变化","停止并发编辑后重载,源文件会保留"],MIGRATION_LIMIT:["候选配置过多","在本地整理旧配置文件"],MIGRATION_IO:["迁移文件操作失败","检查权限、空间和残留文件后重试"],PAYLOAD_JSON:["请求无法序列化为 JSON","检查提供商或其他请求改写扩展,不记录请求内容"],PAYLOAD_SIZE:["请求超过体积上限","减少历史或工具输出,或新建会话"],SCAN_TIMEOUT:["脱敏 worker 超时","简化正则规则或缩小输入"],WORKER_FAILED:["worker 启动失败或提前退出","检查 dist 文件完整性和 Node 版本,必要时重装"],SCAN_INTERNAL:["未预期的扫描错误","检查最近的规则改动并缩小输入"],SCAN_NUMBER_UNSAFE:["替换会把 JSON 数值变成无效数字","缩小规则范围,避免匹配时间戳或结构上限等数值字段"],SCAN_UNIQUE_FAILED:["无法生成唯一的同形替换值","命中内容过短或可选空间过小,请匹配更长内容或设置固定 replacement"],SCAN_ZERO_WIDTH:["正则产生零宽匹配","修改指定规则,要求非空匹配"],SCAN_MATCH_LIMIT:["单次请求匹配次数过多","缩小过宽规则或减少输入"],SCAN_MAPPING_LIMIT:["映射数量达到上限","新建会话或重载,内存映射将重置"],FIXED_MAPPING_CONFLICT:["同一原文存在冲突替换","为重叠匹配器使用同一目标后重载"],FIXED_ALIAS_REUSED:["固定别名已映射其他原文","固定正则在同一实例仅代表一种原文,改用随机替换或拆分字面规则"],FIXED_SENSITIVE_TARGET:["替换目标包含命中原文","选择不含敏感内容的目标"],FIXED_OVERLAP:["固定替换与其他匹配部分重叠","消除部分重叠或改用随机规则,rule 与 otherRule 指出两条规则"],FIXED_ALIAS_CROSSING:["正则跨越已学习的固定别名","缩小正则范围或更换别名"],SCAN_COMPLEXITY:["请求超过层级或节点上限","减少嵌套数据和工具结构体积"],SCAN_JSON_REWRITE:["替换破坏内嵌 JSON 语法","避免跨 JSON 语法的规则,改为匹配字段值"],SCAN_ATTACHMENT:["不支持图片、音频、视频或文件内容","移除附件或仅发送已审查的文本"],SCAN_KEY_COLLISION:["替换后对象键重复","使用不同目标或避免匹配结构键"],RESTORE_COMPLEXITY:["还原响应时层级过深","减少嵌套结构,或使用 /new 开始新会话"],RESTORE_KEY_COLLISION:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],COMPACT_DENIED:["压缩被拒绝",'询问时选择允许,或在本地配置中将 "compaction" 设为 "protected"'],COMPACT_FAILED:["无法对 Pi 即将摘要的内容脱敏",'请重试;若持续失败,将 "compaction" 设为 "off" 并使用 /new'],COMPACT_INSTRUCTIONS:["压缩指令中包含受保护值","Pi 从自身状态读取该指令,无法脱敏:请不带该指令执行 /compact"],CONFIG_COMPACTION:["compaction 配置值无效",'请使用 "ask"、"protected"、"off",或含 mode 的对象'],CONFIG_ATTACHMENT:["附件配置值无效",'请使用 "ask"、"allow" 或 "deny"'],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"],TEST_INPUT:["无法读取离线测试输入","请直接传入文本,或用 @<路径> 指向可读的普通文本文件"],TEST_SIZE:["离线测试输入超出大小上限","改用更小的文件或更短的片段"]},Ie={blockedHeader:"SPI Protecter blocked this request",reasonLabel:"Reason",actionLabel:"Action",detailsLabel:"Details",statusReady:"ready",statusNotReady:"not ready",statusLine:"SPI Protecter: {state}; {rules}; {mappings}. Memory snapshot; /reload to refresh.",statusLastBlock:"Last block @ {at}:",statusNoBlock:"No recorded block in this session.",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · not ready",emptyRules:"SPI Protecter: no rules configured; edit the local machine config, then /reload.",commandDescription:"View local SPI protection records",usage:"Usage: /protecter [logs [1-100|clear]|status|reload|test <text>|test @<path>]. Only `test` takes content; never type secrets as other arguments.",testPreviewTitle:"Offline redaction test; local only, edits are discarded",testRedacted:"SPI Protecter: redacted locally ({chars}); nothing was sent to a model. Replacements are now learned for this session.",testUnchanged:"SPI Protecter: no rule matched ({chars}); the text below is unchanged.",testFileLabel:"Input: local file",testTextLabel:"Input: inline text",capabilityBlocked:"SPI Protecter: this Pi build does not expose {names}, so protection is off and requests are blocked instead of sent as plaintext. Report it: {url}",piNewer:"SPI Protecter: Pi {version} is newer than the suite-verified releases ({verified}). Every required hook is present, so protection is active and each request is verified; report anything that looks wrong: {url}",hookBypassed:"SPI Protecter: Pi did not run the request hook for a model call, so the runtime guard redacted it instead. Protection held, but please report it: {url}",logsTuiOnly:"Open the local TUI to view protection records.",logsNone:"No readable records.",logsSelectTitle:"Protection records",logsTruncated:" (limited tail)",logsRevealTitle:"Sensitive plaintext",logsRevealBody:"Reveal locally? Never share this view.",logsPreviewTitle:"Local preview; edits are discarded",logsPreviewTruncated:"[Preview truncated]",clearTitle:"Clear the current audit log?",clearBody:"Irreversible. Only this machine's log is cleared; configuration and in-memory mappings stay. Later requests may add new records.",cleared:"Audit log cleared: {bytes}.",toolBlocked:"SPI Protecter blocked tool access to the protected configuration or audit log; edit it locally.",compactAskTitle:"Compact this conversation?",compactAskBody:`Pi builds the summarization request internally and never routes it through this extension's redaction, so letting Pi summarize as-is would send the conversation, including protected values, as plaintext.
3
3
 
4
4
  Allowing here does NOT do that: the conversation Pi is about to summarize is redacted first, and Pi then summarizes the redacted text with the current model. No extra model is needed. The stored summary keeps the replacements, and the model reasons about replacements, so it may describe protected values inaccurately.
5
5
 
6
- This request will not be blocked if you allow it.`,compactAllowOnce:"Allow once",compactAllowAlways:"Allow for this session",compactDenyOnce:"Deny once",compactDenyAlways:"Deny for this session",compactRedacted:"SPI Protecter: redacted what Pi is about to summarize; Pi now summarizes with the current model. The stored summary keeps the replacements.",compactIgnoredKeys:"SPI Protecter: the compaction settings {keys} no longer do anything and are ignored. Pi summarizes with the session's own model; remove them from the local config.",guardInstalled:"covered",guardMissing:"NOT covered",statusCompaction:"Compaction: {mode}; Pi summarizes with the current model. Hookless model calls: {guard}.",compactAlwaysNotice:'SPI Protecter: compaction allowed for this session. Set "compaction" in the local config to make it permanent.',compactDenyNotice:"SPI Protecter: compaction refused for this session; use /new to start a new session."},Ye={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 在内部构造摘要请求,不经过本扩展的脱敏环节,因此原样让 Pi 生成摘要会将会话内容(包括受保护的值)以明文发送。
6
+ This request will not be blocked if you allow it.`,compactAllowOnce:"Allow once",compactAllowAlways:"Allow for this session",compactDenyOnce:"Deny once",compactDenyAlways:"Deny for this session",compactRedacted:"SPI Protecter: redacted what Pi is about to summarize; Pi now summarizes with the current model. The stored summary keeps the replacements.",compactIgnoredKeys:"SPI Protecter: the compaction settings {keys} no longer do anything and are ignored. Pi summarizes with the session's own model; remove them from the local config.",guardInstalled:"covered",guardMissing:"NOT covered",statusCompaction:"Compaction: {mode}; Pi summarizes with the current model. Hookless model calls: {guard}.",compactAlwaysNotice:'SPI Protecter: compaction allowed for this session. Set "compaction" in the local config to make it permanent.',compactDenyNotice:"SPI Protecter: compaction refused for this session; use /new to start a new session.",attachmentAskTitle:"Allow attachments?",attachmentAskBody:"This request contains image, audio, video or file content that SPI Protecter cannot inspect or redact. Allowing sends it unchanged to the provider.",attachmentAllow:"Allow this request",attachmentDeny:"Cancel request",attachmentAllowed:"SPI Protecter: attachment allowed for this request without inspection or redaction."},st={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 在内部构造摘要请求,不经过本扩展的脱敏环节,因此原样让 Pi 生成摘要会将会话内容(包括受保护的值)以明文发送。
7
7
 
8
8
  在此处允许**不会**这样做:先对 Pi 即将摘要的内容脱敏,再由 Pi 用当前模型对脱敏后的内容生成摘要,无需额外模型。保存下来的摘要保留替换值;且模型推理的是替换值,对受保护值的描述可能不准确。
9
9
 
10
- 若选择允许,本次请求不会被拦截。`,compactAllowOnce:"允许一次",compactAllowAlways:"本会话内全部允许",compactDenyOnce:"拒绝一次",compactDenyAlways:"本会话内全部拒绝",compactRedacted:"SPI Protecter:已对 Pi 即将摘要的内容脱敏,现由 Pi 用当前模型生成摘要。保存下来的摘要保留替换值。",compactIgnoredKeys:"SPI Protecter:compaction 配置项 {keys} 已不再生效并被忽略。Pi 使用会话自身的模型生成摘要,请从本地配置中删除它们。",guardInstalled:"已覆盖",guardMissing:"未覆盖",statusCompaction:"压缩:{mode};由 Pi 用当前模型生成摘要。无钩子的模型调用:{guard}。",compactAlwaysNotice:'SPI Protecter:本会话内允许压缩。如需永久生效,请在本地配置中设置 "compaction"。',compactDenyNotice:"SPI Protecter:本会话内拒绝压缩;可使用 /new 开启新会话。",blockedHeader:"SPI Protecter 已阻止该请求",reasonLabel:"原因",actionLabel:"处理",detailsLabel:"详情",statusReady:"就绪",statusNotReady:"未就绪",statusLine:"SPI Protecter:{state};{rules};{mappings}。内存快照,重载后更新。",statusLastBlock:"最近一次拦截 @ {at}:",statusNoBlock:"本会话暂无拦截记录。",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · 未就绪",emptyRules:"SPI Protecter:尚未配置规则,请编辑本地机器配置后重载。",commandDescription:"查看本地保护记录",usage:"用法:/protecter [logs [1-100|clear]|status|reload|test <文本>|test @<路径>]。仅 test 接受内容,其他参数不要输入敏感内容。",testPreviewTitle:"离线脱敏测试,仅本地显示,编辑不会保存",testRedacted:"SPI Protecter:已在本地脱敏({chars}),未向任何模型发送内容。替换关系已记入本会话映射。",testUnchanged:"SPI Protecter:没有规则命中({chars}),下方文本未改动。",testFileLabel:"输入:本地文件",testTextLabel:"输入:行内文本",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。"};function X(t=process.env){let e=t.PI_PROTECTER_LANG||t.LC_ALL||t.LC_MESSAGES||t.LANG||t.LANGUAGE||je();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function je(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var C=X();function de(){return C}function fe(t){C=t}function p(t,e={}){return((C==="zh"?Ye:le)[t]??le[t]).replace(/\{(\w+)\}/g,(n,o)=>Object.hasOwn(e,o)?String(e[o]):n)}var We={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"},tokens:{one:"{n} token",other:"{n} tokens"},chars:{one:"{n} character",other:"{n} characters"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"},tokens:{other:"{n} token"},chars:{other:"{n} 个字符"}}},ue=new Map;function _(t,e){let r=C==="zh"?"zh-CN":"en",n=ue.get(C);if(!n){try{n=new Intl.PluralRules(r)}catch{n=void 0}n&&ue.set(C,n)}let o=n?.select(e)??"other",a=We[C][t],f=a[o]??a.other,m=new Intl.NumberFormat(r).format(e);return f.replace("{n}",m)}function me(t){let r=(C==="zh"?Je[t]:K[t])??K[t]??K.INTERNAL;return{reason:r[0],action:r[1]}}var pe={INTERNAL:"internal",NOT_READY:"initialization",UNSUPPORTED_PI:"initialization",GUARD_UNAVAILABLE:"initialization",MACHINE_ID:"initialization",CONFIG_JSON:"configuration",CONFIG_SCHEMA:"configuration",CONFIG_REGEX:"configuration",CONFIG_TARGET:"configuration",CONFIG_CONFLICT:"configuration",CONFIG_SIZE:"configuration",CONFIG_IO:"configuration",CONFIG_UNSAFE:"configuration",CONFIG_EMPTY_MATCH:"configuration",CONFIG_SENSITIVE_TARGET:"configuration",CONFIG_ALIAS_CONFLICT:"configuration",CONFIG_TIMEOUT:"configuration",CONFIG_COMPACTION:"configuration",MIGRATION_LOCKED:"migration",MIGRATION_CHANGED:"migration",MIGRATION_LIMIT:"migration",MIGRATION_IO:"migration",PAYLOAD_JSON:"request",PAYLOAD_SIZE:"request",SCAN_TIMEOUT:"scan",WORKER_FAILED:"worker",SCAN_INTERNAL:"scan",SCAN_NUMBER_UNSAFE:"scan",SCAN_UNIQUE_FAILED:"scan",SCAN_ZERO_WIDTH:"scan",SCAN_MATCH_LIMIT:"scan",SCAN_MAPPING_LIMIT:"scan",FIXED_MAPPING_CONFLICT:"scan",FIXED_ALIAS_REUSED:"scan",FIXED_SENSITIVE_TARGET:"scan",FIXED_OVERLAP:"scan",FIXED_ALIAS_CROSSING:"scan",SCAN_COMPLEXITY:"scan",SCAN_JSON_REWRITE:"scan",SCAN_ATTACHMENT:"scan",SCAN_KEY_COLLISION:"scan",RESTORE_COMPLEXITY:"restore",RESTORE_KEY_COLLISION:"restore",COMPACT_DENIED:"compaction",COMPACT_FAILED:"compaction",COMPACT_INSTRUCTIONS:"compaction",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit",TEST_INPUT:"test",TEST_SIZE:"test"};var ge=["rule","otherRule","node","bytes","limit","timeoutMs","status"],Ke=/^[A-Za-z][A-Za-z0-9]{0,39}$/,Xe=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function qe(t,e={}){let{reason:r,action:n}=me(t),o={};for(let a of ge){let f=e[a];typeof f=="number"&&Number.isSafeInteger(f)&&f>=0&&(o[a]=f)}return typeof e.errno=="string"&&Xe.test(e.errno)&&(o.errno=e.errno),typeof e.errorName=="string"&&Ke.test(e.errorName)&&(o.errorName=e.errorName),{code:t,stage:pe[t],reason:r,action:n,...o}}function R(t){let e=de()==="zh"?":":": ",r=[...ge,"errno","errorName"].filter(n=>t[n]!==void 0).map(n=>`${n}=${t[n]}`).join(" ");return[p("blockedHeader"),`code=${t.code} stage=${t.stage}${r?` ${r}`:""}`,`${p("reasonLabel")}${e}${t.reason}`,`${p("actionLabel")}${e}${t.action}`,`${p("detailsLabel")}${e}/protecter status`].join(`
11
- `)}var S=class extends Error{diagnostic;constructor(e,r={}){let n=qe(e,r);super(R(n)),this.name="ProtectionError",this.diagnostic=n}};function i(t,e={}){return new S(t,e)}function O(t,e,r={}){if(t instanceof S)return i(t.diagnostic.code,{...t.diagnostic,...r});let n=t&&typeof t=="object"&&"code"in t?t.code:void 0;return i(e,{...r,errno:typeof n=="string"?n:void 0})}function U(t,e){if(!t||typeof t!="object")return i(e);let r=t,n=typeof r.code=="string"&&Object.hasOwn(pe,r.code)?r.code:e;return i(n,r)}import{constants as P,closeSync as he,fchmodSync as Ze,fstatSync as Ve,mkdirSync as Qe,openSync as Ie,readFileSync as et,writeFileSync as tt}from"node:fs";import{dirname as nt}from"node:path";var ye=["ask","protected","off"],q=["provider","model","contextWindow","reserveTokens"];function Ne(t){return!t||typeof t!="object"?[]:q.filter(e=>Object.hasOwn(t,e))}var Z={version:1,sensitiveWords:[]},Oe="ask";function rt(t){if(typeof t=="string"){if(!ye.includes(t))throw i("CONFIG_COMPACTION");return}if(!t||typeof t!="object"||Array.isArray(t))throw i("CONFIG_COMPACTION");let e=t,r=["mode",...q];if(Object.keys(e).some(n=>!r.includes(n)))throw i("CONFIG_COMPACTION");if(e.mode!==void 0&&!ye.includes(e.mode))throw i("CONFIG_COMPACTION");for(let n of q){let o=e[n];if(o!==void 0&&typeof o!="string"&&typeof o!="number")throw i("CONFIG_COMPACTION")}}function v(t){return t===void 0?{mode:Oe}:typeof t=="string"?{mode:t}:{mode:t.mode??Oe}}function V(t){let e;try{e=JSON.parse(t)}catch{throw i("CONFIG_JSON")}let r=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(o=>!["version","sensitiveWords","compaction"].includes(o)))throw i("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&rt(e.compaction),e.sensitiveWords.length>1e3)throw i("CONFIG_SIZE",{limit:1e3});let n=new Map;for(let o of e.sensitiveWords){if(r++,typeof o=="string"){if(!o||o.length>8192)throw i("CONFIG_SCHEMA",{rule:r,limit:8192});let f=JSON.stringify(["literal",o]);if(n.has(f)&&n.get(f)!==void 0)throw i("CONFIG_CONFLICT",{rule:r});n.set(f,void 0);continue}if(!o||typeof o!="object")throw i("CONFIG_SCHEMA",{rule:r});if(Object.hasOwn(o,"replacement")&&(typeof o.replacement!="string"||!o.replacement.trim()||o.replacement.length>8192||o.replacement.includes("__PIP_")))throw i("CONFIG_TARGET",{rule:r,limit:8192});if(o.type==="literal"){if(Object.keys(o).some(f=>!["type","value","replacement"].includes(f))||typeof o.value!="string"||!o.value||o.value.length>8192)throw i("CONFIG_SCHEMA",{rule:r,limit:8192})}else if(o.type==="regex"){if(Object.keys(o).some(f=>!["type","pattern","flags","replacement"].includes(f))||typeof o.pattern!="string"||!o.pattern||o.pattern.length>8192||o.flags!==void 0&&(typeof o.flags!="string"||!/^[gimsu]*$/.test(o.flags)))throw i("CONFIG_REGEX",{rule:r,limit:8192});try{new RegExp(o.pattern,o.flags??"")}catch{throw i("CONFIG_REGEX",{rule:r})}}else throw i("CONFIG_SCHEMA",{rule:r});let a=o.type==="literal"?JSON.stringify(["literal",o.value]):JSON.stringify(["regex",o.pattern,[...new Set((o.flags??"")+"g")].sort().join("")]);if(n.has(a)&&n.get(a)!==o.replacement)throw i("CONFIG_CONFLICT",{rule:r});n.set(a,o.replacement)}return e}catch(n){throw n instanceof S?n:i("CONFIG_SCHEMA",{rule:r})}}function Ae(t,e=!1){let r;try{if(e){Qe(nt(t),{recursive:!0,mode:448});try{let a=Ie(t,P.O_WRONLY|P.O_CREAT|P.O_EXCL,384);try{tt(a,JSON.stringify(Z,null,2)+`
12
- `)}finally{he(a)}}catch(a){if(a.code!=="EEXIST")throw O(a,"CONFIG_IO")}}r=Ie(t,P.O_RDONLY|P.O_NOFOLLOW|P.O_NONBLOCK);let n=Ve(r);if(n.size>1024*1024)throw i("CONFIG_SIZE",{bytes:n.size,limit:1024*1024});if(!n.isFile()||n.nlink!==1||process.getuid&&n.uid!==process.getuid())throw i("CONFIG_UNSAFE");process.platform!=="win32"&&Ze(r,384);let o=et(r,"utf8");if(Buffer.byteLength(o)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(o),limit:1024*1024});return{config:V(o),raw:o}}catch(n){let o=n&&typeof n=="object"&&"code"in n?n.code:void 0;throw O(n,o==="ELOOP"||o==="EISDIR"||o==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{r!==void 0&&he(r)}}var ee=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function lt(t){return new Promise((e,r)=>{let n=new at(ct(new URL("./validate-worker.mjs",import.meta.url)),{workerData:t,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),o=!1,a=(m,d=i("WORKER_FAILED"))=>{o||(o=!0,clearTimeout(f),n.terminate(),m?e():r(d))},f=setTimeout(()=>a(!1,i("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);n.once("message",m=>m===!0?a(!0):a(!1,U(m,"CONFIG_SCHEMA"))),n.once("error",()=>a(!1)),n.once("exit",()=>a(!1))})}function ut(t){let e=new Set,r=[];for(let d of t)for(let c of d.sensitiveWords){let l=typeof c=="string"?JSON.stringify(["literal",c,null]):c.type==="literal"?JSON.stringify(["literal",c.value,c.replacement??null]):JSON.stringify(["regex",c.pattern,[...new Set((c.flags??"")+"g")].sort().join(""),c.replacement??null]);e.has(l)||(e.add(l),r.push(c))}let n=[];for(let d of t)d.compaction!==void 0&&n.push(v(d.compaction));let a=["off","ask","protected"].find(d=>n.some(c=>c.mode===d)),f={version:1,sensitiveWords:r};a!==void 0&&(f.compaction={mode:a});let m=JSON.stringify(f);if(Buffer.byteLength(m)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(m),limit:1024*1024});return V(m)}function Te(t){let e;try{e=I.lstatSync(t,{bigint:!0})}catch(r){throw O(r,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw i("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function Q(t,e){let r=D(t,e),n=Te(r),o=Ae(r);if(Te(r)!==n)throw i("MIGRATION_CHANGED");return{name:e,...o,identity:n,digest:it("sha256").update(o.raw).digest("hex")}}function F(t,e){let r=Q(t,e.name);if(r.identity!==e.identity||r.digest!==e.digest)throw i("MIGRATION_CHANGED")}function z(t){return I.readdirSync(t).filter(e=>ee.test(e)).sort()}function Ee(t){if(process.platform==="win32")return;let e=I.openSync(t,I.constants.O_RDONLY);try{I.fsyncSync(e)}finally{I.closeSync(e)}}async function Ce(t,e,r={}){if(!/^[a-f0-9]{32}$/.test(e))throw i("MACHINE_ID");let n=ot(t),o=`protecter.${e}.json`,a=D(n,o);I.mkdirSync(n,{recursive:!0,mode:448});let f=D(n,".protecter-migration.lock"),m=Date.now()+(r.lockTimeoutMs??5e3);for(;;)try{I.mkdirSync(f,{mode:448});break}catch(c){if(c.code!=="EEXIST")throw O(c,"MIGRATION_IO");if(Date.now()>=m)throw i("MIGRATION_LOCKED",{timeoutMs:r.lockTimeoutMs??5e3});await new Promise(l=>setTimeout(l,50))}let d;try{let c=z(n);if(c.length>64)throw i("MIGRATION_LIMIT",{limit:64});let s=[...c].sort((N,x)=>N===o?-1:x===o?1:N.localeCompare(x)).map(N=>Q(n,N)),u=ut(s.length?s.map(N=>N.config):[Z]);if(await lt([u]),s.length===1&&s[0].name===o){if(F(n,s[0]),JSON.stringify(z(n))!==JSON.stringify(c))throw i("MIGRATION_CHANGED");return{path:a,config:s[0].config,raw:s[0].raw,sourceRaws:[s[0].raw],migrated:0}}let h=JSON.stringify(u,null,2)+`
13
- `;if(Buffer.byteLength(h)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(h),limit:1024*1024});d=D(n,`.protecter-migration.${st()}.tmp`);let y=I.openSync(d,I.constants.O_WRONLY|I.constants.O_CREAT|I.constants.O_EXCL,384);try{I.writeFileSync(y,h),I.fsyncSync(y)}finally{I.closeSync(y)}if(r.checkpoint?.("beforeCommit",a),JSON.stringify(z(n))!==JSON.stringify(c))throw i("MIGRATION_CHANGED");for(let N of s)F(n,N);I.renameSync(d,a),d=void 0,Ee(n);let A=Q(n,o);if(A.raw!==h)throw i("MIGRATION_CHANGED");r.checkpoint?.("afterCommit",a);for(let N of s)N.name!==o&&(r.checkpoint?.("beforeDelete",D(n,N.name)),F(n,A),F(n,N),I.unlinkSync(D(n,N.name)));Ee(n);let J=[o];if(JSON.stringify(z(n))!==JSON.stringify(J))throw i("MIGRATION_CHANGED");return F(n,A),{path:a,config:u,raw:h,sourceRaws:[...new Set([...s.map(N=>N.raw),h])],migrated:s.filter(N=>N.name!==o).length}}catch(c){throw c instanceof S?c:O(c,"MIGRATION_IO")}finally{if(d)try{I.unlinkSync(d)}catch{}I.rmdirSync(f)}}import{createHash as dt}from"node:crypto";import{readFileSync as ft}from"node:fs";import{execFileSync as Se}from"node:child_process";function te(t){let e=t.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw i("MACHINE_ID");return dt("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function we(){try{if(process.platform==="linux")return te(ft("/etc/machine-id","utf8"));if(process.platform==="darwin"){let t=Se("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return te(t.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let t=Se("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return te(t.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw i("MACHINE_ID")}import*as g from"node:fs";import{randomUUID as mt}from"node:crypto";var _e=32*1024*1024,Re=8*1024*1024;function pt(t){let e=r=>String(r).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())} ${e(t.getHours())}:${e(t.getMinutes())}:${e(t.getSeconds())}`}function ne(t,e,r=e){let n=g.openSync(t,(e?g.constants.O_RDWR|(r?g.constants.O_CREAT:0):g.constants.O_RDONLY)|g.constants.O_NOFOLLOW|g.constants.O_NONBLOCK,384);try{let o=g.fstatSync(n);if(!o.isFile()||o.nlink!==1||process.getuid&&o.uid!==process.getuid())throw i("AUDIT_UNSAFE");return process.platform!=="win32"&&g.fchmodSync(n,384),n}catch(o){throw g.closeSync(n),O(o,"AUDIT_UNSAFE")}}function B(t){let e=t&&typeof t=="object"&&"code"in t?t.code:void 0;return O(t,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function Pe(t){let e=`${t}.lock`,r=Date.now()+2e3;for(;;)try{return g.mkdirSync(e,{mode:448}),e}catch(n){if(n.code!=="EEXIST")throw O(n,"AUDIT_IO");if(Date.now()>=r)throw i("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(o=>setTimeout(o,25))}}async function De(t){let e=await Pe(t),r;try{try{r=ne(t,!0,!1)}catch(o){if(o.code==="ENOENT")return 0;throw o}let n=g.fstatSync(r).size;return g.ftruncateSync(r,0),g.fsyncSync(r),n}catch(n){throw B(n)}finally{try{r!==void 0&&g.closeSync(r)}finally{g.rmdirSync(e)}}}async function Le(t,e,r){if(!r.length)return;let n=new Date,o=mt(),a=Buffer.from(r.map(([d,c])=>JSON.stringify({version:1,time:pt(n),timezoneOffset:-n.getTimezoneOffset(),requestId:o,provider:e||"unknown",original:c,replacement:d})).join(`
10
+ 若选择允许,本次请求不会被拦截。`,compactAllowOnce:"允许一次",compactAllowAlways:"本会话内全部允许",compactDenyOnce:"拒绝一次",compactDenyAlways:"本会话内全部拒绝",compactRedacted:"SPI Protecter:已对 Pi 即将摘要的内容脱敏,现由 Pi 用当前模型生成摘要。保存下来的摘要保留替换值。",compactIgnoredKeys:"SPI Protecter:compaction 配置项 {keys} 已不再生效并被忽略。Pi 使用会话自身的模型生成摘要,请从本地配置中删除它们。",guardInstalled:"已覆盖",guardMissing:"未覆盖",statusCompaction:"压缩:{mode};由 Pi 用当前模型生成摘要。无钩子的模型调用:{guard}。",compactAlwaysNotice:'SPI Protecter:本会话内允许压缩。如需永久生效,请在本地配置中设置 "compaction"。',compactDenyNotice:"SPI Protecter:本会话内拒绝压缩;可使用 /new 开启新会话。",attachmentAskTitle:"允许发送附件?",attachmentAskBody:"此请求包含 SPI Protecter 无法检查或脱敏的图片、音频、视频或文件内容。允许后会将附件原样发送给提供商。",attachmentAllow:"允许本次请求",attachmentDeny:"取消请求",attachmentAllowed:"SPI Protecter:本次请求的附件未经检查或脱敏,将继续发送。",blockedHeader:"SPI Protecter 已阻止该请求",reasonLabel:"原因",actionLabel:"处理",detailsLabel:"详情",statusReady:"就绪",statusNotReady:"未就绪",statusLine:"SPI Protecter:{state};{rules};{mappings}。内存快照,重载后更新。",statusLastBlock:"最近一次拦截 @ {at}:",statusNoBlock:"本会话暂无拦截记录。",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · 未就绪",emptyRules:"SPI Protecter:尚未配置规则,请编辑本地机器配置后重载。",commandDescription:"查看本地保护记录",usage:"用法:/protecter [logs [1-100|clear]|status|reload|test <文本>|test @<路径>]。仅 test 接受内容,其他参数不要输入敏感内容。",testPreviewTitle:"离线脱敏测试,仅本地显示,编辑不会保存",testRedacted:"SPI Protecter:已在本地脱敏({chars}),未向任何模型发送内容。替换关系已记入本会话映射。",testUnchanged:"SPI Protecter:没有规则命中({chars}),下方文本未改动。",testFileLabel:"输入:本地文件",testTextLabel:"输入:行内文本",capabilityBlocked:"SPI Protecter:当前 Pi 构建未暴露 {names},因此保护未启用,请求会被拦截而不以明文发送。请反馈:{url}",piNewer:"SPI Protecter:Pi {version} 高于已经完整测试验证的版本({verified})。所需钩子均存在,保护已生效并会逐次校验;如发现异常请反馈:{url}",hookBypassed:"SPI Protecter:Pi 未对某次模型调用执行请求钩子,已由运行时闸门脱敏。保护仍然有效,但请反馈:{url}",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。"};function oe(t=process.env){let e=t.PI_PROTECTER_LANG||t.LC_ALL||t.LC_MESSAGES||t.LANG||t.LANGUAGE||at();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function at(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var R=oe();function Oe(){return R}function we(t){R=t}function d(t,e={}){return((R==="zh"?st:Ie)[t]??Ie[t]).replace(/\{(\w+)\}/g,(n,r)=>Object.hasOwn(e,r)?String(e[r]):n)}var ct={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"},tokens:{one:"{n} token",other:"{n} tokens"},chars:{one:"{n} character",other:"{n} characters"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"},tokens:{other:"{n} token"},chars:{other:"{n} 个字符"}}},Ae=new Map;function b(t,e){let o=R==="zh"?"zh-CN":"en",n=Ae.get(R);if(!n){try{n=new Intl.PluralRules(o)}catch{n=void 0}n&&Ae.set(R,n)}let r=n?.select(e)??"other",s=ct[R][t],c=s[r]??s.other,u=new Intl.NumberFormat(o).format(e);return c.replace("{n}",u)}function Ne(t){let o=(R==="zh"?it[t]:ne[t])??ne[t]??ne.INTERNAL;return{reason:o[0],action:o[1]}}var Ce={INTERNAL:"internal",NOT_READY:"initialization",PI_CAPABILITY:"initialization",GUARD_UNAVAILABLE:"initialization",MACHINE_ID:"initialization",CONFIG_JSON:"configuration",CONFIG_SCHEMA:"configuration",CONFIG_REGEX:"configuration",CONFIG_TARGET:"configuration",CONFIG_CONFLICT:"configuration",CONFIG_SIZE:"configuration",CONFIG_IO:"configuration",CONFIG_UNSAFE:"configuration",CONFIG_EMPTY_MATCH:"configuration",CONFIG_SENSITIVE_TARGET:"configuration",CONFIG_ALIAS_CONFLICT:"configuration",CONFIG_TIMEOUT:"configuration",CONFIG_COMPACTION:"configuration",CONFIG_ATTACHMENT:"configuration",MIGRATION_LOCKED:"migration",MIGRATION_CHANGED:"migration",MIGRATION_LIMIT:"migration",MIGRATION_IO:"migration",PAYLOAD_JSON:"request",PAYLOAD_SIZE:"request",SCAN_TIMEOUT:"scan",WORKER_FAILED:"worker",SCAN_INTERNAL:"scan",SCAN_NUMBER_UNSAFE:"scan",SCAN_UNIQUE_FAILED:"scan",SCAN_ZERO_WIDTH:"scan",SCAN_MATCH_LIMIT:"scan",SCAN_MAPPING_LIMIT:"scan",FIXED_MAPPING_CONFLICT:"scan",FIXED_ALIAS_REUSED:"scan",FIXED_SENSITIVE_TARGET:"scan",FIXED_OVERLAP:"scan",FIXED_ALIAS_CROSSING:"scan",SCAN_COMPLEXITY:"scan",SCAN_JSON_REWRITE:"scan",SCAN_ATTACHMENT:"scan",SCAN_KEY_COLLISION:"scan",RESTORE_COMPLEXITY:"restore",RESTORE_KEY_COLLISION:"restore",COMPACT_DENIED:"compaction",COMPACT_FAILED:"compaction",COMPACT_INSTRUCTIONS:"compaction",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit",TEST_INPUT:"test",TEST_SIZE:"test"};var Te=["rule","otherRule","node","bytes","limit","timeoutMs","status"],lt=/^[A-Za-z][A-Za-z0-9]{0,39}$/,ut=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function dt(t,e={}){let{reason:o,action:n}=Ne(t),r={};for(let s of Te){let c=e[s];typeof c=="number"&&Number.isSafeInteger(c)&&c>=0&&(r[s]=c)}return typeof e.errno=="string"&&ut.test(e.errno)&&(r.errno=e.errno),typeof e.errorName=="string"&&lt.test(e.errorName)&&(r.errorName=e.errorName),{code:t,stage:Ce[t],reason:o,action:n,...r}}function M(t){let e=Oe()==="zh"?":":": ",o=[...Te,"errno","errorName"].filter(n=>t[n]!==void 0).map(n=>`${n}=${t[n]}`).join(" ");return[d("blockedHeader"),`code=${t.code} stage=${t.stage}${o?` ${o}`:""}`,`${d("reasonLabel")}${e}${t.reason}`,`${d("actionLabel")}${e}${t.action}`,`${d("detailsLabel")}${e}/protecter status`].join(`
11
+ `)}var P=class extends Error{diagnostic;constructor(e,o={}){let n=dt(e,o);super(M(n)),this.name="ProtectionError",this.diagnostic=n}};function i(t,e={}){return new P(t,e)}function w(t,e,o={}){if(t instanceof P)return i(t.diagnostic.code,{...t.diagnostic,...o});let n=t&&typeof t=="object"&&"code"in t?t.code:void 0;return i(e,{...o,errno:typeof n=="string"?n:void 0})}function H(t,e){if(!t||typeof t!="object")return i(e);let o=t,n=typeof o.code=="string"&&Object.hasOwn(Ce,o.code)?o.code:e;return i(n,o)}import{constants as F,closeSync as Ee,fchmodSync as ft,fstatSync as mt,mkdirSync as pt,openSync as _e,readFileSync as gt,writeFileSync as ht}from"node:fs";import{dirname as yt}from"node:path";var Se=["ask","protected","off"],re=["provider","model","contextWindow","reserveTokens"];function Pe(t){return!t||typeof t!="object"?[]:re.filter(e=>Object.hasOwn(t,e))}var ie={version:1,sensitiveWords:[]},Re="ask";function It(t){if(typeof t=="string"){if(!Se.includes(t))throw i("CONFIG_COMPACTION");return}if(!t||typeof t!="object"||Array.isArray(t))throw i("CONFIG_COMPACTION");let e=t,o=["mode",...re];if(Object.keys(e).some(n=>!o.includes(n)))throw i("CONFIG_COMPACTION");if(e.mode!==void 0&&!Se.includes(e.mode))throw i("CONFIG_COMPACTION");for(let n of re){let r=e[n];if(r!==void 0&&typeof r!="string"&&typeof r!="number")throw i("CONFIG_COMPACTION")}}function $(t){return t===void 0?{mode:Re}:typeof t=="string"?{mode:t}:{mode:t.mode??Re}}function se(t){let e;try{e=JSON.parse(t)}catch{throw i("CONFIG_JSON")}let o=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(r=>!["version","sensitiveWords","compaction","attachments"].includes(r)))throw i("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&It(e.compaction),Object.hasOwn(e,"attachments")&&!["ask","allow","deny"].includes(e.attachments??""))throw i("CONFIG_ATTACHMENT");if(e.sensitiveWords.length>1e3)throw i("CONFIG_SIZE",{limit:1e3});let n=new Map;for(let r of e.sensitiveWords){if(o++,typeof r=="string"){if(!r||r.length>8192)throw i("CONFIG_SCHEMA",{rule:o,limit:8192});let c=JSON.stringify(["literal",r]);if(n.has(c)&&n.get(c)!==void 0)throw i("CONFIG_CONFLICT",{rule:o});n.set(c,void 0);continue}if(!r||typeof r!="object")throw i("CONFIG_SCHEMA",{rule:o});if(Object.hasOwn(r,"replacement")&&(typeof r.replacement!="string"||!r.replacement.trim()||r.replacement.length>8192||r.replacement.includes("__PIP_")))throw i("CONFIG_TARGET",{rule:o,limit:8192});if(r.type==="literal"){if(Object.keys(r).some(c=>!["type","value","replacement"].includes(c))||typeof r.value!="string"||!r.value||r.value.length>8192)throw i("CONFIG_SCHEMA",{rule:o,limit:8192})}else if(r.type==="regex"){if(Object.keys(r).some(c=>!["type","pattern","flags","replacement"].includes(c))||typeof r.pattern!="string"||!r.pattern||r.pattern.length>8192||r.flags!==void 0&&(typeof r.flags!="string"||!/^[gimsu]*$/.test(r.flags)))throw i("CONFIG_REGEX",{rule:o,limit:8192});try{new RegExp(r.pattern,r.flags??"")}catch{throw i("CONFIG_REGEX",{rule:o})}}else throw i("CONFIG_SCHEMA",{rule:o});let s=r.type==="literal"?JSON.stringify(["literal",r.value]):JSON.stringify(["regex",r.pattern,[...new Set((r.flags??"")+"g")].sort().join("")]);if(n.has(s)&&n.get(s)!==r.replacement)throw i("CONFIG_CONFLICT",{rule:o});n.set(s,r.replacement)}return e}catch(n){throw n instanceof P?n:i("CONFIG_SCHEMA",{rule:o})}}function ke(t,e=!1){let o;try{if(e){pt(yt(t),{recursive:!0,mode:448});try{let s=_e(t,F.O_WRONLY|F.O_CREAT|F.O_EXCL,384);try{ht(s,JSON.stringify(ie,null,2)+`
12
+ `)}finally{Ee(s)}}catch(s){if(s.code!=="EEXIST")throw w(s,"CONFIG_IO")}}o=_e(t,F.O_RDONLY|F.O_NOFOLLOW|F.O_NONBLOCK);let n=mt(o);if(n.size>1024*1024)throw i("CONFIG_SIZE",{bytes:n.size,limit:1024*1024});if(!n.isFile()||n.nlink!==1||process.getuid&&n.uid!==process.getuid())throw i("CONFIG_UNSAFE");process.platform!=="win32"&&ft(o,384);let r=gt(o,"utf8");if(Buffer.byteLength(r)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(r),limit:1024*1024});return{config:se(r),raw:r}}catch(n){let r=n&&typeof n=="object"&&"code"in n?n.code:void 0;throw w(n,r==="ELOOP"||r==="EISDIR"||r==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{o!==void 0&&Ee(o)}}var ce=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function Tt(t){return new Promise((e,o)=>{let n=new Nt(Ct(new URL("./validate-worker.mjs",import.meta.url)),{workerData:t,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),r=!1,s=(u,m=i("WORKER_FAILED"))=>{r||(r=!0,clearTimeout(c),n.terminate(),u?e():o(m))},c=setTimeout(()=>s(!1,i("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);n.once("message",u=>u===!0?s(!0):s(!1,H(u,"CONFIG_SCHEMA"))),n.once("error",()=>s(!1)),n.once("exit",()=>s(!1))})}function Et(t){let e=new Set,o=[];for(let l of t)for(let y of l.sensitiveWords){let I=typeof y=="string"?JSON.stringify(["literal",y,null]):y.type==="literal"?JSON.stringify(["literal",y.value,y.replacement??null]):JSON.stringify(["regex",y.pattern,[...new Set((y.flags??"")+"g")].sort().join(""),y.replacement??null]);e.has(I)||(e.add(I),o.push(y))}let n=[];for(let l of t)l.compaction!==void 0&&n.push($(l.compaction));let s=["off","ask","protected"].find(l=>n.some(y=>y.mode===l)),c={version:1,sensitiveWords:o};s!==void 0&&(c.compaction={mode:s});let u=t.map(l=>l.attachments).filter(l=>l!==void 0);u.length&&(c.attachments=["deny","ask","allow"].find(l=>u.includes(l)));let m=JSON.stringify(c);if(Buffer.byteLength(m)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(m),limit:1024*1024});return se(m)}function De(t){let e;try{e=O.lstatSync(t,{bigint:!0})}catch(o){throw w(o,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw i("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function ae(t,e){let o=x(t,e),n=De(o),r=ke(o);if(De(o)!==n)throw i("MIGRATION_CHANGED");return{name:e,...r,identity:n,digest:Ot("sha256").update(r.raw).digest("hex")}}function v(t,e){let o=ae(t,e.name);if(o.identity!==e.identity||o.digest!==e.digest)throw i("MIGRATION_CHANGED")}function j(t){return O.readdirSync(t).filter(e=>ce.test(e)).sort()}function Le(t){if(process.platform==="win32")return;let e=O.openSync(t,O.constants.O_RDONLY);try{O.fsyncSync(e)}finally{O.closeSync(e)}}async function be(t,e,o={}){if(!/^[a-f0-9]{32}$/.test(e))throw i("MACHINE_ID");let n=At(t),r=`protecter.${e}.json`,s=x(n,r);O.mkdirSync(n,{recursive:!0,mode:448});let c=x(n,".protecter-migration.lock"),u=Date.now()+(o.lockTimeoutMs??5e3);for(;;)try{O.mkdirSync(c,{mode:448});break}catch(l){if(l.code!=="EEXIST")throw w(l,"MIGRATION_IO");if(Date.now()>=u)throw i("MIGRATION_LOCKED",{timeoutMs:o.lockTimeoutMs??5e3});await new Promise(y=>setTimeout(y,50))}let m;try{let l=j(n);if(l.length>64)throw i("MIGRATION_LIMIT",{limit:64});let I=[...l].sort((p,C)=>p===r?-1:C===r?1:p.localeCompare(C)).map(p=>ae(n,p)),A=Et(I.length?I.map(p=>p.config):[ie]);if(await Tt([A]),I.length===1&&I[0].name===r){if(v(n,I[0]),JSON.stringify(j(n))!==JSON.stringify(l))throw i("MIGRATION_CHANGED");return{path:s,config:I[0].config,raw:I[0].raw,sourceRaws:[I[0].raw],migrated:0}}let N=JSON.stringify(A,null,2)+`
13
+ `;if(Buffer.byteLength(N)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(N),limit:1024*1024});m=x(n,`.protecter-migration.${wt()}.tmp`);let f=O.openSync(m,O.constants.O_WRONLY|O.constants.O_CREAT|O.constants.O_EXCL,384);try{O.writeFileSync(f,N),O.fsyncSync(f)}finally{O.closeSync(f)}if(o.checkpoint?.("beforeCommit",s),JSON.stringify(j(n))!==JSON.stringify(l))throw i("MIGRATION_CHANGED");for(let p of I)v(n,p);O.renameSync(m,s),m=void 0,Le(n);let a=ae(n,r);if(a.raw!==N)throw i("MIGRATION_CHANGED");o.checkpoint?.("afterCommit",s);for(let p of I)p.name!==r&&(o.checkpoint?.("beforeDelete",x(n,p.name)),v(n,a),v(n,p),O.unlinkSync(x(n,p.name)));Le(n);let g=[r];if(JSON.stringify(j(n))!==JSON.stringify(g))throw i("MIGRATION_CHANGED");return v(n,a),{path:s,config:A,raw:N,sourceRaws:[...new Set([...I.map(p=>p.raw),N])],migrated:I.filter(p=>p.name!==r).length}}catch(l){throw l instanceof P?l:w(l,"MIGRATION_IO")}finally{if(m)try{O.unlinkSync(m)}catch{}O.rmdirSync(c)}}import{createHash as _t}from"node:crypto";import{readFileSync as St}from"node:fs";import{execFileSync as Me}from"node:child_process";function le(t){let e=t.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw i("MACHINE_ID");return _t("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function Fe(){try{if(process.platform==="linux")return le(St("/etc/machine-id","utf8"));if(process.platform==="darwin"){let t=Me("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return le(t.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let t=Me("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return le(t.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw i("MACHINE_ID")}import*as h from"node:fs";import{randomUUID as Rt}from"node:crypto";var xe=32*1024*1024,Ge=8*1024*1024;function Pt(t){let e=o=>String(o).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())} ${e(t.getHours())}:${e(t.getMinutes())}:${e(t.getSeconds())}`}function ue(t,e,o=e){let n=h.openSync(t,(e?h.constants.O_RDWR|(o?h.constants.O_CREAT:0):h.constants.O_RDONLY)|h.constants.O_NOFOLLOW|h.constants.O_NONBLOCK,384);try{let r=h.fstatSync(n);if(!r.isFile()||r.nlink!==1||process.getuid&&r.uid!==process.getuid())throw i("AUDIT_UNSAFE");return process.platform!=="win32"&&h.fchmodSync(n,384),n}catch(r){throw h.closeSync(n),w(r,"AUDIT_UNSAFE")}}function Y(t){let e=t&&typeof t=="object"&&"code"in t?t.code:void 0;return w(t,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function Ue(t){let e=`${t}.lock`,o=Date.now()+2e3;for(;;)try{return h.mkdirSync(e,{mode:448}),e}catch(n){if(n.code!=="EEXIST")throw w(n,"AUDIT_IO");if(Date.now()>=o)throw i("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(r=>setTimeout(r,25))}}async function ve(t){let e=await Ue(t),o;try{try{o=ue(t,!0,!1)}catch(r){if(r.code==="ENOENT")return 0;throw r}let n=h.fstatSync(o).size;return h.ftruncateSync(o,0),h.fsyncSync(o),n}catch(n){throw Y(n)}finally{try{o!==void 0&&h.closeSync(o)}finally{h.rmdirSync(e)}}}async function ze(t,e,o){if(!o.length)return;let n=new Date,r=Rt(),s=Buffer.from(o.map(([m,l])=>JSON.stringify({version:1,time:Pt(n),timezoneOffset:-n.getTimezoneOffset(),requestId:r,provider:e||"unknown",original:l,replacement:m})).join(`
14
14
  `)+`
15
- `);if(a.length>Re)throw i("AUDIT_BATCH_LIMIT",{bytes:a.length,limit:Re});let f=await Pe(t),m;try{m=ne(t,!0);let d=g.fstatSync(m).size;if(d+a.length>_e)throw i("AUDIT_FULL",{bytes:d+a.length,limit:_e});if(d){let c=Buffer.alloc(1);if(g.readSync(m,c,0,1,d-1),c[0]!==10)throw i("AUDIT_PARTIAL")}try{let c=0;for(;c<a.length;){let l=g.writeSync(m,a,c,a.length-c,d+c);if(!l)throw i("AUDIT_IO");c+=l}g.fsyncSync(m)}catch(c){throw g.ftruncateSync(m,d),g.fsyncSync(m),B(c)}}catch(d){throw B(d)}finally{m!==void 0&&g.closeSync(m),g.rmdirSync(f)}}function ke(t,e=20){if(!Number.isInteger(e)||e<1||e>100)throw i("INTERNAL",{limit:100});let r;try{try{r=ne(t,!1)}catch(u){if(u.code==="ENOENT")return{records:[],truncated:!1};throw u}let n=g.fstatSync(r).size,o=Math.max(0,n-1024*1024),a=Buffer.alloc(n-o),f=g.readSync(r,a,0,a.length,o),m=a.subarray(0,f).toString("utf8");if(o){let u=m.indexOf(`
16
- `);m=u<0?"":m.slice(u+1)}let d=m.split(`
17
- `),c=d.pop()!=="",l=[],s=!1;for(let u of d.slice(-e))try{let h=JSON.parse(u);if(h.version!==1||typeof h.time!="string"||typeof h.provider!="string"||typeof h.original!="string"||typeof h.replacement!="string"||typeof h.requestId!="string"||typeof h.timezoneOffset!="number")throw new Error;l.push(h)}catch{s=!0}return{records:l,truncated:o>0||d.length>e||c||s}}catch(n){throw B(n)}finally{r!==void 0&&g.closeSync(r)}}var H=class{constructor(e,r=2e3,n=we){this.configDir=e;this.timeoutMs=r;this.machineId=n}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;snapshot;lastError;restorePattern;restoreSize=-1;get lastDiagnostic(){return this.lastError?{...this.lastError}:void 0}get lastDiagnosticText(){return this.lastError?R(this.lastError):void 0}async initialize(){try{if(this.closed)throw i("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw i("MACHINE_ID")}let r=await Ce(this.configDir,e);if(this.closed)throw i("NOT_READY");this.snapshot=r,this.lastError=void 0}catch(e){let r=O(e,"CONFIG_IO");throw this.lastError=r.diagnostic,r}}get configPath(){return this.snapshot?.path??""}get auditPath(){return this.configPath?this.configPath.replace(/\.json$/,".jsonl"):""}auditRecords(e=20){if(!this.ready)throw this.notReady();return ke(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return De(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get compaction(){return v(this.snapshot?.config.compaction)}get compactionMode(){return this.compaction.mode}get rawCompaction(){return this.snapshot?.config.compaction}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,r="unknown"){let n=this.queue.then(()=>this.scan(e,r)).catch(o=>{let a=O(o,"INTERNAL");throw this.lastError=a.diagnostic,a});return this.queue=n.catch(()=>{}),n}notReady(){return this.lastError?i(this.lastError.code,this.lastError):i("NOT_READY")}async scan(e,r){if(!this.ready)throw this.notReady();let{config:n,sourceRaws:o}=this.snapshot,a;try{a=JSON.stringify(e)}catch{throw i("PAYLOAD_JSON")}if(!a)throw i("PAYLOAD_JSON");if(Buffer.byteLength(a)>8*1024*1024)throw i("PAYLOAD_SIZE",{bytes:Buffer.byteLength(a),limit:8*1024*1024});let f;try{f=JSON.parse(a)}catch{throw i("PAYLOAD_JSON")}let m=await new Promise((d,c)=>{let l=!1,s;try{s=new gt(ht(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:f,rules:n.sensitiveWords,entries:[...this.tokens],configRaws:o},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(y){c(O(y,"WORKER_FAILED"));return}this.workers.add(s);let u=(y,A=i("WORKER_FAILED"))=>{if(!l){if(l=!0,clearTimeout(h),this.workers.delete(s),s.terminate(),!y||this.closed){c(this.closed?i("NOT_READY"):A);return}d(y)}},h=setTimeout(()=>u(void 0,i("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);s.once("message",y=>{y?.failed?u(void 0,U(y.diagnostic,"SCAN_INTERNAL")):y&&Array.isArray(y.additions)&&Array.isArray(y.hits)?u(y):u()}),s.once("error",()=>u()),s.once("exit",()=>u())});if(this.closed)throw i("NOT_READY");if(await Le(this.auditPath,r,m.hits),this.closed)throw i("NOT_READY");for(let[d,c]of m.additions)this.tokens.set(d,c);return m.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let r=n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((n,o)=>o.length-n.length).map(r).join("|"),"g"),this.restoreSize=this.tokens.size}return e.replace(this.restorePattern,r=>this.tokens.get(r)??r)}restore(e,r=0){if(r>80)throw i("RESTORE_COMPLEXITY",{node:r});if(typeof e=="string")return this.restoreText(e);if(typeof e=="number"){let n=this.tokens.get(String(e));if(n===void 0)return e;let o=Number(n);return!Number.isFinite(o)||String(o)!==n?e:o}if(Array.isArray(e))return e.map(n=>this.restore(n,r+1));if(e&&typeof e=="object"){let n=Object.create(null);for(let[o,a]of Object.entries(e)){let f=this.restoreText(o);if(Object.hasOwn(n,f))throw i("RESTORE_KEY_COLLISION");n[f]=this.restore(a,r+1)}return n}return e}close(){this.closed=!0,this.snapshot=void 0,this.tokens.clear();for(let e of this.workers)e.terminate();this.workers.clear()}};import{realpathSync as It,statSync as Me,readdirSync as yt}from"node:fs";import{homedir as Ot}from"node:os";import{basename as re,dirname as b,isAbsolute as Nt,relative as be,resolve as $}from"node:path";import{fileURLToPath as At}from"node:url";function Tt(t,e){let r=t.replace(/^@/,"");return r.startsWith("file://")&&(r=At(r)),(r==="~"||r.startsWith("~/"))&&(r=Ot()+r.slice(1)),$(e,r)}function oe(t){try{return It(t)}catch{let e=b(t);return e===t?t:$(oe(e),be(e,t))}}function Et(t,e){let r=be(t,e);return r===""||!r.startsWith("..")&&!Nt(r)}function Fe(t,e){try{let r=Me(t),n=Me(e);return r.dev===n.dev&&r.ino===n.ino}catch{return!1}}function xe(t,e,r,n){if(!n)return!0;let o=oe($(n));if(["bash","powershell"].includes(t)){let m=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(m)||m.includes(n))return!0}let a=e.path??e.file_path??e.filePath,f=["grep","find","ls"].includes(t);if(typeof a!="string"&&!f)return!1;try{let m=Tt(typeof a=="string"?a:".",r),d=oe(m),c=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,l=yt(b(o)).filter(s=>c.test(s)).some(s=>Fe(m,$(b(o),s)));return b(d)===b(o)&&(ee.test(re(d))||c.test(re(d))||re(d).startsWith(".protecter-migration"))||d===o||Fe(m,n)||l||f&&Et(d,o)}catch{return!0}}function ie(t,e){t.length=0;for(let r of e)t.push(r)}function se(t){if(!Array.isArray(t))throw i("COMPACT_FAILED");return t}function Ge(t){if(typeof t!="string")throw i("COMPACT_FAILED");return t}async function Ue(t,e,r){let{preparation:n}=t,o=await e.redact({messages:n.messagesToSummarize,prefix:n.turnPrefixMessages,previous:n.previousSummary??null,instructions:t.customInstructions??null},r);if(!o||typeof o!="object")throw i("COMPACT_FAILED");if(typeof t.customInstructions=="string"&&o.instructions!==t.customInstructions)throw i("COMPACT_INSTRUCTIONS");let a=se(o.messages),f=se(o.prefix),m=n.previousSummary===void 0?void 0:Ge(o.previous);ie(n.messagesToSummarize,a),ie(n.turnPrefixMessages,f),m!==void 0&&(n.previousSummary=m)}async function ve(t,e,r){let{preparation:n}=t,o=await e.redact({entries:n.entriesToSummarize,instructions:n.customInstructions??null},r);if(!o||typeof o!="object")throw i("COMPACT_FAILED");let a=se(o.entries),f=n.customInstructions===void 0?void 0:Ge(o.instructions);return ie(n.entriesToSummarize,a),f===void 0?{}:{customInstructions:f}}var ze=["stream","streamSimple"],Ct=["classify","generateImages"],Be=ze,ae=Symbol.for("pi-information-protecter.runtime-guard");function St(t){let e=t?.runtime;if(!e||typeof e!="object")throw i("GUARD_UNAVAILABLE");let r=e;for(let n of Be)if(typeof r[n]!="function")throw i("GUARD_UNAVAILABLE");return r}function wt(t){return!t||typeof t!="object"?!1:t.onPayload!==void 0}function He(t,e){let r=St(t);r[ae]?.();let n=[...Be,...Ct.filter(a=>typeof r[a]=="function")].map(a=>[a,r[a]]);for(let[a,f]of n){let m=ze.includes(a),d=function(c,l,s){return m&&wt(s)?f.call(this,c,l,s):(async()=>f.call(this,c,await e(c,l),s))()};r[a]=d}let o=()=>{for(let[a,f]of n)r[a]=f;delete r[ae]};return Object.defineProperty(r,ae,{value:o,configurable:!0,enumerable:!1,writable:!0}),o}import{lstat as Pt,readFile as Dt}from"node:fs/promises";import{resolve as Lt}from"node:path";var $e=256*1024;async function kt(t){let e=a=>/^(['"]).*\1$/s.test(a)?a.slice(1,-1):a,r=e(t);if(!r.startsWith("@"))return{text:r,fromFile:!1};let n=Lt(e(r.slice(1).trim())),o;try{o=await Pt(n)}catch(a){throw O(a,"TEST_INPUT")}if(!o.isFile())throw i("TEST_INPUT");if(o.size>$e)throw i("TEST_SIZE",{bytes:o.size,limit:$e});try{return{text:await Dt(n,"utf8"),fromFile:!0}}catch(a){throw O(a,"TEST_INPUT")}}function Mt(t){fe(X());let e=new H(_t()),r=ce(Rt),n=!1,o,a,f,m=l=>{let s=l?.provider;return typeof s=="string"?s:"unknown"},d=l=>{let s=R(l);return o={at:new Date().toISOString(),text:s},s};t.on("session_start",async(l,s)=>{try{if(!r)throw i("UNSUPPORTED_PI");await e.initialize(),f=He(s.modelRegistry,(y,A)=>e.redact(A,m(y))),n=!0;let u=e.ruleCount;s.ui.setStatus("protecter",p("statusBarRules",{rules:_("rules",u)})),u===0&&s.ui.notify(p("emptyRules"),"warning");let h=Ne(e.rawCompaction);h.length&&s.ui.notify(p("compactIgnoredKeys",{keys:h.join(", ")}),"warning")}catch(u){n=!1,s.ui.setStatus("protecter",p("statusBarNotReady")),s.ui.notify(d(O(u,"CONFIG_IO").diagnostic),"error")}}),t.on("before_provider_request",async(l,s)=>{try{if(!r)throw i("UNSUPPORTED_PI");let u=await e.redact(l.payload,s.model?.provider??"unknown");return n=!0,u}catch(u){n=!1;let h=d(O(u,"INTERNAL").diagnostic);try{s.abort()}catch{}try{s.ui.notify(h,"error")}catch{}return{}}}),t.on("message_end",l=>{if(l.message.role!=="assistant")return;let s=l.message;return{message:{...s,content:s.content.map(u=>u.type==="text"?{...u,text:e.restoreText(u.text)}:u.type==="thinking"?{...u,thinking:e.restoreText(u.thinking)}:u.type==="toolCall"?{...u,arguments:e.restore(u.arguments)}:u),...s.errorMessage?{errorMessage:e.restoreText(s.errorMessage)}:{}}}}),t.registerMarkdownTransformer((l,s)=>s.messageType==="user"?l:e.restoreText(l)),t.on("tool_call",(l,s)=>{if(!n)return{block:!0,reason:e.lastDiagnosticText??o?.text??R(i("NOT_READY").diagnostic)};let u=e.restore(l.input);for(let h of Object.keys(l.input))delete l.input[h];if(Object.assign(l.input,u),xe(l.toolName,l.input,s.cwd,e.configPath))return{block:!0,reason:p("toolBlocked")}});let c=async(l,s)=>{if(!r)throw i("UNSUPPORTED_PI");if(!e.ready)throw e.lastDiagnostic?i(e.lastDiagnostic.code,e.lastDiagnostic):i("NOT_READY");let u=e.compactionMode;if(u==="off")throw i("COMPACT_DENIED");if(a==="deny")throw i("COMPACT_DENIED");if(u!=="ask"||a==="allow")return;let h=[p("compactAllowOnce"),p("compactAllowAlways"),p("compactDenyOnce"),p("compactDenyAlways")],y=await l.ui.select(`${p("compactAskTitle")}
15
+ `);if(s.length>Ge)throw i("AUDIT_BATCH_LIMIT",{bytes:s.length,limit:Ge});let c=await Ue(t),u;try{u=ue(t,!0);let m=h.fstatSync(u).size;if(m+s.length>xe)throw i("AUDIT_FULL",{bytes:m+s.length,limit:xe});if(m){let l=Buffer.alloc(1);if(h.readSync(u,l,0,1,m-1),l[0]!==10)throw i("AUDIT_PARTIAL")}try{let l=0;for(;l<s.length;){let y=h.writeSync(u,s,l,s.length-l,m+l);if(!y)throw i("AUDIT_IO");l+=y}h.fsyncSync(u)}catch(l){throw h.ftruncateSync(u,m),h.fsyncSync(u),Y(l)}}catch(m){throw Y(m)}finally{u!==void 0&&h.closeSync(u),h.rmdirSync(c)}}function Be(t,e=20){if(!Number.isInteger(e)||e<1||e>100)throw i("INTERNAL",{limit:100});let o;try{try{o=ue(t,!1)}catch(A){if(A.code==="ENOENT")return{records:[],truncated:!1};throw A}let n=h.fstatSync(o).size,r=Math.max(0,n-1024*1024),s=Buffer.alloc(n-r),c=h.readSync(o,s,0,s.length,r),u=s.subarray(0,c).toString("utf8");if(r){let A=u.indexOf(`
16
+ `);u=A<0?"":u.slice(A+1)}let m=u.split(`
17
+ `),l=m.pop()!=="",y=[],I=!1;for(let A of m.slice(-e))try{let N=JSON.parse(A);if(N.version!==1||typeof N.time!="string"||typeof N.provider!="string"||typeof N.original!="string"||typeof N.replacement!="string"||typeof N.requestId!="string"||typeof N.timezoneOffset!="number")throw new Error;y.push(N)}catch{I=!0}return{records:y,truncated:r>0||m.length>e||l||I}}catch(n){throw Y(n)}finally{o!==void 0&&h.closeSync(o)}}var J=class{constructor(e,o=2e3,n=Fe){this.configDir=e;this.timeoutMs=o;this.machineId=n}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;attachmentOptions={};snapshot;lastError;restorePattern;restoreSize=-1;get lastDiagnostic(){return this.lastError?{...this.lastError}:void 0}get lastDiagnosticText(){return this.lastError?M(this.lastError):void 0}async initialize(){try{if(this.closed)throw i("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw i("MACHINE_ID")}let o=await be(this.configDir,e);if(this.closed)throw i("NOT_READY");this.snapshot=o,this.attachmentOptions.mode=o.config.attachments??"ask",this.lastError=void 0}catch(e){this.attachmentOptions.mode="deny";let o=w(e,"CONFIG_IO");throw this.lastError=o.diagnostic,o}}get configPath(){return this.snapshot?.path??""}get auditPath(){return this.configPath?this.configPath.replace(/\.json$/,".jsonl"):""}auditRecords(e=20){if(!this.ready)throw this.notReady();return Be(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return ve(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get compaction(){return $(this.snapshot?.config.compaction)}get compactionMode(){return this.compaction.mode}get attachmentMode(){return this.attachmentOptions.mode??"ask"}get rawCompaction(){return this.snapshot?.config.compaction}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,o="unknown"){let n=this.queue.then(()=>this.scan(e,o)).catch(r=>{let s=w(r,"INTERNAL");throw this.lastError=s.diagnostic,s});return this.queue=n.catch(()=>{}),n}notReady(){return this.lastError?i(this.lastError.code,this.lastError):i("NOT_READY")}async scan(e,o){if(!this.ready)throw this.notReady();let{config:n,sourceRaws:r}=this.snapshot,s;try{s=JSON.stringify(e)}catch{throw i("PAYLOAD_JSON")}if(!s)throw i("PAYLOAD_JSON");if(Buffer.byteLength(s)>8*1024*1024)throw i("PAYLOAD_SIZE",{bytes:Buffer.byteLength(s),limit:8*1024*1024});let c;try{c=JSON.parse(s)}catch{throw i("PAYLOAD_JSON")}let u=await new Promise((m,l)=>{let y=!1,I;try{I=new kt(Dt(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:c,rules:n.sensitiveWords,entries:[...this.tokens],configRaws:r},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(f){l(w(f,"WORKER_FAILED"));return}this.workers.add(I);let A=(f,a=i("WORKER_FAILED"))=>{if(!y){if(y=!0,clearTimeout(N),this.workers.delete(I),I.terminate(),!f||this.closed){l(this.closed?i("NOT_READY"):a);return}m(f)}},N=setTimeout(()=>A(void 0,i("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);I.once("message",f=>{f?.failed?A(void 0,H(f.diagnostic,"SCAN_INTERNAL")):f&&Array.isArray(f.additions)&&Array.isArray(f.hits)?A(f):A()}),I.once("error",()=>A()),I.once("exit",()=>A())});if(this.closed)throw i("NOT_READY");if(await ze(this.auditPath,o,u.hits),this.closed)throw i("NOT_READY");for(let[m,l]of u.additions)this.tokens.set(m,l);return u.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let o=n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((n,r)=>r.length-n.length).map(o).join("|"),"g"),this.restoreSize=this.tokens.size}return e.replace(this.restorePattern,o=>this.tokens.get(o)??o)}restore(e,o=0){if(o>80)throw i("RESTORE_COMPLEXITY",{node:o});if(typeof e=="string")return this.restoreText(e);if(typeof e=="number"){let n=this.tokens.get(String(e));if(n===void 0)return e;let r=Number(n);return!Number.isFinite(r)||String(r)!==n?e:r}if(Array.isArray(e))return e.map(n=>this.restore(n,o+1));if(e&&typeof e=="object"){let n=Object.create(null);for(let[r,s]of Object.entries(e)){let c=this.restoreText(r);if(Object.hasOwn(n,c))throw i("RESTORE_KEY_COLLISION");n[c]=this.restore(s,o+1)}return n}return e}close(){this.closed=!0,this.snapshot=void 0,this.tokens.clear();for(let e of this.workers)e.terminate();this.workers.clear()}};import{realpathSync as Lt,statSync as He,readdirSync as bt}from"node:fs";import{homedir as Mt}from"node:os";import{basename as de,dirname as z,isAbsolute as Ft,relative as je,resolve as W}from"node:path";import{fileURLToPath as xt}from"node:url";function Gt(t,e){let o=t.replace(/^@/,"");return o.startsWith("file://")&&(o=xt(o)),(o==="~"||o.startsWith("~/"))&&(o=Mt()+o.slice(1)),W(e,o)}function fe(t){try{return Lt(t)}catch{let e=z(t);return e===t?t:W(fe(e),je(e,t))}}function Ut(t,e){let o=je(t,e);return o===""||!o.startsWith("..")&&!Ft(o)}function $e(t,e){try{let o=He(t),n=He(e);return o.dev===n.dev&&o.ino===n.ino}catch{return!1}}function Ye(t,e,o,n){if(!n)return!0;let r=fe(W(n));if(["bash","powershell"].includes(t)){let u=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(u)||u.includes(n))return!0}let s=e.path??e.file_path??e.filePath,c=["grep","find","ls"].includes(t);if(typeof s!="string"&&!c)return!1;try{let u=Gt(typeof s=="string"?s:".",o),m=fe(u),l=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,y=bt(z(r)).filter(I=>l.test(I)).some(I=>$e(u,W(z(r),I)));return z(m)===z(r)&&(ce.test(de(m))||l.test(de(m))||de(m).startsWith(".protecter-migration"))||m===r||$e(u,n)||y||c&&Ut(m,r)}catch{return!0}}function me(t,e){t.length=0;for(let o of e)t.push(o)}function pe(t){if(!Array.isArray(t))throw i("COMPACT_FAILED");return t}function Je(t){if(typeof t!="string")throw i("COMPACT_FAILED");return t}async function We(t,e,o){let{preparation:n}=t,r=await e.redact({messages:n.messagesToSummarize,prefix:n.turnPrefixMessages,previous:n.previousSummary??null,instructions:t.customInstructions??null},o);if(!r||typeof r!="object")throw i("COMPACT_FAILED");if(typeof t.customInstructions=="string"&&r.instructions!==t.customInstructions)throw i("COMPACT_INSTRUCTIONS");let s=pe(r.messages),c=pe(r.prefix),u=n.previousSummary===void 0?void 0:Je(r.previous);me(n.messagesToSummarize,s),me(n.turnPrefixMessages,c),u!==void 0&&(n.previousSummary=u)}async function qe(t,e,o){let{preparation:n}=t,r=await e.redact({entries:n.entriesToSummarize,instructions:n.customInstructions??null},o);if(!r||typeof r!="object")throw i("COMPACT_FAILED");let s=pe(r.entries),c=n.customInstructions===void 0?void 0:Je(r.instructions);return me(n.entriesToSummarize,s),c===void 0?{}:{customInstructions:c}}var Ke=["stream","streamSimple"],vt=["classify","generateImages"],Xe=Ke,ge=Symbol.for("pi-information-protecter.runtime-guard");function zt(t){let e=t?.runtime;if(!e||typeof e!="object")throw i("GUARD_UNAVAILABLE");let o=e;for(let n of Xe)if(typeof o[n]!="function")throw i("GUARD_UNAVAILABLE");return o}function Bt(t){return!t||typeof t!="object"?!1:t.onPayload!==void 0}function Ht(t,e,o,n){if(!e||typeof e!="object")return;let r=e.onPayload;if(!n||typeof r!="function")return e;let s=r;return{...e,onPayload:async c=>{let u=n.runs(),m=await s(c);return n.runs()>u?m:(n.onBypass?.(),o(t,m===void 0?c:m))}}}function Ze(t,e,o){let n=zt(t);n[ge]?.();let r=[...Xe,...vt.filter(c=>typeof n[c]=="function")].map(c=>[c,n[c]]);for(let[c,u]of r){let m=Ke.includes(c),l=function(y,I,A){return m&&Bt(A)?u.call(this,y,I,Ht(y,A,e,o)):(async()=>u.call(this,y,await e(y,I),A))()};n[c]=l}let s=()=>{for(let[c,u]of r)n[c]=u;delete n[ge]};return Object.defineProperty(n,ge,{value:s,configurable:!0,enumerable:!1,writable:!0}),s}function q(t,e=0){if(e>80||!t||typeof t!="object")return!1;if(Array.isArray(t))return t.some(n=>q(n,e+1));let o=t;return Ve(o)||Object.keys(o).some(Qe)?!0:Object.values(o).some(n=>q(n,e+1))}var Ve=t=>typeof t.type=="string"&&/image|audio|video|document|(^|_)file($|_)/i.test(t.type),Qe=t=>/^(inlineData|inline_data|fileData|file_data|image_url|input_audio)$/.test(t);async function K(t,e,o=0){if(o>80)throw i("SCAN_COMPLEXITY");if(typeof t=="string")return e(t);if(Array.isArray(t))return Promise.all(t.map(n=>K(n,e,o+1)));if(t&&typeof t=="object"){let n=t;if(Ve(n))return t;let r=Object.create(null);for(let[s,c]of Object.entries(n))Qe(s)||s==="type"&&typeof c=="string"?r[s]=c:r[s]=await K(c,e,o+1);return r}return t}import{lstat as jt,readFile as Yt}from"node:fs/promises";import{resolve as Jt}from"node:path";var tt=256*1024;async function Wt(t){let e=s=>/^(['"]).*\1$/s.test(s)?s.slice(1,-1):s,o=e(t);if(!o.startsWith("@"))return{text:o,fromFile:!1};let n=Jt(e(o.slice(1).trim())),r;try{r=await jt(n)}catch(s){throw w(s,"TEST_INPUT")}if(!r.isFile())throw i("TEST_INPUT");if(r.size>tt)throw i("TEST_SIZE",{bytes:r.size,limit:tt});try{return{text:await Yt(n,"utf8"),fromFile:!0}}catch(s){throw w(s,"TEST_INPUT")}}function qt(t){we(oe());let e=new J($t()),o=te(t),n=()=>o.length===0,r=0,s=!1,c=!1,u,m,l,y,I=f=>{let a=f?.provider;return typeof a=="string"?a:"unknown"},A=f=>{let a=M(f);return u={at:new Date().toISOString(),text:a},a};t.on("session_start",async(f,a)=>{try{if(o=te(t,a),!n())throw a.ui.notify(d("capabilityBlocked",{names:o.join(", "),url:_}),"error"),i("PI_CAPABILITY");await e.initialize(),l=void 0,y=Ze(a.modelRegistry,(C,E)=>e.redact(E,I(C)),{runs:()=>r,onBypass:()=>{if(!s){s=!0;try{a.ui.notify(d("hookBypassed",{url:_}),"warning")}catch{}}}}),c=!0,ye(et)||a.ui.notify(d("piNewer",{version:et,verified:he,url:_}),"warning");let g=e.ruleCount;a.ui.setStatus("protecter",d("statusBarRules",{rules:b("rules",g)})),g===0&&a.ui.notify(d("emptyRules"),"warning");let p=Pe(e.rawCompaction);p.length&&a.ui.notify(d("compactIgnoredKeys",{keys:p.join(", ")}),"warning")}catch(g){c=!1,a.ui.setStatus("protecter",d("statusBarNotReady")),a.ui.notify(A(w(g,"CONFIG_IO").diagnostic),"error")}}),t.on("before_provider_request",async(f,a)=>{r++;try{if(!n())throw i("PI_CAPABILITY");let g=e.attachmentMode;if(q(f.payload)){let C=!1;if(g==="deny"||l==="deny")C=!1;else if(g==="allow"||l==="allow")C=!0;else if(g==="ask"&&!l){let k=[d("attachmentAllow"),d("attachmentDeny")],D=await a.ui.select(`${d("attachmentAskTitle")}
18
18
 
19
- ${p("compactAskBody")}`,h,{signal:s}),A=y===void 0?-1:h.indexOf(y);if(A===1?(a="allow",l.ui.notify(p("compactAlwaysNotice"),"info")):A===3&&(a="deny",l.ui.notify(p("compactDenyNotice"),"warning")),A!==0&&A!==1)throw i("COMPACT_DENIED")};t.on("session_before_compact",async(l,s)=>{try{await c(s,l.signal),await Ue(l,e,s.model?.provider??"unknown"),s.ui.notify(p("compactRedacted"),"info");return}catch(u){return s.ui.notify(d(O(u,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.on("session_before_tree",async(l,s)=>{if(l.preparation.userWantsSummary)try{await c(s,l.signal);let u=await ve(l,e,s.model?.provider??"unknown");return s.ui.notify(p("compactRedacted"),"info"),u}catch(u){return s.ui.notify(d(O(u,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.registerCommand("protecter",{description:p("commandDescription"),handler:async(l,s)=>{let u=l.trim(),h=u.split(/\s+/).filter(Boolean),y=h[0]??"logs";if(y==="test"){let T=u.slice(y.length).trim();if(!T){s.ui.notify(p("usage"),"info");return}try{if(!e.ready)throw i("NOT_READY");let{text:w,fromFile:L}=await kt(T),E=(await e.redact({text:w},"test")).text;if(typeof E!="string")throw i("INTERNAL");let k=[p(L?"testFileLabel":"testTextLabel"),p(E===w?"testUnchanged":"testRedacted",{chars:_("chars",E.length)})].join(`
20
- `);s.mode==="tui"?await s.ui.editor(p("testPreviewTitle"),`${k}
19
+ ${d("attachmentAskBody")}`,k);C=D===k[0],D!==void 0&&(l=C?"allow":"deny")}if(!C)throw i("SCAN_ATTACHMENT");a.ui.notify(d("attachmentAllowed"),"warning");let E=a.model?.provider??"unknown",X=await K(f.payload,async k=>{let D=await e.redact(k,E);return typeof D=="string"?D:k});return c=!0,X}let p=await e.redact(f.payload,a.model?.provider??"unknown");return c=!0,p}catch(g){c=!1;let p=A(w(g,"INTERNAL").diagnostic);try{a.abort()}catch{}try{a.ui.notify(p,"error")}catch{}return{}}}),t.on("message_end",f=>{if(f.message.role!=="assistant")return;let a=f.message;return{message:{...a,content:a.content.map(g=>g.type==="text"?{...g,text:e.restoreText(g.text)}:g.type==="thinking"?{...g,thinking:e.restoreText(g.thinking)}:g.type==="toolCall"?{...g,arguments:e.restore(g.arguments)}:g),...a.errorMessage?{errorMessage:e.restoreText(a.errorMessage)}:{}}}}),t.registerMarkdownTransformer((f,a)=>a.messageType==="user"?f:e.restoreText(f)),t.on("tool_call",(f,a)=>{if(!c)return{block:!0,reason:e.lastDiagnosticText??u?.text??M(i("NOT_READY").diagnostic)};let g=e.restore(f.input);for(let p of Object.keys(f.input))delete f.input[p];if(Object.assign(f.input,g),Ye(f.toolName,f.input,a.cwd,e.configPath))return{block:!0,reason:d("toolBlocked")}});let N=async(f,a)=>{if(!n())throw i("PI_CAPABILITY");if(!e.ready)throw e.lastDiagnostic?i(e.lastDiagnostic.code,e.lastDiagnostic):i("NOT_READY");let g=e.compactionMode;if(g==="off")throw i("COMPACT_DENIED");if(m==="deny")throw i("COMPACT_DENIED");if(g!=="ask"||m==="allow")return;let p=[d("compactAllowOnce"),d("compactAllowAlways"),d("compactDenyOnce"),d("compactDenyAlways")],C=await f.ui.select(`${d("compactAskTitle")}
21
20
 
22
- ${E}`):s.ui.notify(`${k}
21
+ ${d("compactAskBody")}`,p,{signal:a}),E=C===void 0?-1:p.indexOf(C);if(E===1?(m="allow",f.ui.notify(d("compactAlwaysNotice"),"info")):E===3&&(m="deny",f.ui.notify(d("compactDenyNotice"),"warning")),E!==0&&E!==1)throw i("COMPACT_DENIED")};t.on("session_before_compact",async(f,a)=>{try{await N(a,f.signal),await We(f,e,a.model?.provider??"unknown"),a.ui.notify(d("compactRedacted"),"info");return}catch(g){return a.ui.notify(A(w(g,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.on("session_before_tree",async(f,a)=>{if(f.preparation.userWantsSummary)try{await N(a,f.signal);let g=await qe(f,e,a.model?.provider??"unknown");return a.ui.notify(d("compactRedacted"),"info"),g}catch(g){return a.ui.notify(A(w(g,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.registerCommand("protecter",{description:d("commandDescription"),handler:async(f,a)=>{let g=f.trim(),p=g.split(/\s+/).filter(Boolean),C=p[0]??"logs";if(C==="test"){let T=g.slice(C.length).trim();if(!T){a.ui.notify(d("usage"),"info");return}try{if(!e.ready)throw i("NOT_READY");let{text:L,fromFile:G}=await Wt(T),S=(await e.redact({text:L},"test")).text;if(typeof S!="string")throw i("INTERNAL");let U=[d(G?"testFileLabel":"testTextLabel"),d(S===L?"testUnchanged":"testRedacted",{chars:b("chars",S.length)})].join(`
22
+ `);a.mode==="tui"?await a.ui.editor(d("testPreviewTitle"),`${U}
23
23
 
24
- ${E}`,"info")}catch(w){s.ui.notify(d(O(w,"TEST_INPUT").diagnostic),"error")}return}if(!["status","reload","logs"].includes(y)||y!=="logs"&&h.length>1||h.length>2||h[1]!==void 0&&h[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(h[1])){s.ui.notify(p("usage"),"info");return}if(y==="logs"){if(s.mode!=="tui"){s.ui.notify(p("logsTuiOnly"),"warning");return}if(h[1]==="clear"){try{if(!e.ready)throw i("NOT_READY");if(!await s.ui.confirm(p("clearTitle"),p("clearBody")))return;await s.waitForIdle();let T=await e.clearAuditRecords();n=e.ready,s.ui.notify(p("cleared",{bytes:_("bytes",T)}),"info")}catch(T){s.ui.notify(d(O(T,"AUDIT_IO").diagnostic),"error")}return}try{let{records:T,truncated:w}=e.auditRecords(Number(h[1]??20));if(!T.length){s.ui.notify(p("logsNone"),"info");return}let L=G=>JSON.stringify(G).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,W=>`\\u${W.charCodeAt(0).toString(16).padStart(4,"0")}`),Y=T.map((G,W)=>`${W+1}. ${L(G.time)} | ${L(G.provider).slice(0,120)}`),E=await s.ui.select(p("logsSelectTitle")+(w?p("logsTruncated"):""),Y),k=E===void 0?-1:Y.indexOf(E);if(k<0||!await s.ui.confirm(p("logsRevealTitle"),p("logsRevealBody")))return;let j=L(T[k]);await s.ui.editor(p("logsPreviewTitle"),j.length>2e4?`${j.slice(0,2e4)}
25
- ${p("logsPreviewTruncated")}`:j)}catch(T){s.ui.notify(d(O(T,"AUDIT_IO").diagnostic),"error")}return}if(y==="reload"){await s.waitForIdle(),await s.reload();return}let A=e.lastDiagnosticText??o?.text,J=p("statusLine",{state:p(e.ready?"statusReady":"statusNotReady"),rules:_("rules",e.ruleCount),mappings:_("mappings",e.mappingCount)}),N=`
26
- ${p("statusCompaction",{mode:e.compactionMode,guard:p(f?"guardInstalled":"guardMissing")})}`,x=A?`
27
- ${p("statusLastBlock",{at:o?.at??""})}
28
- ${A}`:`
29
- ${p("statusNoBlock")}`;s.ui.notify(J+N+x,e.ready?"info":"error")}}),t.on("session_shutdown",()=>{f?.(),f=void 0,e.close()})}export{Mt as default};
24
+ ${S}`):a.ui.notify(`${U}
25
+
26
+ ${S}`,"info")}catch(L){a.ui.notify(A(w(L,"TEST_INPUT").diagnostic),"error")}return}if(!["status","reload","logs"].includes(C)||C!=="logs"&&p.length>1||p.length>2||p[1]!==void 0&&p[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(p[1])){a.ui.notify(d("usage"),"info");return}if(C==="logs"){if(a.mode!=="tui"){a.ui.notify(d("logsTuiOnly"),"warning");return}if(p[1]==="clear"){try{if(!e.ready)throw i("NOT_READY");if(!await a.ui.confirm(d("clearTitle"),d("clearBody")))return;await a.waitForIdle();let T=await e.clearAuditRecords();c=e.ready,a.ui.notify(d("cleared",{bytes:b("bytes",T)}),"info")}catch(T){a.ui.notify(A(w(T,"AUDIT_IO").diagnostic),"error")}return}try{let{records:T,truncated:L}=e.auditRecords(Number(p[1]??20));if(!T.length){a.ui.notify(d("logsNone"),"info");return}let G=B=>JSON.stringify(B).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,Q=>`\\u${Q.charCodeAt(0).toString(16).padStart(4,"0")}`),Z=T.map((B,Q)=>`${Q+1}. ${G(B.time)} | ${G(B.provider).slice(0,120)}`),S=await a.ui.select(d("logsSelectTitle")+(L?d("logsTruncated"):""),Z),U=S===void 0?-1:Z.indexOf(S);if(U<0||!await a.ui.confirm(d("logsRevealTitle"),d("logsRevealBody")))return;let V=G(T[U]);await a.ui.editor(d("logsPreviewTitle"),V.length>2e4?`${V.slice(0,2e4)}
27
+ ${d("logsPreviewTruncated")}`:V)}catch(T){a.ui.notify(A(w(T,"AUDIT_IO").diagnostic),"error")}return}if(C==="reload"){await a.waitForIdle(),await a.reload();return}let E=e.lastDiagnosticText??u?.text,X=d("statusLine",{state:d(e.ready?"statusReady":"statusNotReady"),rules:b("rules",e.ruleCount),mappings:b("mappings",e.mappingCount)}),k=`
28
+ ${d("statusCompaction",{mode:e.compactionMode,guard:d(y?"guardInstalled":"guardMissing")})}`,D=E?`
29
+ ${d("statusLastBlock",{at:u?.at??""})}
30
+ ${E}`:`
31
+ ${d("statusNoBlock")}`;a.ui.notify(X+k+D,e.ready?"info":"error")}}),t.on("session_shutdown",()=>{y?.(),y=void 0,e.close()})}export{qt as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-information-protecter",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Sensitive Personal Information (SPI) Protecter — local reversible privacy redaction for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,10 +40,10 @@
40
40
  "node": ">=22"
41
41
  },
42
42
  "peerDependencies": {
43
- "@earendil-works/pi-coding-agent": ">=0.84.0 <0.85.0 || >=0.85.1 <0.88.0 || >=0.99.0 <1.0.0 || >=1.2.0 <1.4.0"
43
+ "@earendil-works/pi-coding-agent": ">=0.84.0 <0.85.0 || >=0.85.1 <1.0.0 || >=1.2.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@earendil-works/pi-coding-agent": "1.3.0",
46
+ "@earendil-works/pi-coding-agent": "1.4.0",
47
47
  "@types/node": "^22.0.0",
48
48
  "esbuild": "0.28.2",
49
49
  "tsx": "^4.20.0",