@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 +16 -0
- package/LICENSE +21 -0
- package/README.md +268 -2
- package/README.zh-CN.md +250 -0
- package/package.json +52 -4
- package/src/config.ts +184 -0
- package/src/index.ts +230 -0
- package/src/literals.ts +177 -0
- package/src/redactor.ts +141 -0
- package/src/rules.ts +108 -0
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
|
-
#
|
|
1
|
+
# pi-redact-secrets
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/README.zh-CN.md
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|