'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(); } /** * 將 AI CLI 失敗輸出整理成單行、遮罩且限長的診斷片段。 * * @param {string[]} parts - 要附加診斷片段的陣列。 * @param {string} label - 診斷欄位名稱(如 stderr/stdout)。 * @param {*} value - 原始診斷輸出。 * @returns {void} */ function appendRedactedOutput(parts, label, value) { const redacted = redactSecrets(String(value || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); if (redacted) parts.push(`${label}:${redacted.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); } /** * 從 `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}} agentResult * `runAgent` 的回傳物件。 * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。 */ function agentFailureDetail(agentResult) { const parts = []; const err = agentResult && agentResult.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') { appendRedactedOutput(parts, 'stderr', agentResult && agentResult.stderr); appendRedactedOutput(parts, 'stdout', agentResult && agentResult.output); } 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, };