72 lines
3.4 KiB
JavaScript
72 lines
3.4 KiB
JavaScript
'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,
|
||
};
|