# SPI Protecter security boundaries / SPI Protecter 安全边界

## Goal / 目标

Prevent configured personal information from accidentally entering Pi LLM request bodies as plaintext. This assumes trusted Pi core, provider implementations, OS and installed extensions, successful extension loading and invocation of the final request hook.

防止已配置的个人信息意外以明文进入 Pi 的 LLM 请求体。前提是 Pi 核心、提供商实现、操作系统和已加载扩展可信，且插件成功加载、最终请求钩子确实执行。

## Non-goals / 非目标

- No OS sandbox or defense against same-UID programs, malicious extensions, arbitrary code execution, debuggers or memory inspection.
  不提供操作系统沙箱，不防御同 UID 程序、恶意扩展、任意代码执行、调试器或内存检查。
- No protection against arbitrary encoding, splitting, translation or screenshot/OCR reconstruction; no automatic privacy classification or formal anonymization.
  不防御任意编码、拆分、翻译和截图／OCR 重构，不提供自动隐私分类或形式化匿名化。
- No coverage for gateway authentication headers/endpoints, tool requests, an extension's own `fetch`, telemetry, session exports, `/share` or the `/bug` bundle. Model calls are covered, including Pi's own summarizers and nested calls; transfers of the stored transcript are not, and no extension API can intercept them.
  不覆盖网关认证头与地址、工具请求、扩展自身的 `fetch`、遥测、会话导出、分享或 `/bug` 报告包。模型调用（含 Pi 自身的摘要器与嵌套调用）已覆盖；已保存会话记录的传输不在覆盖范围，且任何扩展 API 都无法拦截。
- No local configuration/session/terminal encryption, nor automatic prevention of exfiltration after a model infers a secret.
  不加密本地配置、会话和终端，也不会在模型推断出秘密后自动阻止外传。

## Implementation / 实现策略

- Scan outgoing payload text and replace matches with random shape-preserving values. Runtime mappings stay in memory, but version 0.3.0 adds plaintext original/replacement audit pairs in private JSONL. Audit files are sensitive credential stores, not encrypted archives; never upload them. Logs persist after exit and are not loaded to restore mappings.
  出站文本使用随机同形值替换，运行映射保存在内存，但 0.3.0 新增私有 JSONL 原文与替换值审计。日志属于敏感凭证存储，不是加密归档，禁止上传；退出后仍保留，不用于恢复映射。
- The model reasons about the replacement, so any answer derived from a protected value, such as a digit position, a checksum, a comparison or arithmetic, is wrong. Only the value is restored in the reply, never the conclusion drawn from it, so the reply looks confident and stays consistent across retries. Verify such answers locally.
  模型推理的是替换值，因此任何从受保护值推导出的答案（位数、校验位、比较、算术）都是错的。回复中只有值被还原，由它推导出的结论不会，因此答案看似笃定且重试结果一致。请在本地核对。
- Shape-preserving replacements (0.6.0) keep JSON types, length, character class and punctuation structure, so they leak more metadata than opaque tokens and look like plausible real data. Unsupported scripts and symbols are preserved unchanged, so part of a matched value may stay visible. Short matches may be restored when a model independently emits the same string; prefer long, distinctive targets. Numeric values that cannot round-trip and matches with no unique replacement fail closed.
  同形替换（0.6.0）保留 JSON 类型、长度、字符类别和标点结构，因此比不透明占位符泄露更多元信息，且看似真实数据。不支持的文字体系和符号原样保留，命中值可能部分可见。模型自行输出相同短字符串时可能被还原，应优先使用较长且有辨识度的目标；无法精确往返的数值以及无唯一替换值的匹配均拒绝放行。
- Audit writes use 0600 files, safe-open checks, a cooperative lock and fsync before returning a redacted payload. Logging errors fail closed. Success means local replacement, not provider delivery. File size and preview limits bound resource usage; users manage retention locally. Hard crashes may leave stale locks or partial records. Same-UID writers and terminal capture remain outside the threat model.
  日志使用 0600、安全打开检查、协作锁，并在返回脱敏请求前同步落盘；日志失败则拒绝请求。成功表示本地替换而非网关已接收。文件及预览大小受限，用户本地管理保留策略；硬崩溃可能残留锁或不完整记录，同用户权限攻击和终端录屏仍不在防护范围。
- `/protecter` previews are TUI-only and require confirmation before showing original values; nothing is injected into model context or session entries. Trusted extensions may still observe UI hooks. Configuration caching remains memory-only during requests, but matched requests now perform audit-file writes.
  查看器仅限 TUI，展示原文前需确认，不注入模型上下文或会话条目；可信扩展仍可能观察 UI 钩子。出站规则依然使用内存缓存，但命中请求现在会写审计文件。
- Block diagnostics carry a catalog code, stage, non-negative integer coordinates and an allow-listed OS `errno` only. Rule indexes and node counts are positions, not content. Raw exception messages, request text, matched originals, replacement values, configuration content and file paths are never surfaced, including across the worker boundary, so diagnostics are safe to share. Codes are intentionally coarse, so pinpointing a rule may still need local inspection.
  拦截诊断仅包含目录错误码、阶段、非负整数定位和白名单系统错误码。规则序号和节点数只是位置，不是内容。不输出原始异常消息、请求文本、命中原文、替换值、配置内容和文件路径，worker 边界同样适用，因此诊断可安全分享；错误码有意保持粗粒度，精确定位规则仍可能需要本地查看。
- Limit worker regex scans to 2 seconds, with resource and input limits. Errors exclude original configuration, rules and request text.
  worker 正则扫描限制为 2 秒，并限制资源和输入；错误不包含原始配置、规则或请求文本。
- Every supported Pi release, 0.84.0–0.87.x, 0.99.x and 1.2.0–1.3.x, catches `before_provider_request` errors and continues with the current payload. Error paths therefore return `{}` and attempt cancellation. This does not guarantee zero HTTP requests; it prevents this handler from falling back to the original payload.
  所有受支持的 Pi 版本（0.84.0–0.87.x、0.99.x 与 1.2.0–1.3.x）都会捕获该钩子异常并继续使用当前请求体，因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求，只保证本处理器不回退原始请求体。
- Only the agent's own model requests carry the `onPayload` callback that fires `before_provider_request`. Every other provider call reaches the one `ModelRuntime` the session shares, so `session_start` wraps its `stream`, `streamSimple`, `classify` and `generateImages` methods: calls carrying the payload hook pass through untouched, because they are already redacted and their callers need the synchronous lazy stream, and every other call is redacted first, with a redaction failure cancelling it. This covers Pi's `/bug` summary, which has no extension event and whose command cannot be shadowed, plus nested calls from `codemode` scripts and other extensions. It wraps methods that are internal to Pi, so it is verified at startup and a runtime that cannot be wrapped fails closed with `GUARD_UNAVAILABLE` instead of running unprotected. MCP servers are external programs that receive tool arguments directly, which this does not change.
  只有智能体自身的模型请求会携带本扩展挂接的 `onPayload` 回调。循环之外的嵌套模型调用（如 `codemode` 脚本的 `models.classify()`、`models.generateImages()`、其他扩展的 `modelRegistry` 调用或任何直接 SDK 调用）不经脖敏即发出。Pi 0.99.0 将 `codemode`、MCP 与工具检索内置为扩展；`codemode` 仅在 `defaultTools` 或 `--tools` 启用时生效，MCP 服务器则是直接接收工具参数的外部程序。高风险环境下应将二者保持关闭。
- Create configuration separately with Unix mode 0600. Reject links, non-regular files, multiple links and files owned by others. Users remain responsible for directories and OS permissions; TOCTOU races are not prevented.
  独立创建配置，Unix 权限为 0600；拒绝链接、非普通文件、多链接及他人所有文件。用户仍需维护目录和系统权限，无法防止检查与使用之间的竞态。
- Direct-access blocking is supplementary. File tools inspect target paths rather than document bodies. Shell can bypass string checks, and command mentions or ancestor directory checks can still cause false positives.
  直接访问拦截只是补充；文件工具检查目标路径而非正文。shell 可绕过字符串检查，命令提及及祖先目录检查仍可能误拦截。
- Startup/reload validates and merges machine-hash configurations before caching. Requests use memory only; file deletion does not revoke cached secrets. Reload or process exit clears the instance. Migration uses commit-before-delete and a cooperating-process lock, not a multi-file transaction or protection against hostile local writers. Stale locks require local inspection; Windows does not provide POSIX directory fsync.
  启动或重载时校验并合并机器哈希配置后缓存，请求只使用内存；文件删除不撤销缓存秘密，重载或退出才清理实例。迁移采用先提交后删除及协作进程锁，并非多文件事务或恶意本地写入防护；残留锁需本地检查，Windows 无 POSIX 目录同步。
- Whole-configuration text detection does not guarantee that every reformatted rule definition is masked.
  整体配置文本检测不保证所有重新格式化的规则定义都被遮盖。
- Compaction, split-turn prefix summaries and `/tree` branch summaries are built by Pi, which passes no `onPayload` callback, so `before_provider_request` never fires for them. Pi does hand the hook the same `preparation` object it then summarizes, so it is redacted in place and Pi summarizes the redacted text with the session's own model; routing, credentials, retries and the token budget stay with Pi. The stored summary keeps the replacements, so no plaintext summary is written to the session. Any failure cancels compaction instead of letting Pi summarize stored plaintext. `/compact <instruction>` is cancelled when the instruction holds a protected value, because Pi reads it from its own state and a redacted copy would be ignored.
  压缩、split-turn 前缀摘要与 `/tree` 分支摘要都由 Pi 构造，且不传 `onPayload` 回调，`before_provider_request` 对其不会触发。但 Pi 交给钩子的 `preparation` 正是它随后用于摘要的同一对象，因此对其原地脱敏，再由 Pi 用会话自身的模型对脱敏文本生成摘要；路由、凭证、重试与 token 预算仍由 Pi 负责。保存的摘要保留替换值，因此不会向会话写入明文摘要。任何失败都取消压缩，而不让 Pi 对已存明文生成摘要。当 `/compact <指令>` 的指令含受保护值时取消，因为 Pi 从自身状态读取该指令，脱敏副本不会被采用。
- Transfers that are not model calls are outside this boundary: `/bug`'s uploaded bundle, `/share` and `/export` send the stored transcript over their own HTTP paths, which no extension API can intercept, and that transcript holds restored plaintext. Only the `/bug` summarization request is covered. Review what these produce before sending them.
  非模型调用的传输不在此边界内：`/bug` 上传的报告包、`/share` 与 `/export` 通过各自的 HTTP 路径发送已保存的会话记录，任何扩展 API 都无法拦截，而该记录中保存的是已还原的明文。仅 `/bug` 的摘要请求在覆盖范围内。发送前请先检查这些产物的内容。
- Summarized `/tree` navigation takes the same internal path and is redacted the same way; that event does read its instructions back, so they are returned redacted rather than cancelling. Do not widen Pi compatibility without verification: refusing a working release blocks every request, and accepting a broken one can send plaintext. Each supported version is established by running the full suite against it, and the runtime gate and declared peer range are kept identical.
  带摘要的 `/tree` 导航走同一条内部路径，以同样方式脱敏；该事件会读回指令，因此返回脱敏后的指令而非取消。未经验证不扩大 Pi 兼容范围：误拒可用版本会拦截所有请求，误受损坏版本则可能发送明文。每个受支持版本均以完整测试验证，并保持运行时门禁与声明的 peer 范围一致。

## Fixed aliases and caching / 固定别名与缓存

Fixed targets are reserved literal aliases, not anonymization: they reveal semantics and natural occurrences in model output are also restored after the mapping is learned. Choose distinctive values. Reusing one alias for distinct originals, sensitive targets, ambiguous substring aliases and conflicting matchers is rejected rather than guessing a reverse mapping. Runtime regex collisions and partial fixed-rule overlaps fail closed. Random placeholders remain stable within one extension instance; cache hits still depend on provider prefix, model and routing policies. Reload/restart discards mappings; audit records are not loaded to restore them.

固定目标是保留字面别名，并非匿名化；它会暴露语义，映射学习后模型输出中自然出现的同名内容也会被还原，应选择独特值。不同原文复用别名、敏感目标、子串歧义和匹配器冲突会被拒绝，不猜测反向映射。运行时正则冲突及固定规则部分重叠会拒绝请求。随机占位符在同一实例内保持稳定，但缓存命中仍依赖提供商前缀、模型和路由策略。重载或重启丢弃映射，不从审计恢复。

## Deployment guidance / 安全部署建议

1. Configure precise rules locally and check status/counts. Empty rules provide no SPI detection.
   在本地配置具体规则并检查状态和数量；空规则不具备个人信息检测能力。
2. Inspect requests using fictional data and a trusted test gateway; never test with real secrets.
   使用虚构数据及可信测试网关检查请求，绝不用真实秘密测试。
3. Avoid log-upload, session-export and telemetry extensions; verify payload handler ordering.
   避免日志上传、会话导出和遥测类扩展，确认请求处理器顺序。
4. Use containers, separate users/filesystems and network restrictions for high-risk environments. Keep configuration outside tool-accessible storage. This extension alone cannot enforce that isolation.
   高风险环境使用容器、独立用户或文件系统及网络限制，将配置放在工具不可访问的位置；本扩展无法单独提供此隔离。
5. Never post real configuration, mappings, sessions or personal-information packet captures in public issues.
   不在公共问题报告中提交真实配置、映射、会话或含个人信息的抓包。
