@zxh126/pi-redact-secrets 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - Redacts `input`, `tool_result` (including `structuredContent`) and `message_end`
8
+ before content reaches the model or the session file.
9
+ - Standard rule set: PEM blocks, `Authorization` headers, OpenAI/GitHub/npm/AWS
10
+ tokens, JWTs, DSN passwords, URL userinfo, JSON secret fields, and
11
+ `KEY=value` assignments (prefixes and full-width colons included).
12
+ - Optional `strict` profile with a high-entropy rule.
13
+ - Literal secrets from `secrets`, `secretFiles` (dotenv/TOML/YAML/JSON) and an
14
+ external `secretCommand`.
15
+ - `/redact`, `/redact reload`, `/redact init` commands.
16
+ - Idempotent replacements, per-rule hit counts, and a value-free log file.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 harry zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,269 @@
1
- # Temporary Holding Version
1
+ # pi-redact-secrets
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** · [简体中文](./README.zh-CN.md)
4
+
5
+ Redact secrets **before** they reach the model or the session file.
6
+
7
+ A [pi](https://pi.dev) extension that hooks the three places where text enters a
8
+ conversation and masks credentials in flight:
9
+
10
+ | Hook | Catches |
11
+ |---|---|
12
+ | `input` | Anything you type or paste — including messages relayed from Telegram/QQ/Slack bridges |
13
+ | `tool_result` | Output of every tool: `bash`, `pwsh`, `read`, `grep`, MCP servers, codemode scripts |
14
+ | `message_end` | The model's own output, so it cannot echo a secret back into the transcript |
15
+
16
+ Because redaction happens on the way in, the secret never reaches the model
17
+ context and never lands in `~/.pi/agent/sessions/*.jsonl`.
18
+
19
+ ```
20
+ you / tool ──▶ [ redact ] ──▶ model context ──▶ session file
21
+ │
22
+ └─ patterns (known shapes) + literals (your exact values)
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pi install npm:@zxh126/pi-redact-secrets
29
+ ```
30
+
31
+ Try it without changing your settings:
32
+
33
+ ```bash
34
+ pi -e npm:@zxh126/pi-redact-secrets
35
+ ```
36
+
37
+ Then start pi (or `/reload` a running session) and check the status:
38
+
39
+ ```
40
+ /redact
41
+ ```
42
+
43
+ ## Configure
44
+
45
+ Everything is optional. Without a config file you get the standard rule set and no
46
+ literal secrets.
47
+
48
+ ```bash
49
+ /redact init # writes ~/.pi/agent/redact-secrets.json with the defaults
50
+ ```
51
+
52
+ ```jsonc
53
+ // ~/.pi/agent/redact-secrets.json
54
+ {
55
+ "redactInput": true,
56
+ "redactToolResult": true,
57
+ "redactMessage": true,
58
+ "profile": "standard", // "standard" | "strict"
59
+ "secrets": ["my-exact-password"],
60
+ "secretFiles": ["~/.env", "~/.pi/agent/dbhub/mysql.toml"],
61
+ "secretCommand": {
62
+ "command": "pwsh",
63
+ "args": ["-NoProfile", "-File", "~/.pi/agent/dump-secrets.ps1"],
64
+ "timeoutMs": 8000
65
+ },
66
+ "minSecretLength": 8,
67
+ "logFile": "~/.pi/agent/redact-secrets.log"
68
+ }
69
+ ```
70
+
71
+ Set `PI_REDACT_SECRETS_CONFIG` to use a different config path (handy for projects).
72
+
73
+ | Key | Default | Meaning |
74
+ |---|---|---|
75
+ | `redactInput` | `true` | Redact user input. Turn off if you sometimes need to hand a real secret to the model on purpose. |
76
+ | `redactToolResult` | `true` | Redact tool output. |
77
+ | `redactMessage` | `true` | Redact finalized messages. |
78
+ | `profile` | `"standard"` | `strict` adds a high-entropy rule for random strings that match no known shape. Noisier because base64 blobs match too. |
79
+ | `secrets` | `[]` | Literal values to replace verbatim. |
80
+ | `secretFiles` | `[]` | Files to mine for literals: `.env`/TOML/YAML `KEY=value` lines with secret-looking keys, `scheme://user:pass@` DSNs, and `.json` files with secret-looking keys. |
81
+ | `secretCommand` | — | Executable whose stdout is a JSON array of literal strings, or one secret per line. Runs without a shell, with a timeout. |
82
+ | `minSecretLength` | `8` | Shorter literals are ignored — they would corrupt ordinary text. |
83
+ | `logFile` | `~/.pi/agent/redact-secrets.log` | One line per redaction: counts per rule, never the values. |
84
+
85
+ ### Why literals matter
86
+
87
+ Patterns only catch *recognisable* secrets (`sk-…`, `ghp_…`, PEM blocks, DSNs,
88
+ `password=…`). A database password like `264613@Ms` looks like ordinary text and
89
+ would slip through. Literals close that gap: point `secretFiles` at the config
90
+ files your machine already uses, or have `secretCommand` produce the values at
91
+ session start.
92
+
93
+ The extension ships **no cryptography of its own**. It reads plaintext once at
94
+ session start and keeps it in memory, so encryption is the job of the secret
95
+ store you already trust — one per platform:
96
+
97
+ | Platform | Store | `secret-tool`-style command |
98
+ |---|---|---|
99
+ | Windows | DPAPI (`CryptProtectData`) | `pwsh -File dump-secrets.ps1` |
100
+ | macOS | Keychain | `security find-generic-password -s pi-secrets -w` |
101
+ | Linux (desktop) | Secret Service (GNOME Keyring / KWallet) | `secret-tool lookup service pi-secrets` |
102
+ | Linux (server) | `0600` file, or `systemd-creds` on systemd ≥ 250 | `cat ~/.pi-secrets` / `systemd-creds decrypt x.cred -` |
103
+
104
+ Two constraints matter when you pick one, because `secretCommand` runs without a
105
+ TTY (so nothing can prompt for a passphrase at session start):
106
+
107
+ - **Must be unlocked without a prompt.** DPAPI, Keychain and Secret Service all
108
+ qualify: the OS already holds the key from your login. `gpg`, `age` and `pass`
109
+ do **not** — they ask for a passphrase through pinentry, which fails from a
110
+ background command unless an agent is already unlocked. If you want them
111
+ anyway, use `--pinentry-mode loopback --passphrase-file`, or a wrapper that
112
+ pulls the passphrase from a keyring.
113
+ - **Durability.** User-scope DPAPI blobs are bound to the user profile. Windows
114
+ re-encrypts the master keys when you change your *own* password, but an
115
+ **administrator password reset** (or a profile rebuild) leaves them
116
+ undecryptable — the secrets are gone, and Windows Credential Manager entries
117
+ (git credentials, RDP passwords) go with them. Keep a password reset disk, or
118
+ put long-lived secrets somewhere whose recovery does not depend on the account
119
+ (password manager, hardware token, `systemd-creds` with TPM). This is the same
120
+ trade-off any login-bound key store makes, including Keychain and Secret
121
+ Service — DPAPI is not special here, it is just the one that fails loudest.
122
+
123
+ Each of those is one executable that prints the values, so `secretCommand` stays
124
+ platform-neutral:
125
+
126
+ ```jsonc
127
+ // Windows — DPAPI blobs, decrypted by PowerShell
128
+ { "secretCommand": { "command": "pwsh", "args": ["-NoProfile", "-File", "~/dump-secrets.ps1"] } }
129
+
130
+ // macOS — one Keychain item per line
131
+ { "secretCommand": { "command": "security", "args": ["find-generic-password", "-s", "pi-secrets", "-w"] } }
132
+
133
+ // Linux — Secret Service
134
+ { "secretCommand": { "command": "secret-tool", "args": ["lookup", "service", "pi-secrets"] } }
135
+
136
+ // Linux without a keyring — a file only your account can read
137
+ { "secretCommand": { "command": "cat", "args": ["/home/me/.pi-secrets"] } }
138
+ ```
139
+
140
+ A command may print either a JSON array of strings or plain lines (one secret per
141
+ line). Shell pipelines need an explicit shell, because `secretCommand` never uses
142
+ one: `{ "command": "sh", "args": ["-c", "pass show pi/secrets"] }`.
143
+
144
+ A Windows DPAPI example (`dump-secrets.ps1`) that prints a JSON array:
145
+
146
+ ```powershell
147
+ $list = [System.Collections.Generic.List[string]]::new()
148
+ function AddV($v) {
149
+ if (-not $v) { return }
150
+ $s = [string]$v
151
+ if ($s.Length -ge 8 -and $s -notmatch '^\d+$') { if (-not $list.Contains($s)) { $list.Add($s) } }
152
+ }
153
+ # … read your own config files or decrypt DPAPI blobs here …
154
+ AddV (ConvertTo-SecureString (Get-Content "$HOME\.secret.dpapi" -Raw) |
155
+ ForEach-Object { [Runtime.InteropServices.Marshal]::PtrToStringBSTR(
156
+ [Runtime.InteropServices.Marshal]::SecureStringToBSTR($_)) })
157
+ ConvertTo-Json -InputObject @($list) -Compress
158
+ ```
159
+
160
+ On Linux, the equivalent without a keyring is a `chmod 600` file; if you need it
161
+ encrypted and prompt-free, use `systemd-creds` (systemd ≥ 250) or a TPM-backed
162
+ store:
163
+
164
+ ```bash
165
+ printf '%s\n' "$DB_PASSWORD" "$SMTP_CODE" > ~/.pi-secrets
166
+ chmod 600 ~/.pi-secrets
167
+ # secretCommand = { command = "cat", args = ["/home/me/.pi-secrets"] }
168
+ ```
169
+
170
+ `secretFiles` is the zero-setup option: it works with a `chmod 600` file you
171
+ already have (`.env`, `~/.pgpass`-style configs, `dbhub/mysql.toml`). It gives you
172
+ no encryption, only whatever filesystem permissions you set — fine for a
173
+ single-user laptop, not enough for a shared host.
174
+
175
+ ## Rules
176
+
177
+ | Rule | Example input | Output |
178
+ |---|---|---|
179
+ | `pem` | `-----BEGIN OPENSSH PRIVATE KEY-----…` | `<REDACTED-PEM>` |
180
+ | `auth-header` | `Authorization: Bearer abcdef123456` | `Authorization: Bearer <REDACTED>` |
181
+ | `openai-key` | `sk-proj-abcdefghijklmnop` | `sk-<REDACTED>` |
182
+ | `github-token` | `ghp_abcdefghijklmnopqrst` | `<REDACTED-GH>` |
183
+ | `npm-token` | `npm_abcdefghijklmnopqrstuvwxyz` | `npm_<REDACTED>` |
184
+ | `aws-access-key` | `AKIAIOSFODNN7EXAMPLE` | `AKIA<REDACTED>` |
185
+ | `jwt` | `eyJhbGciOi….eyJzdWIi….s3cr3t` | `<REDACTED-JWT>` |
186
+ | `dsn` | `mysql://root:pa55word@host/db` | `mysql://root:***@host/db` |
187
+ | `url-userinfo` | `https://user:pass@host` | `https://***:***@host` |
188
+ | `json-secret` | `{"appSecret": "eFh2DD4o…"}` | `{"appSecret": "<REDACTED>"}` |
189
+ | `key-value` | `SMTP_PASS=…`, `db_password: …`, `密码:…` | `SMTP_PASS=<REDACTED>` |
190
+ | `high-entropy` (strict) | long mixed-case random string | `<REDACTED-HIGH-ENTROPY>` |
191
+
192
+ Rules are **idempotent**: already-masked values (`***`, `<REDACTED>`) are not
193
+ matched again, so text is not rewritten twice when a session is resumed.
194
+
195
+ ## Commands
196
+
197
+ | Command | Effect |
198
+ |---|---|
199
+ | `/redact` | Rule count, literal count and sources, toggles, config and log paths |
200
+ | `/redact reload` | Re-read the config file and re-run `secretCommand` |
201
+ | `/redact init` | Write a sample config (never overwrites) |
202
+
203
+ ## Platform support
204
+
205
+ The extension is plain Node.js: no native modules, no shell-outs, no platform branches.
206
+
207
+ | | Status |
208
+ |---|---|
209
+ | Windows 10/11 | Verified (Node 24, PowerShell 7 + pi 1.4) |
210
+ | macOS | Expected to work — same code paths as Linux |
211
+ | Linux (x64/arm64) | Expected to work; `ubuntu-latest` × Node 22/24 in CI |
212
+
213
+ - **Node ≥ 22.18** is required, because the package ships TypeScript sources and
214
+ relies on Node's native type stripping (pi itself already requires Node 22+).
215
+ - Paths use `path.join`/`os.homedir()`; `~` and `~/` are expanded in config values.
216
+ No drive letters or Windows-only APIs anywhere.
217
+ - `secretCommand` runs through `execFile` **without a shell**, so the command and
218
+ its arguments are platform-native (`pwsh` on Windows, `sh`/`node`/`python3` on
219
+ Linux). Nothing is hardcoded.
220
+ - The log file is created with mode `0600` on POSIX (ignored on Windows).
221
+
222
+ ## What it does not do
223
+
224
+ - **Tool arguments are not redacted.** Rewriting `bash`/`pwsh` arguments would
225
+ change what actually runs, so `pwsh -Command "login --password hunter2"` keeps the
226
+ secret in the tool call. Put secrets in environment variables or files instead.
227
+ - **`tool_result.details` is left alone** — UI renderers depend on it.
228
+ - Redaction is best-effort, pattern-based. It reduces exposure; it is not a
229
+ guarantee. Anything that never leaves the machine is still the safer design.
230
+ - Secrets already written to a session file stay there. Use a one-off pass over
231
+ `~/.pi/agent/sessions/**/*.jsonl` if you need to clean history.
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ git clone <repo> && cd pi-redact-secrets
237
+ npm test # node --test, no build step needed (Node ≥ 22.18 strips types)
238
+ npm run check # tsc --noEmit
239
+ pi -e . # load the package from this directory
240
+ ```
241
+
242
+ CI runs `npm test` and `npm run check` on `ubuntu-latest` with Node 22 and 24.
243
+ `test/extension.test.ts` drives the extension through a fake `ExtensionAPI`, so the
244
+ three hooks, config loading, literal collection and the log file are covered without
245
+ starting pi.
246
+
247
+ Layout:
248
+
249
+ ```
250
+ src/index.ts pi wiring: hooks + /redact command
251
+ src/rules.ts pattern rules and replacement templates
252
+ src/redactor.ts pure redaction engine (unit-tested, no I/O)
253
+ src/literals.ts literal-secret collection (config, files, command)
254
+ src/config.ts config schema, defaults and validation
255
+ ```
256
+
257
+ The engine has no pi dependency, so it can be embedded elsewhere:
258
+
259
+ ```ts
260
+ import { Redactor } from "@zxh126/pi-redact-secrets/src/redactor.ts";
261
+
262
+ const redactor = new Redactor({ profile: "strict" });
263
+ redactor.setSecrets(["my-exact-password"]);
264
+ redactor.redact("password=my-exact-password"); // { text: "password=<REDACTED>", hits: 1, byRule: {…} }
265
+ ```
266
+
267
+ ## License
268
+
269
+ MIT
@@ -0,0 +1,250 @@
1
+ # pi-redact-secrets
2
+
3
+ [English](./README.md) · **简体中文**
4
+
5
+ 在内容**进入模型上下文或写入会话文件之前**把敏感串替换掉。
6
+
7
+ 一个 [pi](https://pi.dev) 扩展,挂在文本进入对话的三条路径上:
8
+
9
+ | 钩子 | 覆盖范围 |
10
+ |---|---|
11
+ | `input` | 你输入/粘贴的内容——包括从 QQ、Telegram、Slack 等桥接进来的消息 |
12
+ | `tool_result` | 所有工具输出:`bash`、`pwsh`、`read`、`grep`、MCP、codemode 脚本 |
13
+ | `message_end` | 模型自己的输出,避免它把密钥复述回对话里 |
14
+
15
+ 因为脱敏发生在入口处,密钥不会进入模型上下文,也不会写进
16
+ `~/.pi/agent/sessions/*.jsonl`。
17
+
18
+ ```
19
+ 你 / 工具 ──▶ [ 脱敏 ] ──▶ 模型上下文 ──▶ 会话文件
20
+ │
21
+ └─ 规则(已知形状) + 字面量(你机器上的真实值)
22
+ ```
23
+
24
+ ## 安装
25
+
26
+ ```bash
27
+ pi install npm:@zxh126/pi-redact-secrets
28
+ ```
29
+
30
+ 只想临时试用、不动配置:
31
+
32
+ ```bash
33
+ pi -e npm:@zxh126/pi-redact-secrets
34
+ ```
35
+
36
+ 然后在 pi 里 `/reload`(或新开),查看状态:
37
+
38
+ ```
39
+ /redact
40
+ ```
41
+
42
+ ## 配置
43
+
44
+ 全部可选。没有配置文件时使用标准规则集、没有字面量。
45
+
46
+ ```bash
47
+ /redact init # 生成 ~/.pi/agent/redact-secrets.json
48
+ ```
49
+
50
+ ```jsonc
51
+ {
52
+ "redactInput": true,
53
+ "redactToolResult": true,
54
+ "redactMessage": true,
55
+ "profile": "standard", // "standard" | "strict"
56
+ "secrets": ["我的精确口令"],
57
+ "secretFiles": ["~/.env", "~/.pi/agent/dbhub/mysql.toml"],
58
+ "secretCommand": {
59
+ "command": "pwsh",
60
+ "args": ["-NoProfile", "-File", "~/.pi/agent/dump-secrets.ps1"],
61
+ "timeoutMs": 8000
62
+ },
63
+ "minSecretLength": 8,
64
+ "logFile": "~/.pi/agent/redact-secrets.log"
65
+ }
66
+ ```
67
+
68
+ 环境变量 `PI_REDACT_SECRETS_CONFIG` 可以指定别的配置路径。
69
+
70
+ | 键 | 默认 | 说明 |
71
+ |---|---|---|
72
+ | `redactInput` | `true` | 是否脱敏用户输入。若有时确实要把口令交给模型,可关掉 |
73
+ | `redactToolResult` | `true` | 是否脱敏工具输出 |
74
+ | `redactMessage` | `true` | 是否脱敏最终消息 |
75
+ | `profile` | `"standard"` | `strict` 额外启用高熵规则,能抓未知形状的随机串,但 base64 也会被误伤 |
76
+ | `secrets` | `[]` | 逐字替换的字面量 |
77
+ | `secretFiles` | `[]` | 从文件提取字面量:`.env`/TOML/YAML 里 key 名像密钥的 `KEY=value`、`scheme://user:pass@` 形式的 DSN、以及含密钥字段的 `.json` |
78
+ | `secretCommand` | — | 执行该程序,stdout 输出 JSON 字符串数组或每行一个值作为字面量;不经 shell,有超时 |
79
+ | `minSecretLength` | `8` | 太短的字面量会被忽略(否则会误伤普通文本) |
80
+ | `logFile` | `~/.pi/agent/redact-secrets.log` | 每次脱敏记一行:按规则的命中数,**不含值** |
81
+
82
+ ### 为什么需要字面量
83
+
84
+ 规则只能抓「有特征的」密钥(`sk-…`、`ghp_…`、PEM 块、DSN、`password=…`)。
85
+ 像 `264613@Ms` 这种数据库口令在文本里看起来就是普通词,规则抓不到。
86
+ 字面量补这个缺口:把 `secretFiles` 指向你机器上已有的配置文件,或用
87
+ `secretCommand` 在会话启动时现算。
88
+
89
+ 扩展本体**不带任何加密实现**:它只在会话启动时读一次明文并保存在内存里。加密交给你
90
+ 本来就信任的那个密钥库,按平台来:
91
+
92
+ | 平台 | 密钥库 | 对应命令 |
93
+ |---|---|---|
94
+ | Windows | DPAPI(`CryptProtectData`) | `pwsh -File dump-secrets.ps1` |
95
+ | macOS | 钥匙串 | `security find-generic-password -s pi-secrets -w` |
96
+ | Linux(桌面) | Secret Service(GNOME Keyring / KWallet) | `secret-tool lookup service pi-secrets` |
97
+ | Linux(服务器) | `0600` 文件,或 systemd ≥ 250 上的 `systemd-creds` | `cat ~/.pi-secrets` / `systemd-creds decrypt x.cred -` |
98
+
99
+ 选哪个之前得知道两条限制,因为 `secretCommand` 是**没有 TTY** 的后台执行:
100
+
101
+ - **必须能无提示解锁**。DPAPI、钥匙串、Secret Service 都满足:系统在登录时就已经拿到了密钥。
102
+ `gpg`、`age`、`pass` **不**满足:它们通过 pinentry 要口令,后台命令直接失败
103
+ (除非 agent 已解锁)。真想用它们就写 `--pinentry-mode loopback --passphrase-file`,
104
+ 或者包一层从密钥环取口令的脚本。
105
+ - **耐久性**。用户级 DPAPI 密文绑在用户配置上。你**自己改密码**时 Windows 会重新包装主密钥,
106
+ 但**管理员重置密码**(或重建用户配置)后主密钥就解不开了——密钥数据直接报废,
107
+ Windows 凭据管理器里的东西(git 凭据、RDP 密码)一起没。要么做一张密码重设盘,
108
+ 要么把长期密钥放到不依赖账户的地方(密码管理器、硬件令牌、带 TPM 的 `systemd-creds`)。
109
+ 这其实是所有「绑定登录态」的密钥库共有的问题,钥匙串和 Secret Service 也一样——
110
+ 只是 DPAPI 这条翻车声音最大。
111
+
112
+ 每个都只是一个打印值的可执行文件,所以 `secretCommand` 本身保持平台无关:
113
+
114
+ ```jsonc
115
+ // Windows:DPAPI blob,由 PowerShell 解密
116
+ { "secretCommand": { "command": "pwsh", "args": ["-NoProfile", "-File", "~/dump-secrets.ps1"] } }
117
+
118
+ // macOS:钥匙串里一条,每行一个值
119
+ { "secretCommand": { "command": "security", "args": ["find-generic-password", "-s", "pi-secrets", "-w"] } }
120
+
121
+ // Linux:Secret Service
122
+ { "secretCommand": { "command": "secret-tool", "args": ["lookup", "service", "pi-secrets"] } }
123
+
124
+ // Linux 没有密钥环:一个只有你账户能读的文件
125
+ { "secretCommand": { "command": "cat", "args": ["/home/me/.pi-secrets"] } }
126
+ ```
127
+
128
+ 命令可以输出 **JSON 字符串数组**,也可以输出**纯文本、每行一个**(`secret-tool`、
129
+ `security`、`pass`、`gpg` 都是这种形式)。需要管道就用显式 shell,因为
130
+ `secretCommand` 从不经 shell:`{ "command": "sh", "args": ["-c", "pass show pi/secrets"] }`。
131
+
132
+ Windows DPAPI 示例(`dump-secrets.ps1`,输出 JSON 数组):
133
+
134
+ ```powershell
135
+ $list = [System.Collections.Generic.List[string]]::new()
136
+ function AddV($v) {
137
+ if (-not $v) { return }
138
+ $s = [string]$v
139
+ if ($s.Length -ge 8 -and $s -notmatch '^\d+$') { if (-not $list.Contains($s)) { $list.Add($s) } }
140
+ }
141
+ # …在这里读你自己的配置文件或解密 DPAPI blob…
142
+ AddV (ConvertTo-SecureString (Get-Content "$HOME\.secret.dpapi" -Raw) |
143
+ ForEach-Object { [Runtime.InteropServices.Marshal]::PtrToStringBSTR(
144
+ [Runtime.InteropServices.Marshal]::SecureStringToBSTR($_)) })
145
+ ConvertTo-Json -InputObject @($list) -Compress
146
+ ```
147
+
148
+ Linux 上无密钥环时的等价做法是一个 `chmod 600` 文件;要加密又不能提示,
149
+ 就用 `systemd-creds`(systemd ≥ 250)或 TPM 支持的存储:
150
+
151
+ ```bash
152
+ printf '%s\n' "$DB_PASSWORD" "$SMTP_CODE" > ~/.pi-secrets
153
+ chmod 600 ~/.pi-secrets
154
+ # secretCommand = { command = "cat", args = ["/home/me/.pi-secrets"] }
155
+ ```
156
+
157
+ `secretFiles` 是零配置选项:直接用你已经有的 `chmod 600` 文件(`.env`、类 `~/.pgpass`
158
+ 的配置、`dbhub/mysql.toml`)。它不提供加密,只有文件系统权限——单用户笔记本尚可,
159
+ 共享主机不够。
160
+
161
+ ## 规则
162
+
163
+ | 规则 | 输入示例 | 输出 |
164
+ |---|---|---|
165
+ | `pem` | `-----BEGIN OPENSSH PRIVATE KEY-----…` | `<REDACTED-PEM>` |
166
+ | `auth-header` | `Authorization: Bearer abcdef123456` | `Authorization: Bearer <REDACTED>` |
167
+ | `openai-key` | `sk-proj-abcdefghijklmnop` | `sk-<REDACTED>` |
168
+ | `github-token` | `ghp_abcdefghijklmnopqrst` | `<REDACTED-GH>` |
169
+ | `npm-token` | `npm_abcdefghijklmnopqrstuvwxyz` | `npm_<REDACTED>` |
170
+ | `aws-access-key` | `AKIAIOSFODNN7EXAMPLE` | `AKIA<REDACTED>` |
171
+ | `jwt` | `eyJhbGciOi….eyJzdWIi….s3cr3t` | `<REDACTED-JWT>` |
172
+ | `dsn` | `mysql://root:pa55word@host/db` | `mysql://root:***@host/db` |
173
+ | `url-userinfo` | `https://user:pass@host` | `https://***:***@host` |
174
+ | `json-secret` | `{"appSecret": "eFh2DD4o…"}` | `{"appSecret": "<REDACTED>"}` |
175
+ | `key-value` | `SMTP_PASS=…`、`db_password: …`、`密码:…` | `SMTP_PASS=<REDACTED>` |
176
+ | `high-entropy`(strict) | 长随机串 | `<REDACTED-HIGH-ENTROPY>` |
177
+
178
+ 规则是**幂等**的:已脱敏的值(`***`、`<REDACTED>`)不会被二次匹配,恢复会话时
179
+ 不会出现占位符套占位符。
180
+
181
+ ## 命令
182
+
183
+ | 命令 | 作用 |
184
+ |---|---|
185
+ | `/redact` | 规则数、字面量数量与来源、开关、配置与日志路径 |
186
+ | `/redact reload` | 重新读取配置并重跑 `secretCommand` |
187
+ | `/redact init` | 写一份示例配置(不会覆盖已有文件) |
188
+
189
+ ## 跨平台
190
+
191
+ 纯 Node 实现:没有原生模块、没有 shell 调用、没有平台分支。
192
+
193
+ | 平台 | 状态 |
194
+ |---|---|
195
+ | Windows 10/11 | 已验证:Node 24、PowerShell 7、pi 1.4 |
196
+ | Linux (x64) | 已验证:Ubuntu 20.04 + Node 22.23.4(28 个测试,含钩子接线测试) |
197
+ | macOS / arm64 | 预期可用——与 Linux 同一套代码路径 |
198
+
199
+ - **需要 Node ≥ 22.18**:包直接发布 TypeScript 源码,依赖 Node 原生类型剥离(pi 本身也要求 Node 22+)。
200
+ - 路径一律走 `path.join` / `os.homedir()`,配置值里的 `~`、`~/` 会被展开;代码里没有任何盘符或 Windows 专有 API。
201
+ - `secretCommand` 通过 `execFile` 执行,**不经 shell**,命令与参数由你按平台填写(Windows 用 `pwsh`,Linux 用 `sh`/`node`/`python3`),没有任何写死的可执行文件。
202
+ - 日志文件在 POSIX 上以 `0600` 权限创建(Windows 忽略该模式)。
203
+
204
+ ## 它不做什么
205
+
206
+ - **不脱敏工具参数**。改写 `bash`/`pwsh` 的参数等于改变实际执行的命令,所以
207
+ `pwsh -Command "login --password hunter2"` 里的口令仍会留在工具调用里。
208
+ 请改用环境变量或文件传参。
209
+ - **不动 `tool_result.details`**,TUI 渲染依赖它。
210
+ - 脱敏是「尽力而为 + 基于模式」的:它降低暴露面,不是保证。最安全的做法仍然是
211
+ 让敏感数据不离开本机。
212
+ - 已经写进会话文件的历史不会自动清理。需要的话对
213
+ `~/.pi/agent/sessions/**/*.jsonl` 做一次离线处理。
214
+
215
+ ## 开发
216
+
217
+ ```bash
218
+ npm test # node --test,无需构建(Node ≥ 22.18 原生剥离类型)
219
+ npm run check # tsc --noEmit
220
+ pi -e . # 从本目录加载包
221
+ ```
222
+
223
+ CI 在 `ubuntu-latest` 上用 Node 22 和 24 跑 `npm test` 与 `npm run check`。
224
+ `test/extension.test.ts` 用一个假的 `ExtensionAPI` 驱动扩展,不用启动 pi 就能覆盖三个钩子、
225
+ 配置读取、字面量收集和日志文件。
226
+
227
+ 结构:
228
+
229
+ ```
230
+ src/index.ts pi 接线:三个钩子 + /redact 命令
231
+ src/rules.ts 规则与替换模板
232
+ src/redactor.ts 纯脱敏引擎(有单测、无 I/O)
233
+ src/literals.ts 字面量收集(配置、文件、命令)
234
+ src/config.ts 配置结构、默认值与校验
235
+ ```
236
+
237
+ 引擎不依赖 pi,可以单独使用:
238
+
239
+ ```ts
240
+ import { Redactor } from "@zxh126/pi-redact-secrets/src/redactor.ts";
241
+
242
+ const redactor = new Redactor({ profile: "strict" });
243
+ redactor.setSecrets(["我的精确口令"]);
244
+ redactor.redact("password=我的精确口令");
245
+ // { text: "password=<REDACTED>", hits: 1, byRule: {…} }
246
+ ```
247
+
248
+ ## 许可
249
+
250
+ MIT
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@zxh126/pi-redact-secrets",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Redact secrets from pi tool output, user input and messages before they reach the model or the session file.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "pi",
9
+ "redaction",
10
+ "secrets",
11
+ "security",
12
+ "privacy"
13
+ ],
14
+ "type": "module",
15
+ "license": "MIT",
16
+ "author": "harry zhang <zxh_126@qq.com>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://gitee.com/zxh126/pi-redact-secrets.git"
20
+ },
21
+ "homepage": "https://gitee.com/zxh126/pi-redact-secrets#readme",
22
+ "bugs": {
23
+ "url": "https://gitee.com/zxh126/pi-redact-secrets/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.18.0"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "README.md",
31
+ "README.zh-CN.md",
32
+ "CHANGELOG.md",
33
+ "LICENSE"
34
+ ],
35
+ "pi": {
36
+ "extensions": [
37
+ "./src/index.ts"
38
+ ]
39
+ },
40
+ "scripts": {
41
+ "test": "node --test test/*.test.ts",
42
+ "test:watch": "node --test --watch test/*.test.ts",
43
+ "check": "tsc --noEmit",
44
+ "pack:check": "npm pack --dry-run"
45
+ },
46
+ "peerDependencies": {
47
+ "@earendil-works/pi-coding-agent": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-coding-agent": "^1.4.0",
51
+ "@types/node": "^22.10.0",
52
+ "typescript": "^5.7.0"
53
+ }
54
+ }