# 第 4 批：清版控裡的憑證殘留（10 件 → 4 張新卡 + 1 張沿用既有卡）

## 這批一句話
這批 10 件裡有 8 件**據報**密碼／金鑰已撤銷換發（決策者原則九），剩下的工作是把版控裡那些字串清乾淨（含對話紀錄、腳本、交接文件）；另外 2 件（M03-9 派工表明文殘留、M23-3 Google 金鑰）本身還沒處理完，需要多一步程式修正或金鑰重設。這批**現在就能開**，不依賴其他批——清字串是收尾動作，跟前面批次的程式修正互不阻擋。

## 🔴 本棒（開卡前座標補查）改了什麼，派工前必讀
**座標全部補齊了**（每件的檔案清單、grep 指令、命中檔數都寫進各卡），但查證同時翻掉幾個前提：

1. **「已撤銷換發」這句對不上現況**——下列三把與**現行 `.env` 完全一致**，也就是**很可能根本沒換**：
   - 資料庫管理員密碼（4-2）：`.env` 的 `DB_PASSWORD` ＝版控殘留值，而這個帳號是 `BYPASSRLS`、四處通行
   - 系統管理員登入密碼（4-1 的 #72）：就是 `docs/claude/memory/reference_dev_login.md` 現行那組
   - Google 的 AI 服務金鑰（4-1 的 #11 四把之一）：`.env` 的 `GOOGLE_API_KEY` ＝版控殘留值
   **所以這幾件的性質可能不是「打掃」而是「洞還在」。** 已寫成 D-b4-2 與各件的 ⚠️ 註記，**清字串前要先實查換沒換**。
2. **件數與實數有落差**（開卡照實數寫）：#10「26 個檔」實查 **8 檔**、「4 把通行證」只找到 **1 把真值＋1 把假值**（另 2 把 BE 與套件都零命中）；#72「三支腳本」實數 **5 支**；#12「249 檔」實數 **250 檔**；#74「15 檔」實數 **14 檔**。
3. **多找到兩個容易漏的落點**：`docs/features-site/site/search/search_index.json`（MkDocs 搜尋索引，**憑證原文在裡面**）、`docs/claude/memory/` 下 6 份 memory（每個 session 都會讀）。
4. **查到但判定不必清的**：`TURNSTILE_SECRET_KEY`（30 檔）是 Cloudflare 官方公開的測試 dummy key、`glpat-fake…`（6 檔）是範例值——**標記為假陽性，免得下次掃描再撈一次**。
5. **全部落在 BE 主專案**，套件 monorepo 對這批所有憑證特徵字串**零命中**——不需要開套件側 worktree（4-3 那張是程式修正、另當別論）。
6. **本批一律只清「受版控的檔」，`git grep` 就是正確的範圍**。本棒另跑過一次「整個工作目錄」的 grep 對照，資料庫密碼從 250 檔（受版控）變成 **547 檔**——多出來的 297 檔**全部是 gitignored 的本機產物**，已逐項確認 `git check-ignore` 都通過，**不會被 commit、不在本批範圍**：
   - `.claude/` 250 檔（本機 session 暫存）
   - `CLAUDE-SECURITY-*/` 的掃描產物 20 檔（`RESULTS.jsonl` / `.sarif` / `.md`）—— ⚠️ **這是掃描工具自己把撈到的憑證原文寫進報告檔**，雖然沒進版控，但那些目錄躺在 repo 根目錄下、每跑一次掃描就多一份。**建議順手回報決策者「掃描產物要不要定期清」**（屬本機衛生、不是本批工作）
   - `reports/allure-results/` 21 檔（測試報告）、`.env` / `.env.bak` / `.env.bak-20260814-pre1g` / `docker/production/.env` / `docker/production/guidant.env`（設定檔本體，**本來就該有值、不要動**）
   **驗收一律用 `git grep`，不要用 `grep -r`**——後者會把上面這些算進來，永遠驗不到零命中。

## 卡片清單
| 卡 | 卡名（做什麼，不寫代號） | repo／套件 | 涵蓋 SUMMARY # | 建議 model／effort | 序列組 |
|---|---|---|---|---|---|
| 4-1 | 清掉版控裡的憑證殘留字串（外部平台通行證／AI服務金鑰／系統管理員密碼／交接文件帳密，共 5 件） | **BE 主專案**（`docs/conversation-history/`、`scripts/`、`docs/analysis/`、`docs/features/FR-039*/handoff/`、`docs/claude/memory/`；**套件側全部零命中，已查證**） | #10、#11、#72、#73、#122 | sonnet／medium | A |
| 4-2 | 資料庫管理員密碼（**250 檔**殘留＋堵住還在寫入的那 1 支產生器） | **BE 主專案**（已查證無套件側） | #12（含其中的 M16-7、M17-8 兩個具體實例） | sonnet／medium | A |
| 4-3 | 移除掃描派工資料表裡的明文帳密殘留（程式修正，不是清字串） | jedi-detection 套件（**寫入只有 1 行，兩條路徑共用**） | #70 | sonnet／medium | B |
| 4-4 | Google 雲端硬碟金鑰：清殘留字串＋（等決策）金鑰重設 | **BE 主專案**（**14 檔全在 `docs/conversation-history/`**；套件側零命中，已查證） | #74 | sonnet／medium | A |
| （沿用） | 打包前端映像檔腳本內嵌 Nexus 帳密 | — | #75 | — | — |

（#75 直接沿用既有卡 **CM-1998**，不重開、不寫小節；序列組欄位留空。）

---

## 卡 4-1：清掉版控裡的憑證殘留字串（外部平台通行證／AI服務金鑰／系統管理員密碼／交接文件帳密）
- **範圍**：**BE 主專案**（已查證：五件的殘留全在 BE，落在 `docs/conversation-history/`、`scripts/`、`docs/analysis/`、`docs/features/FR-039*/handoff/`、`docs/claude/memory/`、`docs/features-site/`；**套件 monorepo 對五件的憑證特徵字串全部零命中**，不需開套件側 worktree）；worktree 名建議 `wt-fix-b4-cred-strings`；涵蓋 #10、#11、#72、#73、#122
- **每件的修法**（白話，一件一小節）：
> 🔴 **本節所有清單都是「用 key 名或憑證前 4 碼 grep」查出來的路徑與檔數，計畫檔內不寫任何憑證值**。runner 開工時自己從 `.env`（或報告內位置）取值再 grep，**不要把值貼進 commit message、Notion 卡或回報**。
> ⚠️ **本棒查證的一個共同發現**：這批「已撤銷」的憑證裡，**有幾把與現行 `.env` 的值一模一樣**（見各件的 ⚠️ 註記）。所以清字串前要先確認該值到底換過沒有——**若沒換，清版控只是打掃，洞還在**。

  - **#10 外部程式碼平台的存取通行證（4 把）殘留在 26 個版控檔案裡**
    - 修法：密碼已全數撤銷（無效），純粹是清掉現存檔案裡的舊字串；清完後這幾把通行證的字串在 repo 裡要零命中。
    - 🔴 入口清單（已查證）：
      - grep 指令：`git grep -lIE 'glpat-[A-Za-z0-9_-]{15,}'`（GitLab personal access token 的固定前綴）
      - **命中 8 個檔**（不是 26；26 可能是「行數」或含 features-site 產出的舊數）：
        `docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-01-of-03.md`
        `docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-03-of-03.md`
        `docs/conversation-history/2026-05-06-my-tasks-completion-gate/part-01-of-05-bug-fix-handover-and-standards-audit.md`
        `docs/conversation-history/2026-05-08-project-flow-engine-arc/part-01-of-11-m0-spike-and-spec-amend.md`
        `docs/conversation-history/2026-05-13-spec2-phase-e-decision/part-01-of-01-phase-e-defer-and-phase-f-kickoff.md`
        `docs/conversation-history/2026-05-14-to-05-15-full/part-04-of-09-37cd69d1-prompt-session.md`
        `docs/conversation-history/2026-05-22/ssp-edit-in-project/25ad9e8d-docs-features-ssp-edit-in-project-handof.md`
        `docs/conversation-history/2026-05-22/ssp-import-export-phase2/7bed5ff9-session-docs-features-ssp-import-ex.md`
      - **distinct token 只有 2 個、不是 4 個**：一個前綴 `glpat-fake…`（**這把是假值／範例，不是真憑證**，6 個檔）、一個前綴 `glpat-raUg…`（2 個檔，這把才要清）。runner 用 `git grep -hoIE 'glpat-[A-Za-z0-9_-]{15,}' | sort -u` 自己取出兩個值比對。
      - **GitHub 側零命中**：`git grep -lIE 'ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|gho_[A-Za-z0-9]{30,}'` → **0 檔**；`glrt-` / `gldt-` 亦 0。所以「4 把」裡至少 2 把在本 repo 找不到——**可能在套件 monorepo 或已被清掉**。⚠️ **套件 monorepo 也查了：`glpat-` 零命中。** 建議開卡時註明「4 把只找到 1 把真值＋1 把假值，其餘 2 把查無，請 M15 報告作者或 CM-1607 工單補位置」。
      - 另有 env var 名的大量命中（`GITLAB_PRIVATE_TOKEN` 152 處、`GITHUB_PRIVATE_TOKEN` 195 處）——**那是變數名不是值，不必清**。
    - 功能不能壞：這些字串本身已失效，清除不影響任何執行中功能；只要小心別誤刪同一份文件裡其他還有效的段落。
  - **#11 一個測試設定檔裡的四把對外 AI 服務金鑰（工單 CM-1607）**
    - 修法：金鑰已撤銷換發，清掉殘留字串。
    - 🔴 入口清單（已查證）：
      - **原檔 `.env.test` 已刪**（commit `5746cef1`，2026-09-08）——**要清的是對話紀錄裡的殘留**。
      - grep 指令：`git grep -lIE 'sk-ant-[A-Za-z0-9_-]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|AIza[A-Za-z0-9_-]{30,}'`
      - **命中 9 個檔**（與 FR-079 B2 F2 報的「九檔」一致）：
        `docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-01-of-03.md`（B2 報告點名的精確位置是此檔 `:3651`）
        `docs/conversation-history/2026-05-19/ssp-import-export-phase2-A0.1/e500cf61.md`
        `docs/conversation-history/2026-05-27/participant-picker-framework-drive-fixes/0043-617066cd-session-projectplannin.md`
        `docs/conversation-history/2026-05-29/conversation-history-housekeeping/1035-c5c2e417-5-28.md`
        `docs/conversation-history/2026-05-31/evidence-classify-reports/1029-d0b63dd1-feature-ai-analysis-result-landing.md`
        `docs/conversation-history/2026-06-02/db-erd-tooling-convergence/2024-d2a67903.md`
        `docs/conversation-history/2026-06-04/erd-db-diagram/2024-d2a67903-db-db-diagram-mcp.md`
        `docs/conversation-history/2026-06-14/fr038-oscal-redesign-wave1/2034-3fc2f2b1-oscal-v1-2-2-schema-c.md`
        `docs/conversation-history/2026-06-14/fr038-oscal-redesign-wave1/2034-6219587d-oscal-v1-2-2-schema-c.md`
      - **四家分別是**：Anthropic（`sk-ant-…`，1 個 distinct）／OpenAI（`sk-proj-…`，1 個）／Google（`AIza…`，1 個）／LangChain LangSmith（`lsv2_pt_…`，**本棒未單獨 grep，runner 補跑** `git grep -lIE 'lsv2_(pt|sk)_[A-Za-z0-9]{20,}'`）
      - ⚠️ **`AIza…` 那把與現行 `.env` 的 `GOOGLE_API_KEY` 完全一致**（本棒比對過，命中 9 檔）——**Google 這把很可能沒真的換**。B2 報告也寫「commit `5746cef1` 宣稱已撤銷，但那次只刪了 `.env.test`，撤銷與否無法從程式碼查證」。**開卡務必寫「清字串前先到 Google Cloud Console 實查這把還有效嗎」**。Anthropic／OpenAI 那兩把與 `.env` 不同（`.env` 內的值在版控零命中），這兩把可信已換。
    - 功能不能壞：純字串清理（原設定檔已不存在）。⚠️ 但若 Google 那把確認仍有效，清字串**不等於補洞**，要回報決策者決定要不要換發。
  - **#72 系統管理員帳號密碼寫死在腳本裡（工單 CM-1608）**
    - 修法：清掉腳本裡「環境變數沒設就用這個密碼」的預設值寫死字串。**這件不只是清字串、是程式修正**：改成「沒設就報錯」而不是「沒設就靜默用真密碼」。
    - 🔴 入口清單（已查證）：
      - grep 指令：先從 `scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py:80` 取那個 12 字元的預設值（前 4 碼 `Bill`），再 `git grep -lIF -- '<該值>'`
      - **腳本實數是 5 支、不是 3 支**（材料寫「三支腳本」偏少，開卡要照實寫 5 支）：
        - `scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py:80` — `SEED_PASS = os.getenv("FR062_SEED_PASS", "<真密碼>")`（B2 報告 F14 點名的就是這支；`:127` 拿它去打真的 `/login`）
        - `scripts/seed_2026-08-01_fr059_detection_profiles.py:65` — `SEED_PASS = os.getenv("FR059_SEED_PASS", "<真密碼>")`（`:150` 同樣打 `/login`）
        - `scripts/e2e_test_module_frame_with_docx.py:38` — `TEST_PASS = "<真密碼>"`（**連 env var 都沒有，純寫死**）
        - `scripts/smoke_test_ssp_docx_parser.py:34` — `TEST_PASS = "<真密碼>"`（純寫死）
        - `scripts/smoke_test_ssp_docx_preselect.py:34` — `TEST_PASS = "<真密碼>"`（純寫死）
        - 分兩種修法：**前兩支**拿掉 `os.getenv` 的第二個參數，未設時 `raise` 並印「請設 FR0xx_SEED_PASS」；**後三支**改成 `os.environ["..."]`（缺就自然炸）或同樣 getenv+raise。
      - **同一組密碼的其他殘留（純清字串那半）**：**共 151 個受版控檔案**，分布：
        `docs/conversation-history/` 63 檔／`docs/features-site/` 39 檔／`docs/features/` 38 檔／`docs/claude/` 4 檔／`docs/system-design/` 2 檔／`scripts/` 5 檔（即上面那 5 支）
        `docs/claude/` 那 4 檔要逐一改（是 memory，會被每個 session 讀）：
        `docs/claude/memory/reference_dev_login.md:11`／`:12`（登入帳密對照表）
        `docs/claude/memory/feedback_playwright_screenshot_for_blind_fe.md:17`
        `docs/claude/memory/project_fr039_distributed_file_agent.md:16`
        `docs/claude/memory/reference_architecture_quick_notes.md:36`
      - ⚠️ **這組密碼與現行 DEV 環境的登入密碼一致**（`docs/claude/memory/reference_dev_login.md` 就是現行對照表，帳號 `blsadmin` / `blsit`）——**「已換發」的說法對不上**。開卡要寫「清字串前先確認 DEV 這組帳密換過沒有」。
      - 🔴 **`docs/features-site/` 那 39 檔是 MkDocs 產出的副本與 HTML**——**不要手改**，改完來源 md 重跑 build 即可（M23 報告「網頁版不用手改」講的就是這個）。
    - 功能不能壞：改成「沒設環境變數就報錯」後，**要手測確認有設環境變數的正常路徑（DEV/CI）不會被擋**——安全措施不能擋正常操作者。那五支腳本至少各跑一次「有設變數」的路徑。
  - **#73 套件倉庫與檔案儲存服務帳密寫在一份交接文件裡**
    - 修法：帳密已換發，清掉該交接文件裡的殘留字串（M17 與 M15 模組頁引用的是同一份，清一次即可）。
    - 🔴 入口清單（已查證，FR-079 B2 F3 給的位置核對成立）：
      - **來源檔**：`docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md:89`（Nexus 私有 PyPI 帳密，`admin/<pw>` 形式）＋ 同檔 `:59`（一條 `UPDATE public.…` 指令裡嵌著 MinIO 的 access key / secret key / endpoint `192.168.50.171:9002`）
      - grep 指令：`git grep -lIF -- 'admin/<那個密碼>'` → **6 個檔**：
        `docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md`（來源）
        `docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.html`（同目錄產出的 HTML）
        `docs/features-site/docs/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md`（站台鏡射副本）
        `docs/features-site/site/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff/index.html`（站台產出）
        `docs/features-site/site/search/search_index.json`（**MkDocs 搜尋索引，憑證原文也在裡面**——最容易漏的一個）
        `docs/conversation-history/2026-06-19/fr039-agent-auth-v2/2331-5f8f5906-session-docs-features-fr-039.md`
      - 只 grep 密碼部分（不含 `admin/` 前綴）→ **13 個檔**（多出來的 7 檔是那組密碼單獨出現的地方，runner 要一起看）
      - 🔴 **處理順序**：先改 `docs/features/…handoff.md` 來源 → 重跑 `doc-site-build` 產 HTML 與 features-site（**含 search_index.json**）→ 最後清 conversation-history。**只改 md 不重 build，HTML 與搜尋索引裡的原文還在。**
      - ⚠️ 那份文件自己註明「憑證檔是 gitignored」——但**被寫在文件裡的那組憑證本身就在 commit 裡**，這句註記是誤導，清的時候順手改掉。
    - 功能不能壞：純文件清理，無程式邏輯風險。但 Nexus 這把是供應鏈的根（有發布權就能推含後門的 `jedi-common`），**若查出還沒換發要立刻回報**。
  - **#122 測試設定檔裡的簽發登入憑證金鑰＋資料庫密碼＋檔案儲存金鑰（工單 CM-1607；決策者已裁定開發環境那把簽發金鑰不需更換）**
    - 修法：清殘留字串（簽發金鑰那把決策者裁「不需更換」，只清字串）。
    - 🔴 入口清單（已查證）：
      - **簽發登入憑證金鑰（JWT）**：從 `.env` 的 `JWT_SECRET_KEY` 取值（36 字元，前 4 碼 `b542`）→ `git grep -lIF -- '<值>'` → **30 個檔，全在 `docs/conversation-history/`**（與 FR-079 B2 F12 報的「34 處」一致——那是行數，檔數 30）。B2 點名的精確位置：`docs/conversation-history/2026-05-20/ssp-import-export-phase2/1802c4fb-a3-writing-plans.md:6908`
        ⚠️ **與現行 `.env` 完全一致、是還在用的金鑰**（B2 已確認，本棒複核仍一致）。決策者裁「DEV 這把不換」的理由是 `scripts/installer/install.sh:1252` 每套安裝各自跑 `openssl rand -base64 32`（`gen_key` 在 `:211`），**外流的只是我們開發機那把、不會跟著出貨**。
      - **資料庫密碼**：同第 4-2 卡那組（`cmmgr`／`cm_app`），**不要在這張卡重複清**——4-2 卡負責。
      - **檔案儲存金鑰（MinIO）**：同 #73 那份 handoff 內的那組，**由 #73 那半負責**，本件不重複。
      - **另一把同形狀的**：`.env` 的 `TURNSTILE_SECRET_KEY`（前 4 碼 `1x00`）→ **30 個檔**（28 檔在 conversation-history、`config/` 1 檔、repo 根 1 檔）。⚠️ **`1x00…` 是 Cloudflare 官方公開的測試用 dummy key（永遠通過），不是真憑證** —— 本件**不必清**，但開卡要註明「查到但判定非憑證」，免得 runner 白做或下次掃描再撈出來當假陽性。
      - **本棒確認零殘留、不必處理的**：`ANTHROPIC_API_KEY`（版控 0 檔）／`DETECTION_TOOL_ENCRYPTION_KEY`（0 檔）／`AI_PROVIDER_ENCRYPTION_KEY`（0 檔）
    - 功能不能壞：純字串清理。**簽發金鑰那把本機 `.env` 與部署文件裡還在用，只清版控對話紀錄裡的殘留、不要動 `.env`**（動了所有人當下的登入 token 全失效）。
- **這張卡的手測總清單**（批次驗：五件一次清完、一次驗完，不要逐件來回）：
  - 五件的憑證值各自 `git grep -c -I -F -- '<值>'` 零命中（值自己從 `.env` 或報告位置取，**不要寫進卡片或 commit message**）
  - **五支腳本（#72）各跑一次「有設環境變數」的正常路徑**，確認沒被新加的報錯擋住；再各跑一次「沒設」確認會明確報錯而非靜默用真密碼
  - #73 的處理順序驗收：改完 `docs/features/FR-039*/handoff/*.md` → 重跑 `doc-site-build` → 再 grep 一次，確認 `.html`、`docs/features-site/docs/`、`docs/features-site/site/` 與 **`search_index.json`** 四處都乾淨
  - `docs/claude/memory/` 那 6 份（#72 的 4 份＋#12 的 2 份）逐一開檔確認密碼改成「請查 `.env`」而非整段刪掉（那些是還在用的操作說明）
  - 抽查 2 個被清過的對話紀錄檔，確認憑證以外內容沒被誤刪
- **需決策者先裁的**：
  - D-b4-1：清版控殘留字串範圍要不要含「git 歷史」（用 `git filter-repo` 之類改寫 history）還是只清「現存檔案」？改寫歷史風險較高（會動到所有人的 commit hash）。我的建議：這批只清現存檔案的殘留字串，不改寫 git 歷史（憑證本身已失效，改歷史的成本與風險不成比例）。

## 卡 4-2：資料庫管理員密碼（250 檔殘留＋堵住還在寫入的那 1 支產生器）
- **範圍**：**BE 主專案**（已查證實數 **250 檔**，全在 BE；**套件 monorepo 零命中**）；worktree 名建議 `wt-fix-b4-db-admin-pwd`；涵蓋 #12（M23-1 主案，合併 M16-7＝M17-8 兩個具體實例）
- **每件的修法**（白話，一件一小節）：
  - **#12 資料庫管理員密碼（可繞過所有客戶隔離）散在 249 個版控檔案裡，其中一份湊成可直接複製貼上的連線指令**
    - 修法：模組頁三步驟裡的「先堵住再寫進去的路」與「清掉已經寫進去的」這兩步材料標「未修」——這張卡要做的是：① 找出現在還有哪個地方會把這組密碼繼續寫進新的版控檔案，把那個來源改成不寫死密碼（改用「密碼請查 `.env`」的占位字樣，照 CLAUDE.md 憑證規範）；② 清掉既有檔案裡的殘留字串。
    - 🔴 入口清單（已查證）：
      - **取值方式**（runner 自己做，不要貼進任何文件）：`.env` 的 `DB_PASSWORD`（9 字元，前 4 碼 `jedi`）。**`REDIS_PASSWORD` 與它是同一組值**（M23 報告說「這組密碼同時也是快取服務的密碼」，本棒比對確認兩者相同）——所以清一次就兩件一起解決。
      - grep 指令：`git grep -lIF -- '<該值>'` → **250 個受版控檔案**（M23 報告說 249，本棒重數 250，差 1 檔屬正常漂移）
      - **分布**：`docs/conversation-history/` **112 檔**／`docs/features/` **67 檔**／`docs/features-site/` **65 檔**／`docs/issues/` 2 檔／`docs/claude/` 2 檔／`docs/system-design/` 1 檔／`docs/analysis/` 1 檔
        （行數層級：`git grep -c -I -F -- '<值>'` 加總 **2,012 行**）
      - 🔴 **「還在寫入的那條路」找到了，只有 1 支程式**：
        **`docs/system-design/scripts/generate_db_schema_docx.py:34`** — `user="cm_app", password="<真密碼>"` 直接寫死在連線參數裡。這支是**產生資料庫結構 DOCX 的產生器**，每跑一次就把密碼帶進產出。**這就是 M16 第 7 條「一支產生文件的腳本把完整資料庫連線資訊寫死」那支。**
        修法：改讀 `os.environ`（缺就 raise），或沿用 repo 既有 `.env` 讀法。
      - **`docs/claude/` 那 2 檔（memory，每個 session 都會讀，優先清）**：
        `docs/claude/memory/project_drive_sync_test_data.md:18`（`192.168.50.188:25432 / guidant_ai_stg / cm_app / <pw>`）
        `docs/claude/memory/project_ssp_doc_parser_progress.md:101`（`cm_app + cmmgr 同密碼 <pw>`）
      - **M23 報告點名「湊成可直接複製貼上的連線指令」那份**：`docs/analysis/2026-05-28-poc-db-migration-plan.md:73`（`export PGPASSWORD='<pw>'`）＋ `:95`／`:99`（另兩處文字描述）。**FR-079 B2 F1 也是指這一檔一行**，優先處理。
      - 🔴 **`docs/features-site/` 那 65 檔不要手改**——MkDocs 產出的鏡射副本與 HTML（含 `docs/features-site/site/search/search_index.json`，**憑證原文也在搜尋索引裡**）。改完來源 md 重跑 `doc-site-build` 即可。`docs/features/` 下的 `.html` 同理。
      - **其他環境的同組密碼（唯讀盤點，不在本卡動）**：M23 報告寫這組密碼**四處通行**——DEV、出貨基線庫（`188:25432 / guidant_ai`）、以及兩座「已退役但還連得進去」的舊庫（`188:25432/guidant_ai_stg`、`189:25432/guidant_ai_poc`）。**換密碼與關掉舊庫屬環境異動，要決策者明示，不在本卡**。
    - 功能不能壞：改 `generate_db_schema_docx.py` 後要**實跑一次產生 DOCX 的流程**，確認只是密碼來源換了、產出內容不變。
- **這張卡的手測總清單**（批次驗）：
  - 清完後 `git grep -c -I -F -- '<該值>'` 零命中（含 `docs/features-site/site/search/search_index.json`）
  - `docs/system-design/scripts/generate_db_schema_docx.py` 實跑一次（有設環境變數的正常路徑），確認 DOCX 產得出來且內容不變；再跑一次「沒設環境變數」確認會明確報錯而不是靜默用預設
  - 重跑 `doc-site-build` 後再 grep 一次（驗 features-site 與 HTML 都乾淨）
  - 抽查 2 個被清過的 md，確認憑證以外的內容沒被誤刪
- **需決策者先裁的**：
  - **D-b4-2（原問題已解，改成新的）**：盤點已由本棒做完（250 檔清單與唯一寫入來源都在上面），原「要不要先盤點」的問題取消。**新的問題是**：這組密碼**現在到底換過沒有？** M23 報告把它列在「已換發」那 8 件裡，但 `.env` 現值與版控殘留值**完全一致**——若沒換，清版控只是打掃、洞還在（而且這個帳號是 `BYPASSRLS`、四處通行）。建議：**先確認換沒換再決定這張卡的性質**（打掃 vs 堵漏）；換密碼本身屬環境異動（要動出貨基線庫與兩座舊庫），必須決策者明示放行，不在本卡。

## 卡 4-3：移除掃描派工資料表裡的明文帳密殘留（程式修正）
- **範圍**：套件 `jedi-detection`（M03）；worktree 名建議 `wt-fix-b4-detection-plaintext`；涵蓋 #70
- **每件的修法**：
  - **#70 按下「開始掃描」時，解密後的客戶主機帳密會多存一份明文進派工資料表，從頭到尾沒有程式讀過，且無限累積**
    - 修法：刪掉寫入那一行（已確認代理程式讀的是另一份當場重新解密的，刪掉不影響代理程式），再補一支清掉既有存量殘留資料的腳本／migration。
    - 🔴 入口清單（已查證，file:line）：
      - **要刪的那一行**：`jedi-detection/jedi_detection/app/service/detection_orchestration_service.py:480`
        ```
        params["_credentials"] = creds
        ```
        在 `_dispatch_one()`（`:438-517`）內，緊接在 `:478-479` 的 `source_file` 處理之後、`:485-493` 的 `create_task()` 之前。`params` 整包被塞進 `agent_tasks.params`（JSONB），**所以明文帳密就落在那張表裡**。
      - **creds 是怎麼來的**：`:215-217` 呼叫 `_resolve_credentials()`（定義在 `:1397-1430`，`:1429` 是 `json.loads(self._crypto.decrypt(config.credentials_encrypted))`）。另一條路徑 `:942-947`（`start_assignment_now` 那支重跑流程）也拿了 `creds` 並在 `:980` 傳進同一個 `_dispatch_one` → **同樣會經過 `:480`**。🔴 **所以是「一行寫入、兩條路徑都會走到」，刪那一行兩條一起解決；不要以為要改兩處。**
      - **零讀取查證**：`cd ~/Projects/Jedicogy/module/jedi-python-package && grep -rn '_credentials' --include="*.py" . | grep -v /.venv/ | grep -v /.claude/worktrees/ | grep -v credentials_encrypted`
        → 命中只有：`:215`／`:218`／`:480`（寫入側）＋`:603`／`:605`／`:799`／`:2273` 四處**註解**提到它＋`:1370`／`:1391`／`:1395` 的 `_tool_requires_credentials`（不同東西）。**沒有任何地方讀 `params["_credentials"]`。**
        BE 側 `grep -rn '"_credentials"' --include="*.py" .` → **0 命中**。
      - 🔴 **代理程式真正拿到帳密的路徑（證實刪掉不影響 agent）**：BE `infra/remote_agent/adapter/detection_task_payload_provider.py:68`
        ```
        "credentials": self._resolve_tool_credentials(t.detection_tool_id, tenant_id),
        ```
        →`_resolve_tool_credentials()` 定義在同檔 `:109-125`，`:124` 自己做一次 `json.loads(self._crypto.decrypt(config.credentials_encrypted))`。**這是心跳組裝時當場重新解密的第二份，與 `agent_tasks.params._credentials` 無關。** 套件 `:799` 的註解也寫明「改由心跳組裝時當場注入（與 `_credentials` 同時機…）」——**設計上早就分開了，`:480` 那行是舊的殘留**。既有測試 `test/test_agent_enrollment_service.py:376` `test_heartbeat_attaches_pending_tasks_with_decrypted_credentials` 測的就是這條活路徑。
      - **要清的存量資料**：表 `agent_tasks` 的 `params` JSONB 欄（model 在 `jedi-remote-agent/jedi_remote_agent/infra/agent_task/model/agent_task.py:45`）。清法：`UPDATE ... SET params = params - '_credentials' WHERE params ? '_credentials'`（JSONB 刪 key，不動其他欄位）。**先在 DEV 跑；STG/POC 要決策者明示放行**（屬寫入類環境異動）。
      - ⚠️ **清理前先數一次**（唯讀）：`SELECT count(*) FROM agent_tasks WHERE params ? '_credentials';` 在 DEV 跑，把數字寫進 Notion 卡當驗收基準。
    - 功能不能壞：刪 `:480` 前確認 agent 走的是 `detection_task_payload_provider.py:68` 那條（上面已證實）——手測要含「DEV 正常跑一次掃描，代理程式仍能正確拿到主機帳密並執行完成」。
  - 清理既有殘留資料的腳本：對照「資料庫寫入類異動」規範，這支清理腳本若要在 STG/POC 跑，要走決策者明示放行；DEV 可以先跑。
- **這張卡的手測總清單**（批次驗）：
  - DEV 跑一次完整「開始掃描」流程，確認 `agent_tasks.params` **不再出現 `_credentials` key**：`SELECT count(*) FROM agent_tasks WHERE params ? '_credentials';` 應為 0（新單）
  - 同一輪確認**代理程式仍拿得到帳密並執行完成**（掃描跑到有結果，不是卡在「缺欄位」）
  - 再跑一次「立即開始／重跑」那條路徑（走 `start_assignment_now` → 同一個 `_dispatch_one`），確認也乾淨
  - 執行清理腳本後，DEV 該表既有殘留歸零（與開工前數的基準數字對照）
  - 跑一次套件既有測試（jedi-detection repo 內與 orchestration 相關的 test file）＋ BE `pytest test/test_agent_enrollment_service.py`（心跳帶憑證那支，驗活路徑沒被弄壞）
- **需決策者先裁的**：
  - **清理腳本要不要套 STG／POC**：DEV 可以先跑；STG／POC 屬寫入類環境異動，**要決策者當次明示**（CLAUDE.md 環境異動鐵律）。建議：先只清 DEV，STG／POC 等整批修正上版時一起放行。

## 卡 4-4：Google 雲端硬碟金鑰——清殘留字串（金鑰重設另待決策）
- **範圍**：**BE 主專案**（已查證：14 檔全在 `docs/conversation-history/`，**套件 monorepo 零命中**）；worktree 名建議 `wt-fix-b4-gdrive-keys`；涵蓋 #74
- **每件的修法**：
  - **#74 Google 雲端硬碟應用程式密鑰與加密金鑰，散在 15 個版控檔案裡，是 Google 後台唯一一組、所有客戶環境共用**
    - 修法：這件**跟這批其他件不同——密鑰本身還沒重設**（模組頁狀態「未修」，不在「已撤銷換發」的 6 件之列）。這張卡先做「清掉這 15 個檔案裡的殘留字串」這半段（跟其他清字串卡同性質）；**金鑰本身重設與「先寫好重新加密既有通行證的程式」這半段，材料裁定要先有重新加密程式才能換鑰，範圍較大、且要決策者明確指示才能做（比照 CLAUDE.md 環境異動鐵律「換密碼要決策者裁示」），這張卡不包含這半段**。
    - 🔴 入口清單（已查證；落點是 **BE 主專案，不是套件**）：
      - **取值方式**（runner 自己做）：`.env` 的三個 key
        - `GOOGLE_DRIVE_OAUTH_CLIENT_SECRET`（35 字元，前 4 碼 `GOCS`）→ **12 檔**
        - `DRIVE_TOKEN_ENCRYPTION_KEY`（前 4 碼 `0JGh`）→ **10 檔**
        - `GOOGLE_DRIVE_OAUTH_CLIENT_ID`（前 4 碼 `1051`，嚴格說是識別碼不是密鑰，但它與 secret 成對、一起清比較乾淨）→ **14 檔**
      - grep 指令：`git grep -lIF -- '<各值>'`
      - **應用程式密鑰＋加密金鑰的聯集 = 14 檔**（M23 說 15，本棒實數 14，差 1 屬漂移）；**再加 client_id 的聯集 = 17 檔**。全部落在 `docs/conversation-history/`，清單：
        `2026-04-21-google-drive-integration/…-part-03-of-14.md`
        `2026-04-21-google-drive-integration/…-part-05-of-14.md`
        `2026-04-21-google-drive-integration/…-part-08-of-14.md`
        `2026-04-21-google-drive-integration/…-part-09-of-14.md`
        `2026-04-28-to-04-30-survey-answer-arc/part-verbatim-01-of-03.md`
        `2026-05-19/ssp-import-export-phase2-A0.1/e500cf61.md`
        `2026-05-22/ssp-import-export-phase2/7bed5ff9-session-docs-features-ssp-import-ex.md`
        `2026-05-27/participant-picker-framework-drive-fixes/0043-617066cd-session-projectplannin.md`
        `2026-05-29/conversation-history-housekeeping/1035-c5c2e417-5-28.md`
        `2026-05-31/evidence-classify-reports/1029-d0b63dd1-feature-ai-analysis-result-landing.md`
        `2026-06-02/db-erd-tooling-convergence/2024-d2a67903.md`
        `2026-06-14/fr038-oscal-redesign-wave1/2034-3fc2f2b1-oscal-v1-2-2-schema-c.md`
        `2026-06-14/fr038-oscal-redesign-wave1/2034-6219587d-oscal-v1-2-2-schema-c.md`
        （client_id 另外多 3 檔，runner 用 `git grep -lIF` 自取）
      - **套件側零命中**：`cd ~/Projects/Jedicogy/module/jedi-python-package && git grep -lIF -- '<各值>'` → 0。**所以是 BE 主專案獨有，落點確定。**
      - **好消息**：**全部是 `docs/conversation-history/` 底下的對話紀錄，沒有一個是現行正式設定檔**。程式實際讀值走的是 `.env` → `config/`（`GOOGLE_DRIVE_OAUTH_CLIENT_ID` / `_SECRET` / `DRIVE_TOKEN_ENCRYPTION_KEY` 三個環境變數），**清對話紀錄完全不會動到執行期路徑**，風險極低。
      - ⚠️ **與 `.env` 現值完全一致**（本棒比對）——**這把確實還沒換**，與 M23 第 3 條「未修」狀態相符。所以這張卡只是「打掃」，**洞還在**，要等金鑰重設那半（見 D-b4-3）。
    - 功能不能壞：純清對話紀錄字串。**不要動 `.env`、不要動 `config/` 下讀取那三個環境變數的程式**（金鑰本身沒換，執行期要維持能讀到）。
- **這張卡的手測總清單**（批次驗）：
  - 三個值各自 `git grep -c -I -F -- '<值>'` 零命中
  - 手測一次「連接 Google 雲端硬碟」（授權一次、上傳／讀一個檔），確認清字串沒動到執行期讀取路徑
  - 抽查 2 個被清過的對話紀錄檔，確認憑證以外內容沒被誤刪
- **需決策者先裁的**：
  - D-b4-3：Google 金鑰重設＋重新加密既有通行證的程式，要不要現在排進這批（第 4 批）或另開一批／另一張卡處理？我的建議：這件密鑰本身还沒撤銷，性質跟這批其他「已換發只剩清字串」的件不同、範圍也更大（要先寫重新加密程式），建議切成獨立一張卡另外排（可以緊接在這張清字串卡後面做，但不要混在同一張卡裡拖慢驗收）。

---

## 決策者要裁的事（彙整本批的 D）
- **D-b4-1**：清版控殘留字串範圍要不要含 git 歷史（改寫 history）？建議：只清現存檔案，不改寫歷史。
  - 🔴 **本棒補一個新事實影響這條判斷**：`docs/features-site/site/search/search_index.json` 這個 MkDocs 搜尋索引裡有多把憑證原文。它是**產出物**，只要重跑 `doc-site-build` 就會乾淨，**不需要改寫 git 歷史**。同理 `docs/features/*/*.html` 與 `docs/features-site/` 下 100 多檔鏡射副本都是「改來源 md ＋ 重 build」就好。所以「只清現存檔案」的成本比原估低很多。
- **D-b4-2（原問題已解，換成新的）**：原問題是「249 檔清單要誰盤點」——已由本棒盤完（清單在 4-2 卡內），該問題取消。
  **新問題**：**這批標「已撤銷換發」的憑證，有三把與現行 `.env` 完全一致，到底換過沒有？**（資料庫管理員密碼、系統管理員登入密碼、Google AI 服務金鑰）
  若沒換，這三件就不是「打掃」而是「洞還在」，清字串只解決一半。建議：**開卡前先各實查一次**（資料庫那把試連一次即知；Google 那把到 Cloud Console 看；系統管理員那把用 DEV 登入試一次），再決定卡片性質與優先序。**換密碼本身屬環境異動**（要動出貨基線庫與兩座舊庫），必須決策者明示放行，不在這批。
- **D-b4-3**：Google 金鑰重設＋重新加密程式要不要現在排進這批？建議：切獨立一張卡另外排，不要混進這張清字串卡。
  - 本棒補查證實：**這把確實還沒換**（與 `.env` 現值一致），且 14 檔殘留全在對話紀錄、**沒有一個是現行設定檔**，所以「清字串」那半風險極低、可以先做；「重設金鑰」那半照原建議另開卡。
- **D-b4-4（新增）**：#10 那「4 把外部程式碼平台通行證」**只找到 1 把真值**（另 1 把是 `glpat-fake…` 範例值，其餘 2 把在 BE 與套件 monorepo 都零命中，GitHub 系列的 token 前綴也全零命中）。要不要請 M15 報告作者或 CM-1607 工單補出另 2 把的實際位置？建議：**先照查到的 1 把清掉**，另 2 把標「查無、待補位置」，不要為了湊數字擋住這張卡。
