pi-information-protecter 0.6.2 → 0.6.4
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 +26 -0
- package/README.md +34 -4
- package/SECURITY.md +4 -2
- package/dist/index.mjs +11 -11
- package/dist/scan-worker.mjs +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog / 更新日志
|
|
2
2
|
|
|
3
|
+
## 0.6.4 — Pi 0.86 support / 支持 Pi 0.86
|
|
4
|
+
|
|
5
|
+
### Added / 新增
|
|
6
|
+
|
|
7
|
+
- Support Pi 0.86.x. The runtime gate accepted only 0.85.x, so on 0.86 every request failed closed with `UNSUPPORTED_PI`. Verified against 0.85.1, 0.86.0 and 0.86.1, including the real loader and worker integration tests.
|
|
8
|
+
支持 Pi 0.86.x。运行时门禁只接受 0.85.x,因此在 0.86 上每个请求都以 `UNSUPPORTED_PI` 拒绝放行。已在 0.85.1、0.86.0 和 0.86.1 上验证,包含真实加载器与 worker 集成测试。
|
|
9
|
+
|
|
10
|
+
### Changed / 变更
|
|
11
|
+
|
|
12
|
+
- Wait up to six minutes for the registry to serve a published version, because npm accepts a publish before the read path serves it; a three-minute wait reported a false failure for an accepted 0.6.2 publish.
|
|
13
|
+
发布后最多等待六分钟直到 registry 可读,因为 npm 接受发布早于读取端可见;三分钟的等待曾对已被接受的 0.6.2 发布报出虚假失败。
|
|
14
|
+
|
|
15
|
+
## 0.6.3 — Conversations no longer block after a rule learns a value / 规则学习到值后不再拦截会话
|
|
16
|
+
|
|
17
|
+
### Fixed / 修复
|
|
18
|
+
|
|
19
|
+
- Stop blocking a conversation once a regex rule has learned a value. Since 0.6.0 a replacement keeps the shape of what the rule matches, so the rule matches its own replacement in later turns; the alias-crossing guard treated that as a crossing and blocked every following request with `FIXED_ALIAS_CROSSING`. Only a match that straddles a replacement boundary is a crossing now; a match that is, or sits inside, a replacement is expected and skipped.
|
|
20
|
+
正则规则学习到值之后不再拦截整个会话。0.6.0 起替换值与规则所匹配的内容同形,因此后续轮次中规则会匹配到自身的替换值;别名跨越防护把这种情况当作跨越,使之后每个请求都以 `FIXED_ALIAS_CROSSING` 被拦。现在只有横跨替换值边界的匹配才算跨越,匹配到替换值本身或其内部属于预期并跳过。
|
|
21
|
+
- Add a regression covering later turns that carry a learned replacement in history, while keeping the existing straddling-match rejection.
|
|
22
|
+
新增回归,覆盖历史中带有已学习替换值的后续轮次,同时保留对横跨匹配的拒绝。
|
|
23
|
+
|
|
24
|
+
### Documentation / 文档
|
|
25
|
+
|
|
26
|
+
- Document that the model reasons about the replacement, so answers derived from a protected value, such as a digit position or a checksum, are wrong even though the value itself is restored in the reply.
|
|
27
|
+
文档说明模型推理的是替换值,因此从受保护值推导出的答案(如第几位、校验位)是错的,尽管值本身在回复中会被还原。
|
|
28
|
+
|
|
3
29
|
## 0.6.2 — Numeric replacement retry and restoration / 数值替换重试与还原
|
|
4
30
|
|
|
5
31
|
### Fixed / 修复
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A Pi extension that inspects outgoing LLM request bodies locally, replaces confi
|
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
|
-
Requires Node.js 22+ and `@earendil-works/pi-coding-agent` **0.85.1–0.
|
|
13
|
+
Requires Node.js 22+ and `@earendil-works/pi-coding-agent` **0.85.1–0.86.x**, tested against 0.85.1, 0.86.0 and 0.86.1. Legacy `@mariozechner/*` releases are not supported.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# Install the pinned npm release.
|
|
@@ -105,6 +105,21 @@ Letters and digits are replaced inside their own writing system: ASCII, Latin-1,
|
|
|
105
105
|
|
|
106
106
|
**This is a deliberate trade-off.** An opaque token revealed only that a redaction happened; a shape-preserving value also reveals length, character classes and punctuation structure, and it looks like plausible real data to the model. Text in unsupported scripts is preserved as-is, so a rule matching such text may leave part of it visible.
|
|
107
107
|
|
|
108
|
+
**The model reasons about the replacement, so answers derived from a protected value are wrong.** Asking for the third digit of a protected ID returns the third digit of the replacement, and only the value itself is restored in the reply, never the conclusion drawn from it. The answer therefore looks confident and consistent but does not match the restored value. An opaque token made this obvious, because the model could only report that it could not read the value. Do not ask the model about digits, checksums, ordering, arithmetic or comparisons involving protected values; compute those locally.
|
|
109
|
+
|
|
110
|
+
Worked example, asking for the third digit of a protected phone number:
|
|
111
|
+
|
|
112
|
+
| Step | Value |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| What you typed | `13800138000` — third digit is `8` |
|
|
115
|
+
| What the provider received | `27431905882` — third digit is `4` |
|
|
116
|
+
| What the model answered | "the third digit is `4`" |
|
|
117
|
+
| What you see after restoration | `13800138000` … "the third digit is `4`" |
|
|
118
|
+
|
|
119
|
+
The number is restored, the reasoning is not, so the reply quietly contradicts itself. Nothing flags this: a derived answer is ordinary text, indistinguishable from any other sentence. The reply is also consistent across retries, because the same value keeps the same replacement, so re-asking does not reveal the error.
|
|
120
|
+
|
|
121
|
+
If a field must be reasoned about correctly, either leave it unprotected, or give it a fixed `replacement` and accept that the alias is semantically recognisable. There is no option that both hides a value and lets the model compute over it.
|
|
122
|
+
|
|
108
123
|
Replacements are checked so restoration stays exact: a candidate is rejected if it equals the original, is already mapped, or already appears anywhere in the same request. When no unique candidate can be produced, typically for very short matches, the request is blocked with `SCAN_UNIQUE_FAILED`. Because a shaped value is indistinguishable from ordinary content, a model that independently emits the same short string will have it restored to the original, so prefer rules that match longer, distinctive values.
|
|
109
124
|
|
|
110
125
|
### Message language
|
|
@@ -198,7 +213,7 @@ Tests use temporary directories and fictional secrets, never real user configura
|
|
|
198
213
|
|
|
199
214
|
`npm run build` bundles local modules with esbuild, removes TypeScript types and minifies output. Node built-ins and Pi peer packages remain external. No source maps or source files are published; source remains available on GitHub. Minification reduces size, not visibility or security. `npm pack` rebuilds via `prepack`; `npm publish` first runs checks. Tests also extract the actual npm tarball and run Pi's loader and both workers from it. See the repository's `docs/BUILDING.md` for details.
|
|
200
215
|
|
|
201
|
-
API references: [Pi Extensions](https://pi.dev/docs/latest/extensions), local 0.
|
|
216
|
+
API references: [Pi Extensions](https://pi.dev/docs/latest/extensions), local 0.86.1 documentation and runner implementation. Licensed under MIT.
|
|
202
217
|
|
|
203
218
|
---
|
|
204
219
|
|
|
@@ -210,7 +225,7 @@ API references: [Pi Extensions](https://pi.dev/docs/latest/extensions), local 0.
|
|
|
210
225
|
|
|
211
226
|
## 安装
|
|
212
227
|
|
|
213
|
-
要求 Node.js 22+ 和 `@earendil-works/pi-coding-agent` **0.85.1–0.
|
|
228
|
+
要求 Node.js 22+ 和 `@earendil-works/pi-coding-agent` **0.85.1–0.86.x**,已在 0.85.1、0.86.0 和 0.86.1 上验证。不支持旧 `@mariozechner/*` 版本。
|
|
214
229
|
|
|
215
230
|
```bash
|
|
216
231
|
# 安装固定 npm 版本。
|
|
@@ -305,6 +320,21 @@ pi -e ./src/index.ts
|
|
|
305
320
|
|
|
306
321
|
**这是有意为之的权衡。** 不透明占位符仅暴露“发生了脱敏”;同形值还会暴露长度、字符类别和标点结构,并且在模型看来像真实数据。不支持的文字体系会原样保留,因此匹配这类文本的规则可能残留部分可见内容。
|
|
307
322
|
|
|
323
|
+
**模型推理的是替换值,因此从受保护值推导出的答案是错的。** 询问受保护身份证号的第 3 位,得到的是替换值的第 3 位;回复中只有值本身会被还原,由它推导出的结论不会。因此答案看似笃定且自洽,却与还原后的值对不上。不透明占位符不会有这个问题,因为模型只能表示自己读不到该值。请勿让模型回答涉及受保护值的位数、校验位、排序、算术或比较,这类计算应在本地完成。
|
|
324
|
+
|
|
325
|
+
实例:询问受保护手机号的第 3 位。
|
|
326
|
+
|
|
327
|
+
| 环节 | 值 |
|
|
328
|
+
| --- | --- |
|
|
329
|
+
| 你输入的内容 | `13800138000`——第 3 位是 `8` |
|
|
330
|
+
| 提供商实际收到的 | `27431905882`——第 3 位是 `4` |
|
|
331
|
+
| 模型的回答 | “第 3 位是 `4`” |
|
|
332
|
+
| 还原后你看到的 | `13800138000` … “第 3 位是 `4`” |
|
|
333
|
+
|
|
334
|
+
号码被还原了,推理没有,因此回复会静默地自相矛盾。没有任何机制能发现它:推导出的答案就是普通文本,与其他句子无法区分。重试也不会暴露错误,因为同一值始终对应同一替换值,结果前后一致。
|
|
335
|
+
|
|
336
|
+
如果某个字段必须被正确推理,要么不保护它,要么为它设置固定 `replacement` 并接受别名在语义上可识别。不存在既隐藏值、又能让模型对它运算的方案。
|
|
337
|
+
|
|
308
338
|
为保证还原精确,候选值会被校验:与原文相同、已被占用、或已出现在同一请求中的候选值会被拒绝。无法生成唯一值时(通常是极短匹配),以 `SCAN_UNIQUE_FAILED` 拦截请求。由于同形值与普通内容难以区分,若模型自行输出了相同的短字符串,它会被还原为原文,因此应优先匹配更长、更有辨识度的值。
|
|
309
339
|
|
|
310
340
|
### 消息语言
|
|
@@ -398,4 +428,4 @@ npm run check
|
|
|
398
428
|
|
|
399
429
|
`npm run build` 使用 esbuild 合并本地模块、移除类型并压缩产物,Node 内置模块和 Pi peer 依赖保持外部引用。不发布源码或 source map,源码仍可在 GitHub 查阅。压缩是体积优化,不是加密或安全隔离。`npm pack` 通过 `prepack` 自动构建,`npm publish` 先执行检查。测试还会解压实际 npm 压缩包,用 Pi 加载器和两个 worker 验证产物。详见仓库内的 `docs/BUILDING.md`。
|
|
400
430
|
|
|
401
|
-
接口依据为 [Pi 官方扩展文档](https://pi.dev/docs/latest/extensions)、本机 0.
|
|
431
|
+
接口依据为 [Pi 官方扩展文档](https://pi.dev/docs/latest/extensions)、本机 0.86.1 文档及执行器实现;采用 MIT 许可证。
|
package/SECURITY.md
CHANGED
|
@@ -21,6 +21,8 @@ Prevent configured personal information from accidentally entering Pi LLM reques
|
|
|
21
21
|
|
|
22
22
|
- Scan outgoing payload text and replace matches with random shape-preserving values. Runtime mappings stay in memory, but version 0.3.0 adds plaintext original/replacement audit pairs in private JSONL. Audit files are sensitive credential stores, not encrypted archives; never upload them. Logs persist after exit and are not loaded to restore mappings.
|
|
23
23
|
出站文本使用随机同形值替换,运行映射保存在内存,但 0.3.0 新增私有 JSONL 原文与替换值审计。日志属于敏感凭证存储,不是加密归档,禁止上传;退出后仍保留,不用于恢复映射。
|
|
24
|
+
- The model reasons about the replacement, so any answer derived from a protected value, such as a digit position, a checksum, a comparison or arithmetic, is wrong. Only the value is restored in the reply, never the conclusion drawn from it, so the reply looks confident and stays consistent across retries. Verify such answers locally.
|
|
25
|
+
模型推理的是替换值,因此任何从受保护值推导出的答案(位数、校验位、比较、算术)都是错的。回复中只有值被还原,由它推导出的结论不会,因此答案看似笃定且重试结果一致。请在本地核对。
|
|
24
26
|
- Shape-preserving replacements (0.6.0) keep JSON types, length, character class and punctuation structure, so they leak more metadata than opaque tokens and look like plausible real data. Unsupported scripts and symbols are preserved unchanged, so part of a matched value may stay visible. Short matches may be restored when a model independently emits the same string; prefer long, distinctive targets. Numeric values that cannot round-trip and matches with no unique replacement fail closed.
|
|
25
27
|
同形替换(0.6.0)保留 JSON 类型、长度、字符类别和标点结构,因此比不透明占位符泄露更多元信息,且看似真实数据。不支持的文字体系和符号原样保留,命中值可能部分可见。模型自行输出相同短字符串时可能被还原,应优先使用较长且有辨识度的目标;无法精确往返的数值以及无唯一替换值的匹配均拒绝放行。
|
|
26
28
|
- Audit writes use 0600 files, safe-open checks, a cooperative lock and fsync before returning a redacted payload. Logging errors fail closed. Success means local replacement, not provider delivery. File size and preview limits bound resource usage; users manage retention locally. Hard crashes may leave stale locks or partial records. Same-UID writers and terminal capture remain outside the threat model.
|
|
@@ -31,8 +33,8 @@ Prevent configured personal information from accidentally entering Pi LLM reques
|
|
|
31
33
|
拦截诊断仅包含目录错误码、阶段、非负整数定位和白名单系统错误码。规则序号和节点数只是位置,不是内容。不输出原始异常消息、请求文本、命中原文、替换值、配置内容和文件路径,worker 边界同样适用,因此诊断可安全分享;错误码有意保持粗粒度,精确定位规则仍可能需要本地查看。
|
|
32
34
|
- Limit worker regex scans to 2 seconds, with resource and input limits. Errors exclude original configuration, rules and request text.
|
|
33
35
|
worker 正则扫描限制为 2 秒,并限制资源和输入;错误不包含原始配置、规则或请求文本。
|
|
34
|
-
- Pi 0.85.1 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.
|
|
35
|
-
Pi 0.85.1 会捕获该钩子异常并继续使用当前请求体,因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求,只保证本处理器不回退原始请求体。
|
|
36
|
+
- Pi 0.85.1–0.86.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.85.1–0.86.x 会捕获该钩子异常并继续使用当前请求体,因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求,只保证本处理器不回退原始请求体。
|
|
36
38
|
- 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.
|
|
37
39
|
独立创建配置,Unix 权限为 0600;拒绝链接、非普通文件、多链接及他人所有文件。用户仍需维护目录和系统权限,无法防止检查与使用之间的竞态。
|
|
38
40
|
- Direct-access blocking is supplementary. File tools inspect target paths rather than document bodies. Shell can bypass string checks, and command mentions or ancestor directory checks can still cause false positives.
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
|
|
2
|
-
import{getAgentDir as et,VERSION as Ee}from"@earendil-works/pi-coding-agent";import{Worker as Ye}from"node:worker_threads";import{fileURLToPath as We}from"node:url";import*as d from"node:fs";import{join as C,resolve as Ge}from"node:path";import{createHash as Me,randomUUID as ke}from"node:crypto";import{Worker as Ue}from"node:worker_threads";import{fileURLToPath as ve}from"node:url";var z={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 0.85.1-0.85.x"],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"],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"]},_e={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["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:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"]},q={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]. Never type secrets as arguments.",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.",compactUnsupported:"SPI Protecter does not support remote context compaction yet; use /new to start a new session."},Ae={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]。不要在参数中输入敏感内容。",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。",compactUnsupported:"SPI Protecter 暂不支持远程上下文压缩,请使用 /new 开始新会话。"};function H(n=process.env){let e=n.PI_PROTECTER_LANG||n.LC_ALL||n.LC_MESSAGES||n.LANG||n.LANGUAGE||Se();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function Se(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var _=H();function V(){return _}function Q(n){_=n}function m(n,e={}){return((_==="zh"?Ae:q)[n]??q[n]).replace(/\{(\w+)\}/g,(t,o)=>Object.hasOwn(e,o)?String(e[o]):t)}var Te={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"}}},Z=new Map;function R(n,e){let r=_==="zh"?"zh-CN":"en",t=Z.get(_);if(!t){try{t=new Intl.PluralRules(r)}catch{t=void 0}t&&Z.set(_,t)}let o=t?.select(e)??"other",s=Te[_][n],l=s[o]??s.other,i=new Intl.NumberFormat(r).format(e);return l.replace("{n}",i)}function ee(n){let r=(_==="zh"?_e[n]:z[n])??z[n]??z.INTERNAL;return{reason:r[0],action:r[1]}}var te={INTERNAL:"internal",NOT_READY:"initialization",UNSUPPORTED_PI:"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",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",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit"},re=["rule","otherRule","node","bytes","limit","timeoutMs"],Ce=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function we(n,e={}){let{reason:r,action:t}=ee(n),o={};for(let s of re){let l=e[s];typeof l=="number"&&Number.isSafeInteger(l)&&l>=0&&(o[s]=l)}return typeof e.errno=="string"&&Ce.test(e.errno)&&(o.errno=e.errno),{code:n,stage:te[n],reason:r,action:t,...o}}function S(n){let e=V()==="zh"?":":": ",r=[...re,"errno"].filter(t=>n[t]!==void 0).map(t=>`${t}=${n[t]}`).join(" ");return[m("blockedHeader"),`code=${n.code} stage=${n.stage}${r?` ${r}`:""}`,`${m("reasonLabel")}${e}${n.reason}`,`${m("actionLabel")}${e}${n.action}`,`${m("detailsLabel")}${e}/protecter status`].join(`
|
|
3
|
-
`)}var
|
|
4
|
-
`)}finally{
|
|
5
|
-
`;if(Buffer.byteLength(
|
|
2
|
+
import{getAgentDir as et,VERSION as tt}from"@earendil-works/pi-coding-agent";import{Worker as Ye}from"node:worker_threads";import{fileURLToPath as We}from"node:url";import*as g from"node:fs";import{join as C,resolve as Ge}from"node:path";import{createHash as Me,randomUUID as ke}from"node:crypto";import{Worker as Ue}from"node:worker_threads";import{fileURLToPath as ve}from"node:url";var z={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 0.85.1-0.85.x"],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"],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"]},_e={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["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:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"]},Z={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]. Never type secrets as arguments.",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.",compactUnsupported:"SPI Protecter does not support remote context compaction yet; use /new to start a new session."},Ae={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]。不要在参数中输入敏感内容。",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。",compactUnsupported:"SPI Protecter 暂不支持远程上下文压缩,请使用 /new 开始新会话。"};function H(n=process.env){let e=n.PI_PROTECTER_LANG||n.LC_ALL||n.LC_MESSAGES||n.LANG||n.LANGUAGE||Se();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function Se(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var E=H();function Q(){return E}function ee(n){E=n}function m(n,e={}){return((E==="zh"?Ae:Z)[n]??Z[n]).replace(/\{(\w+)\}/g,(t,o)=>Object.hasOwn(e,o)?String(e[o]):t)}var Te={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"}}},V=new Map;function D(n,e){let r=E==="zh"?"zh-CN":"en",t=V.get(E);if(!t){try{t=new Intl.PluralRules(r)}catch{t=void 0}t&&V.set(E,t)}let o=t?.select(e)??"other",s=Te[E][n],u=s[o]??s.other,l=new Intl.NumberFormat(r).format(e);return u.replace("{n}",l)}function te(n){let r=(E==="zh"?_e[n]:z[n])??z[n]??z.INTERNAL;return{reason:r[0],action:r[1]}}var re={INTERNAL:"internal",NOT_READY:"initialization",UNSUPPORTED_PI:"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",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",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit"},ne=["rule","otherRule","node","bytes","limit","timeoutMs"],Ce=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function we(n,e={}){let{reason:r,action:t}=te(n),o={};for(let s of ne){let u=e[s];typeof u=="number"&&Number.isSafeInteger(u)&&u>=0&&(o[s]=u)}return typeof e.errno=="string"&&Ce.test(e.errno)&&(o.errno=e.errno),{code:n,stage:re[n],reason:r,action:t,...o}}function S(n){let e=Q()==="zh"?":":": ",r=[...ne,"errno"].filter(t=>n[t]!==void 0).map(t=>`${t}=${n[t]}`).join(" ");return[m("blockedHeader"),`code=${n.code} stage=${n.stage}${r?` ${r}`:""}`,`${m("reasonLabel")}${e}${n.reason}`,`${m("actionLabel")}${e}${n.action}`,`${m("detailsLabel")}${e}/protecter status`].join(`
|
|
3
|
+
`)}var _=class extends Error{diagnostic;constructor(e,r={}){let t=we(e,r);super(S(t)),this.name="ProtectionError",this.diagnostic=t}};function a(n,e={}){return new _(n,e)}function y(n,e,r={}){if(n instanceof _)return a(n.diagnostic.code,{...n.diagnostic,...r});let t=n&&typeof n=="object"&&"code"in n?n.code:void 0;return a(e,{...r,errno:typeof t=="string"?t:void 0})}function b(n,e){if(!n||typeof n!="object")return a(e);let r=n,t=typeof r.code=="string"&&Object.hasOwn(re,r.code)?r.code:e;return a(t,r)}import{constants as T,closeSync as oe,fchmodSync as Re,fstatSync as De,mkdirSync as Le,openSync as ie,readFileSync as Fe,writeFileSync as Pe}from"node:fs";import{dirname as be}from"node:path";var J={version:1,sensitiveWords:[]};function B(n){let e;try{e=JSON.parse(n)}catch{throw a("CONFIG_JSON")}let r=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(o=>!["version","sensitiveWords"].includes(o)))throw a("CONFIG_SCHEMA");if(e.sensitiveWords.length>1e3)throw a("CONFIG_SIZE",{limit:1e3});let t=new Map;for(let o of e.sensitiveWords){if(r++,typeof o=="string"){if(!o||o.length>8192)throw a("CONFIG_SCHEMA",{rule:r,limit:8192});let u=JSON.stringify(["literal",o]);if(t.has(u)&&t.get(u)!==void 0)throw a("CONFIG_CONFLICT",{rule:r});t.set(u,void 0);continue}if(!o||typeof o!="object")throw a("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 a("CONFIG_TARGET",{rule:r,limit:8192});if(o.type==="literal"){if(Object.keys(o).some(u=>!["type","value","replacement"].includes(u))||typeof o.value!="string"||!o.value||o.value.length>8192)throw a("CONFIG_SCHEMA",{rule:r,limit:8192})}else if(o.type==="regex"){if(Object.keys(o).some(u=>!["type","pattern","flags","replacement"].includes(u))||typeof o.pattern!="string"||!o.pattern||o.pattern.length>8192||o.flags!==void 0&&(typeof o.flags!="string"||!/^[gimsu]*$/.test(o.flags)))throw a("CONFIG_REGEX",{rule:r,limit:8192});try{new RegExp(o.pattern,o.flags??"")}catch{throw a("CONFIG_REGEX",{rule:r})}}else throw a("CONFIG_SCHEMA",{rule:r});let s=o.type==="literal"?JSON.stringify(["literal",o.value]):JSON.stringify(["regex",o.pattern,[...new Set((o.flags??"")+"g")].sort().join("")]);if(t.has(s)&&t.get(s)!==o.replacement)throw a("CONFIG_CONFLICT",{rule:r});t.set(s,o.replacement)}return e}catch(t){throw t instanceof _?t:a("CONFIG_SCHEMA",{rule:r})}}function se(n,e=!1){let r;try{if(e){Le(be(n),{recursive:!0,mode:448});try{let s=ie(n,T.O_WRONLY|T.O_CREAT|T.O_EXCL,384);try{Pe(s,JSON.stringify(J,null,2)+`
|
|
4
|
+
`)}finally{oe(s)}}catch(s){if(s.code!=="EEXIST")throw y(s,"CONFIG_IO")}}r=ie(n,T.O_RDONLY|T.O_NOFOLLOW|T.O_NONBLOCK);let t=De(r);if(t.size>1024*1024)throw a("CONFIG_SIZE",{bytes:t.size,limit:1024*1024});if(!t.isFile()||t.nlink!==1||process.getuid&&t.uid!==process.getuid())throw a("CONFIG_UNSAFE");process.platform!=="win32"&&Re(r,384);let o=Fe(r,"utf8");if(Buffer.byteLength(o)>1024*1024)throw a("CONFIG_SIZE",{bytes:Buffer.byteLength(o),limit:1024*1024});return{config:B(o),raw:o}}catch(t){let o=t&&typeof t=="object"&&"code"in t?t.code:void 0;throw y(t,o==="ELOOP"||o==="EISDIR"||o==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{r!==void 0&&oe(r)}}var Y=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function xe(n){return new Promise((e,r)=>{let t=new Ue(ve(new URL("./validate-worker.mjs",import.meta.url)),{workerData:n,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),o=!1,s=(l,i=a("WORKER_FAILED"))=>{o||(o=!0,clearTimeout(u),t.terminate(),l?e():r(i))},u=setTimeout(()=>s(!1,a("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);t.once("message",l=>l===!0?s(!0):s(!1,b(l,"CONFIG_SCHEMA"))),t.once("error",()=>s(!1)),t.once("exit",()=>s(!1))})}function ze(n){let e=new Set,r=[];for(let o of n)for(let s of o.sensitiveWords){let u=typeof s=="string"?JSON.stringify(["literal",s,null]):s.type==="literal"?JSON.stringify(["literal",s.value,s.replacement??null]):JSON.stringify(["regex",s.pattern,[...new Set((s.flags??"")+"g")].sort().join(""),s.replacement??null]);e.has(u)||(e.add(u),r.push(s))}let t=JSON.stringify({version:1,sensitiveWords:r});if(Buffer.byteLength(t)>1024*1024)throw a("CONFIG_SIZE",{bytes:Buffer.byteLength(t),limit:1024*1024});return B(t)}function ae(n){let e;try{e=g.lstatSync(n,{bigint:!0})}catch(r){throw y(r,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw a("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function $(n,e){let r=C(n,e),t=ae(r),o=se(r);if(ae(r)!==t)throw a("MIGRATION_CHANGED");return{name:e,...o,identity:t,digest:Me("sha256").update(o.raw).digest("hex")}}function L(n,e){let r=$(n,e.name);if(r.identity!==e.identity||r.digest!==e.digest)throw a("MIGRATION_CHANGED")}function G(n){return g.readdirSync(n).filter(e=>Y.test(e)).sort()}function ce(n){if(process.platform==="win32")return;let e=g.openSync(n,g.constants.O_RDONLY);try{g.fsyncSync(e)}finally{g.closeSync(e)}}async function le(n,e,r={}){if(!/^[a-f0-9]{32}$/.test(e))throw a("MACHINE_ID");let t=Ge(n),o=`protecter.${e}.json`,s=C(t,o);g.mkdirSync(t,{recursive:!0,mode:448});let u=C(t,".protecter-migration.lock"),l=Date.now()+(r.lockTimeoutMs??5e3);for(;;)try{g.mkdirSync(u,{mode:448});break}catch(c){if(c.code!=="EEXIST")throw y(c,"MIGRATION_IO");if(Date.now()>=l)throw a("MIGRATION_LOCKED",{timeoutMs:r.lockTimeoutMs??5e3});await new Promise(h=>setTimeout(h,50))}let i;try{let c=G(t);if(c.length>64)throw a("MIGRATION_LIMIT",{limit:64});let d=[...c].sort((N,R)=>N===o?-1:R===o?1:N.localeCompare(R)).map(N=>$(t,N)),I=ze(d.length?d.map(N=>N.config):[J]);if(await xe([I]),d.length===1&&d[0].name===o){if(L(t,d[0]),JSON.stringify(G(t))!==JSON.stringify(c))throw a("MIGRATION_CHANGED");return{path:s,config:d[0].config,raw:d[0].raw,sourceRaws:[d[0].raw],migrated:0}}let O=JSON.stringify(I,null,2)+`
|
|
5
|
+
`;if(Buffer.byteLength(O)>1024*1024)throw a("CONFIG_SIZE",{bytes:Buffer.byteLength(O),limit:1024*1024});i=C(t,`.protecter-migration.${ke()}.tmp`);let p=g.openSync(i,g.constants.O_WRONLY|g.constants.O_CREAT|g.constants.O_EXCL,384);try{g.writeFileSync(p,O),g.fsyncSync(p)}finally{g.closeSync(p)}if(r.checkpoint?.("beforeCommit",s),JSON.stringify(G(t))!==JSON.stringify(c))throw a("MIGRATION_CHANGED");for(let N of d)L(t,N);g.renameSync(i,s),i=void 0,ce(t);let A=$(t,o);if(A.raw!==O)throw a("MIGRATION_CHANGED");r.checkpoint?.("afterCommit",s);for(let N of d)N.name!==o&&(r.checkpoint?.("beforeDelete",C(t,N.name)),L(t,A),L(t,N),g.unlinkSync(C(t,N.name)));ce(t);let w=[o];if(JSON.stringify(G(t))!==JSON.stringify(w))throw a("MIGRATION_CHANGED");return L(t,A),{path:s,config:I,raw:O,sourceRaws:[...new Set([...d.map(N=>N.raw),O])],migrated:d.filter(N=>N.name!==o).length}}catch(c){throw c instanceof _?c:y(c,"MIGRATION_IO")}finally{if(i)try{g.unlinkSync(i)}catch{}g.rmdirSync(u)}}import{createHash as He}from"node:crypto";import{readFileSync as Je}from"node:fs";import{execFileSync as ue}from"node:child_process";function W(n){let e=n.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw a("MACHINE_ID");return He("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function fe(){try{if(process.platform==="linux")return W(Je("/etc/machine-id","utf8"));if(process.platform==="darwin"){let n=ue("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return W(n.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let n=ue("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return W(n.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw a("MACHINE_ID")}import*as f from"node:fs";import{randomUUID as Be}from"node:crypto";var de=32*1024*1024,ge=8*1024*1024;function $e(n){let e=r=>String(r).padStart(2,"0");return`${n.getFullYear()}-${e(n.getMonth()+1)}-${e(n.getDate())} ${e(n.getHours())}:${e(n.getMinutes())}:${e(n.getSeconds())}`}function X(n,e,r=e){let t=f.openSync(n,(e?f.constants.O_RDWR|(r?f.constants.O_CREAT:0):f.constants.O_RDONLY)|f.constants.O_NOFOLLOW|f.constants.O_NONBLOCK,384);try{let o=f.fstatSync(t);if(!o.isFile()||o.nlink!==1||process.getuid&&o.uid!==process.getuid())throw a("AUDIT_UNSAFE");return process.platform!=="win32"&&f.fchmodSync(t,384),t}catch(o){throw f.closeSync(t),y(o,"AUDIT_UNSAFE")}}function M(n){let e=n&&typeof n=="object"&&"code"in n?n.code:void 0;return y(n,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function me(n){let e=`${n}.lock`,r=Date.now()+2e3;for(;;)try{return f.mkdirSync(e,{mode:448}),e}catch(t){if(t.code!=="EEXIST")throw y(t,"AUDIT_IO");if(Date.now()>=r)throw a("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(o=>setTimeout(o,25))}}async function pe(n){let e=await me(n),r;try{try{r=X(n,!0,!1)}catch(o){if(o.code==="ENOENT")return 0;throw o}let t=f.fstatSync(r).size;return f.ftruncateSync(r,0),f.fsyncSync(r),t}catch(t){throw M(t)}finally{try{r!==void 0&&f.closeSync(r)}finally{f.rmdirSync(e)}}}async function he(n,e,r){if(!r.length)return;let t=new Date,o=Be(),s=Buffer.from(r.map(([i,c])=>JSON.stringify({version:1,time:$e(t),timezoneOffset:-t.getTimezoneOffset(),requestId:o,provider:e||"unknown",original:c,replacement:i})).join(`
|
|
6
6
|
`)+`
|
|
7
|
-
`);if(s.length>
|
|
8
|
-
`);
|
|
9
|
-
`),
|
|
10
|
-
${m("logsPreviewTruncated")}`:v)}catch(p){i.ui.notify(
|
|
11
|
-
${m("statusLastBlock",{at:
|
|
12
|
-
${
|
|
13
|
-
${m("statusNoBlock")}`;i.ui.notify(
|
|
7
|
+
`);if(s.length>ge)throw a("AUDIT_BATCH_LIMIT",{bytes:s.length,limit:ge});let u=await me(n),l;try{l=X(n,!0);let i=f.fstatSync(l).size;if(i+s.length>de)throw a("AUDIT_FULL",{bytes:i+s.length,limit:de});if(i){let c=Buffer.alloc(1);if(f.readSync(l,c,0,1,i-1),c[0]!==10)throw a("AUDIT_PARTIAL")}try{let c=0;for(;c<s.length;){let h=f.writeSync(l,s,c,s.length-c,i+c);if(!h)throw a("AUDIT_IO");c+=h}f.fsyncSync(l)}catch(c){throw f.ftruncateSync(l,i),f.fsyncSync(l),M(c)}}catch(i){throw M(i)}finally{l!==void 0&&f.closeSync(l),f.rmdirSync(u)}}function Ie(n,e=20){if(!Number.isInteger(e)||e<1||e>100)throw a("INTERNAL",{limit:100});let r;try{try{r=X(n,!1)}catch(I){if(I.code==="ENOENT")return{records:[],truncated:!1};throw I}let t=f.fstatSync(r).size,o=Math.max(0,t-1024*1024),s=Buffer.alloc(t-o),u=f.readSync(r,s,0,s.length,o),l=s.subarray(0,u).toString("utf8");if(o){let I=l.indexOf(`
|
|
8
|
+
`);l=I<0?"":l.slice(I+1)}let i=l.split(`
|
|
9
|
+
`),c=i.pop()!=="",h=[],d=!1;for(let I of i.slice(-e))try{let O=JSON.parse(I);if(O.version!==1||typeof O.time!="string"||typeof O.provider!="string"||typeof O.original!="string"||typeof O.replacement!="string"||typeof O.requestId!="string"||typeof O.timezoneOffset!="number")throw new Error;h.push(O)}catch{d=!0}return{records:h,truncated:o>0||i.length>e||c||d}}catch(t){throw M(t)}finally{r!==void 0&&f.closeSync(r)}}var k=class{constructor(e,r=2e3,t=fe){this.configDir=e;this.timeoutMs=r;this.machineId=t}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?S(this.lastError):void 0}async initialize(){try{if(this.closed)throw a("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw a("MACHINE_ID")}let r=await le(this.configDir,e);if(this.closed)throw a("NOT_READY");this.snapshot=r,this.lastError=void 0}catch(e){let r=y(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 Ie(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return pe(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,r="unknown"){let t=this.queue.then(()=>this.scan(e,r)).catch(o=>{let s=y(o,"INTERNAL");throw this.lastError=s.diagnostic,s});return this.queue=t.catch(()=>{}),t}notReady(){return this.lastError?a(this.lastError.code,this.lastError):a("NOT_READY")}async scan(e,r){if(!this.ready)throw this.notReady();let{config:t,sourceRaws:o}=this.snapshot,s;try{s=JSON.stringify(e)}catch{throw a("PAYLOAD_JSON")}if(!s)throw a("PAYLOAD_JSON");if(Buffer.byteLength(s)>8*1024*1024)throw a("PAYLOAD_SIZE",{bytes:Buffer.byteLength(s),limit:8*1024*1024});let u;try{u=JSON.parse(s)}catch{throw a("PAYLOAD_JSON")}let l=await new Promise((i,c)=>{let h=!1,d;try{d=new Ye(We(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:u,rules:t.sensitiveWords,entries:[...this.tokens],configRaws:o},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(p){c(y(p,"WORKER_FAILED"));return}this.workers.add(d);let I=(p,A=a("WORKER_FAILED"))=>{if(!h){if(h=!0,clearTimeout(O),this.workers.delete(d),d.terminate(),!p||this.closed){c(this.closed?a("NOT_READY"):A);return}i(p)}},O=setTimeout(()=>I(void 0,a("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);d.once("message",p=>{p?.failed?I(void 0,b(p.diagnostic,"SCAN_INTERNAL")):p&&Array.isArray(p.additions)&&Array.isArray(p.hits)?I(p):I()}),d.once("error",()=>I()),d.once("exit",()=>I())});if(this.closed)throw a("NOT_READY");if(await he(this.auditPath,r,l.hits),this.closed)throw a("NOT_READY");for(let[i,c]of l.additions)this.tokens.set(i,c);return l.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let r=t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((t,o)=>o.length-t.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 a("RESTORE_COMPLEXITY",{node:r});if(typeof e=="string")return this.restoreText(e);if(typeof e=="number"){let t=this.tokens.get(String(e));if(t===void 0)return e;let o=Number(t);return!Number.isFinite(o)||String(o)!==t?e:o}if(Array.isArray(e))return e.map(t=>this.restore(t,r+1));if(e&&typeof e=="object"){let t=Object.create(null);for(let[o,s]of Object.entries(e)){let u=this.restoreText(o);if(Object.hasOwn(t,u))throw a("RESTORE_KEY_COLLISION");t[u]=this.restore(s,r+1)}return t}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 Xe,statSync as Ne,readdirSync as je}from"node:fs";import{homedir as Ke}from"node:os";import{basename as j,dirname as F,isAbsolute as qe,relative as Oe,resolve as U}from"node:path";import{fileURLToPath as Ze}from"node:url";function Ve(n,e){let r=n.replace(/^@/,"");return r.startsWith("file://")&&(r=Ze(r)),(r==="~"||r.startsWith("~/"))&&(r=Ke()+r.slice(1)),U(e,r)}function K(n){try{return Xe(n)}catch{let e=F(n);return e===n?n:U(K(e),Oe(e,n))}}function Qe(n,e){let r=Oe(n,e);return r===""||!r.startsWith("..")&&!qe(r)}function ye(n,e){try{let r=Ne(n),t=Ne(e);return r.dev===t.dev&&r.ino===t.ino}catch{return!1}}function Ee(n,e,r,t){if(!t)return!0;let o=K(U(t));if(["bash","powershell"].includes(n)){let l=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(l)||l.includes(t))return!0}let s=e.path??e.file_path??e.filePath,u=["grep","find","ls"].includes(n);if(typeof s!="string"&&!u)return!1;try{let l=Ve(typeof s=="string"?s:".",r),i=K(l),c=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,h=je(F(o)).filter(d=>c.test(d)).some(d=>ye(l,U(F(o),d)));return F(i)===F(o)&&(Y.test(j(i))||c.test(j(i))||j(i).startsWith(".protecter-migration"))||i===o||ye(l,t)||h||u&&Qe(i,o)}catch{return!0}}function rt(n){ee(H());let e=new k(et()),r=/^(\d+)\.(\d+)\.(\d+)/.exec(tt),t=r!==null&&Number(r[1])===0&&(Number(r[2])===85&&Number(r[3])>=1||Number(r[2])===86),o=!1,s,u=l=>{let i=S(l);return s={at:new Date().toISOString(),text:i},i};n.on("session_start",async(l,i)=>{try{if(!t)throw a("UNSUPPORTED_PI");await e.initialize(),o=!0;let c=e.ruleCount;i.ui.setStatus("protecter",m("statusBarRules",{rules:D("rules",c)})),c===0&&i.ui.notify(m("emptyRules"),"warning")}catch(c){o=!1,i.ui.setStatus("protecter",m("statusBarNotReady")),i.ui.notify(u(y(c,"CONFIG_IO").diagnostic),"error")}}),n.on("before_provider_request",async(l,i)=>{try{if(!t)throw a("UNSUPPORTED_PI");let c=await e.redact(l.payload,i.model?.provider??"unknown");return o=!0,c}catch(c){o=!1;let h=u(y(c,"INTERNAL").diagnostic);try{await i.abort()}catch{}try{i.ui.notify(h,"error")}catch{}return{}}}),n.on("message_end",l=>{if(l.message.role!=="assistant")return;let i=l.message;return{message:{...i,content:i.content.map(c=>c.type==="text"?{...c,text:e.restoreText(c.text)}:c.type==="thinking"?{...c,thinking:e.restoreText(c.thinking)}:c.type==="toolCall"?{...c,arguments:e.restore(c.arguments)}:c),...i.errorMessage?{errorMessage:e.restoreText(i.errorMessage)}:{}}}}),n.registerMarkdownTransformer((l,i)=>i.messageType==="user"?l:e.restoreText(l)),n.on("tool_call",(l,i)=>{if(!o)return{block:!0,reason:e.lastDiagnosticText??s?.text??S(a("NOT_READY").diagnostic)};let c=e.restore(l.input);for(let h of Object.keys(l.input))delete l.input[h];if(Object.assign(l.input,c),Ee(l.toolName,l.input,i.cwd,e.configPath))return{block:!0,reason:m("toolBlocked")}}),n.on("session_before_compact",(l,i)=>(i.ui.notify(m("compactUnsupported"),"warning"),{cancel:!0})),n.on("session_before_tree",l=>l.preparation.userWantsSummary?{cancel:!0}:void 0),n.registerCommand("protecter",{description:m("commandDescription"),handler:async(l,i)=>{let c=l.trim().split(/\s+/).filter(Boolean),h=c[0]??"logs";if(!["status","reload","logs"].includes(h)||h!=="logs"&&c.length>1||c.length>2||c[1]!==void 0&&c[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(c[1])){i.ui.notify(m("usage"),"info");return}if(h==="logs"){if(i.mode!=="tui"){i.ui.notify(m("logsTuiOnly"),"warning");return}if(c[1]==="clear"){try{if(!e.ready)throw a("NOT_READY");if(!await i.ui.confirm(m("clearTitle"),m("clearBody")))return;await i.waitForIdle();let p=await e.clearAuditRecords();o=e.ready,i.ui.notify(m("cleared",{bytes:D("bytes",p)}),"info")}catch(p){i.ui.notify(u(y(p,"AUDIT_IO").diagnostic),"error")}return}try{let{records:p,truncated:A}=e.auditRecords(Number(c[1]??20));if(!p.length){i.ui.notify(m("logsNone"),"info");return}let w=P=>JSON.stringify(P).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,x=>`\\u${x.charCodeAt(0).toString(16).padStart(4,"0")}`),N=p.map((P,x)=>`${x+1}. ${w(P.time)} | ${w(P.provider).slice(0,120)}`),R=await i.ui.select(m("logsSelectTitle")+(A?m("logsTruncated"):""),N),q=R===void 0?-1:N.indexOf(R);if(q<0||!await i.ui.confirm(m("logsRevealTitle"),m("logsRevealBody")))return;let v=w(p[q]);await i.ui.editor(m("logsPreviewTitle"),v.length>2e4?`${v.slice(0,2e4)}
|
|
10
|
+
${m("logsPreviewTruncated")}`:v)}catch(p){i.ui.notify(u(y(p,"AUDIT_IO").diagnostic),"error")}return}if(h==="reload"){await i.waitForIdle(),await i.reload();return}let d=e.lastDiagnosticText??s?.text,I=m("statusLine",{state:m(e.ready?"statusReady":"statusNotReady"),rules:D("rules",e.ruleCount),mappings:D("mappings",e.mappingCount)}),O=d?`
|
|
11
|
+
${m("statusLastBlock",{at:s?.at??""})}
|
|
12
|
+
${d}`:`
|
|
13
|
+
${m("statusNoBlock")}`;i.ui.notify(I+O,e.ready?"info":"error")}}),n.on("session_shutdown",()=>e.close())}export{rt as default};
|
package/dist/scan-worker.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
|
|
2
|
-
import{parentPort as
|
|
2
|
+
import{parentPort as k,workerData as te}from"node:worker_threads";import{randomBytes as Z}from"node:crypto";var z=[[48,57],[65,90],[97,122],[192,214],[216,246],[248,255],[256,383],[7680,7935],[913,937],[945,969],[940,974],[1040,1103],[1488,1514],[1569,1594],[1601,1610],[3585,3630],[3632,3635],[3636,3642],[3648,3652],[3655,3662],[12353,12438],[12449,12538],[44032,55203],[19968,40869]],X=BigInt(Number.MAX_SAFE_INTEGER);function g(n){if(n<=1n)return 0n;let t=n.toString(2).length,o=Math.ceil(t/8),h=(1n<<BigInt(t))-1n;for(let d=0;d<64;d++){let a=0n;for(let x of Z(o))a=a<<8n|BigInt(x);if(a&=h,a<n)return a}return n-1n}function K(n){return z.find(([t,o])=>n>=t&&n<=o)}function Q(n){let t="";for(let o of n){let h=o.codePointAt(0),d=K(h);if(!d){t+=o;continue}let[a,x]=d,S=BigInt(x-a+1);t+=String.fromCodePoint(a+Number(g(S)))}return t}var q=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,v={1:[0n,9n],2:[10n,99n],3:[100n,255n]};function ee(n){if(n.length>1&&n.startsWith("0")){let h="0";for(let d=1;d<n.length;d++)h+=String(g(10n));return h}let[t,o]=v[n.length];return String(t+g(o-t+1n))}function G(n){let t=q.exec(n);if(!(!t||t.slice(1).some(o=>Number(o)>255)))return t.slice(1).map(ee).join(".")}function H(n){let t="";for(let o of n)o>="0"&&o<="9"?t+=String(g(10n)):o>="a"&&o<="f"?t+=String.fromCharCode(97+Number(g(6n))):o>="A"&&o<="F"?t+=String.fromCharCode(65+Number(g(6n))):t+=o;return t}function ne(n){if(!n.includes(":"))return;let t=n.lastIndexOf(":"),o=n.slice(t+1);if(o.includes(".")){let h=G(o);return!h||!/^[0-9A-Fa-f:]*$/.test(n.slice(0,t+1))?void 0:H(n.slice(0,t+1))+h}if(/^[0-9A-Fa-f:]+$/.test(n))return H(n)}function U(n){return G(n)??ne(n)??Q(n)}function V(n,t=!0){if(!/^\d+$/.test(n))return;if(n.length===1)return n==="0"?void 0:String(1n+g(9n));if(n.startsWith("0")){let a="";for(let x of n)a+=x==="0"?"0":String(1n+g(9n));return a}let o=10n**BigInt(n.length-1),h=10n**BigInt(n.length)-1n,d=t&&h>X?X:h;if(!(d<o))return String(o+g(d-o+1n))}function W(n){let t=/^([0-9.+-]*?)([eE][+-]?\d+)?$/.exec(n);if(!t||!/\d/.test(t[1]))return;let o=t[1],h=t[2]??"",d=o.indexOf("."),a=o.search(/\d(?!.*\d)/),x=d===-1?o.length:d,S="";for(let[m,I]of[...o].entries()){if(I<"0"||I>"9"){S+=I;continue}let R=d===-1||m<d,y=R&&!/\d/.test(S),T=!R&&m===a;if(I==="0"&&(y||T)){S+="0";continue}let L=y&&x>1||T;S+=String(L?1n+g(9n):g(10n))}return S+h}function w(n){let t=Number(n);return Number.isFinite(t)&&String(t)===n}var _,F=0;function l(n,t){throw{code:n,rule:_,otherRule:t,node:F}}try{let L=function(e,i,r){for(let f=0;f<64;f++){let c;if(/^\d+$/.test(e)?c=V(e,i):i?c=W(e):c=U(e),!(!c||c===e)&&!a.has(c)&&!d.has(c)&&!R.includes(c)&&!(r&&!r(c)))return c}},j=function(e,i,r=!1,f){let c=d.get(e);return c?(i!==void 0&&c!==i&&l("FIXED_MAPPING_CONFLICT"),c):i!==void 0?(a.size>=5e4&&l("SCAN_MAPPING_LIMIT"),a.has(i)&&a.get(i)!==e&&l("FIXED_ALIAS_REUSED"),i.includes(e)&&l("FIXED_SENSITIVE_TARGET"),d.set(e,i),a.set(i,e),x.push([i,e]),i):(a.size>=5e4&&l("SCAN_MAPPING_LIMIT"),c=L(e,r,f),c||l("SCAN_UNIQUE_FAILED"),d.set(e,c),a.set(c,e),x.push([c,e]),c)},P=function(e,i=!1){let r=[];for(let u of m)if(_=u.rule,u.literal===void 0){u.regex.lastIndex=0;for(let s of e.matchAll(u.regex))s[0].length||l("SCAN_ZERO_WIDTH"),r.push([s.index,s.index+s[0].length,u.replacement,u.rule]),++y>1e5&&l("SCAN_MATCH_LIMIT")}else{if(!u.literal)continue;let s=0;for(;(s=e.indexOf(u.literal,s))!==-1;)r.push([s,s+u.literal.length,u.replacement,u.rule]),++y>1e5&&l("SCAN_MATCH_LIMIT"),s++}r.sort((u,s)=>u[0]-s[0]||s[1]-u[1]);let f=[];for(let u of r){let s=f.at(-1);s&&u[0]<s[1]?((s[2]!==void 0||u[2]!==void 0)&&((s[0]!==u[0]||s[1]!==u[1]||s[2]!==void 0&&u[2]!==void 0&&s[2]!==u[2])&&(_=s[3],l("FIXED_OVERLAP",u[3])),s[2]===void 0&&u[2]!==void 0&&(s[3]=u[3]),s[2]??=u[2]),s[1]=Math.max(s[1],u[1])):f.push([...u])}let c="",A=0;for(let[u,s,p,N]of f){_=N;let D=i?b=>w(e.slice(0,u)+b+e.slice(s)):void 0,M=j(e.slice(u,s),p,i,D);S.add(M),c+=e.slice(A,u)+M,A=s}return c+e.slice(A)},E=function(e,i=!1){let r="",f=0,c=p=>p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),A=[...a.keys()];if(!A.length)return P(e,i);let u=new RegExp(A.sort((p,N)=>N.length-p.length).map(c).join("|"),"g"),s=[...e.matchAll(u)].filter(p=>a.has(p[0]));for(let p of m)if(!(!p.regex||!A.length)){_=p.rule,p.regex.lastIndex=0;for(let N of e.matchAll(p.regex)){++y>1e5&&l("SCAN_MATCH_LIMIT"),N[0].length||l("SCAN_ZERO_WIDTH");let D=N.index,M=N.index+N[0].length;s.some(b=>{let $=b.index,J=b.index+b[0].length;return D>=J||M<=$?!1:D<$||M>J})&&l("FIXED_ALIAS_CROSSING")}}for(let p of s)a.has(p[0])&&(r+=P(e.slice(f,p.index),i)+p[0],f=p.index+p[0].length);return r+P(e.slice(f),i)},C=function(e,i=0){if(F=++T,_=void 0,(T>2e5||i>80)&&l("SCAN_COMPLEXITY"),typeof e=="string"){if(I.some(r=>e.includes(r)))return E(e);if(/^\s*[[{]/.test(e)){let r;try{r=JSON.parse(e)}catch{}if(r&&typeof r=="object"){let f=JSON.stringify(C(r,i+1)),c=E(f);if(c===f)return f;try{JSON.parse(c)}catch{l("SCAN_JSON_REWRITE")}return c}}return E(e)}if(typeof e=="number"){let r=String(e),f=E(r,!0);return f===r?e:(w(f)||l("SCAN_NUMBER_UNSAFE"),Number(f))}if(Array.isArray(e))return e.map(r=>C(r,i+1));if(e&&typeof e=="object"){typeof e.type=="string"&&/image|audio|video|document|(^|_)file($|_)/i.test(e.type)&&l("SCAN_ATTACHMENT"),Object.keys(e).some(f=>/^(inlineData|inline_data|fileData|file_data|image_url|input_audio)$/.test(f))&&l("SCAN_ATTACHMENT");let r=Object.create(null);for(let[f,c]of Object.entries(e)){let A=E(f);Object.hasOwn(r,A)&&l("SCAN_KEY_COLLISION"),r[A]=C(c,i+1)}return r}if(e===null||typeof e=="boolean")return e;l("PAYLOAD_JSON")},O=function(e,i,r=0){if(r>80&&l("SCAN_COMPLEXITY"),typeof e=="string"){if(e.includes(i))return!0;let f;try{f=JSON.parse(e)}catch{return!1}return f&&typeof f=="object"?O(f,i,r+1):!1}return Array.isArray(e)?e.some(f=>O(f,i,r+1)):e&&typeof e=="object"?Object.entries(e).some(([f,c])=>f.includes(i)||O(c,i,r+1)):!1};re=L,ie=j,se=P,oe=E,fe=C,ce=O;let{payload:n,rules:t,entries:o,configRaws:h}=te,d=new Map(o.map(([e,i])=>[i,e])),a=new Map(o),x=[],S=new Set,m=t.map((e,i)=>{let r=i+1;return typeof e=="string"?{literal:e,rule:r}:e.type==="literal"?{literal:e.value,replacement:e.replacement,rule:r}:{rule:r,replacement:e.replacement,regex:new RegExp(e.pattern,[...new Set((e.flags??"")+"g")].join(""))}});for(let e of d.keys())m.push({literal:e});let I=[...new Set(h.flatMap(e=>[e,JSON.stringify(JSON.parse(e))]))];for(let e of I)m.push({literal:e});let R=JSON.stringify(n),y=0,T=0,B=C(n),Y=[...S].filter(e=>O(B,e)).map(e=>[e,a.get(e)]);k.postMessage({payload:B,additions:x,hits:Y})}catch(n){k.postMessage({failed:!0,diagnostic:{code:typeof n?.code=="string"?n.code:"SCAN_INTERNAL",rule:n?.rule??_,otherRule:n?.otherRule,node:n?.node??F}})}var re,ie,se,oe,fe,ce;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-information-protecter",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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.85.1 <0.
|
|
43
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1 <0.87.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
47
47
|
"@types/node": "^22.0.0",
|
|
48
48
|
"esbuild": "0.28.2",
|
|
49
49
|
"tsx": "^4.20.0",
|