Skip to content

feat: [ source-risk ] PII/密鑰偵測前移到讀取來源之前 - #104

Merged
CarlLee1983 merged 1 commit into
mainfrom
agent/issue-101-pii-source-risk
Aug 17, 2026
Merged

feat: [ source-risk ] PII/密鑰偵測前移到讀取來源之前#104
CarlLee1983 merged 1 commit into
mainfrom
agent/issue-101-pii-source-risk

Conversation

@CarlLee1983

Copy link
Copy Markdown
Owner

Closes #101。這是 2026-08-16 來源受理面 grill 排出來的第一個前置條件——#102(次級佐證載體:信件摘錄與試算表)依賴這個閘先就位,因為那些載體正是最容易夾帶金鑰、測試帳號與聯絡人資料的形式,放行之前守衛必須到位。

問題

privacy.py 的確定性偵測只守在輸出端(feedback 報告與 Foundry 治理寫入)。而 inspect-source-risk 現有的七條規則全是注入類——Unicode tag、bidi override、控制字元、指令覆寫文字——關心的是來源會不會操縱 agent,沒有一條關心來源會不會洩漏東西給 agent。這是同一個閘上的空白,不是需要新增一道閘。

做法

leak 類規則複用 privacy.py 的樣式,但只有自證性材料是 blocker。

Rule Severity 內容
SR-SECRET-VALUE blocker PEM 私鑰區塊、JWT
SR-CREDENTIAL-REFERENCE warning Authorization: Bearer …api_key: …、bearer/basic
SR-CONTACT-PII warning email
SR-PII-VALUE warning 身分證字號、護照、SSN、電話
SR-PAYMENT-CARD warning 經 Luhn 驗證、排除卡組織公告測試卡號

憑證引用為什麼只給 warning:實作到一半在真實的 Adyen OpenAPI 上實測,verdict 變成 reject,兩筆 blocker 命中是 API-Key: YOUR_API_KEYBasic {{credentials}}——都是文件裡的佔位符。每一份寫得好的 API 文件都會示範認證標頭,照原本的做法這個閘會擋掉幾乎每一份合格來源。值是真是假無法從文字確定,而一個永遠需要 waiver 的閘不是閘。改成只認結構本身即證據的材料,猜的就從「這個值是不是真的」變成「這段文字的結構是否只可能來自真材料」。

Warning 的獨立額度:共用的 1,000 筆上限原本只被稀有的操縱類樣式餵到;leak 類規則在合格的大型文件裡是高頻的。實測一份只有 1,200 個聯絡信箱、其餘完全乾淨的文件,verdict 是 reject 而唯一的 blocker 就是截斷本身。新增 500 筆的 warning 額度與 warning 級的 SR-WARNINGS-TRUNCATED;blocker 仍可用滿整個上限,警告擠不掉真正的命中。

治理端的兩處行為變更

拆分本身不改變偵測能力(find_sensitive_value 取所有樣式的聯集),但有兩處樣式內容確實變了,皆為刻意:

卡號候選不再跨行。 相鄰兩行的裸數字欄位接起來,約十分之一會通過 Luhn,產生一張沒有人寫過的卡號。但分隔符仍涵蓋 NBSP 與全形空格——先前的版本用 [-. \t] 把它們一起排掉了,而從 PDF 貼出的付款範例帶 U+00A0、zh-TW 文件帶 U+3000,那會讓真卡號通過治理閘被明文持久化。

CONTACT_PII 的網域改成有界的 label 結構。 原本的 [^\s@]+\.[^\s@]+ 在整份文件上是二次時間:

輸入 修正前 修正後
184 KB minified CSS 52.7 s 0.04 s
533 KB base64 0.02 s

觸發條件不需要攻擊者:HTML 來源內嵌一張 base64 圖片或一段壓縮 CSS 就夠了,而 inspect-source-risk 不解析 HTML,直接掃原始文字。這是擋不可信來源的閘被不可信來源癱瘓。治理端只掃單一 JSON 字串值(短),所以前移到整份文件才是暴露點。收窄之處:單字元 TLD(x@y.z)不再命中;pkg@1.0.0 從誤報變成不報。

語意讓步

「高密度輸入必定 fail closed」現在只對 blocker 級內容成立。原本那個測試用的是 20 萬個零寬字元,而零寬格式是 warning 級規則。測試已拆成兩個:純警告密集必須 pass,控制字元密集必須仍然 reject。

相容性

RULESET_VERSION 2 → 3。source_risk/loader.py 對版本不符 fail closed,所以所有既有的 source-risk 與 source-quality 產物失效,需以 manifest → inspect-source-risk → assess-sources 重建。本 PR 已重建 benchmark 的十三份套件(gitignored,operator 本機物件)。

Test plan

  • uv run pytest → 2238 passed
  • uv run ruff check . → clean
  • npm run docs:check → clean
  • 十三個 benchmark 來源逐一過新規則:12 案 pass;stripe-basic-rest 需沿用其既有的 20 MiB --max-bytes,沿用後亦 pass

新增的回歸測試全部在既有的 inspect-source-risk CLI seam 上,涵蓋:PEM 私鑰擋下且不回顯 payload、聯絡信箱只警告、身分證與手機號碼只警告、卡號經 Luhn 驗證、非 Luhn 的長編號不誤報、卡組織測試卡號完全不報、佔位符憑證只警告、相鄰兩行數字不接成卡號、NBSP 與全形空格分隔的卡號仍偵測得到、標點密集文字在有界時間內掃完、1,200 個信箱的文件 verdict 為 pass、控制字元密集仍 reject。

分隔符那條決定先前完全沒有測試守著——把樣式改回去整個套件依然全綠,所以那兩個回歸測試是這批裡最該留的。

後續

MEDIUM 級的兩項記在 #103:緊鄰數字會讓卡號完全偵測不到(既有行為,但本 PR 新增了「Luhn-validated」的公開宣稱,邊界該對齊),以及 _rule_matches 裡的 rule_id 字串特例該換成 accept 回呼。

`privacy.py` 的確定性偵測原本只守在輸出端(feedback 報告與 Foundry 治理
寫入)。source-risk 現有的規則全是注入類 —— 關心的是來源會不會「操縱」
agent,沒有一條關心來源會不會「洩漏東西給」agent。這是同一個閘上的空白,
不是新增一道閘。

只有自證性材料是 blocker:`SR-SECRET-VALUE` 只認 PEM 私鑰區塊與 JWT,
結構本身就是證據。`SR-CREDENTIAL-REFERENCE` 只給 warning,因為合格的
API 文件本來就會示範 `Authorization: Bearer <TOKEN>` —— 實測 Adyen 的
OpenAPI,兩筆命中都是 `YOUR_API_KEY` 與 `{{credentials}}` 這類佔位符。
值是真是假無法從文字確定,而一個永遠需要 waiver 的閘不是閘。
`SR-CONTACT-PII`/`SR-PII-VALUE`/`SR-PAYMENT-CARD` 同為 warning。

Warning 另給 500 筆的獨立額度(`SR-WARNINGS-TRUNCATED`,warning 級)。
共用的 1,000 筆上限原本只被稀有的操縱類樣式餵到,leak 類規則在合格的
大型文件裡是高頻的 —— 沒有這道分隔,一份只是聯絡信箱很多的文件會被
截斷 blocker 拒絕,而報告裡沒有任何一筆實質命中。Blocker 仍可用滿整個
上限,警告擠不掉真正的命中。代價是「高密度輸入必定 fail closed」現在
只對 blocker 級內容成立,測試已拆成兩個分別釘住。

治理端的兩處行為變更(非單純拆分,`AGENTS.md` 有記):

- 卡號候選不再跨行。相鄰兩行的裸數字欄位接起來約十分之一會通過 Luhn,
  產生一張沒有人寫過的卡號。但分隔符仍涵蓋 NBSP 與全形空格 —— 從 PDF
  貼出的付款範例帶 U+00A0、zh-TW 文件帶 U+3000,排掉它們會讓真卡號
  通過治理閘被明文持久化。
- `CONTACT_PII` 的網域改成有界的 label 結構。原本的 `[^\s@]+\.[^\s@]+`
  在整份文件上是二次時間:184 KB 的 minified CSS 要 52.7 秒,5 MiB 來源
  足以讓這個前置閘停擺 —— 擋不可信來源的閘被不可信來源癱瘓。治理端
  只掃單一 JSON 字串值,所以前移到整份文件才暴露。收窄之處是單字元 TLD
  不再命中,`pkg@1.0.0` 從誤報變成不報。

`find_sensitive_value` 取所有樣式的聯集,拆分本身不改變治理端偵測能力。
`RULESET_VERSION` 升到 3;既有的 source-risk 與 source-quality 產物因此
失效,benchmark 的十三份套件已用真實 CLI 鏈重建。

MEDIUM 級的兩項(緊鄰數字讓卡號漏偵測、rule_id 字串特例改成 accept 回呼)
記在 #103Closes #101

🤖 Generated with Claude Code
@CarlLee1983
CarlLee1983 merged commit c0209ff into main Aug 17, 2026
1 check passed
CarlLee1983 added a commit that referenced this pull request Aug 17, 2026
`scripts/release.py prepare` 不碰 HTML 手冊,而 #104 改了 source-risk 的
規則集、#105 新增了一個取源指令與一個來源等級 —— 依 AGENTS.md 的非商榷
規則,這些必須在同一次發布裡改完。

operator manual(zh + en):
- source-risk 段落改寫成兩個方向(操縱 vs 洩漏),補上五條新規則、只有
  自證性材料會擋的理由、Luhn 與測試卡號排除、以及 warning 的 500 筆
  獨立額度與它存在的原因
- 新增 `import-supplementary-note` 一節與目錄項,含接受的破口與
  sidecar 的 fail-closed 行為

architecture manual(zh + en):
- source_risk/ 模組描述補上洩漏方向與 warning 額度
- manifest/ 模組描述補上 authority 欄位、sidecar 標定與 fail-closed
- 流程圖加上 import-supplementary-note

🤖 Generated with Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

來源受理衍生:PII/密鑰偵測前移到 inspect-source-risk

1 participant