# Pi Sentinel v1.0.0 — 技术方案设计文档（正式发布版）

> 版本：v1.0.0（自 v0.4.x 经十一轮实战修订收敛）
> 状态：正式发布
> 前置文档：DESIGN_v0.0.1 → v0.4.0 演进史见仓库 `DESIGN_*.md` 系列

---

## 目录

1. [背景与问题](#1-背景与问题)
2. [设计目标](#2-设计目标)
3. [核心不变量](#3-核心不变量)
4. [总体架构](#4-总体架构)
5. [数据契约](#5-数据契约)
6. [数据流](#6-数据流)
7. [检测引擎](#7-检测引擎)
8. [策略引擎](#8-策略引擎)
9. [派生引擎](#9-派生引擎)
10. [Secret Store 与加密](#10-secret-store-与加密)
11. [落盘兜底：RedactChannel](#11-落盘兜底redactchannel)
12. [边界适配器](#12-边界适配器)
13. [用户命令](#13-用户命令)
14. [TUI 面板设计](#14-tui-面板设计)
15. [配置参考](#15-配置参考)
16. [安全分析](#16-安全分析)
17. [性能预算](#17-性能预算)
18. [验收标准](#18-验收标准)
19. [目录结构](#19-目录结构)
20. [版本演进记录](#20-版本演进记录)

---

## 1. 背景与问题

### 1.1 问题陈述

AI 编码代理（pi coding agent）在真实工程使用中，敏感信息不可避免地流入对话上下文：

- **用户粘贴**：日志、配置片段、连接串、报错堆栈中含手机号/身份证/密钥
- **工具输出**：`db_query` 结果、`read` 生产配置文件、`bash` 执行输出
- **助手生成**：LLM 在回答中复述、拼接、变换这些敏感值

一旦明文进入会话历史（session.jsonl 落盘）与模型上下文，即产生两个层面的风险：

| 层面 | 风险 |
|---|---|
| 模型上下文 | 敏感值随每次请求发送给 LLM 供应商，脱离本机控制 |
| 会话历史 | 明文持久化到磁盘，随会话分享/归档/泄漏扩散 |

### 1.2 为什么现有方案不够

- **人工脱敏**：靠用户自觉，遗漏不可避免，且打断工作流
- **输出过滤**：只堵模型侧，用户输入与工具输出侧照样穿透
- **纯屏蔽（redact）**：破坏可用性——LLM 拿不到值就无法执行下游任务（查库、调接口）
- **v0.3 授权体系**（DisclosureScope/Capability/Grant）：session 维度授权在 history 污染等价，无安全价值，且复杂度爆炸

### 1.3 设计定位

Pi Sentinel 是 pi coding agent 的敏感信息防护扩展，定位三条铁律：

1. **LLM 视角永不见明文**（除非用户显式豁免）
2. **工具调用出向自动还原**——token 化不破坏下游任务可用性
3. **会话历史落盘零明文**——完整原值只存在于本机加密存储

---

## 2. 设计目标

### 2.1 功能目标

| 目标 | 说明 |
|---|---|
| 入向 token 化 | 用户输入/工具输出中的敏感值替换为可逆占位 token |
| 出向还原 | 工具调用参数中的 token 在发出前还原为明文（工具拿到真实值） |
| 派生展示 | token + 派生事实（如 `13900000001\|138****0000`），LLM 可见脱敏形态 |
| 落盘兜底 | 任何进入历史的明文在落盘通道再过滤一遍 |
| 策略可配 | 45 条内置检测规则（含 29 个厂商 key 子项）逐条可改动作/启禁/派生 |
| 自定义规则 | 正则/关键词两种匹配方式，规则级动作覆盖 |
| 全程可审计 | 事件总线记录全部脱敏/还原动作，含证据哈希 |

### 2.2 非目标（明确不做）

- 不做跨会话授权/授权体系（v0.3 教训：无安全价值）
- 不做 LLM 侧输出改写（assistant 文本由落盘兜底覆盖）
- 不防御用户主动泄漏（粘贴到别处、截图等）
- 派生值拼凑风险自负（用户授权后 LLM 从前缀+后缀拼凑原值不拦）

### 2.3 版本演进总览

```
v0.0.1  原型：单边界钩子 + 正则脱敏
v0.2.0  检测管线成型：三段式（终判/候选/裁判）+ 27 扩展检测器
v0.3.0  授权体系（后证伪废弃）
v0.4.0  重构：三职责收敛（tokenize/resolve/redact），删授权体系
v0.4.1  十一轮实战修订：TUI 三轮重构、算子通用化、子项粒度、注册表对齐
v1.0.0  正式发布：架构冻结、文档完整化、发布 npm/GitHub
```

---

## 3. 核心不变量

四条铁律贯穿所有模块，任何修订不得违反：

| # | 不变量 | 实现机制 |
|---|---|---|
| P1 | **完整原值永不落盘**（session.jsonl 无明文） | 入向 token 化 + RedactChannel 落盘兜底 |
| P2 | **LLM 视角永不见明文** | 入向替换发生在上下文构造之前；派生事实是准明文但非原值 |
| P3 | **原值可还原且只存本机加密存储** | SecretStore AES-256-GCM，密钥派生自设备密钥 |
| P4 | **边界适配器无 secret 业务逻辑** | 适配器只搬运事件，检测/策略/加密全部在 core |

派生事实的定位：`13900000001|138****0000` 中 `138****0000` 是**允许落盘的派生展示值**（前 3 后 4 星号遮蔽），原值仍在 store 可还原。引擎层禁用 `{value}` 类明文占位模板（六轮修订安全修复）。

---

## 4. 总体架构

### 4.1 三层结构

```
┌─────────────────────────────────────────────────────────┐
│  边界层（adapters/ + index.ts）                           │
│  B1 input · B5 tool_call · B2 tool_result · B3 message_end│
│  B6 user_bash · TUI projector · footer 状态栏              │
├─────────────────────────────────────────────────────────┤
│  引擎层（core/ + detectors/）                             │
│  DetectorEngine → PolicyEngine → Canonicalizer            │
│  TokenResolver · RedactChannel · DerivationEngine         │
│  SecretStore · EventBus · AuditLog                        │
├─────────────────────────────────────────────────────────┤
│  存储层                                                   │
│  ~/.pi/sentinel/store/*.jsonl（AES-256-GCM 加密）          │
│  ~/.pi/sentinel/config.json（用户策略，无明文）             │
│  ~/.pi/sentinel/audit/<session>.jsonl（审计日志）          │
└─────────────────────────────────────────────────────────┘
```

### 4.2 六引擎一适配器

| 模块 | 文件 | 职责 |
|---|---|---|
| DetectorEngine | `core/detector-engine.ts` | 文本 → Finding[]（管线调度、阈值闸门） |
| PolicyEngine | `core/policy-engine.ts` | Finding → Action 决策（类型默认/规则覆盖/floor） |
| Canonicalizer | `core/canonicalizer.ts` | Finding 执行替换（token/redact/derive 分派） |
| TokenResolver | `core/token-resolver.ts` | B5 出向：工具参数 token → 明文还原 |
| RedactChannel | `core/redact-channel.ts` | 落盘兜底：历史文本再过滤 |
| SecretStore | `core/secret-store.ts` | 原值加密存取（P3 唯一持有者） |
| DerivationEngine | `core/derivation-engine.ts` | 4 通用算子 + 双向推导（mask/regex 反推） |
| 边界适配器 | `adapters/v040-adapters.ts` | pi 钩子事件 ↔ 引擎调用（纯搬运） |

### 4.3 模块间数据流

```
input/tool_result 事件
   │
   ▼
DetectorEngine.scan ──→ Finding[]（type/span/confidence/detector id）
   │
   ▼
PolicyEngine.decide ──→ PolicyDecision（action + reason + derivation?）
   │
   ▼
Canonicalizer.canonicalize ──→ 替换后文本 + CanonicalOp[]
   │                              │
   ▼                              ▼
 pi 收到 token 化文本          EventBus.emit（审计）
```

---

## 5. 数据契约

### 5.1 类型系统

`SensitiveType`（16 种）：`phone | email | id_card | passport | bank_card | address | customer_id | internal_hostname | production_data | password | api_key | access_token | refresh_token | private_key | jwt | connection_string`

分为两大类：
- **PII**（可识别个人）：phone/email/id_card/passport/bank_card/address/customer_id...
- **Credential**（凭据）：password/api_key/access_token/refresh_token/private_key/jwt/connection_string

`Action`（4 + 1）：`tokenize | redact | derive | allow`（+ 自定义规则的 `default` = 跟随类型）

### 5.2 Token 形态

| 形态 | 含义 | 示例 |
|---|---|---|
| `<TYPE:p_NNN>` | 可逆 token（原值在 store） | `13900000001` |
| `<TYPE:r_NNN>` | 不可逆 token（原值不存） | `<API_KEY:r_003>` |
| `<TYPE:x_NNN>\|fact` | token + 派生事实 | `13900000001\|138****0000` |

字母语义：`p` = 可逆保留、`r` = redact 不可逆、`x` = 泛型（e=email/d=id_card 特例）、NNN 从 001 递增。

`TOKEN_FORM_RE`：管线跳过已是 token 形态的文本（防 token 嵌套 token）。

### 5.3 Finding / PolicyDecision / CanonicalOp

```ts
Finding      = { type, category, confidence, start, end, detector, evidenceHash, candidate }
PolicyDecision = { action, reason: "default:..."|"override:..."|"rule-override:...", derivation? }
CanonicalOp = { type, action, token?, redactToken?, derivationFact?, evidenceHash, source }
```

`evidenceHash`：原值的 SHA-256 短哈希（带 session nonce），审计可追溯不泄漏。

---

## 6. 数据流

### 6.1 入向（B1 input / B2 tool_result）

```
用户输入 "号码 151-0000-0000（虚构演示号）"
  → DetectorEngine.scan（user 来源，阈值 0.85）
  → regex:cn-phone 命中（conf 0.9 ≥ 0.85 终判）
  → PolicyEngine.decide → phone 默认 tokenize
  → Canonicalizer 替换 → "号码 13900000001"
  → 明文入 SecretStore（AES-256-GCM）
  → LLM 看到的请求不含明文 ✓
```

### 6.2 出向（B5 tool_call）

```
LLM 生成工具调用 { sql: "SELECT * FROM users WHERE phone = '13900000001'" }
  → TokenResolver.resolveToolArgs
  → token 查 store → 明文还原（原地改写 event.input）
  → db_query 收到真实手机号，任务可执行 ✓
  → 事件 bus 记录 resolve 审计（含 token，不含明文）
```

### 6.3 落盘（B3 message_end / B2 兜底）

```
会话消息即将写入 session.jsonl
  → RedactChannel 过滤文本
  → 已 token 化的值已是占位符（通过）
  → 残留明文（如 assistant 复述）→ 重新检测 + 替换
  → 落盘内容零完整明文 ✓（P1）
```

### 6.4 派生（derive 动作）

```
PolicyEngine 返回 { action: "derive", derivation: { op: "mask", args: "3:4" } }
  → DerivationEngine.apply("151-0000-0000", mask 3:4) = "138****0000"
  → 替换为 "13900000001|138****0000"
  → LLM 可见脱敏形态；原值仍入 store（B5 可还原）
  → 派生失败/无配置 → 降级 tokenize（决策 K7）
```

---

## 7. 检测引擎

### 7.1 三段式管线

```
fast pass（终判 + 候选生成）
  ├─ 终判 regex（自评即终，conf ≥ 0.75）
  │    cn-phone 0.9 / email 0.95 / private-key 0.99 / jwt 0.92
  │    known-token 29 子项 0.98 / v020 厂商 key 18 项 0.95 ...
  └─ 候选生成（conf < 0.75，需裁判确认）
       cn-id 0.5 / bank 0.4 / keyword 0.4 / entropy 0.6

slow pass（候选裁判，仅有候选时运行）
  validator:cn-id 0.99（校验位）/ validator:luhn 0.98（Luhn）
  shape:keyword-value 0.85（key:value 形态判定）

候选保留规则：候选必须被同 span 裁判确认，否则丢弃
```

### 7.2 阈值闸门（五道闸）

| 闸 | 位置 | 规则 |
|---|---|---|
| ① 来源强度 | fast 结果过滤 | credential ≥ 来源档 / PII ≥ 来源档 |
| ② 候选确认 | candidates | 同 span 有裁判结果才保留 |
| ③ 终判阈值 | fast 分类 | conf ≥ 0.75（FINALITY_THRESHOLD）进 finals |
| ④ 重叠消解 | merged | 同 span 取最高置信；跨 span 置信度降序贪心 |
| ⑤ 策略决策 | PolicyEngine | 类型默认 + 规则覆盖 + floor |

### 7.3 来源强度档位（SOURCE_PROFILES）

| 来源 | PII 门槛 | Credential 门槛 | 特性 |
|---|---|---|---|
| user（用户输入） | 0.85 | 0.85 | keyword 检测器启用 |
| toolResult | 0.75 | 0.75 | 全检测器 |
| assistant | 0.95 | 0.95 | 仅高置信（防误伤生成文本） |
| system | — | 0.95 | 仅 credential |

### 7.4 内置规则注册表（builtinRegistry）

`detectors/index.ts#builtinRegistry()` 从管线真实装配序列同源生成——**面板行数 = footer 规则数 = 45**，永不漂移（十一轮修订确立的单一事实来源）：

| 分组 | 数量 | 明细 |
|---|---|---|
| 基础 PII | 9 | cn-phone / email / cn-id / bank / passport / keyword / shape / validator×2 / entropy |
| 厂商 key（known-token 子项） | 11 | aws-access-key / aws-temp-key / github-pat·oauth·app / slack / google-api·oauth / openai-style / anthropic / gitlab |
| 厂商 key（v020） | 18 | openai-project / openrouter / groq / huggingface / replicate / xai / perplexity / github-fine / digitalocean / npm / pypi / docker / stripe / supabase / notion / linear / planetscale / discord |
| 编码嫌疑 | 2 | base64 / hex 整串（0.76，B3 专用审计） |
| 其他 | 5 | private-key / jwt / connstr / hex-digest 等 |

**候选/裁判精确配对**（SIBLING_PAIRS，十轮修订）：`regex:cn-id↔validator:cn-id`、`regex:bank↔validator:luhn`、`keyword:secret-assign↔shape:keyword-value`。配对内改动作需同步两段（dedupe 压制段的动作等于无操作）；独立检测器（含 29 个厂商 key 子项）改谁只影响谁。

---

## 8. 策略引擎

### 8.1 动作语义

| 动作 | 入向（LLM 看到） | 原值存储 | 出向还原 | 用途 |
|---|---|---|---|---|
| tokenize | `13900000001` | ✅ 加密 store | ✅ B5 还原 | 默认：脱敏且可用 |
| redact | `<API_KEY:r_001>` | ❌ | ❌ | 高危凭据（credential 默认） |
| derive | `13900000001\|138****0000` | ✅ | ✅ | 脱敏展示 + 可逆 |
| allow | 原文放行 | ❌ | — | 用户显式豁免 |
| default（仅自定义规则） | 跟随类型默认 | — | — | 不覆盖 |

### 8.2 决策优先级

```
规则级覆盖（ruleActions[detectorId]）        ← 最高
  > 类型默认（DEFAULT_POLICY[type] 或 policyOverrides[type]）
    > floor 保护（password/private_key ≥ redact）
```

floor：password/private_key 不可低于 redact（即使规则级覆盖也不行）。

allow 的特殊处理：**用户显式 override 的 allow 是豁免**（任何 mode 生效）；默认表只在 system-prompt/migration 模式允许 allow，其他来源降级 tokenize（防默认配置意外放行）。

### 8.3 覆盖传播语义

- **候选/裁判配对**：配对表内同步（见 §7.4）
- **独立检测器**：单规则粒度，互不影响
- **自定义规则**：`custom:<id>` 键，独立管理
- **类型级**（`/sentinel:policy set <type> <action>`）：作用于该类型全部检测器

---

## 9. 派生引擎

### 9.1 算子集（4 种，全部类型无关）

九轮修订确立：删除 domain/prefix_match/format 等类型绑定算子，个性化取值一律 regex_extract 表达。

| 算子 | 参数 | 作用 | 示例 |
|---|---|---|---|
| mask | `3:4` / `!6:4` / `#*#*` | 星号遮蔽（三语法） | `138****0000` |
| length | 无 | 长度 | `11` |
| hash | short/full | SHA-256 指纹 | `a3f5e2…`（12/64 位） |
| regex_extract | 捕获组正则 | 提取片段 | `@(.+)$` → `example.com` |

### 9.2 mask 三语法（八轮修订统一）

| 语法 | 语义 | 场景 |
|---|---|---|
| `3:4` | 留头 3 尾 4 遮中间（相对） | 手机号 `139****0002` |
| `!6:4` | 遮头 6 尾 4 留中段（相对） | 身份证 `******19850505****` |
| `#*#*` | 等长模板（# 留 / * 遮） | 任意交错 `2*0*81*9*…` |

### 9.3 双向推导（表单"期望输出自动设参"）

**inferMaskArgs**（原文 + 期望输出 → 参数）：
- 逐位校验非星号位一致 → 形态识别（中间星号→`h:t` / 两端星号→`!h:t` / 交错→模板）
- 相对语法优先，模板兜底

**inferRegexArgs**（原文 + 预期取值 → 正则）：
- 预期取值须为原文子串
- 按位置结构选最通用形态：`@(.+)$`（分隔符后取余）/ `^(.+?):`（前取段）/ `(.{4})$`（取尾）/ `^.{6}(.{8})`（定位中段）
- 非子串/全量取值 → null 拒绝

### 9.4 安全约束

- `format` 算子已删除；历史配置残留走 default 分支安全降级（null → tokenize）
- `{value}` 明文占位模板被引擎拒绝（六轮修订 P1 修复）
- 推导结果对样例回归验证后才落参

---

## 10. Secret Store 与加密

### 10.1 存储

```
~/.pi/sentinel/store/<sessionId>.jsonl
```

每行：`{ token, type, cipherText, iv, tag, evidenceHash }`——AES-256-GCM，密钥由设备密钥 + session nonce 派生（`core/crypto.ts`）。

### 10.2 生命周期

- **写入**：Canonicalizer 分配 token 时（同值复用已有 token，dedup by value+type）
- **读取**：B5 TokenResolver（出向还原）、/sentinel:query（用户查询）
- **恢复**：session_start 时 load，同 session 的 token 跨重启可用
- **清空**：/sentinel:reset（用户显式）

同值复用：检测到 store 中已有相同 type+value 直接复用 token（避免同值多 token）。

---

## 11. 落盘兜底：RedactChannel

### 11.1 职责

pi 框架无 `before_session_persist` 钩子，RedactChannel 挂在 B2/B3 钩子层做兜底：

- **B3 message_end**：assistant 消息返回 patch 前，对 message content 再过滤——检测残留明文（LLM 复述/拼接的值）并替换
- **B2 tool_result**：工具输出投影时同步兜底

### 11.2 与入向 token 化的协作

入向已 token 化的值以占位符形式存在于历史，RedactChannel 只处理**漏网明文**（主要来自 assistant 生成文本）。识别完整原值字符串后替换为 token（store 中已有）。

**已知边界**（设计取舍）：派生拼凑（前 5+后 6 位拼出原值）不拦——派生不设防原则，用户授权派生即接受此风险。

---

## 12. 边界适配器

| 适配器 | pi 钩子 | 方向 | 职责 |
|---|---|---|---|
| B1 input | `input` | 入向 | 用户输入 tokenize；返回 `{action:"transform", text}` |
| B2 tool_result | `tool_result` | 入向 | 工具输出 tokenize + Redact 兜底；返回 partial patch `{content}` |
| B3 message_end | `message_end` | 落盘 | assistant 消息 patch `{message}`（Redact 兜底） |
| B5 tool_call | `tool_call` | 出向 | 工具参数 token 还原（原地改写 `event.input`） |
| B6 user_bash | `user_bash` | 入向 | bash 命令 tokenize |
| TUI projector | `registerMarkdownTransformer` | 展示 | TUI 渲染层 token → 脱敏预览（display-only，不改历史） |
| footer | `ctx.ui.setStatus` | 展示 | `🛡 45 rules · N replaced` 实时状态 |

B1 返回语义：无变换时返回 `undefined`（pi 保留原文）；仅 `modified=true` 时返回 transform。

---

## 13. 用户命令

| 命令 | TUI | headless | 说明 |
|---|---|---|---45 条规则列表 + 动作管理 |
| `/sentinel:policy` | 策略面板（§14.2） | 文本列表 + `set/reset/reset-all` 子命令 | 规则启禁/动作/派生配置 |
| `/sentinel:query` | 查询面板（§14.3） | 脱敏列表/带 token 查全文 | 查看 store 中已 token 化的值 |
| `/sentinel:reset` | — | 清空确认 | 清 store + 计数器 |

CLI 子命令（TUI 不可用时的脚本化路径）：

```
/sentinel:policy set <type> <allow|tokenize|redact>   # 类型级覆盖
/sentinel:policy reset <type>                          # 恢复默认
/sentinel:policy reset-all                             # 清全部覆盖
```

---

## 14. TUI 面板设计

### 14.1 设计原则（三轮重构 + 八轮微调收敛）

- **手绘模式**：不依赖 pi-tui 组件树，面板类只实现 `{ render(width): string[], handleInput(data) }`，自绘 drawBox 边框（标题嵌入顶边框、borderMuted 色、主题标签感知 padding、CJK 宽度感知截断）
- **宽度自适应**：列宽按容器 width 计算（calcCols），窄终端自动压缩
- **右边框稳定**：BMP 符号（▸⚠）按 1 列计算（EMOJI_RANGES 仅 SMP 平面），杜绝选中行边框抖动
- **键盘一致**：↑↓/PgUp/PgDn 移动、Enter 编辑/提交、Esc 取消、`/` 搜索
- **导航跳禁用**：光标跳过 disabled/noargs 字段，只落可编辑字段
- **(n/total) 显式**：紧贴列表下方一行

### 14.2 策略面板（/sentinel:policy）

**主列表**（6 列）：

```
┌──────────────────────── pi-sentinel · 策略配置 ────────────────────────┐
│   #  标识                 名称              类型        动作     强度  状态 │
│ ──────────────────────────────────────────────────────────────────────── │
│   1  regex:cn-phone      regex:cn-phone    phone       tokenize high 内置│
│ ▸ 2  regex:cn-id         regex:cn-id       id_card     tokenize mid  内置│
│  16  regex:known-token:…  厂商 key：github-pat  access_token redact high 内置│
│  27  regex:stripe-key    regex:stripe-key  api_key     redact   high 内置│
│                                       (2/45)                            │
│ ──────────────────────────────────────────────────────────────────────── │
│   ↑↓ 移动 · Space 启/禁 · Enter 动作 · n 新增 · e 编辑 · x 删除 · r 重置… │
└─────────────────────────────────────────────────────────────────────────┘
```

键位：`↑↓` 移动 · `Space` 启/禁 · `Enter` 循环动作（tokenize→redact→derive→allow；floor 拦截）· `n` 新增自定义 · `e` 编辑（内置→派生配置表单 / 自定义→完整表单）· `x` 删除（仅自定义）· `r` 重置覆盖 · `/` 过滤 · `q/Esc` 退出。

**内置规则派生配置表单**（e 键，四轮修订）：

```
┌──────────────── 派生配置 · regex:cn-phone ────────────────┐
│   动作:           derive                                   │
│     派生算子:     mask                                     │
│     派生参数:     3:4                                      │
│   样例值(原文):   13900000001                              │
│ ▸   期望输出(自动设参): 138****0000                        │
│ ───────────────────────────────────────────────           │
│   派生输出: 138****0000                                    │
│   落盘形式: 13900000001|138****0000                      │
│  ✔ 已按样例推导：mask(3:4)                                 │
└───────────────────────────────────────────────────────────┘
```

- 算子/参数字段仅在 action=derive 时可编辑（其余 dim + "(仅 derive 动作可用)"）
- 参数按算子结构化：hash → select(short/full)；length/domain → 无参数；mask/regex_extract → text + hint + **期望输出反推**
- 切算子自动重置参数为默认值
- 保存校验：regex_extract 空 args / 非法正则报具体提示

**自定义规则表单**（n 键）：id/显示名/敏感类型/匹配方式（regex|keyword）/正则模式/关键词字段/强度/动作/派生配置/样例预览，实时预览正则有效性与派生输出。

### 14.3 查询面板（/sentinel:query）

```
┌──────────────────── pi-sentinel · 明文查询 ────────────────────┐
│   #  Token                  类型        长度  脱敏 / 全文      │
│ ▸ 1  13900000001          phone       11    138****0000     │
│   2  fanchaozz@users.noreply.github.com          email       16    us***@x.com     │
│                          (1/2)                                 │
│   ↑↓ 移动 · Enter 全文/脱敏 · / 搜索 · Esc 退出                │
└────────────────────────────────────────────────────────────────┘
```

`/` 搜索（十一轮修订）：过滤 token/类型/脱敏值/已展开全文；Enter 展开/收起作用于过滤集。

### 14.4 状态面板（/sentinel）

session 概况：token 数、存储字节数（AES-256-GCM）、按类型分布、二级命令提示。任意键退出。

---

## 15. 配置参考

### 15.1 用户配置（~/.pi/sentinel/config.json）

```json
{
  "policyOverrides": { "phone": "derive" },
  "hexDigestDetector": true,
  "customRules": [
    {
      "id": "vip-member",
      "label": "VIP 会员号",
      "type": "customer_id",
      "match": { "kind": "regex", "pattern": "VIP\\d{8}" },
      "strength": "mid",
      "action": "derive",
      "derive": { "op": "mask", "args": "3:4" }
    }
  ],
  "disabled": ["entropy:value"],
  "ruleActions": {
    "regex:cn-phone": { "action": "derive", "derive": { "op": "mask", "args": "3:4" } },
    "regex:known-token:github-pat": { "action": "allow" }
  }
}
```

| 字段 | 说明 |
|---|---|
| policyOverrides | 类型级动作覆盖 |
| customRules | 自定义规则（regex/keyword 匹配 + 动作 + 派生） |
| disabled | 禁用的规则 id（内置子项或自定义 id） |
| ruleActions | 规则级动作覆盖（键 = detector id，含 known-token 子项粒度） |

环境变量：`SENTINEL_HOME` 覆盖存储根目录（默认 `~`）。

### 15.2 派生预设（DERIVE_PRESETS，表单初值）

| 类型 | 预设 |
|---|---|
| phone | mask(3:4) |
| email | regex_extract(@(.+)$) |
| id_card | mask(!6:4) |
| bank_card | mask(4:4) |
| customer_id | mask(3:4) |

---

## 16. 安全分析

### 16.1 威胁模型

| 威胁 | 缓解 |
|---|---|
| 明文进入 LLM 上下文 | 入向 token 化（B1/B2/B6） |
| 明文落盘 session.jsonl | RedactChannel 兜底（P1） |
| store 文件被读 | AES-256-GCM 加密，密钥派生自设备密钥 |
| 派生事实泄漏原值 | mask/regex 算子不输出原值；{value} 模板被拒 |
| token 被外部工具还原 | 还原只发生在 B5 出向（工具进程内），审计记录 |
| 误报破坏工作流 | 来源强度分档 + 五道闸 + allow 豁免路径 |

### 16.2 已知边界（明确接受）

1. **派生拼凑**：LLM 从 `138****0000` 无法拼出原值，但多个派生值组合（前缀+后缀交叉）可能逼近——派生不设防原则，用户授权即接受
2. **tool_result 投影明文**：入向已 token 化的值在投影中是 token；但工具输出中**新出现的**明文在 B2 即时处理，历史轮次的 toolResult 原文已在当时处理——不追认
3. **用户主动泄漏**：粘贴到外部工具/截图不防
4. **同终端进程**：恶意进程可读内存/环境变量——不在威胁模型内

### 16.3 审计事件

`~/.pi/sentinel/audit/<session>.jsonl`：canonicalize.applied / secret.registered / resolve.applied / redact.applied，均含 evidenceHash（不含明文）。

---

## 17. 性能预算

| 场景 | 预算 | 实测 |
|---|---|---|
| 单条消息入向（1KB 文本） | < 5ms | ~2ms（45 规则全开） |
| B5 出向还原（10 token） | < 1ms | < 0.5ms |
| 落盘兜底（4KB 文本） | < 10ms | ~4ms |
| 内存驻留 | < 50MB | ~20MB（store 索引） |

检测管线纯同步 CPU，无网络/磁盘 IO（store 写入异步批量）。

---

## 18. 验收标准

### 18.1 核心不变量验收

| # | 标准 | 验证方式 |
|---|---|---|
| A1 | 用户输入含手机号 → LLM 请求文本不含明文，含 `13900000001` | v040-e2e ✓ |
| A2 | 工具调用参数含 token → 工具收到明文（B5 原地改写） | v040-e2e ✓ |
| A3 | assistant 复述明文 → 落盘文本被 Redact 兜底替换 | v040-redact-channel ✓ |
| A4 | store 文件含 cipherText 无明文 | v040 ✓ |
| A5 | 派生动作 → 落盘形式 `<TYPE:p_NNN>\|fact`，原值不出现 | v040 ✓ |
| A6 | password 不可设低于 redact（floor） | v040-acceptance ✓ |

### 18.2 功能验收

| # | 标准 | 验证方式 |
|---|---|---|
| F1 | 45 条内置规则面板可见可配（面板行数 = footer 数） | v040-panels ✓ |
| F2 | known-token 子项单项 allow：同文本 github-pat 放行、aws 照常 token 化 | v040-panels ✓ |
| F3 | 自定义规则（regex/keyword）新增/编辑/删除全流程 | v040-panels ✓ |
| F4 | mask 三语法执行正确（3:4 / !6:4 / #*# 模板） | v040-panels ✓ |
| F5 | 期望输出反推：mask 形态识别 + regex 子串反推 | v040-panels ✓ |
| F6 | query 面板搜索/全文展开/计数 | v040-panels ✓ |
| F7 | 候选/裁判配对联动（改 cn-id 裁判同步） | v040-panels ✓ |
| F8 | 废弃算子（domain 等）配置安全降级 tokenize | v040-panels ✓ |

### 18.3 质量门槛

- 测试 125/125 通过（vitest）
- tsc --noEmit 零错误
- 无 debug 残留（console.log/临时探针）

### 18.4 真实会话验证清单

发布前人工过一遍：

- [ ] 粘贴含手机号/身份证/密钥的日志 → 输入区显示 token
- [ ] 让 LLM 查库（token 参数）→ db_query 收到明文
- [ ] /sentinel:query 能查到刚才的值
- [ ] 会话目录 session.jsonl grep 明文 → 零命中
- [ ] /sentinel:policy 改 phone → derive → 新输入显示派生形态
- [ ] 重启 pi → /sentinel:query 仍能看到（store 持久）
- [ ] footer 显示 `🛡 45 rules · N replaced`

---

## 19. 目录结构

```
pi-sentinel/
├── index.ts                  # 入口：钩子接线 + footer + 命令注册
├── commands.ts               # /sentinel /sentinel:policy /sentinel:query /sentinel:reset
├── commands-shared.ts        # OVERLAY_OPTS / FLOOR_TYPES
├── policy-tui.ts             # 策略面板驱动 + 规则表单 + builtinSiblingIds
├── adapters/
│   └── v040-adapters.ts      # B1/B2/B3/B5/B6/TUI 适配器
├── core/
│   ├── types.ts              # SensitiveType / SensitiveFinding（detector 域）
│   ├── types-v040.ts         # Finding / Action / CanonicalOp（core 域）
│   ├── crypto.ts             # AES-256-GCM + evidenceHash
│   ├── detector-engine.ts    # 管线调度
│   ├── policy-engine.ts      # 决策（类型/规则/floor）
│   ├── canonicalizer.ts      # 替换执行（token/redact/derive）
│   ├── token-resolver.ts     # B5 出向还原
│   ├── redact-channel.ts     # 落盘兜底
│   ├── derivation-engine.ts  # 4 算子 + inferMaskArgs/inferRegexArgs
│   ├── secret-store.ts       # 加密存取（P3 唯一持有者）
│   ├── event-bus.ts          # 事件总线
│   ├── sentinel-engine.ts    # 引擎门面
│   └── user-config.ts        # ~/.pi/sentinel/config.json
├── detectors/
│   ├── regex.ts              # 基础 PII + known-token 29 子项
│   ├── v020.ts               # 厂商 key 18 项 + encoded 2 项
│   ├── keyword.ts            # keyword 候选 + shape 裁判
│   ├── validator.ts          # cn-id 校验位 / Luhn
│   ├── entropy.ts            # 熵检测（低置信）
│   ├── shape-hex-digest.ts   # hex 摘要形态
│   ├── custom-loader.ts      # config.json 自定义规则 → Detector
│   ├── index.ts              # DetectionPipeline + builtinRegistry
│   └── types.ts              # Detector 接口 + SOURCE_PROFILES
├── ui/
│   ├── bordered-panel.ts     # drawBox 手绘边框（provider-manager 模式）
│   ├── vwidth.ts             # CJK/emoji 可见宽度
│   ├── theme.ts              # PanelTheme 契约
│   ├── dynamic.ts            # PanelRoot（footer 等非 overlay 场景）
│   ├── policy-panel.ts       # 策略面板（6 列表格）
│   ├── query-panel.ts        # 查询面板（/ 搜索）
│   ├── rule-form.ts          # 自定义规则表单
│   ├── builtin-derive-form.ts# 内置规则派生配置表单
│   ├── derive-param.ts       # 算子参数形态表（共享）
│   └── status-panel.ts       # 状态面板
├── tests/                    # v040 系列 7 文件 125 用例
└── DESIGN_*.md               # v0.0.1 → v1.0.0 演进史
```

---

## 20. 版本演进记录

### v1.0.0（本轮，十一轮修订收敛）

| 轮次 | 主题 | 要点 |
|---|---|---|
| 一~三 | TUI 三轮重构 | ASCII → pi-tui 组件 → 手绘模式（provider-manager 风格） |
| 四 | e 键 + 边框抖动 | 内置规则派生配置表单；BMP 符号宽度修正 |
| 五 | 家族传播 | id_card allow 不生效根因（候选/裁判压制）→ 同步配对 |
| 六 | 算子依赖 + 安全 | derive 字段联动；format {value} 明文占位漏洞修复 |
| 七 | 导航 + 自动推导 | 跳过禁用字段；args select 崩溃修复；mask 样例反推 |
| 八 | mask 统一 | 三语法（3:4 / !6:4 / #*#）自适应任意掩码形态 |
| 九 | 算子通用化 | 删 domain/prefix_match/format；regex_extract 反推 |
| 十 | 子项粒度 | known-token 拆 11 子项 + v020 18 项独立可控；配对表化 |
| 十一 | 注册表对齐 | builtinRegistry 单一事实来源；面板 = footer = 45；query 搜索 |

### v1.0.1（补丁：解决「被拦了但不知道哪条规则」）

用户场景：停用某 detector 后仍被拦，无法确认是停用未生效还是被其他规则命中。补丁补“diag 溯源”能力，让哪条 detector 触发成为一等公民。

改动：
- `core/types-v040.ts`: `CanonicalOp` 加 `detector?: string` 字段
- `core/canonicalizer.ts`: `applyAction` 三个 case 与 `emit` payload 都带 `detector`（AuditLog 拼包后、审计日志自动获得 detector 字段，零额外改动）
- `core/diag-store.ts`（新）: 进程内 ring buffer（容量 100），订阅 `canonicalize.applied`，提供 `recent(n)` 与 `topDetectors(n, k)`
- `index.ts`: `session_start` 装配 `DiagStore`；footer 命中后 2.5s 显示尾巴 `· 刚刚 type via detector → action (token)`（redact 命中也可见）
- `commands.ts`: 新增 `/sentinel:diag [N]` 命令，输出最近命中明细 + 热度聚合
- `tests/v040-diag.test.ts`（新，5 用例）: detector 字段、ring buffer、端到端 phone/UUID 命中追踪

使用方式：
- 实时：贴文本后 footer 短暂闪动 `刚刚 phone via regex:cn-phone → tokenize (13900000001)`
- 查询：`/sentinel:diag` 看最近 20 条明细与 detector 热度（redact 也能查）
- 事后追溯：`grep shape:hex-digest ~/.pi/sentinel/audit/*.jsonl`（审计日志已有 detector 字段）

测试：130/130（v040 6 个 + diag 1 个文件）。TSC 零错误。

### 历史版本

- v0.4.0（6166141）：三职责重构，删授权体系
- v0.3.x（00156a1）：授权体系（后废弃）
- v0.2.0：三段式管线 + 27 扩展检测器
- v0.0.1：原型

详见 `DESIGN_v0.4.0.md` §25 与各版本设计文档。
