pi-information-protecter 0.11.0 → 0.11.1
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 +20 -0
- package/README.md +330 -339
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog / 更新日志
|
|
2
2
|
|
|
3
|
+
## 0.11.1 — Documented IP rules, and a README that leads with coverage / 文档内的 IP 规则,以覆盖范围开篇的 README
|
|
4
|
+
|
|
5
|
+
### Added / 新增
|
|
6
|
+
|
|
7
|
+
- Add IPv4 and IPv6 rules to the README configuration example. IPv6 needs two patterns, because the `::`-compressed form is a different shape from the full one. The patterns are the ones already shipped in `protecter.example.json`.
|
|
8
|
+
在 README 的配置示例中加入 IPv4 与 IPv6 规则。IPv6 需要两条正则,因为 `::` 压缩形式与完整形式的形态不同。所用正则与 `protecter.example.json` 中随包发布的一致。
|
|
9
|
+
- Test the README examples the way the shipped example file is tested: every documented configuration must load through the extension's own parser, and the documented address rules must produce addresses that `net.isIPv4`/`net.isIPv6` still accept, keep the compressed IPv6 shape, and round-trip on restoration. A documented rule that does not work is now a test failure.
|
|
10
|
+
像对待随包示例文件那样测试 README 示例:文档中的每份配置都必须能通过本扩展自己的解析器加载,文档中的地址规则必须产出 `net.isIPv4`/`net.isIPv6` 仍然接受的地址、保持 IPv6 压缩形态,并在还原时精确往返。文档里不能用的规则现在会导致测试失败。
|
|
11
|
+
|
|
12
|
+
### Changed / 变更
|
|
13
|
+
|
|
14
|
+
- Restructure the README around two questions: what is redacted and what is not, then how to develop and debug. The coverage answer comes first, before installation, as two tables — every model request and how it is covered, and every transfer that is not a model request, with `/bug`, `/share`, `/export`, tools, MCP servers, shell, request headers and session files named explicitly together with the reason no extension can intercept them.
|
|
15
|
+
围绕两个问题重构 README:哪些会脖敏、哪些不会,以及如何开发与调试。覆盖范围的答案置于安装之前,以两张表格呈现——所有模型请求及其覆盖方式,以及所有不属于模型请求的传输,并明确点出 `/bug`、`/share`、`/export`、工具、MCP 服务器、shell、请求头与会话文件,连带任何扩展都无法拦截的原因。
|
|
16
|
+
- Add a development and debugging section: the commands, running from source against a throwaway `PI_CODING_AGENT_DIR`, a map of `src/`, how to reproduce a block as a test instead of against a live gateway, which tests cover the hookless paths and the packed tarball, and how to widen the Pi version gate.
|
|
17
|
+
新增开发与调试章节:常用命令、使用一次性 `PI_CODING_AGENT_DIR` 从源码运行、`src/` 代码位置一览、把问题复现成测试而不是打到真实网关、哪些测试覆盖无钩子路径与打包产物,以及如何扩大 Pi 版本门禁。
|
|
18
|
+
- Pin the `@earendil-works/pi-coding-agent` development dependency to an exact version again. 0.11.0 recorded `^1.3.0`, so a fresh clone could install a Pi release the runtime gate refuses, which fails every test for a reason that has nothing to do with the change under test.
|
|
19
|
+
将 `@earendil-works/pi-coding-agent` 开发依赖重新固定为精确版本。0.11.0 记录的是 `^1.3.0`,全新克隆可能装到运行时门禁拒绝的 Pi 版本,导致所有测试因与待测改动无关的原因失败。
|
|
20
|
+
- Cut implementation detail that belongs in `SECURITY.md` and this changelog (hook names, worker internals, long code lists), and drop the pinned-version install example, which drifted behind every release. The README is 53.6 KB to 31.5 KB with both languages mirrored section for section.
|
|
21
|
+
删去应留在 `SECURITY.md` 与本更新日志的实现细节(钩子名称、worker 内部、冗长的错误码清单),并移除每次发布都会落后的固定版本安装示例。README 从 53.6 KB 精简至 31.5 KB,两种语言逐节对应。
|
|
22
|
+
|
|
3
23
|
## 0.11.0 — Pi summarizes again, and every hookless path is covered / 摘要交回 Pi,并覆盖所有无钩子路径
|
|
4
24
|
|
|
5
25
|
### Changed / 变更
|
package/README.md
CHANGED
|
@@ -4,199 +4,169 @@
|
|
|
4
4
|
|
|
5
5
|
## English
|
|
6
6
|
|
|
7
|
-
**Sensitive Personal Information (SPI) Protecter.**
|
|
7
|
+
**Sensitive Personal Information (SPI) Protecter for Pi.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
You list the values you never want to send to a model. Before a model request leaves, this extension replaces them with realistic fake values, and restores them locally in replies and tool arguments. The model sees fake data; you see your own.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
It protects **values you configured, in model requests** — nothing more. The next section is the important one.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## What is redacted and what is not
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
# Install the latest npm release.
|
|
17
|
-
pi install npm:pi-information-protecter
|
|
15
|
+
### Redacted: every request to a model
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
| Model request | How |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Your prompts, conversation history, system prompt, tool definitions, tool results | Pi's outgoing-request hook |
|
|
20
|
+
| Prompt-cache warming | Same hook |
|
|
21
|
+
| Context compaction and `/tree` branch summaries | The conversation is redacted before Pi summarizes it |
|
|
22
|
+
| The summary `/bug` writes | Model-runtime guard |
|
|
23
|
+
| `codemode` classifier and image calls | Model-runtime guard |
|
|
24
|
+
| Model calls made by other extensions | Model-runtime guard |
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
pi -e ./src/index.ts
|
|
26
|
+
Pi offers no hook for the last three, so this extension wraps the model runtime the session shares and redacts anything sent through it. If that is not possible, startup fails with `GUARD_UNAVAILABLE` and **all** requests are blocked instead of running unprotected. `/protecter status` shows whether it is in place.
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
pi update npm:pi-information-protecter
|
|
32
|
-
```
|
|
28
|
+
If redaction itself fails for any reason, the request is **blocked**, never sent as plaintext.
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
### Not redacted: anything that is not a model request
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
| Action | What it sends | Why it cannot be intercepted |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `/bug` | The uploaded bundle: the whole transcript — your messages, model output, tool calls and their results, including file contents and command output — plus Pi version, OS, model and provider configuration, extensions and settings | Pi handles `/bug` itself and uploads over its own HTTP connection. It is also handled before extension commands, so an extension cannot even replace it. Only the summary inside it is redacted |
|
|
35
|
+
| `/share` | The transcript, uploaded as a gist | Pi's own upload path, not a model request |
|
|
36
|
+
| `/export` | The transcript, written to a file | Same, to local disk |
|
|
37
|
+
| Session files | Everything you and the model wrote, with real values restored | Pi stores sessions; this extension does not encrypt them |
|
|
38
|
+
| Tools, shell, MCP servers | Tool arguments are deliberately restored to real values so tools work | What a tool, shell command or MCP server then does with them is outside this extension. Shell can also encode or split a value past any text matching |
|
|
39
|
+
| Request headers, API keys, endpoints, Pi telemetry | Credentials and routing metadata | Not part of the request body this extension rewrites |
|
|
40
|
+
| Another extension's own `fetch` | Whatever it chooses | Extensions run with your permissions |
|
|
39
41
|
|
|
40
|
-
|
|
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.
|
|
41
43
|
|
|
42
|
-
|
|
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.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
## Installation
|
|
45
47
|
|
|
46
|
-
|
|
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.
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
```bash
|
|
51
|
+
# Install the latest release.
|
|
52
|
+
pi install npm:pi-information-protecter
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"version": 1,
|
|
53
|
-
"sensitiveWords": [
|
|
54
|
-
"Alex Morgan",
|
|
55
|
-
{ "type": "literal", "value": "your-private-password" },
|
|
56
|
-
{ "type": "regex", "pattern": "https?://[^\\s\\\"<>]+", "flags": "i" },
|
|
57
|
-
{ "type": "regex", "pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)" }
|
|
58
|
-
]
|
|
59
|
-
}
|
|
54
|
+
# Upgrade later.
|
|
55
|
+
pi update npm:pi-information-protecter
|
|
60
56
|
```
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
Then run `/reload` in an open session. Load this extension **after** any other extension that rewrites outgoing requests, and check `/protecter status` before typing secrets: if it failed to load, Pi keeps working without it.
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
- **Regex:** JavaScript regex replaces the entire match. Use lookbehind to match only a credential value. Flags `g i m s u` are supported; `g` is added automatically. `/pattern/flags` shorthand is not supported.
|
|
66
|
-
- Escape backslashes as `\\` in JSON. Empty-string matches are invalid; runtime zero-width matches reject the request.
|
|
67
|
-
- Overlapping matches are merged to avoid exposing a longer secret's suffix. Equal values reuse a token within the extension instance.
|
|
68
|
-
- Learned originals remain exact-match protected within the instance even if regex context disappears or a rule is removed. Exit/reload clears these records.
|
|
69
|
-
- `/protecter` opens the local audit viewer; `/protecter logs 50` shows up to 50 recent records. `/protecter status` reports memory counts without reading configuration. `/protecter reload` invokes Pi's full reload after idle. Do not enter secrets as command arguments.
|
|
70
|
-
- Avoid rules that can match a very short string. A short match has few shape-preserving candidates, and on a large payload such as a compaction request every candidate already occurs elsewhere, so the request fails with `SCAN_UNIQUE_FAILED`. Require a realistic minimum length, for example `[A-Za-z0-9._~+/-]{16,}` rather than `+`.
|
|
71
|
-
- Limits: 1000 rules, 8192 characters per literal/pattern, 2-second scan timeout and 8 MiB request JSON. Exceeding limits rejects the request rather than sending plaintext.
|
|
60
|
+
## Configuration
|
|
72
61
|
|
|
73
|
-
|
|
62
|
+
The configuration lives at `~/.pi/agent/protecter.<machineHash>.json` (it follows `PI_CODING_AGENT_DIR`). It starts empty, so **nothing is protected until you add rules**.
|
|
74
63
|
|
|
75
|
-
|
|
64
|
+
**Edit it in a local editor. Never ask the model to write your real secrets into it, and never commit it.**
|
|
76
65
|
|
|
77
66
|
```json
|
|
78
67
|
{
|
|
79
68
|
"version": 1,
|
|
80
69
|
"sensitiveWords": [
|
|
70
|
+
"Alex Morgan",
|
|
71
|
+
{ "type": "literal", "value": "your-private-password" },
|
|
81
72
|
{ "type": "literal", "value": "Apple Inc.", "replacement": "Alphabet Inc." },
|
|
82
|
-
{ "type": "
|
|
83
|
-
{ "type": "
|
|
73
|
+
{ "type": "regex", "pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)" },
|
|
74
|
+
{ "type": "regex", "pattern": "(?<![\\w.])(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?![\\w.])" },
|
|
75
|
+
{ "type": "regex", "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}(?![\\w:.])" },
|
|
76
|
+
{ "type": "regex", "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){1,6}:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5})?(?![\\w:.])" }
|
|
84
77
|
]
|
|
85
78
|
}
|
|
86
79
|
```
|
|
87
80
|
|
|
88
|
-
|
|
81
|
+
| Rule | Meaning |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `"text"` | Match this exact text (case-sensitive) |
|
|
84
|
+
| `{ "type": "literal", "value": "..." }` | Same, with options |
|
|
85
|
+
| `{ "type": "regex", "pattern": "..." }` | JavaScript regex; the whole match is replaced |
|
|
86
|
+
| `"replacement": "..."` | Always use this fixed fake value instead of a random one |
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
The last four rules are a phone number, an IPv4 address, a full IPv6 address and a `::`-compressed IPv6 address. IPv6 needs both patterns because the compressed form is a different shape. Matched addresses are replaced by **valid** addresses, so tools and provider validators still accept them.
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
Tips that matter in practice:
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
- **Be precise.** A rule like `.` or "all digits" rewrites model names, IDs and tool schemas, and breaks requests.
|
|
93
|
+
- **Avoid very short matches.** They cannot be replaced uniquely, and the request is blocked with `SCAN_UNIQUE_FAILED`. Prefer something like `[A-Za-z0-9._~+/-]{16,}`.
|
|
94
|
+
- **Review patterns copied from examples.** [`protecter.example.json`](protecter.example.json) has ready-made rules for internal domains, emails, IPv4/IPv6, phone numbers and bearer tokens. None are enabled automatically, and broad address rules also match documentation examples and dependency hosts.
|
|
95
|
+
- In JSON, write backslashes as `\\`. Flags `g i m s u` are supported (`g` is automatic); `/pattern/flags` shorthand is not.
|
|
96
|
+
- A fixed `replacement` is a **reserved alias**: if the model writes that same text by itself, it is restored too. Choose distinctive values; fixed aliases also reveal more meaning than random ones.
|
|
97
|
+
- Limits: 1000 rules, 8192 characters per rule, 2-second scan, 8 MiB request. Exceeding a limit blocks the request instead of sending plaintext.
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
| --- | --- | --- |
|
|
98
|
-
| `9007199254740991` (number) | `2844008842109708` (number) | JSON type, digit count, safe-integer range |
|
|
99
|
-
| `13800138000` | `06864831332` | length, digits only |
|
|
100
|
-
| `Alex.Morgan@Example.COM` | `Ufqz.Vwtudb@Rjdnpxe.HGZ` | length, case pattern, punctuation positions |
|
|
101
|
-
| `张三` | `丅欕` | CJK script, character count |
|
|
102
|
-
| `Nguyễn Văn Tèo` | `Ufefṉg Zżt Sàb` | Latin with diacritics, word boundaries |
|
|
103
|
-
| `ひらがな` | `ゅせはっ` | Hiragana |
|
|
104
|
-
| `192.168.1.10` | `101.203.3.77` | valid IPv4, octets ≤ 255 |
|
|
105
|
-
| `fe80::1ff:fe23:4567` | `af44::6ea:bf19:2700` | valid IPv6, `::` and group widths |
|
|
99
|
+
After editing, run `/protecter reload`.
|
|
106
100
|
|
|
107
|
-
|
|
101
|
+
### Fake values keep the shape of the original
|
|
108
102
|
|
|
109
|
-
|
|
103
|
+
A match is replaced by a random value of the **same shape**, so requests stay valid:
|
|
110
104
|
|
|
111
|
-
|
|
105
|
+
| Original | Sent to the model |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `13800138000` | `06864831332` |
|
|
108
|
+
| `9007199254740991` (number) | `2844008842109708` (number) |
|
|
109
|
+
| `Alex.Morgan@Example.COM` | `Ufqz.Vwtudb@Rjdnpxe.HGZ` |
|
|
110
|
+
| `192.168.1.10` | `101.203.3.77` |
|
|
111
|
+
| `张三` | `丅欕` |
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Numbers stay numbers, IP addresses stay valid, and letters are replaced within their own script, so emails, URLs and embedded JSON still parse. Punctuation, emoji and unsupported scripts are left as they are, so part of a matched value can stay visible. The trade-off: a fake value reveals length and structure, and looks like real data to the model.
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
### The model reasons about the fake value
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
Only the **value** is restored in a reply — never a conclusion drawn from it:
|
|
118
118
|
|
|
119
119
|
| Step | Value |
|
|
120
120
|
| --- | --- |
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
126
|
-
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.
|
|
127
|
-
|
|
128
|
-
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.
|
|
129
|
-
|
|
130
|
-
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.
|
|
121
|
+
| You typed | `13800138000` — third digit is `8` |
|
|
122
|
+
| Model received | `27431905882` — third digit is `4` |
|
|
123
|
+
| Model answered | "the third digit is `4`" |
|
|
124
|
+
| You see | `13800138000` … "the third digit is `4`" |
|
|
131
125
|
|
|
132
|
-
|
|
126
|
+
The reply quietly contradicts itself, looks confident, and does not change if you retry. **Do not ask the model about digits, checksums, ordering, arithmetic or comparisons of protected values.** Compute those locally, or leave that field unprotected.
|
|
133
127
|
|
|
134
|
-
|
|
128
|
+
### Commands
|
|
135
129
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
| Agent requests, prompt-cache warming | `onPayload` | `before_provider_request` |
|
|
143
|
-
| Context compaction, split-turn prefix summaries | `session_before_compact` | The `preparation` Pi is about to summarize is redacted in place |
|
|
144
|
-
| `/tree` branch summaries | `session_before_tree` | `entriesToSummarize` and the instructions are redacted in place |
|
|
145
|
-
| `/bug` report summary | none | Model-runtime guard |
|
|
146
|
-
| `codemode` `models.classify()`, `models.generateImages()` | none | Model-runtime guard |
|
|
147
|
-
| Nested model calls by other extensions | none | Model-runtime guard |
|
|
148
|
-
|
|
149
|
-
**Compaction.** Pi hands the hook the same `preparation` object it then summarizes, so the extension redacts that object in place and returns no summary of its own. Pi summarizes the redacted text with the session's own model, which means **one configured model is enough** and Pi keeps routing, credentials, retries and the token budget. The stored summary keeps the replacements, so the session file holds no plaintext summary and the next outgoing request needs no further work. Any failure cancels compaction instead of falling through to Pi summarizing stored plaintext, and reports `COMPACT_FAILED`.
|
|
150
|
-
|
|
151
|
-
`/compact <instruction>` is the one exception: Pi reads those instructions from its own state, not from the event, so an instruction containing a protected value cannot be redacted. That compaction is cancelled with `COMPACT_INSTRUCTIONS`; run `/compact` without the protected value. Automatic compaction has no instructions and is unaffected.
|
|
130
|
+
| Command | What it does |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `/protecter status` | Ready state, rule and mapping counts, compaction mode, guard state, last blocked request |
|
|
133
|
+
| `/protecter logs [1-100]` | Open the local audit log viewer (default 20 records) |
|
|
134
|
+
| `/protecter logs clear` | Empty this machine's audit log |
|
|
135
|
+
| `/protecter reload` | Reload Pi once it is idle |
|
|
152
136
|
|
|
153
|
-
|
|
137
|
+
Never type a secret as a command argument.
|
|
154
138
|
|
|
155
|
-
|
|
139
|
+
### Context compaction
|
|
156
140
|
|
|
157
|
-
|
|
141
|
+
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.
|
|
158
142
|
|
|
159
|
-
|
|
|
143
|
+
| `compaction` | Behaviour |
|
|
160
144
|
| --- | --- |
|
|
161
|
-
| `ask` (default) |
|
|
145
|
+
| `ask` (default) | Ask before each compaction, then redact |
|
|
162
146
|
| `protected` | Redact without asking |
|
|
163
|
-
| `off` |
|
|
147
|
+
| `off` | Never compact; use `/new` instead |
|
|
164
148
|
|
|
165
149
|
```json
|
|
166
|
-
{
|
|
167
|
-
"version": 1,
|
|
168
|
-
"compaction": "protected",
|
|
169
|
-
"sensitiveWords": []
|
|
170
|
-
}
|
|
150
|
+
{ "version": 1, "compaction": "protected", "sensitiveWords": [] }
|
|
171
151
|
```
|
|
172
152
|
|
|
173
|
-
|
|
153
|
+
"Allow for this session" lasts until reload or exit; the configuration value is the permanent setting. If redaction fails, compaction is cancelled rather than letting Pi send the original text.
|
|
174
154
|
|
|
175
|
-
|
|
155
|
+
One exception: `/compact <instruction>` reads the instruction from Pi's own state, which this extension cannot reach. If your instruction contains a protected value, that compaction is cancelled (`COMPACT_INSTRUCTIONS`) — run `/compact` without it. Automatic compaction and `/tree` summaries are unaffected.
|
|
176
156
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
Summarized `/tree` navigation takes the same internal Pi path and is protected the same way, using the same setting and prompt. Navigation without a summary sends nothing and is never gated.
|
|
180
|
-
|
|
181
|
-
### Multi-turn consistency and prompt caching
|
|
182
|
-
|
|
183
|
-
Random placeholders are generated once per exact original and reused within the same extension instance, including after local response restoration. Starting another scan worker does not reset the mapping. This supports stable prompt prefixes but does not guarantee provider KV-cache hits; model, tools, message ordering, cache lifetime and routing also matter. `/reload`, process restart and session-instance replacement clear mappings. Different processes do not share mappings, and audit timestamps/request IDs are never included in the model payload.
|
|
157
|
+
`compaction.provider`, `compaction.model`, `compaction.contextWindow` and `compaction.reserveTokens` configured a separate summarization model before 0.11.0. They are still accepted so an upgrade cannot break a working setup, but they do nothing; remove them.
|
|
184
158
|
|
|
185
159
|
## Private audit log
|
|
186
160
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
One record is written per distinct original/token pair per successful scan; repeated occurrences in that request are deduplicated. Replacing the same original on a later request creates a new record, even when its token is reused. Existing tokens and requests without matches add no records. These are **local redaction events, not proof of network delivery**: cancellation, another hook or a provider failure can occur afterward.
|
|
161
|
+
Every successful redaction is appended to `protecter.<machineHash>.jsonl` next to the configuration: time, provider, the original value, the fake value and a request ID.
|
|
190
162
|
|
|
191
|
-
|
|
163
|
+
**This file contains your real secrets and the mapping back to them.** It is created with `0600` permissions, is never uploaded and is not encrypted. Do not share it. `/protecter logs` is local-TUI only and confirms before showing plaintext; RPC and print modes never display it.
|
|
192
164
|
|
|
193
|
-
|
|
165
|
+
A record means "replaced locally", not "delivered to the provider". `/protecter logs clear` empties this machine's log after confirmation — logical deletion, not secure erasure. If the log is full, locked or unwritable, matched requests are blocked rather than silently losing the audit trail.
|
|
194
166
|
|
|
195
|
-
|
|
167
|
+
## When a request is blocked
|
|
196
168
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
When a request is blocked, the notification names the failing stage, a stable code and a suggested action instead of one generic message:
|
|
169
|
+
Blocked requests fail closed: the outgoing payload is replaced with `{}`, and you get a code and a suggested action.
|
|
200
170
|
|
|
201
171
|
```text
|
|
202
172
|
SPI Protecter blocked this request
|
|
@@ -206,331 +176,352 @@ Action: Require a non-empty match in the numbered rule
|
|
|
206
176
|
Details: /protecter status
|
|
207
177
|
```
|
|
208
178
|
|
|
209
|
-
Stages are `initialization`, `configuration`, `migration`, `request`, `scan`, `worker`, `audit` and `internal`.
|
|
210
|
-
|
|
211
|
-
Common codes include `CONFIG_JSON`, `CONFIG_SCHEMA`, `CONFIG_REGEX`, `CONFIG_TARGET`, `CONFIG_CONFLICT`, `CONFIG_EMPTY_MATCH`, `CONFIG_SENSITIVE_TARGET`, `CONFIG_ALIAS_CONFLICT`, `CONFIG_UNSAFE`, `MIGRATION_LOCKED`, `MIGRATION_CHANGED`, `PAYLOAD_JSON`, `PAYLOAD_SIZE`, `SCAN_TIMEOUT`, `SCAN_MATCH_LIMIT`, `SCAN_COMPLEXITY`, `SCAN_ATTACHMENT`, `SCAN_JSON_REWRITE`, `SCAN_NUMBER_UNSAFE`, `SCAN_UNIQUE_FAILED`, `FIXED_OVERLAP`, `FIXED_ALIAS_REUSED`, `AUDIT_FULL`, `AUDIT_PARTIAL`, `AUDIT_LOCKED`, `AUDIT_UNSAFE` and `WORKER_FAILED`.
|
|
179
|
+
`rule=1` is the 1-based index in `sensitiveWords`. Stages are `initialization`, `configuration`, `migration`, `request`, `scan`, `worker`, `audit` and `internal`. Notifications scroll away, so `/protecter status` repeats the last one.
|
|
212
180
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
**Diagnostics never include request text, matched originals, replacement values, configuration content, file paths or raw OS messages.** Only catalog codes, non-negative integer coordinates and an allow-listed `errno` are emitted, so they are safe to paste into an issue. Because matched content is excluded, locating an offending rule may still require local inspection using the reported rule index.
|
|
181
|
+
**Diagnostics never contain request text, matched values, fake values, configuration content, file paths or raw OS messages** — only a code, numbers and an allow-listed `errno`. They are safe to paste into an issue, which also means locating the offending rule may need a local look.
|
|
216
182
|
|
|
217
183
|
## How it works
|
|
218
184
|
|
|
219
185
|
```text
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
186
|
+
your input / history / tool definitions / tool results
|
|
187
|
+
↓
|
|
188
|
+
scan the final request JSON, replace matches
|
|
189
|
+
↓
|
|
190
|
+
fake values → model provider
|
|
191
|
+
↓
|
|
192
|
+
restore values in replies and tool arguments
|
|
227
193
|
```
|
|
228
194
|
|
|
229
|
-
1. Scan JSON strings, keys
|
|
230
|
-
2. Generate
|
|
231
|
-
3. Restore
|
|
232
|
-
4.
|
|
233
|
-
5. Execute regex scans in a terminable worker. On failure, return `{}` and request cancellation rather than relying on hook exceptions swallowed by Pi. An empty request or provider validation error may still occur, but the handler does not return the original payload.
|
|
195
|
+
1. Scan the final request JSON — strings, keys, numbers and JSON-encoded tool arguments. Your local input and history are never modified.
|
|
196
|
+
2. Generate fake values with `crypto.randomBytes`. Mappings stay in memory, and one value always maps to the same fake value within a session, which also keeps prompt prefixes stable.
|
|
197
|
+
3. Restore values in the finished reply, in thinking blocks, and in tool arguments before a tool runs, so local tools work on real data.
|
|
198
|
+
4. Run your regexes in a worker with a 2-second limit. On any failure, send `{}` and cancel instead of falling back to the original request.
|
|
234
199
|
|
|
235
|
-
|
|
200
|
+
`/reload`, restart or switching sessions clears the mappings; fake values in older summaries can no longer be turned back after that.
|
|
236
201
|
|
|
237
|
-
|
|
202
|
+
The extension also stops tools from reading its own configuration and audit log, resolving `~`, `@`, relative paths, `file://` and symlinks. This is a convenience guard, not a sandbox: shell and third-party tools can work around it.
|
|
238
203
|
|
|
239
|
-
|
|
240
|
-
- Resolve `@`, `~`, relative paths and `file://`; check symlink/inode aliases. Block `grep/find/ls` over ancestor directories containing the configuration.
|
|
241
|
-
- Attempt whole-text masking if the complete original configuration accidentally appears in a request.
|
|
242
|
-
- **Best effort only:** dynamic shell, base64, split output, nested symlinks, arbitrary third-party tools and network sends can bypass these checks. This cannot guarantee that a malicious model never obtains configuration.
|
|
204
|
+
## Development and debugging
|
|
243
205
|
|
|
244
|
-
|
|
206
|
+
```bash
|
|
207
|
+
npm ci
|
|
208
|
+
npm run check # build + typecheck + all tests
|
|
209
|
+
npm run typecheck # types only
|
|
210
|
+
npm test # tests only (also rebuilds dist/)
|
|
211
|
+
npm run build # bundle and minify into dist/
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Run the extension from source against a **throwaway** Pi configuration directory, never your real one:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
export PI_CODING_AGENT_DIR="$(mktemp -d)"
|
|
218
|
+
export PI_PROTECTER_LANG=en # pin message language
|
|
219
|
+
pi -e ./src/index.ts
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Write fictional rules into `$PI_CODING_AGENT_DIR/protecter.json`, start Pi, and the machine-hash configuration is created from it on first run.
|
|
245
223
|
|
|
246
|
-
|
|
247
|
-
- Compaction and summarized `/tree` navigation redact what Pi is about to summarize, in place, because Pi builds those requests internally and never routes them through `before_provider_request`; Pi then summarizes the redacted text with the session's own model. Navigation without summaries sends nothing. Pi's other hookless model calls, the `/bug` summary and nested `codemode` or extension calls, are covered by the model-runtime guard. See [Context compaction and every hookless path](#context-compaction-and-every-hookless-path).
|
|
248
|
-
- Exit, reload and session switching discard mappings. Final restored local messages remain readable; crash leftovers, old summaries and raw deltas cannot recover tokens across restarts.
|
|
249
|
-
- Broad rules such as `.` or all digits may alter protocol fields, model names, tool schemas or IDs and break requests. Matching numbers become token strings. Prefer precise rules.
|
|
250
|
-
- Reused tokens reveal equality relationships; context may imply identity. This is not formal anonymization.
|
|
251
|
-
- Model calls are covered; transfers that are not model calls are not. `/bug`'s uploaded bundle, `/share` and `/export` send the stored transcript over their own HTTP paths, which no extension API can intercept, and the transcript holds restored plaintext. Hooks also do not cover HTTP headers, authentication keys, gateway endpoints, Pi telemetry, an extension's own `fetch`, or tool networking, including arguments handed to MCP servers. The gateway still receives its authentication credentials. Treat `/bug`, `/share` and `/export` as plaintext disclosure and review what they contain first.
|
|
252
|
-
- **Load after other payload rewriters and trust every installed extension.** Later handlers can reintroduce originals. Extensions have local permissions, and Pi may continue if this extension fails to load. Check status before entering secrets.
|
|
253
|
-
- Local sessions, terminals, exports and files written by tools may contain plaintext. They are not encrypted or cleaned by this project.
|
|
224
|
+
### Where things live
|
|
254
225
|
|
|
255
|
-
|
|
226
|
+
| Path | Role |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `src/index.ts` | Hook and command wiring: outgoing requests, restoration, tool calls, compaction, startup checks |
|
|
229
|
+
| `src/engine.ts` | Scan/restore orchestration, mapping table, worker lifecycle, audit writes |
|
|
230
|
+
| `src/scan-worker.mjs` | Runs your regexes in a terminable worker |
|
|
231
|
+
| `src/shape.mjs` | Generates shape-preserving fake values |
|
|
232
|
+
| `src/compaction.ts` | Redacts what Pi is about to summarize (compaction and `/tree`) |
|
|
233
|
+
| `src/guard-runtime.ts` | Model-runtime choke point for calls that have no hook |
|
|
234
|
+
| `src/config.ts`, `src/migration.ts` | Parsing, validation, machine-hash configuration migration |
|
|
235
|
+
| `src/guard.ts` | Blocks tool access to the configuration and audit log |
|
|
236
|
+
| `src/diagnostics.ts`, `src/locale.ts` | Error codes, stages and bilingual messages |
|
|
237
|
+
| `src/compat.ts` | The verified Pi version gate |
|
|
238
|
+
|
|
239
|
+
### Debugging workflow
|
|
240
|
+
|
|
241
|
+
- **Run one test file:** `npx tsx --test test/guard-runtime.test.ts`.
|
|
242
|
+
- **Reproduce a block as a test, not against a live gateway.** `test/integration.test.ts` drives Pi's real loader and runner and asserts on the actual outgoing payload; copy one of its cases and substitute a fictional value. Never test with real secrets or a real provider.
|
|
243
|
+
- **Check what actually left the process.** The audit log records each original/fake pair, and `/protecter status` repeats the last block with its code and stage. Diagnostics deliberately exclude content, so the rule index is the pointer back to your configuration.
|
|
244
|
+
- **Hookless paths** are covered by `test/hookless-paths.test.ts` (compaction and `/tree` through the real runner) and `test/guard-runtime.test.ts` (the model-runtime guard, including assertions against Pi's own classes so a Pi rename fails the suite instead of silently dropping coverage).
|
|
245
|
+
- **The packed artifact is tested too.** `npm test` builds the real npm tarball, extracts it, and loads `dist/index.mjs` with Pi's loader and both workers, so minification and worker paths are verified, not assumed.
|
|
246
|
+
|
|
247
|
+
### Widening Pi compatibility
|
|
256
248
|
|
|
257
|
-
|
|
249
|
+
`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:
|
|
258
250
|
|
|
259
251
|
```bash
|
|
260
|
-
npm
|
|
252
|
+
npm i -D @earendil-works/pi-coding-agent@<version>
|
|
261
253
|
npm run check
|
|
262
254
|
```
|
|
263
255
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
`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.
|
|
256
|
+
Only then update `supportsPi`, `SUPPORTED_PI` and `peerDependencies`, keeping the runtime gate and the declared range identical.
|
|
267
257
|
|
|
268
|
-
|
|
258
|
+
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.
|
|
269
259
|
|
|
270
|
-
|
|
260
|
+
## Messages and language
|
|
271
261
|
|
|
272
|
-
|
|
262
|
+
Messages appear in one language, chosen from `PI_PROTECTER_LANG`, then `LC_ALL`, `LC_MESSAGES`, `LANG`, `LANGUAGE`, then the runtime locale. `zh*` selects Simplified Chinese, anything else English. Codes and numbers stay language-neutral.
|
|
273
263
|
|
|
274
|
-
|
|
264
|
+
Licensed under MIT.
|
|
275
265
|
|
|
276
|
-
|
|
266
|
+
---
|
|
277
267
|
|
|
278
|
-
##
|
|
268
|
+
## 简体中文
|
|
279
269
|
|
|
280
|
-
|
|
270
|
+
**Pi 的个人敏感信息(SPI)保护插件。**
|
|
281
271
|
|
|
282
|
-
|
|
283
|
-
# 安装最新 npm 版本。
|
|
284
|
-
pi install npm:pi-information-protecter
|
|
272
|
+
你列出不希望发给模型的值,本扩展在模型请求发出之前把它们替换成形态逼真的假值,并在回复和工具参数中于本地还原。模型看到的是假数据,你看到的仍是自己的数据。
|
|
285
273
|
|
|
286
|
-
|
|
287
|
-
pi install npm:pi-information-protecter@0.11.0
|
|
274
|
+
它保护的是**你配置过的值,且仅限模型请求**——仅此而已。下一节是最重要的内容。
|
|
288
275
|
|
|
289
|
-
|
|
290
|
-
npm ci
|
|
291
|
-
npm run build
|
|
292
|
-
pi install "$PWD"
|
|
276
|
+
## 哪些会脱敏,哪些不会
|
|
293
277
|
|
|
294
|
-
|
|
295
|
-
pi -e ./src/index.ts
|
|
278
|
+
### 会脱敏:所有发往模型的请求
|
|
296
279
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
280
|
+
| 模型请求 | 覆盖方式 |
|
|
281
|
+
| --- | --- |
|
|
282
|
+
| 你的提示、对话历史、系统提示、工具定义、工具结果 | Pi 的出站请求钩子 |
|
|
283
|
+
| 提示缓存预热 | 同一钩子 |
|
|
284
|
+
| 上下文压缩与 `/tree` 分支摘要 | 在 Pi 生成摘要之前先脱敏对话 |
|
|
285
|
+
| `/bug` 生成的那段摘要 | 模型运行时闸门 |
|
|
286
|
+
| `codemode` 的分类与图像调用 | 模型运行时闸门 |
|
|
287
|
+
| 其他扩展发起的模型调用 | 模型运行时闸门 |
|
|
300
288
|
|
|
301
|
-
|
|
289
|
+
Pi 没有为后三者提供任何钩子,因此本扩展包装会话共享的模型运行时,对经由它发出的内容一律脱敏。若无法完成包装,启动会以 `GUARD_UNAVAILABLE` 失败并拦截**所有**请求,而不是在无保护状态下运行。`/protecter status` 会显示它是否已就位。
|
|
302
290
|
|
|
303
|
-
|
|
291
|
+
若脱敏本身因任何原因失败,请求会被**拦截**,绝不以明文发出。
|
|
304
292
|
|
|
305
|
-
|
|
293
|
+
### 不会脱敏:一切不属于模型请求的传输
|
|
306
294
|
|
|
307
|
-
|
|
295
|
+
| 操作 | 发送的内容 | 为什么无法拦截 |
|
|
296
|
+
| --- | --- | --- |
|
|
297
|
+
| `/bug` | 上传的报告包:完整会话记录——你的消息、模型输出、工具调用及其结果(含读取到的文件内容与命令输出)——以及 Pi 版本、操作系统、模型与提供商配置、已加载扩展和设置 | `/bug` 由 Pi 自己处理,并通过它自身的 HTTP 连接上传。它还在扩展命令之前被处理,扩展连替换它都做不到。只有其中那段摘要会被脱敏 |
|
|
298
|
+
| `/share` | 会话记录,以 gist 形式上传 | 走 Pi 自己的上传路径,不是模型请求 |
|
|
299
|
+
| `/export` | 会话记录,写入文件 | 同理,写到本地磁盘 |
|
|
300
|
+
| 会话文件 | 你与模型写下的全部内容,且其中的值已还原为真实值 | 会话由 Pi 保存,本扩展不对其加密 |
|
|
301
|
+
| 工具、shell、MCP 服务器 | 工具参数会被刻意还原为真实值,工具才能正常工作 | 此后工具、shell 命令或 MCP 服务器拿这些值做什么都不在本扩展范围内;shell 还可通过编码或拆分绕过任何文本匹配 |
|
|
302
|
+
| 请求头、API key、网关地址、Pi 遥测 | 凭证与路由元数据 | 不属于本扩展改写的请求体 |
|
|
303
|
+
| 其他扩展自己的 `fetch` | 由它自行决定 | 扩展以你的权限运行 |
|
|
308
304
|
|
|
309
|
-
|
|
305
|
+
**请把 `/bug`、`/share`、`/export` 视为公开明文。** 若需要为接触过真实秘密的会话提交错误报告,请使用本地 zip/导出选项,打开它,确认内容之后再决定发给谁。
|
|
310
306
|
|
|
311
|
-
|
|
307
|
+
另外两点同样要紧:只有命中你的规则的文本会被保护——改写、片段值以及模型推断出的内容都不会;包含图片、音频、视频或文件块的请求会被拒绝而不是发送,因为它们无法被检查。
|
|
312
308
|
|
|
313
|
-
|
|
309
|
+
## 安装
|
|
314
310
|
|
|
315
|
-
|
|
311
|
+
要求 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**。清单中的每个版本都经过完整测试验证;其余版本会被有意拒绝——此处判断失误意味着发送明文。
|
|
316
312
|
|
|
317
|
-
|
|
313
|
+
```bash
|
|
314
|
+
# 安装最新版本。
|
|
315
|
+
pi install npm:pi-information-protecter
|
|
318
316
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"version": 1,
|
|
322
|
-
"sensitiveWords": [
|
|
323
|
-
"张三",
|
|
324
|
-
{ "type": "literal", "value": "your-private-password" },
|
|
325
|
-
{ "type": "regex", "pattern": "https?://[^\\s\\\"<>]+", "flags": "i" },
|
|
326
|
-
{ "type": "regex", "pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)" }
|
|
327
|
-
]
|
|
328
|
-
}
|
|
317
|
+
# 后续升级。
|
|
318
|
+
pi update npm:pi-information-protecter
|
|
329
319
|
```
|
|
330
320
|
|
|
331
|
-
|
|
321
|
+
随后在已打开的会话中执行 `/reload`。请把本扩展加载在任何其他改写出站请求的扩展**之后**;输入秘密前先看 `/protecter status`:若它加载失败,Pi 会在没有它的情况下继续工作。
|
|
332
322
|
|
|
333
|
-
|
|
334
|
-
- **正则:**JavaScript 正则替换整个匹配,可用后行断言仅匹配凭证值。支持 `g i m s u`,自动补 `g`,不支持 `/pattern/flags` 简写。
|
|
335
|
-
- JSON 中的反斜杠写作 `\\`。不允许匹配空字符串;运行时零宽匹配会拒绝请求。
|
|
336
|
-
- 合并重叠匹配,避免暴露长敏感值的后缀;同一实例内相同原文复用占位符。
|
|
337
|
-
- 已识别原文在当前实例内持续受到精确匹配保护,即使正则上下文消失或规则被删除;退出或重载清除记录。
|
|
338
|
-
- `/protecter` 打开本地审计查看器,`/protecter logs 50` 查看最近最多 50 条;`/protecter status` 只报告内存数量,不读取配置。`/protecter reload` 等待空闲后完整重载。不要在命令参数中填写秘密。
|
|
339
|
-
- 避免可能匹配极短字符串的规则。短匹配的同形候选值很少,而在压缩请求这类大载荷下每个候选值都已出现在别处,请求会以 `SCAN_UNIQUE_FAILED` 失败。请设定合理的最小长度,例如用 `[A-Za-z0-9._~+/-]{16,}` 而非 `+`。
|
|
340
|
-
- 限制为 1000 条规则、每个词或模式 8192 字符、扫描 2 秒、请求 JSON 8 MiB;超限拒绝,不降级发送明文。
|
|
323
|
+
## 配置
|
|
341
324
|
|
|
342
|
-
|
|
325
|
+
配置文件位于 `~/.pi/agent/protecter.<machineHash>.json`(遵循 `PI_CODING_AGENT_DIR`)。它初始为空,因此**在你添加规则之前,什么都不会被保护**。
|
|
343
326
|
|
|
344
|
-
|
|
327
|
+
**请在本地编辑器中修改。不要让模型把你的真实秘密写进去,也不要提交到 Git。**
|
|
345
328
|
|
|
346
329
|
```json
|
|
347
330
|
{
|
|
348
331
|
"version": 1,
|
|
349
332
|
"sensitiveWords": [
|
|
333
|
+
"Alex Morgan",
|
|
334
|
+
{ "type": "literal", "value": "your-private-password" },
|
|
350
335
|
{ "type": "literal", "value": "Apple Inc.", "replacement": "Alphabet Inc." },
|
|
351
|
-
{ "type": "
|
|
352
|
-
{ "type": "
|
|
336
|
+
{ "type": "regex", "pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)" },
|
|
337
|
+
{ "type": "regex", "pattern": "(?<![\\w.])(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?![\\w.])" },
|
|
338
|
+
{ "type": "regex", "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}(?![\\w:.])" },
|
|
339
|
+
{ "type": "regex", "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){1,6}:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5})?(?![\\w:.])" }
|
|
353
340
|
]
|
|
354
341
|
}
|
|
355
342
|
```
|
|
356
343
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
从 0.6.0 起,命中内容不再替换为不透明占位符,而是替换为**同形**随机值,使请求保持结构有效:
|
|
364
|
-
|
|
365
|
-
| 原文 | 替换值 | 保留特征 |
|
|
366
|
-
| --- | --- | --- |
|
|
367
|
-
| `9007199254740991`(数字) | `2844008842109708`(数字) | JSON 类型、位数、安全整数范围 |
|
|
368
|
-
| `13800138000` | `06864831332` | 长度、纯数字 |
|
|
369
|
-
| `Alex.Morgan@Example.COM` | `Ufqz.Vwtudb@Rjdnpxe.HGZ` | 长度、大小写形态、标点位置 |
|
|
370
|
-
| `张三` | `丅欕` | 汉字体系、字数 |
|
|
371
|
-
| `Nguyễn Văn Tèo` | `Ufefṉg Zżt Sàb` | 带重音拉丁字母、词边界 |
|
|
372
|
-
| `ひらがな` | `ゅせはっ` | 平假名 |
|
|
373
|
-
| `192.168.1.10` | `101.203.3.77` | 合法 IPv4,每段不超过 255 |
|
|
374
|
-
| `fe80::1ff:fe23:4567` | `af44::6ea:bf19:2700` | 合法 IPv6,保留 `::` 与分组宽度 |
|
|
344
|
+
| 规则 | 含义 |
|
|
345
|
+
| --- | --- |
|
|
346
|
+
| `"text"` | 匹配该精确文本(区分大小写) |
|
|
347
|
+
| `{ "type": "literal", "value": "..." }` | 同上,可带选项 |
|
|
348
|
+
| `{ "type": "regex", "pattern": "..." }` | JavaScript 正则,整个匹配被替换 |
|
|
349
|
+
| `"replacement": "..."` | 固定使用该假值,而非随机值 |
|
|
375
350
|
|
|
376
|
-
|
|
351
|
+
最后四条规则分别匹配手机号、IPv4 地址、完整形式的 IPv6 地址,以及 `::` 压缩形式的 IPv6 地址。IPv6 需要两条规则,因为压缩形式的形态不同。命中的地址会被替换成**合法**地址,因此工具与提供商的校验仍能通过。
|
|
377
352
|
|
|
378
|
-
|
|
353
|
+
实践中要紧的几点:
|
|
379
354
|
|
|
380
|
-
|
|
355
|
+
- **要精确。** 像 `.` 或"所有数字"这样的规则会改写模型名、ID 与工具结构,导致请求失败。
|
|
356
|
+
- **避免过短的匹配。** 它们无法生成唯一替换值,请求会以 `SCAN_UNIQUE_FAILED` 被拦截。建议写成 `[A-Za-z0-9._~+/-]{16,}` 之类。
|
|
357
|
+
- **核对从示例复制的正则。** [`protecter.example.json`](protecter.example.json) 提供了内部域名、邮箱、IPv4/IPv6、手机号与 Bearer 令牌的现成规则,它们都不会自动启用;过宽的地址规则也会命中文档示例和依赖主机名。
|
|
358
|
+
- JSON 中反斜杠要写成 `\\`。支持 `g i m s u` 标志(`g` 自动添加);不支持 `/pattern/flags` 简写。
|
|
359
|
+
- 固定 `replacement` 是**保留别名**:若模型自己写出同样的文本,也会被还原。请选择独特的值;固定别名也比随机值暴露更多语义。
|
|
360
|
+
- 限制:1000 条规则、每条 8192 字符、扫描 2 秒、请求 8 MiB。超限会拦截请求,而不是发送明文。
|
|
381
361
|
|
|
382
|
-
|
|
362
|
+
修改后执行 `/protecter reload`。
|
|
383
363
|
|
|
384
|
-
|
|
364
|
+
### 假值保持原值的形态
|
|
385
365
|
|
|
386
|
-
|
|
366
|
+
命中的值会被替换为**同形态**的随机值,使请求依然合法:
|
|
387
367
|
|
|
388
|
-
|
|
|
368
|
+
| 原值 | 发给模型的值 |
|
|
389
369
|
| --- | --- |
|
|
390
|
-
|
|
|
391
|
-
|
|
|
392
|
-
|
|
|
393
|
-
|
|
|
394
|
-
|
|
395
|
-
号码被还原了,推理没有,因此回复会静默地自相矛盾。没有任何机制能发现它:推导出的答案就是普通文本,与其他句子无法区分。重试也不会暴露错误,因为同一值始终对应同一替换值,结果前后一致。
|
|
370
|
+
| `13800138000` | `06864831332` |
|
|
371
|
+
| `9007199254740991`(数字) | `2844008842109708`(数字) |
|
|
372
|
+
| `Alex.Morgan@Example.COM` | `Ufqz.Vwtudb@Rjdnpxe.HGZ` |
|
|
373
|
+
| `192.168.1.10` | `101.203.3.77` |
|
|
374
|
+
| `张三` | `丅欕` |
|
|
396
375
|
|
|
397
|
-
|
|
376
|
+
数字仍是数字,IP 仍是合法地址,字母在各自文字体系内替换,因此邮箱、URL 与内嵌 JSON 仍可解析。标点、表情与不支持的文字体系原样保留,因此命中值可能部分可见。代价是:假值会暴露长度与结构,并且在模型看来像真实数据。
|
|
398
377
|
|
|
399
|
-
|
|
378
|
+
### 模型推理的是假值
|
|
400
379
|
|
|
401
|
-
|
|
380
|
+
回复中只有**值**会被还原,由它推导出的结论不会:
|
|
402
381
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
382
|
+
| 步骤 | 值 |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| 你输入 | `13800138000`——第三位是 `8` |
|
|
385
|
+
| 模型收到 | `27431905882`——第三位是 `4` |
|
|
386
|
+
| 模型回答 | "第三位是 `4`" |
|
|
387
|
+
| 你看到 | `13800138000` … "第三位是 `4`" |
|
|
408
388
|
|
|
409
|
-
|
|
410
|
-
| --- | --- | --- |
|
|
411
|
-
| 智能体请求、提示缓存预热 | `onPayload` | `before_provider_request` |
|
|
412
|
-
| 上下文压缩、split-turn 前缀摘要 | `session_before_compact` | 对 Pi 即将摘要的 `preparation` 原地脱敏 |
|
|
413
|
-
| `/tree` 分支摘要 | `session_before_tree` | 对 `entriesToSummarize` 与指令原地脱敏 |
|
|
414
|
-
| `/bug` 报告摘要 | 无 | 模型运行时闸门 |
|
|
415
|
-
| `codemode` 的 `models.classify()`、`models.generateImages()` | 无 | 模型运行时闸门 |
|
|
416
|
-
| 其他扩展发起的嵌套模型调用 | 无 | 模型运行时闸门 |
|
|
389
|
+
回复因此自相矛盾,却看起来笃定,重试也不会变。**不要让模型回答受保护值的位数、校验位、排序、算术或比较问题。** 这些请在本地计算,或让该字段不受保护。
|
|
417
390
|
|
|
418
|
-
|
|
391
|
+
### 命令
|
|
419
392
|
|
|
420
|
-
|
|
393
|
+
| 命令 | 作用 |
|
|
394
|
+
| --- | --- |
|
|
395
|
+
| `/protecter status` | 就绪状态、规则与映射数量、压缩模式、闸门状态、最近一次拦截 |
|
|
396
|
+
| `/protecter logs [1-100]` | 打开本地审计日志查看器(默认 20 条) |
|
|
397
|
+
| `/protecter logs clear` | 清空本机审计日志 |
|
|
398
|
+
| `/protecter reload` | 等待空闲后重载 Pi |
|
|
421
399
|
|
|
422
|
-
|
|
400
|
+
不要把秘密作为命令参数输入。
|
|
423
401
|
|
|
424
|
-
|
|
402
|
+
### 上下文压缩
|
|
425
403
|
|
|
426
|
-
|
|
404
|
+
上下文写满时,Pi 会对对话生成摘要。该请求由 Pi 内部构造,**不经过**常规钩子,因此本扩展会先对 Pi 即将摘要的对话脱敏,再由 Pi 用你当前的模型生成摘要。不需要第二个模型;保存下来的摘要保留假值,因此会话文件中不会写入明文摘要。
|
|
427
405
|
|
|
428
|
-
|
|
|
406
|
+
| `compaction` | 行为 |
|
|
429
407
|
| --- | --- |
|
|
430
|
-
| `ask`(默认) |
|
|
408
|
+
| `ask`(默认) | 每次压缩前询问,然后脱敏 |
|
|
431
409
|
| `protected` | 不询问,直接脱敏 |
|
|
432
|
-
| `off` |
|
|
410
|
+
| `off` | 从不压缩;请改用 `/new` |
|
|
433
411
|
|
|
434
412
|
```json
|
|
435
|
-
{
|
|
436
|
-
"version": 1,
|
|
437
|
-
"compaction": "protected",
|
|
438
|
-
"sensitiveWords": []
|
|
439
|
-
}
|
|
413
|
+
{ "version": 1, "compaction": "protected", "sensitiveWords": [] }
|
|
440
414
|
```
|
|
441
415
|
|
|
442
|
-
|
|
416
|
+
"本会话内全部允许"在重载或退出前有效;配置中的取值才是永久设置。若脱敏失败,压缩会被取消,而不是让 Pi 发送原文。
|
|
443
417
|
|
|
444
|
-
|
|
418
|
+
一个例外:`/compact <指令>` 的指令由 Pi 从自身状态读取,本扩展无法触及。若指令中含受保护值,该次压缩会被取消(`COMPACT_INSTRUCTIONS`)——请不带该值执行 `/compact`。自动压缩与 `/tree` 摘要不受影响。
|
|
445
419
|
|
|
446
|
-
|
|
420
|
+
`compaction.provider`、`compaction.model`、`compaction.contextWindow` 与 `compaction.reserveTokens` 在 0.11.0 之前用于配置独立的摘要模型。它们仍被接收,以免升级后破坏可用环境,但已不起作用,请删除。
|
|
447
421
|
|
|
448
|
-
|
|
449
|
-
### 多轮一致性与提示词缓存
|
|
422
|
+
## 私有审计日志
|
|
450
423
|
|
|
451
|
-
|
|
424
|
+
每次成功脱敏都会追加到配置旁的 `protecter.<machineHash>.jsonl`:时间、provider、原值、假值与请求 ID。
|
|
452
425
|
|
|
453
|
-
|
|
426
|
+
**该文件包含你的真实秘密以及还原映射。** 它以 `0600` 权限创建,不会被上传,也未加密。请勿分享。`/protecter logs` 仅限本地 TUI,展示明文前需确认;RPC 与 print 模式从不显示。
|
|
454
427
|
|
|
455
|
-
|
|
428
|
+
一条记录代表"已在本地替换",不代表"已送达提供商"。`/protecter logs clear` 在确认后清空本机日志——这是逻辑删除,而非安全擦除。日志写满、被锁定或不可写时,命中请求会被拦截,而不是静默丢失审计记录。
|
|
456
429
|
|
|
457
|
-
|
|
430
|
+
## 请求被拦截时
|
|
458
431
|
|
|
459
|
-
|
|
432
|
+
被拦截的请求一律拒绝放行:出站内容被替换为 `{}`,并给出错误码与处理建议。
|
|
460
433
|
|
|
461
|
-
|
|
434
|
+
```text
|
|
435
|
+
SPI Protecter blocked this request
|
|
436
|
+
code=CONFIG_EMPTY_MATCH stage=configuration rule=1
|
|
437
|
+
Reason: Regex matches an empty string
|
|
438
|
+
Action: Require a non-empty match in the numbered rule
|
|
439
|
+
Details: /protecter status
|
|
440
|
+
```
|
|
462
441
|
|
|
463
|
-
|
|
442
|
+
`rule=1` 是 `sensitiveWords` 中从 1 开始的序号。阶段包括 `initialization`、`configuration`、`migration`、`request`、`scan`、`worker`、`audit` 与 `internal`。通知会滚动消失,可用 `/protecter status` 重新查看最近一次。
|
|
464
443
|
|
|
465
|
-
|
|
444
|
+
**诊断信息从不包含请求文本、命中原值、假值、配置内容、文件路径或原始系统消息**——只有错误码、数字和白名单内的 `errno`,因此可以安全粘贴到 issue;相应地,定位具体规则可能仍需在本地查看。
|
|
466
445
|
|
|
467
|
-
|
|
446
|
+
## 工作方式
|
|
468
447
|
|
|
469
448
|
```text
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
449
|
+
你的输入/历史/工具定义/工具结果
|
|
450
|
+
↓
|
|
451
|
+
扫描最终请求 JSON,替换命中内容
|
|
452
|
+
↓
|
|
453
|
+
假值 → 模型提供商
|
|
454
|
+
↓
|
|
455
|
+
在回复与工具参数中还原真实值
|
|
475
456
|
```
|
|
476
457
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
458
|
+
1. 扫描最终请求 JSON——字符串、键、数字以及 JSON 编码的工具参数。你的本地输入与历史不会被修改。
|
|
459
|
+
2. 用 `crypto.randomBytes` 生成假值。映射保存在内存中;同一会话内同一个值始终对应同一个假值,这也使提示前缀保持稳定。
|
|
460
|
+
3. 在完成的回复、思考块以及工具执行前的参数中还原真实值,使本地工具基于真实数据工作。
|
|
461
|
+
4. 你的正则在 worker 中执行,限时 2 秒。任何失败都发送 `{}` 并取消请求,而不是回退到原始请求。
|
|
480
462
|
|
|
481
|
-
|
|
463
|
+
`/reload`、重启或切换会话会清空映射;此后旧摘要中的假值将无法再被还原。
|
|
482
464
|
|
|
483
|
-
|
|
465
|
+
本扩展还会阻止工具读取它自己的配置与审计日志,并会解析 `~`、`@`、相对路径、`file://` 与符号链接。这是便利性防护,不是沙箱:shell 与第三方工具可以绕过。
|
|
484
466
|
|
|
485
|
-
##
|
|
467
|
+
## 开发与调试
|
|
486
468
|
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
↓
|
|
494
|
-
本地回复和工具参数还原
|
|
469
|
+
```bash
|
|
470
|
+
npm ci
|
|
471
|
+
npm run check # 构建 + 类型检查 + 全部测试
|
|
472
|
+
npm run typecheck # 仅类型检查
|
|
473
|
+
npm test # 仅测试(同时会重新构建 dist/)
|
|
474
|
+
npm run build # 打包并压缩到 dist/
|
|
495
475
|
```
|
|
496
476
|
|
|
497
|
-
|
|
498
|
-
2. 使用 `crypto.randomBytes` 生成同形替换值。运行时映射保存在内存,不跨重启恢复。**从 0.3.0 起,成功替换的原文和占位符也会持久化到私有审计日志。** 不注入配置、会话自定义条目或模型提示。
|
|
499
|
-
3. 还原最终 assistant 文本、思考内容和工具参数。TUI 在完整占位符到达后还原,片段可能短暂显示;RPC/JSON 增量仍脱敏,直到最终消息还原。被改写或截断的占位符无法还原。
|
|
500
|
-
4. 工具执行前还原参数,让合法本地操作使用原值;后续请求再次脱敏。**凭证还原不等于工具授权或防外传。**
|
|
501
|
-
5. 正则扫描运行于可终止 worker。失败时返回 `{}` 并请求取消,不依赖被 Pi 吞掉的钩子异常;仍可能产生空请求或提供商校验错误,但处理器不返回原始请求体。
|
|
477
|
+
请使用**一次性**的 Pi 配置目录从源码运行,绝不要用你的真实目录:
|
|
502
478
|
|
|
503
|
-
|
|
479
|
+
```bash
|
|
480
|
+
export PI_CODING_AGENT_DIR="$(mktemp -d)"
|
|
481
|
+
export PI_PROTECTER_LANG=en # 固定消息语言
|
|
482
|
+
pi -e ./src/index.ts
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
把虚构规则写入 `$PI_CODING_AGENT_DIR/protecter.json` 后启动 Pi,首次运行会据此创建机器哈希配置。
|
|
504
486
|
|
|
505
|
-
|
|
487
|
+
### 代码位置
|
|
506
488
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
489
|
+
| 路径 | 作用 |
|
|
490
|
+
| --- | --- |
|
|
491
|
+
| `src/index.ts` | 钩子与命令接线:出站请求、还原、工具调用、压缩、启动检查 |
|
|
492
|
+
| `src/engine.ts` | 脱敏/还原编排、映射表、worker 生命周期、审计写入 |
|
|
493
|
+
| `src/scan-worker.mjs` | 在可终止的 worker 中执行你的正则 |
|
|
494
|
+
| `src/shape.mjs` | 生成同形态的假值 |
|
|
495
|
+
| `src/compaction.ts` | 对 Pi 即将摘要的内容脱敏(压缩与 `/tree`) |
|
|
496
|
+
| `src/guard-runtime.ts` | 针对无钩子调用的模型运行时总闸门 |
|
|
497
|
+
| `src/config.ts`、`src/migration.ts` | 解析、校验、机器哈希配置迁移 |
|
|
498
|
+
| `src/guard.ts` | 阻止工具访问配置与审计日志 |
|
|
499
|
+
| `src/diagnostics.ts`、`src/locale.ts` | 错误码、阶段与双语文案 |
|
|
500
|
+
| `src/compat.ts` | 已验证的 Pi 版本门禁 |
|
|
511
501
|
|
|
512
|
-
|
|
502
|
+
### 调试流程
|
|
513
503
|
|
|
514
|
-
-
|
|
515
|
-
-
|
|
516
|
-
-
|
|
517
|
-
-
|
|
518
|
-
-
|
|
519
|
-
- 模型调用已覆盖,但非模型调用的传输不在覆盖范围:`/bug` 上传的报告包、`/share` 与 `/export` 通过各自的 HTTP 路径发送已保存的会话记录,任何扩展 API 都无法拦截,而会话记录中保存的是已还原的明文。钩子同样不覆盖认证头、API key、网关地址、遥测、扩展自身的 `fetch`,以及工具联网(含交给 MCP 服务器的参数);网关仍收到其认证凭证。请把 `/bug`、`/share`、`/export` 视为明文披露,使用前先检查其内容。
|
|
520
|
-
- **在其他请求改写扩展之后加载,并只使用可信扩展。** 后续处理器可重新加入原文;扩展有本机权限,插件加载失败时 Pi 可能继续运行,请先检查状态再输入秘密。
|
|
521
|
-
- 本地会话、终端、导出和工具写出的文件可能包含明文,本项目不加密或清理它们。
|
|
504
|
+
- **只跑一个测试文件:** `npx tsx --test test/guard-runtime.test.ts`。
|
|
505
|
+
- **把问题复现成测试,而不是打到真实网关。** `test/integration.test.ts` 驱动 Pi 真实的加载器与执行器,并对实际出站内容做断言;复制其中一个用例并换成虚构值即可。绝不要用真实秘密或真实提供商测试。
|
|
506
|
+
- **确认到底发出了什么。** 审计日志记录每一对原值/假值,`/protecter status` 会重新展示最近一次拦截及其错误码与阶段。诊断有意不含内容,因此规则序号就是指回你配置的线索。
|
|
507
|
+
- **无钩子路径**由 `test/hookless-paths.test.ts`(经真实执行器验证压缩与 `/tree`)和 `test/guard-runtime.test.ts`(模型运行时闸门,并直接对 Pi 自身的类做断言,使 Pi 重命名会导致测试失败而非静默丢掉覆盖)覆盖。
|
|
508
|
+
- **打包产物同样在测试内。** `npm test` 会构建真实的 npm tarball、解压,并用 Pi 的加载器加载 `dist/index.mjs` 及两个 worker,因此压缩与 worker 路径是被验证过的,而非假设。
|
|
522
509
|
|
|
523
|
-
|
|
510
|
+
### 扩大 Pi 兼容范围
|
|
524
511
|
|
|
525
|
-
|
|
512
|
+
`src/compat.ts` 会拒绝任何未经验证的 Pi 版本,因为此处判断失误意味着发送明文,而不仅是功能失效。要新增一个版本,请安装它并在其上运行完整测试:
|
|
526
513
|
|
|
527
514
|
```bash
|
|
528
|
-
npm
|
|
515
|
+
npm i -D @earendil-works/pi-coding-agent@<版本>
|
|
529
516
|
npm run check
|
|
530
517
|
```
|
|
531
518
|
|
|
532
|
-
|
|
519
|
+
通过之后再更新 `supportsPi`、`SUPPORTED_PI` 与 `peerDependencies`,并保持运行时门禁与声明范围完全一致。
|
|
520
|
+
|
|
521
|
+
更多资料:双语与评审规范见 [`CONTRIBUTING.md`](CONTRIBUTING.md),打包见 [`docs/BUILDING.md`](docs/BUILDING.md),发布见 [`docs/RELEASING.md`](docs/RELEASING.md),威胁模型见 [`SECURITY.md`](SECURITY.md),版本变更见 [`CHANGELOG.md`](CHANGELOG.md)。
|
|
522
|
+
|
|
523
|
+
## 消息与语言
|
|
533
524
|
|
|
534
|
-
|
|
525
|
+
消息只使用一种语言,依次取自 `PI_PROTECTER_LANG`、`LC_ALL`、`LC_MESSAGES`、`LANG`、`LANGUAGE`,最后是运行时语言。`zh*` 选择简体中文,其余为英文。错误码与数字与语言无关。
|
|
535
526
|
|
|
536
|
-
|
|
527
|
+
本项目基于 MIT 许可。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-information-protecter",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Sensitive Personal Information (SPI) Protecter — local reversible privacy redaction for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
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"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-coding-agent": "
|
|
46
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
47
47
|
"@types/node": "^22.0.0",
|
|
48
48
|
"esbuild": "0.28.2",
|
|
49
49
|
"tsx": "^4.20.0",
|