Author SHA1 Message Date
ai-review-bot 941d763129 chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Codex) (pull_request) Failing after 24s
CI / TEST (Claude) (pull_request) Failing after 28s
CI / TEST (Antigravity) (pull_request) Failing after 33s
2026-07-21 06:27:43 +00:00
Jeffery 9e7f8a2a0a chore(ai-review 狀態): 回寫本輪已處理 findings
node-actions/template: CI / BUILD (pull_request) Successful in 3s
CI / TEST (Claude) (pull_request) Successful in 27s
CI / TEST (Antigravity) (pull_request) Successful in 48s
CI / TEST (Codex) (pull_request) Successful in 3m18s
2026-07-21 14:24:14 +08:00
Jeffery 8b50a0d643 test(ai-review): 補上建問題模式結果檔測試 2026-07-21 14:24:10 +08:00
Jeffery 555611db07 fix(ai-review): 避免建問題模式嚴重結果 fail-open 2026-07-21 14:24:04 +08:00
ai-review-bot 24b248884f chore: update ai-review findings [ai-review-bot][failure]
node-actions/template: CI / BUILD (pull_request) Successful in 4s
CI / TEST (Claude) (pull_request) Failing after 26s
CI / TEST (Codex) (pull_request) Failing after 32s
CI / TEST (Antigravity) (pull_request) Failing after 46s
2026-07-21 05:50:00 +00:00
7 changed files with 664 additions and 128 deletions
+209
View File
@@ -1582,5 +1582,214 @@
"endLine": 370, "endLine": 370,
"problem": "建問題模式下把 `others` 交給 `review.postOthersToIssue` 逐條發 issue 留言,這是在拿遠端 API latency 燒時間。警告/建議通常可能比嚴重問題多很多,若有 50 條、每次 Gitea API 往返 200ms,光留言就可能多花 10 秒以上,還會增加 rate limit 壓力。", "problem": "建問題模式下把 `others` 交給 `review.postOthersToIssue` 逐條發 issue 留言,這是在拿遠端 API latency 燒時間。警告/建議通常可能比嚴重問題多很多,若有 50 條、每次 Gitea API 往返 200ms,光留言就可能多花 10 秒以上,還會增加 rate limit 壓力。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出建問題模式將警告/建議逐條發成 issue 留言,會造成 O(n) 遠端 POST、增加 CI 時間與 rate limit 壓力。" "reason": "Paladin:可排除(重複)。歷史 finding 已指出建問題模式將警告/建議逐條發成 issue 留言,會造成 O(n) 遠端 POST、增加 CI 時間與 rate limit 壓力。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "警告",
"file": "action.yml",
"startLine": 19,
"endLine": 24,
"problem": "文件鼓勵呼叫端傳入「能觸發 CI 的 PAT」。這把原本自動 token 的防遞迴與範圍限制換成長效、較高權限的秘密。若 workflow 會在不受信任的 PR 程式碼或可被 PR 修改的 action 版本上執行,攻擊者可以在 CI 中讀取或外送 PAT,接著用它 push commit、改 issue、或觸發更多 workflow。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 action.yml 建議使用可觸發 CI 的 PAT 所帶來的長效高權限憑證風險提出相同問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "action.yml",
"startLine": 2,
"endLine": 3,
"problem": "檔頭 `更新時間` 仍寫著 `2026/07/17 18:49:58`,但本次變更描述顯示此檔最後更新為 `2026/07/20 17:24:18`。文件時間戳若成了舊拍子,讀者會開始懷疑整份 manifest 的註解是否也同樣過期。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手動更新時間容易過期且與實際內容不同。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Bard",
"severity": "建議",
"file": "src/index.js",
"startLine": 7,
"endLine": 7,
"problem": "啟動 banner 的 `更新時間` 被改成 `2026/07/17 18:49:58`,但這次主流程明顯新增了建 issue、延後解決舊留言等大量段落。這個時間戳像留在舊譜上的小節號,和目前程式內容不再合拍。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動 banner 的手動更新時間與程式實際變更不同,屬同一問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 390,
"problem": "`main()` 這次把建問題模式的暫存留言、issue 建立、標籤挑選、PR 回貼、相依設定、一般模式舊留言清理全部塞進同一個流程函式。半年後要改任何一個輸出通道時,維護者必須重新推演 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds` 與步驟 2 延後執行之間的狀態關係,這會讓主流程變成難以安全修改的編排巨石。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。main() 內建問題模式狀態、留言路由、issue 建立與發布職責耦合,已由既有排除事項與歷史 findings 涵蓋。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 199,
"endLine": 232,
"problem": "`queueOrPostComment` 透過閉包同時讀寫 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds`,回傳型別也會依狀態變成 `Object|null`。這種隱性狀態機短期能跑,但未來要追「這則留言到底會發到 PR、issue,還是只進 buffer」時,必須靠讀整個 `main()` 的執行順序才能理解。",
"reason": "Paladin:可排除(重複)。queueOrPostComment 的閉包狀態與留言目的地隱性分流,與既有 main() 留言路由、issue 狀態與緩衝佇列耦合問題相同。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Leo",
"severity": "建議",
"file": "readme.md",
"startLine": 76,
"endLine": 131,
"problem": "README 的功能列表繼續維護大量硬編碼到 `develop` 分支與精確行號的連結。這次 diff 已經為了行號漂移更新一整片表格;之後任何新增註解、移動函式或插入測試都會讓文件連結過期,維護成本會隨每次重構累積。",
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 develop 分支與精確行號連結,會隨程式碼移動失準。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Mage",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 409,
"endLine": 418,
"problem": "建問題模式下若有嚴重問題但 `exclusions.json` 沒變更,`filesToCommit` 會是空陣列,流程會走到「略過 commit/push」後直接 `return 0`。最小重現:`create-issue=true`、攻擊方保留 1 條 `severity: '嚴重'`、防守方沒有排除任何 finding,因此 `exclusionsChanged=false`。此時本輪檢查成功,且沒有推出 `[ai-review-bot][failure]` commit 觸發下一輪步驟 1 回報失敗;若 issue dependency API 未啟用或設定失敗,嚴重問題不會阻擋合併。",
"reason": "Paladin:可排除(重複)。與 F001 指涉同一個建問題模式下嚴重 finding 可能未產生可阻擋失敗訊號、最後回傳成功的問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 323,
"endLine": 390,
"problem": "建問題模式的核心流程新增了多個可觀察行為,但目前測試沒有驗證:有保留問題才建立 issue、無保留問題靜默通過、暫存的工具/diff/角色留言會 flush 到 issue、PR 只回貼 issue 連結、且只有嚴重問題才呼叫 `addIssueDependency`。這些都是本次 PR 新增/改動的主要行為,沒有試煉過就等於還沒完成。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式有問題才建 issue、無問題靜默通過、暫存留言 flush、PR 回貼與嚴重問題相依設定等核心分支缺少測試。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 190,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每步後重試 merge-base、淺層 repo 才 `--unshallow`、以及失敗診斷彙整,但新增測試只驗證不安全 `baseRef` 會被拒絕,沒有覆蓋這些成功與失敗路徑。這裡最怕的是淺層 checkout 或 PR head 歷史不足時,實際 CI 才發現 fetch 順序或 refspec 錯了。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、淺層/非淺層分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/21 13:49:59",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 303,
"endLine": 358,
"problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindingspushWithCredential 的 token 認證推送路徑、推送目標、錯誤遮蔽與安全行為缺少測試。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Assassin",
"severity": "嚴重",
"file": "src/lib/diagnostics.js",
"startLine": 23,
"endLine": 23,
"problem": "攻擊者只要讓 AI CLI 在 debug 模式下把 `Authorization: token <secret>` 或 `Authorization: Basic <base64>` 印到 stderr/stdout,這條遮罩規則只會遮掉 `token``Basic` 這個 scheme,後面的真正憑證仍會進 CI log。這正好打穿本檔宣稱的「安全診斷」防線,PAT 或 API token 會被長期保存並暴露給可讀 log 的人。",
"reason": "Paladin:可排除(重複)。歷史 finding 已指出除錯診斷輸出中 Authorization 遮罩規則可能只遮到 scheme、留下後方憑證,並造成 CI log 洩漏,與本條同一問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "src/index.js",
"startLine": 181,
"endLine": 239,
"problem": "`main()` 這次把「PR 留言」、「issue 暫存」、「issue 建立後 flush」三種狀態直接塞進閉包變數 `currentRunCommentIds`、`issueBuffer`、`issue` 與 `queueOrPostComment()`。未來只要再新增一種落地方式或調整留言順序,維護者必須同時理解整個主流程的時序,否則很容易出現留言漏發、舊留言誤標、或 issue 尚未建立卻被使用的狀態錯誤。這段已經不是單純編排,而是隱含一個留言發布狀態機。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布狀態機耦合的同一問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Leo",
"severity": "警告",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 這次把大量連結從 `branch/master` 改成 `branch/develop`,同時保留許多硬編碼行號。這種文件會隨分支名稱、預設分支、或任一檔案插入幾行就整批失準;未來維護者每次調整程式都得同步修幾十個 URL,否則文件看起來精準、實際上卻導到錯誤位置。",
"reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 branch 與行號深連結,造成文件與原始碼高同步成本;本條為同一問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Mage",
"severity": "警告",
"file": "src/index.js",
"startLine": 217,
"endLine": 225,
"problem": "建問題模式先建立 issue,再逐則 flush `issueBuffer`,但中途任一留言 API 失敗會直接拋出,沒有回滾或冪等處理。最小重現:`gitea.createIssue` 成功建立 #10,第一則 buffered comment 成功,第二則因 500/timeout 失敗 → 主流程 exit 1,PR 沒有 issue 連結、也沒有結果 commit;重跑時又建立 #11,留下半成品 issue #10。這會在暫時性 API 錯誤下造成重複追蹤 issue 與狀態不一致。",
"reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立後 flush 暫存留言失敗、重跑又建立重複 issue,正是已知排除事項與多筆歷史 findings 已記錄的問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 241,
"problem": "建問題模式新增了「先暫存留言、確定有保留問題才建立 issue、再 flush 暫存留言」的流程,但目前新增測試只驗到低階 helper,沒有驗證主流程在 create-issue=true 時的留言去向與靜默通過行為。這裡若順序錯了,可能會把原本應該留在 issue 的工具/diff/角色留言發到 PR,或在無保留問題時留下不該出現的 PR 留言。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 create-issue 模式中留言先暫存、issue 建立後依序寫入、無 finding 靜默通過與一般模式留言目的地缺少主流程測試。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/index.js",
"startLine": 354,
"endLine": 395,
"problem": "新增的建問題模式會依嚴重度分流:嚴重問題改發到 issue、PR 回貼 issue 連結,且只有嚴重問題才呼叫 `addIssueDependency`。目前測試沒有覆蓋這個決策矩陣,等於沒有驗證「警告/建議不阻擋合併」與「嚴重問題要建立相依」這兩個關鍵行為。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的嚴重/非嚴重分流、PR 回貼、相依 API 降級與核心分支缺少測試;本條屬同一測試缺口。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 188,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每次補抓後重試 merge-base、以及淺層 repo 才執行 `--unshallow` 的行為,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證這些成功/失敗路徑。這段是 diff 基準的核心,一旦 fallback 順序或條件壞掉,review 可能拿錯範圍或在 shallow checkout 直接失敗。",
"reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、shallow 與非 shallow 分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 331,
"endLine": 366,
"problem": "`pushWithCredential` 是這次結果 commit 能否用 PAT 觸發下一輪 CI 的關鍵,但目前沒有測試驗證它真的清掉 checkout 的自動 token、再注入 PAT Authorization,也沒有測失敗時不外洩 remote URL/token。這條失敗路徑沒被試煉過,可能讓 `[failure]` 結果 commit 無法觸發快速回報,或在錯誤訊息裡洩漏認證資訊。",
"reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredentialcommitAndPushFindings 的認證推送策略、token 注入、推送目標與失敗時不外洩 token 或 URL 的測試缺口。"
},
{
"addedAt": "2026/07/21 14:27:42",
"prNumber": 6,
"reviewer": "Maya",
"severity": "建議",
"file": "src/lib/diagnostics.js",
"startLine": 21,
"endLine": 32,
"problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。",
"reason": "Paladin:可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
} }
] ]
@@ -134,20 +134,6 @@
"suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", "suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 15 "sourceIssue": 15
}, },
{
"id": "F019",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 13,
"endLine": 72,
"problem": "`redactSecrets()` 與 `agentFailureDetail()` 是低階日誌診斷/遮罩邏輯,現在放在 `review.js` 這個負責 diff 整理與審查決策的模組頂端。這會讓 `review.js` 的職責繼續膨脹:未來若其他模組也要安全輸出 CLI 錯誤,只能複製這段或反向依賴 review 模組,邊界會越來越不清楚。",
"suggestion": "把這兩個函式搬到專門的工具模組,例如 `src/lib/diagnostics.js` 或 `src/lib/log-redaction.js`,並由 `review.js` 引入。這樣遮罩規則可集中測試與重用,`review.js` 也能維持在「審查流程資料處理」的邊界內。",
"suggestedCode": "",
"sourceIssue": 17
},
{ {
"id": "F022", "id": "F022",
"reviewer": "Leo", "reviewer": "Leo",
@@ -232,20 +218,6 @@
"suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```", "suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```",
"sourceIssue": 19 "sourceIssue": 19
}, },
{
"id": "F028",
"reviewer": "Leo",
"focus": "",
"badge": "🧰",
"severity": "建議",
"file": "src/lib/review.js",
"startLine": 14,
"endLine": 75,
"problem": "`review.js` 這次新增 `redactSecrets()` 與 `agentFailureDetail()`,但這兩個函式處理的是 AI CLI 執行失敗診斷與機密遮罩,責任更接近 `agents.js` 或共用 log/sanitize 工具。現在審查結果整理模組同時負責 diff、裁決、issue 發文與 CLI 診斷格式,模組邊界越來越鬆;之後其他地方若也要記錄 agent 失敗,很容易複製一份遮罩邏輯或反向依賴 `review.js`。",
"suggestion": "將這兩個函式移到 `src/lib/agents.js`(例如匯出 `formatAgentFailure()`),或新增 `src/lib/sanitize.js`/`src/lib/diagnostics.js`。`review.js` 只消費格式化後的錯誤摘要,避免讓審查編排模組承擔 CLI 診斷細節。",
"suggestedCode": "",
"sourceIssue": 19
},
{ {
"id": "F029", "id": "F029",
"reviewer": "Bard", "reviewer": "Bard",
@@ -302,20 +274,6 @@
"suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", "suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```",
"sourceIssue": 21 "sourceIssue": 21
}, },
{
"id": "F036",
"reviewer": "Rogue",
"focus": "",
"badge": "⚡",
"severity": "警告",
"file": "src/index.js",
"startLine": 305,
"endLine": 305,
"problem": "攻擊方沒有找出任何 finding 時,這裡還是照樣啟動防守方 `runDefenders`。空陣列沒有東西可裁決,卻會多跑一輪 AI CLI/sub agent、讀 exclusions/history、組 prompt;每個乾淨 PR 都被偷走 1 次防守方呼叫的 CPU、等待時間與 token。",
"suggestion": "在 `findings.length === 0` 時直接略過防守方裁決,令 `kept/excluded` 都是空陣列,直接進入保存結果與收尾。這不是微優化,是整輪 AI 呼叫直接歸零。",
"suggestedCode": "```\nconst { kept, excluded } = findings.length === 0\n ? { kept: [], excluded: [] }\n : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });\n```",
"sourceIssue": 22
},
{ {
"id": "F040", "id": "F040",
"reviewer": "Assassin", "reviewer": "Assassin",
@@ -0,0 +1,318 @@
{
"generatedAt": "2026/07/21 14:27:42",
"commitSha": "9e7f8a2a0a807c26e12e9bf0381e7decfb9e5568",
"prNumber": 6,
"tool": {
"name": "codex",
"version": "codex-cli 0.144.6",
"model": "gpt-5.5"
},
"findings": [
{
"reviewer": "Mage",
"focus": "logic",
"badge": "🔮",
"severity": "嚴重",
"file": "src/index.js",
"startLine": 149,
"endLine": 153,
"problem": "這個變更把「本輪審查」改成即使有嚴重問題也回傳 0,並假設後續由 `[ai-review-bot][failure]` 結果 commit 觸發下一輪 CI 再失敗。最小重現:workflow 仍依 action.yml 常見用法傳入自動 token(或任一不會觸發 synchronize CI 的 token)→ AI 找到 1 條嚴重問題 → 本輪成功建立留言與 push 結果 commit,但該 push 不觸發下一輪 → 沒有任何檢查讀到 `[failure]` commitPR 最終呈現通過。這是未驗證的外部時序假設;嚴重 finding 會被靜默放行。",
"suggestion": "不要讓失敗判定完全依賴下一輪 CI。若 `severe.length > 0`,本輪在完成留言與結果落地後仍應回傳 1;或至少提供明確 input 控制 direct-fail,並在無法驗證 token 會觸發 CI 時預設直接 fail。",
"suggestedCode": "",
"id": "F009",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。此條指控嚴重 finding 需仰賴下一輪 CI 才失敗,且自動 token 可能不觸發 CI,與既有 pushToken 測試缺口或 PAT 風險 finding 不屬同一處同一失效模式;未能排除。"
}
}
},
{
"reviewer": "Mage",
"focus": "logic",
"badge": "🔮",
"severity": "警告",
"file": "src/lib/review.js",
"startLine": 707,
"endLine": 718,
"problem": "`postOthersToIssue` 以並行方式送出多則 issue 留言時,issue 上的實際留言順序取決於 API 回應與資料庫寫入完成順序,不保證等於已排序的 findings 順序。最小重現:兩條建議分別位於 `a.js:10` 與 `a.js:20`,第二個 API 較快完成,issue 會先出現第 20 行問題,再出現第 10 行問題;使用者逐檔逐行處理時順序會錯亂。",
"suggestion": "若 issue 留言順序是介面契約,逐則 `await gitea.createCommentOnIssue(...)` 發送;若要保留並行,需在每則留言標題加入穩定序號,例如 `2/5`,讓非同步完成不破壞閱讀順序。",
"suggestedCode": "",
"id": "F011",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。歷史 findings 未涵蓋 postOthersToIssue 並行送出導致 issue 留言順序不穩定;目前也無排除事項可套用,故保留。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "action.yml",
"startLine": 22,
"endLine": 24,
"problem": "manifest 的註解忽然奏起一整段實作細節:PAT、CI 觸發、主程式步驟 1 全擠在 input 說明旁,和同檔其他「介面用途」型註解的節奏不一致。讀者只是想知道 token 該填什麼,卻被迫聽完流程旁白。",
"suggestion": "把 action.yml 留給介面契約;細節移到 README 或主流程文件。此處可濃縮成「建議使用可觸發 CI 的 PAT」即可。",
"suggestedCode": "# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。",
"id": "F002",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。已知排除事項與歷史 findings 主要涵蓋 push-token 說明重複或 PAT 安全風險;本條指向 action.yml 中 token input 註解混入主流程步驟細節,並非同一處同一問題,未能排除。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "readme.md",
"startLine": 40,
"endLine": 47,
"problem": "mermaid 圖的節點 ID 旋律走岔了:畫面標示是 3→4→5→...→8→2→9,但節點名稱卻用 S3、S4、S2 來回跳。即使流程語意想表達「步驟 2 延後」,節點 ID 與視覺順序不一致,會讓維護者在對照圖與文字時多繞一圈。",
"suggestion": "讓節點 ID 維持閱讀順序,將真正的流程步驟放在節點文字裡。例如用 N2、N3 或 A、B 這類中性 ID,避免 S2 看起來像應該排在 S1 後面。",
"suggestedCode": "S1 -->|未命中| N3[3 偵測 AI 工具並留言]\n N3 --> N4[4 讀 .reviewignore 整理 diff 並留言]\n N4 --> N5[5 攻擊方登場留言]\n N5 --> N6[6 攻擊方 sub agent 並行找問題]\n N6 --> N7[7 防守方登場留言]\n N7 --> N8[8 防守方裁決 → 保存 findings 誤判回寫 exclusions.json]\n N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)]\n N2 --> S9[9 嚴重問題逐條掛行留言]",
"id": "F003",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。歷史 findings 雖有流程步驟硬編碼與文件同步成本問題,但本條聚焦 mermaid 節點 ID 與視覺流程順序不一致,屬不同可讀性指控,未命中既有排除。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/index.js",
"startLine": 122,
"endLine": 139,
"problem": "`main()` 的 JSDoc 從函式說明變成流程章回。步驟、例外模式、issue 建立時機、相依關係、commit 規則全塞在同一段,和程式下方已存在的分段註解重複,讀起來像同一旋律被兩把琴同時彈奏。",
"suggestion": "JSDoc 保留函式職責、回傳值與關鍵模式差異即可;完整 10 步驟流程交給 README 或下方區塊註解。這會讓 `main()` 開頭更快進入正題。",
"suggestedCode": "",
"id": "F004",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。歷史 findings 有步驟編號散落與 main() 內閉包 JSDoc 過重等問題,但本條指向 main() 函式 JSDoc 與下方流程註解重複,範圍與主張不完全相同,證據不足以排除。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "src/lib/diagnostics.js",
"startLine": 45,
"endLine": 47,
"problem": "`res` 這個參數名太短促,和檔內其他 `text`、`parts`、`stderr`、`stdout` 這些直白命名相比顯得含糊。診斷工具本該讓人少猜一點,這裡卻讓讀者先猜它是哪一種 result。",
"suggestion": "改用 `result` 或 `agentResult`,讓函式簽名本身就說清楚資料來源。",
"suggestedCode": "function agentFailureDetail(agentResult) {\n const parts = [];\n const err = agentResult && agentResult.error;",
"id": "F005",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。未見已知排除事項或歷史 finding 涵蓋 diagnostics.js 中 res 參數命名過短的問題;屬新的命名可讀性指控。"
}
}
},
{
"reviewer": "Bard",
"focus": "style",
"badge": "🎼",
"severity": "建議",
"file": "test/gitea.test.js",
"startLine": 8,
"endLine": 8,
"problem": "`fn` 是一個太倉促的縮寫,放在測試輔助函式裡尤其刺耳;同一行已有 `handler` 這種完整命名,`fn` 顯得像漏拍的音符。",
"suggestion": "改成 `callback` 或 `run`,讓呼叫意圖更清楚,也和此專案偏完整語意的命名風格一致。",
"suggestedCode": "function withFetchStub(handler, callback) {",
"id": "F006",
"verdicts": {
"Paladin": {
"exclude": false,
"reason": "保留。未見已知排除事項或歷史 finding 涵蓋 test/gitea.test.js 中 fn 測試輔助參數命名問題;屬新的命名可讀性指控。"
}
}
}
],
"excluded": [
{
"reviewer": "Assassin",
"focus": "security",
"badge": "🗡️",
"severity": "嚴重",
"file": "src/lib/diagnostics.js",
"startLine": 23,
"endLine": 23,
"problem": "攻擊者只要讓 AI CLI 在 debug 模式下把 `Authorization: token <secret>` 或 `Authorization: Basic <base64>` 印到 stderr/stdout,這條遮罩規則只會遮掉 `token``Basic` 這個 scheme,後面的真正憑證仍會進 CI log。這正好打穿本檔宣稱的「安全診斷」防線,PAT 或 API token 會被長期保存並暴露給可讀 log 的人。",
"suggestion": "把整個 Authorization header value 視為機密,不要只特判 Bearer。應遮掉 scheme 與 credential 全段;若要保留 scheme,也只能保留固定白名單字樣,不能留下後面的 token。",
"suggestedCode": ".replace(/(authorization\\s*[:=]\\s*)(?:\\S+\\s+)?\\S+/gi, '$1***')",
"id": "F001",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 finding 已指出除錯診斷輸出中 Authorization 遮罩規則可能只遮到 scheme、留下後方憑證,並造成 CI log 洩漏,與本條同一問題。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "src/index.js",
"startLine": 181,
"endLine": 239,
"problem": "`main()` 這次把「PR 留言」、「issue 暫存」、「issue 建立後 flush」三種狀態直接塞進閉包變數 `currentRunCommentIds`、`issueBuffer`、`issue` 與 `queueOrPostComment()`。未來只要再新增一種落地方式或調整留言順序,維護者必須同時理解整個主流程的時序,否則很容易出現留言漏發、舊留言誤標、或 issue 尚未建立卻被使用的狀態錯誤。這段已經不是單純編排,而是隱含一個留言發布狀態機。",
"suggestion": "把留言目的地抽成小型模組,例如 `CommentSink` / `ReviewPublisher`,讓 `main()` 只負責流程編排:`publisher.postContext()`、`publisher.flushIssue()`、`publisher.resolveOldPrComments()`。建問題模式的暫存與 flush 可獨立測試,半年後改流程時不用在 `main()` 裡追閉包狀態。",
"suggestedCode": "",
"id": "F007",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布狀態機耦合的同一問題。"
}
}
},
{
"reviewer": "Leo",
"focus": "maintainability",
"badge": "🧰",
"severity": "警告",
"file": "readme.md",
"startLine": 56,
"endLine": 132,
"problem": "README 這次把大量連結從 `branch/master` 改成 `branch/develop`,同時保留許多硬編碼行號。這種文件會隨分支名稱、預設分支、或任一檔案插入幾行就整批失準;未來維護者每次調整程式都得同步修幾十個 URL,否則文件看起來精準、實際上卻導到錯誤位置。",
"suggestion": "改用 repo 相對連結並盡量避免行號,例如 `src/lib/log.js` 或 `src/lib/log.js#L20` 只保留少數真正需要精準定位的 API。若這段是產生文件,請把分支名稱與連結格式集中在產生器設定,不要把同一個 branch 字串散落在 README 表格裡。",
"suggestedCode": "",
"id": "F008",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已多次指出 README 大量硬編 branch 與行號深連結,造成文件與原始碼高同步成本;本條為同一問題。"
}
}
},
{
"reviewer": "Mage",
"focus": "logic",
"badge": "🔮",
"severity": "警告",
"file": "src/index.js",
"startLine": 217,
"endLine": 225,
"problem": "建問題模式先建立 issue,再逐則 flush `issueBuffer`,但中途任一留言 API 失敗會直接拋出,沒有回滾或冪等處理。最小重現:`gitea.createIssue` 成功建立 #10,第一則 buffered comment 成功,第二則因 500/timeout 失敗 → 主流程 exit 1,PR 沒有 issue 連結、也沒有結果 commit;重跑時又建立 #11,留下半成品 issue #10。這會在暫時性 API 錯誤下造成重複追蹤 issue 與狀態不一致。",
"suggestion": "建立 issue 後立即寫入可重試識別資訊,重跑前先搜尋/復用既有本 PR 的 AI review issue;或在 flush 失敗時捕捉錯誤,將已建立 issue 補上失敗狀態與 PR 回連,再讓流程以可診斷方式收束。",
"suggestedCode": "",
"id": "F010",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(命中已知排除事項且重複)。issue 建立後 flush 暫存留言失敗、重跑又建立重複 issue,正是已知排除事項與多筆歷史 findings 已記錄的問題。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 180,
"endLine": 241,
"problem": "建問題模式新增了「先暫存留言、確定有保留問題才建立 issue、再 flush 暫存留言」的流程,但目前新增測試只驗到低階 helper,沒有驗證主流程在 create-issue=true 時的留言去向與靜默通過行為。這裡若順序錯了,可能會把原本應該留在 issue 的工具/diff/角色留言發到 PR,或在無保留問題時留下不該出現的 PR 留言。",
"suggestion": "請補主流程層級測試,stub `gitea`、`review.runAttackers`、`review.runDefenders`、`agents.detectTool`,至少驗證:\n\n| 情境 | 應斷言 |\n| --- | --- |\n| `createIssue=true` 且 `kept.length > 0` | 建 issue 前的情境留言先暫存,issue 建立後依序寫入該 issue |\n| `createIssue=true` 且 `kept.length === 0` | 不建立 issue、不對 PR 留言、不呼叫 `resolveOldComments` |\n| 一般模式 | 情境留言仍發到 PR,且 id 被加入 `currentRunCommentIds` |",
"suggestedCode": "",
"id": "F012",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋 create-issue 模式中留言先暫存、issue 建立後依序寫入、無 finding 靜默通過與一般模式留言目的地缺少主流程測試。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/index.js",
"startLine": 354,
"endLine": 395,
"problem": "新增的建問題模式會依嚴重度分流:嚴重問題改發到 issue、PR 回貼 issue 連結,且只有嚴重問題才呼叫 `addIssueDependency`。目前測試沒有覆蓋這個決策矩陣,等於沒有驗證「警告/建議不阻擋合併」與「嚴重問題要建立相依」這兩個關鍵行為。",
"suggestion": "請補 create-issue 模式的結果分流測試,至少涵蓋:\n\n| findings | 應斷言 |\n| --- | --- |\n| 只有警告/建議 | 建 issue、逐條留言、PR 回貼連結,但不呼叫 `addIssueDependency` |\n| 有嚴重問題 | 呼叫 `postSevereToIssue`、PR 回貼連結,並呼叫 `addIssueDependency(ctx, ctx.prNumber, issue.number)` |\n| `addIssueDependency` 拋錯 | 流程降級完成,不中斷收尾 commit |",
"suggestedCode": "",
"id": "F013",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式的嚴重/非嚴重分流、PR 回貼、相依 API 降級與核心分支缺少測試;本條屬同一測試缺口。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 130,
"endLine": 188,
"problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每次補抓後重試 merge-base、以及淺層 repo 才執行 `--unshallow` 的行為,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證這些成功/失敗路徑。這段是 diff 基準的核心,一旦 fallback 順序或條件壞掉,review 可能拿錯範圍或在 shallow checkout 直接失敗。",
"suggestion": "請把 git 執行層抽成可注入或以臨時 repo 測試,補齊:首次 merge-base 成功不跑 fallback、deepen base 後成功即停止、deepen PR HEAD 用目前 HEAD SHA、只有 shallow repo 才嘗試 `--unshallow`、全部策略失敗時錯誤訊息含診斷且保留 cause。",
"suggestedCode": "",
"id": "F014",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、shallow 與非 shallow 分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "警告",
"file": "src/lib/gitrepo.js",
"startLine": 331,
"endLine": 366,
"problem": "`pushWithCredential` 是這次結果 commit 能否用 PAT 觸發下一輪 CI 的關鍵,但目前沒有測試驗證它真的清掉 checkout 的自動 token、再注入 PAT Authorization,也沒有測失敗時不外洩 remote URL/token。這條失敗路徑沒被試煉過,可能讓 `[failure]` 結果 commit 無法觸發快速回報,或在錯誤訊息裡洩漏認證資訊。",
"suggestion": "請補 `commitAndPushFindings``pushWithCredential` 的測試,stub `execFileSync` 驗證:`git push` 使用不含帳密的 remote URL、`GIT_CONFIG_COUNT=2`、第 0 筆 extraheader 為空值、第 1 筆為 Basic Authorization、remote origin 不符時拒絕、push 失敗時拋出的錯誤不包含 token 或 remote URL。",
"suggestedCode": "",
"id": "F015",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已涵蓋 pushWithCredentialcommitAndPushFindings 的認證推送策略、token 注入、推送目標與失敗時不外洩 token 或 URL 的測試缺口。"
}
}
},
{
"reviewer": "Maya",
"focus": "testing",
"badge": "🧪",
"severity": "建議",
"file": "src/lib/diagnostics.js",
"startLine": 21,
"endLine": 32,
"problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。",
"suggestion": "請補 `redactSecrets` 的直接單元測試,包含:`https://user:pass@host`、`ghp_...`、40 字以上 token、含換行/tab 的假日誌注入、`null``undefined`。斷言重點是輸出為單行、敏感片段被 `***` 取代,且一般錯誤文字仍保留可診斷性。",
"suggestedCode": "",
"id": "F016",
"verdicts": {
"Paladin": {
"exclude": true,
"reason": "可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。"
}
}
}
]
}
+15 -10
View File
@@ -210,7 +210,7 @@ async function main() {
}; };
/** /**
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
* 並把 `issueBuffer` 內暫存的情境留言批次寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 * 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
* *
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選); * @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
@@ -224,9 +224,9 @@ async function main() {
labels: labelIds, labels: labelIds,
}); });
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
await Promise.all( for (const body of issueBuffer) {
issueBuffer.map((body) => gitea.createCommentOnIssue(ctx, issue.number, body)), await gitea.createCommentOnIssue(ctx, issue.number, body);
); }
issueBuffer.length = 0; issueBuffer.length = 0;
}; };
@@ -302,7 +302,9 @@ async function main() {
await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); const { kept, excluded } = findings.length === 0
? { kept: [], excluded: [] }
: await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
review.sortFindings(kept); review.sortFindings(kept);
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
@@ -398,12 +400,15 @@ async function main() {
} }
// ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)─────── // ── 收尾:commit 並 pushsuccess=無嚴重問題、failure=有嚴重問題)───────
// 一般模式:findingsexclusions.json;建問題模式:問題明細已在 issue 留言,只 commit exclusions.json。 // 一般模式:findingsexclusions.json;建問題模式通常只 commit exclusions.json。
// 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。
const result = severe.length === 0 ? 'success' : 'failure'; const result = severe.length === 0 ? 'success' : 'failure';
const filesToCommit = ctx.createIssue ? [] : [relativePath]; const filesToCommit = review.resultFilesToCommit({
if (exclusionsChanged) { createIssue: ctx.createIssue,
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json')); severeCount: severe.length,
} relativePath,
exclusionsChanged,
});
if (filesToCommit.length > 0) { if (filesToCommit.length > 0) {
commitFindings({ cwd, ctx, files: filesToCommit, result }); commitFindings({ cwd, ctx, files: filesToCommit, result });
} else { } else {
+71
View File
@@ -0,0 +1,71 @@
'use strict';
// AI CLI 失敗診斷與機密遮罩工具:供 review 流程記錄安全、限長的一行錯誤摘要。
// 遮罩前先截去的輸入上限,避免對數 MB 失敗輸出跑整份 O(k*n) 正規掃描。
const AGENT_DIAGNOSTIC_INPUT_LIMIT = 2_000;
// 每段診斷片段(stderr/stdout)寫入日誌的字元上限。
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
/**
* 遮罩診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
*
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
* `Authorization` 標頭、`token=``token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
* 長 hex`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
*
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
* 遮罩且去除控制字元的 stderr/stdout 片段。
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
}
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
module.exports = {
AGENT_DIAGNOSTIC_INPUT_LIMIT,
AGENT_DIAGNOSTIC_OUTPUT_LIMIT,
redactSecrets,
agentFailureDetail,
};
+24 -74
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { log, taipeiFromIso, taipeiNow } = require('./log'); const { log, taipeiFromIso, taipeiNow } = require('./log');
const { runAgent, extractJson } = require('./agents'); const { runAgent, extractJson } = require('./agents');
const { agentFailureDetail } = require('./diagnostics');
const templates = require('./templates'); const templates = require('./templates');
// 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。 // 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
@@ -13,76 +14,6 @@ const templates = require('./templates');
const PER_FILE_DIFF_LIMIT = 16_000; const PER_FILE_DIFF_LIMIT = 16_000;
const TOTAL_DIFF_LIMIT = 160_000; const TOTAL_DIFF_LIMIT = 160_000;
// AI CLI 失敗診斷輸出政策常數(agentFailureDetail 使用):與上方送審上限並列於模組頂層,
// 集中管理長度政策,避免藏在函式中段。
// INPUT_LIMIT:遮罩前先截去的輸入上限——先截再跑 redactSecrets,避免對數 MB 失敗輸出跑整份
// O(k×n) 正規掃描;2000 字已足以涵蓋跨界機密樣式。
const INPUT_LIMIT = 2_000;
// AGENT_DIAGNOSTIC_OUTPUT_LIMIT:每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策。
const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500;
/**
* 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
*
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
* `Authorization` 標頭、`token=``token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
* 長 hex`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出
* CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。
*
* @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。
* @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。
* @remarks 本函式未匯出,僅供模組內部使用。
*/
function redactSecrets(text) {
return String(text ?? '')
.replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ')
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***')
.replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***')
.replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@')
.replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***')
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***')
.trim();
}
/**
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。
*
* 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或
* 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、
* 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets}
* 遮罩且去除控制字元的 stderr/stdout 片段(各先截去過長輸入再取前 500 字)。
*
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
* `runAgent` 的回傳物件。
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
* @remarks
* 使用情境:{@link runAttackers}{@link runDefenders}{@link fillPurposes}{@link selectLabels}
* 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗;
* 需要輸出片段輔助診斷時,於 workflow 設定 `ACTIONS_STEP_DEBUG=true` 再重跑。
* 本函式未匯出,僅供模組內部使用。
*/
function agentFailureDetail(res) {
const parts = [];
const err = res && res.error;
if (err) {
if (err.killed) parts.push('已逾時終止');
if (typeof err.code === 'number') parts.push(`exit ${err.code}`);
else if (err.code) parts.push(`code ${err.code}`);
else if (err.signal) parts.push(`signal ${err.signal}`);
}
// 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。
if (process.env.ACTIONS_STEP_DEBUG === 'true') {
const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
if (stderr) parts.push(`stderr${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
if (stdout) parts.push(`stdout${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`);
}
if (parts.length === 0) {
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
}
return parts.join('');
}
/** /**
* 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。 * 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。
* *
@@ -626,6 +557,28 @@ function appendExclusions({ cwd, excluded, prNumber }) {
return true; return true;
} }
/**
* 依審查模式與結果決定收尾要提交的結果檔。
*
* 一般模式永遠提交本回合 findings 檔;建問題模式通常把問題明細留在 issue,不提交
* findings。例外是有嚴重問題時仍提交 findings 檔,讓 `[failure]` 結果 commit 一定能產生,
* 避免問題相依 API 不支援或設定失敗時 PR 缺少失敗檢查。
*
* @param {Object} params - 解構參數。
* @param {boolean} params.createIssue - 是否啟用建問題模式。
* @param {number} params.severeCount - 嚴重 finding 數量。
* @param {string} params.relativePath - 本回合保存的 findings 檔 repo 相對路徑。
* @param {boolean} params.exclusionsChanged - exclusions.json 是否有實際異動。
* @returns {string[]} 應交給 `commitFindings` 的 repo 相對路徑清單。
*/
function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusionsChanged }) {
const files = createIssue ? (severeCount > 0 ? [relativePath] : []) : [relativePath];
if (exclusionsChanged) {
files.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
}
return files;
}
/** /**
* 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。 * 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
* *
@@ -926,13 +879,10 @@ module.exports = {
runDefenders, runDefenders,
sortFindings, sortFindings,
appendExclusions, appendExclusions,
resultFilesToCommit,
selectLabels, selectLabels,
postSevereToIssue, postSevereToIssue,
postOthersToIssue, postOthersToIssue,
resolveOldComments, resolveOldComments,
postSevereComments, postSevereComments,
__test: {
agentFailureDetail,
redactSecrets,
},
}; };
+27 -2
View File
@@ -4,12 +4,13 @@ const assert = require('node:assert/strict');
const test = require('node:test'); const test = require('node:test');
const review = require('../src/lib/review'); const review = require('../src/lib/review');
const diagnostics = require('../src/lib/diagnostics');
test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => { test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG; const oldDebug = process.env.ACTIONS_STEP_DEBUG;
delete process.env.ACTIONS_STEP_DEBUG; delete process.env.ACTIONS_STEP_DEBUG;
try { try {
const detail = review.__test.agentFailureDetail({ const detail = diagnostics.agentFailureDetail({
ok: false, ok: false,
error: Object.assign(new Error('boom'), { code: 1 }), error: Object.assign(new Error('boom'), { code: 1 }),
stderr: 'token=super-secret-value', stderr: 'token=super-secret-value',
@@ -27,7 +28,7 @@ test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
const oldDebug = process.env.ACTIONS_STEP_DEBUG; const oldDebug = process.env.ACTIONS_STEP_DEBUG;
process.env.ACTIONS_STEP_DEBUG = 'true'; process.env.ACTIONS_STEP_DEBUG = 'true';
try { try {
const detail = review.__test.agentFailureDetail({ const detail = diagnostics.agentFailureDetail({
ok: false, ok: false,
error: Object.assign(new Error('boom'), { code: 2 }), error: Object.assign(new Error('boom'), { code: 2 }),
stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890', stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890',
@@ -72,3 +73,27 @@ test('postOthersToIssue 批次送出 issue 留言', async () => {
assert.equal(calls[0].issueNumber, 7); assert.equal(calls[0].issueNumber, 7);
assert.ok(maxActive > 1); assert.ok(maxActive > 1);
}); });
test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => {
assert.deepEqual(
review.resultFilesToCommit({
createIssue: true,
severeCount: 1,
relativePath: '.gitea/ai-review/findings/run.json',
exclusionsChanged: false,
}),
['.gitea/ai-review/findings/run.json'],
);
});
test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusions 異動', () => {
assert.deepEqual(
review.resultFilesToCommit({
createIssue: true,
severeCount: 0,
relativePath: '.gitea/ai-review/findings/run.json',
exclusionsChanged: true,
}),
['.gitea/ai-review/exclusions.json'],
);
});