From 7a2bef2e1c3e4aac1a5002b3334e7b9748bd04a6 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 19 Aug 2026 11:40:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(version-guard):=20=E9=81=A0=E7=AB=AF?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=9F=A5=E8=A9=A2=E5=8A=A0=E4=B8=8A=E9=87=8D?= =?UTF-8?q?=E8=A9=A6=EF=BC=8C=E9=81=BF=E5=85=8D=E6=9A=AB=E6=99=82=E6=80=A7?= =?UTF-8?q?=E7=B6=B2=E8=B7=AF=E5=A4=B1=E6=95=97=E8=A2=AB=E8=AA=A4=E5=88=A4?= =?UTF-8?q?=E7=82=BA=20fail-closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原本一次 fetch 失敗(逾時/連線中斷/非 2xx)就直接視為查無遠端版本並擋下工具呼叫, 對暫時性網路問題過度敏感;改為最多重試 3 次、每次間隔 300ms,仍全部失敗才 fail-closed。 TLS 憑證驗證全程不跳過,維持 --use-system-ca 既有信任機制;--timeout-ms 預設由 5000 降到 3000,讓重試後的最長總耗時仍落在 hooks.json 設定的 15 秒逾時內。 Co-Authored-By: Claude Sonnet 5 --- scripts/version-guard.mjs | 44 ++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/scripts/version-guard.mjs b/scripts/version-guard.mjs index 2d6a188..f83be78 100644 --- a/scripts/version-guard.mjs +++ b/scripts/version-guard.mjs @@ -12,7 +12,7 @@ * --plugin-root 本地 plugin 根目錄(含 plugin.json)。省略時使用 $CLAUDE_PLUGIN_ROOT; * 兩者皆無則視為無法判定,fail-closed。 * --host Gitea 主機,省略時依序取 $GITEA_HOST,再退回 gitea.jsc.idv.tw。 - * --timeout-ms 遠端查詢逾時毫秒數,預設 5000。 + * --timeout-ms 遠端查詢單次逾時毫秒數,預設 3000(最多重試 3 次,見下方「重試」說明)。 * * 兩種呼叫模式(自動偵測,不需額外旗標): * 1. hook 模式:stdin 餵進合法 JSON(Claude Code/Copilot 的 PreToolUse event)時, @@ -22,12 +22,18 @@ * 2. CLI/測試模式:stdin 沒有合法 JSON(互動終端機或空管線)時,用 exit code 表示結果 * (0=相符,非 0=阻擋),訊息走 stderr——供人工測試與端到端驗證使用。 * - * 更新時間:2026/08/17 14:25:00 + * 更新時間:2026/08/19 11:39:27 * 相依:Node.js 標準內建功能(fetch/fs/path),無外部套件;需 Node ≥ 20.12 並以 * `node --use-system-ca` 啟動,讓 fetch 走系統信任的 CA store(而非 Node 內建的 * Mozilla CA 清單)——在企業網路/代理攔截 TLS 的環境下,curl 等工具通常已信任 * 系統憑證,但 Node fetch 預設不會,若不加這個旗標會誤判成「查無法取得遠端版本」 - * 而非真正的版本落差;呼叫端(hooks.json)的 command 必須帶上此旗標。 + * 而非真正的版本落差;呼叫端(hooks.json)的 command 必須帶上此旗標。TLS 憑證驗證 + * 全程不跳過——此腳本的查詢結果直接決定要不要放行工具呼叫,跳過驗證等於讓中間人可 + * 偽造回應誘導誤判,risk 遠大於單次連線失敗;遇到暫時性網路問題改用下方重試機制處理。 + * 重試:遠端查詢對暫時性失敗(逾時/連線中斷/非 2xx/回應格式錯誤)最多重試到 + * REMOTE_FETCH_MAX_ATTEMPTS 次,每次間隔 REMOTE_FETCH_RETRY_DELAY_MS,仍全部失敗才 + * 視為「查無法取得遠端版本」fail-closed;--timeout-ms 預設從 5000 降到 3000,讓 + * 「重試次數 × 單次逾時+重試間隔」的最長總耗時控制在 hooks.json 設定的 15 秒內。 * 機密:不涉及 token(遠端 raw plugin.json 端點匿名可讀),輸出仍套用 * shared/scripts/lib/redact-patterns.json 的遮蔽規則以防萬一。 * 退出碼(CLI/測試模式):0=版本相符;2=版本不符、查不到遠端、或本地版本無法判定(皆為 fail-closed)。 @@ -50,8 +56,15 @@ function redact(text) { return out; } +const REMOTE_FETCH_MAX_ATTEMPTS = 3; +const REMOTE_FETCH_RETRY_DELAY_MS = 300; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + function parseArgs(argv) { - const args = { pluginRoot: null, host: null, timeoutMs: 5000 }; + const args = { pluginRoot: null, host: null, timeoutMs: 3000 }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--plugin-root') args.pluginRoot = argv[++i]; @@ -78,7 +91,7 @@ function shortNameFromPluginName(name) { return name.startsWith('jsc-') ? name.slice('jsc-'.length) : name; } -async function fetchRemoteVersion(host, shortName, timeoutMs) { +async function fetchRemoteVersionOnce(host, shortName, timeoutMs) { const url = `https://${host}/api/v1/repos/plugins/${shortName}/raw/plugin.json?ref=master`; const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); if (!res.ok) { @@ -91,6 +104,27 @@ async function fetchRemoteVersion(host, shortName, timeoutMs) { return body.version; } +/** 對暫時性失敗(逾時/連線中斷/非 2xx/回應格式錯誤)重試,TLS 憑證驗證全程不跳過。 */ +async function fetchRemoteVersion(host, shortName, timeoutMs) { + let lastErr; + for (let attempt = 1; attempt <= REMOTE_FETCH_MAX_ATTEMPTS; attempt++) { + try { + return await fetchRemoteVersionOnce(host, shortName, timeoutMs); + } catch (err) { + lastErr = err; + if (attempt < REMOTE_FETCH_MAX_ATTEMPTS) { + log( + 'WRN', + `遠端版本查詢第 ${attempt} 次失敗(${String(err.message)}),${REMOTE_FETCH_RETRY_DELAY_MS}ms 後重試…`, + '版本檢查' + ); + await sleep(REMOTE_FETCH_RETRY_DELAY_MS); + } + } + } + throw lastErr; +} + function blockMessage(pluginName, remoteVersion, loadedVersion) { const remoteText = remoteVersion || '查無法取得遠端版本'; return ( From a3b29abf63641aec9299fa1628f430d0aba7ecc7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 19 Aug 2026 11:40:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore(plugin=20=E7=89=88=E6=9C=AC):=20jsc-s?= =?UTF-8?q?hared=20=E5=8D=87=E7=89=88=E8=87=B3=200.2.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 對應 version-guard.mjs 遠端查詢重試機制的修正。 Co-Authored-By: Claude Sonnet 5 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 9da7c2b..a469bc0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-shared", - "version": "0.2.3", + "version": "0.2.4", "description": "JSC 跨 AI 助理共用規範 skills plugin(Claude Code / Codex / Antigravity / OpenCode / GitHub Copilot CLI),`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準;於 Claude Code 以 /jsc-shared: 前綴呼叫。", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c7c8688..9c412d0 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-shared", - "version": "0.2.3", + "version": "0.2.4", "description": "JSC 跨 AI 助理共用規範 skills plugin,`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準。", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 889db7a..23ddca0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-shared", - "version": "0.2.3", + "version": "0.2.4", "description": "JSC 跨 AI 助理共用規範 skills plugin,`skills/` 為唯一真實來源,並提供整組 plugin 的安裝/更新/移除管理(plugins-install 一次安裝或更新 jsc-code/jsc-doc/jsc-persona/jsc-shared,plugins-uninstall 一次移除四個 JSC plugin)。安裝與更新一律以 Gitea 遠端 repo 的 README 與檔案為準,不依賴既有本機存取庫;所有 skills 以 SKILL.md 為共通標準;於 Antigravity 以 /jsc-shared: 前綴呼叫。", "skills": "./skills" }