處理 AI review findings 並改寫 Node.js entrypoint #1
@@ -0,0 +1,158 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"location": "action.yaml:23",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "將包含認證資訊的 `OAUTH` 透過環境變數傳遞,這使得該敏感資訊在容器內的任何進程皆可透過環境變數(如 `/proc/self/environ`)輕易竊取。",
|
||||||
|
"reason": "此 action 透過 Gitea/GitHub Docker Action input 接收 secret,action metadata 只能將 input 映射為容器環境變數;呼叫端仍使用 secrets.CODEX_OAUTH,未在 workflow 明文暴露。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:18",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "雖然有 `chmod 600`,但 `auth.json` 放在 `/root/.codex/` 目錄下,若發生容器逃逸,該敏感金鑰極易被讀取。",
|
||||||
|
"reason": "Codex CLI 目前需要讀取 CODEX_HOME/auth.json;容器內已限制為 600 權限,外部 Secret Store 掛載不是此 Docker Action 目前可控制的介面。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "action.yaml:6",
|
||||||
|
"role": "Leo",
|
||||||
|
"original_finding": "將 Base64 編碼的認證資訊設為 Action input,導致明文暴露在 Workflow 配置中,難以維護。",
|
||||||
|
"reason": "workflow 傳入的是 secrets.CODEX_OAUTH,不是明文值;使用 input 是 Docker Action 接收呼叫端 secret 的標準介面。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:22",
|
||||||
|
"role": "Rogue",
|
||||||
|
"original_finding": "在腳本中頻繁進行 I/O 操作(重複寫入 auth.json),缺乏快取檢查。",
|
||||||
|
"reason": "每次 Docker Action 執行都是短生命週期容器,auth.json 需要從當次 secret 重建;快取檢查不會降低跨執行 I/O,也可能增加 secret 狀態判斷複雜度。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "action.yaml:4",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "輸入參數 `oauth` 實質為 base64 編碼的 json,命名易生誤解。",
|
||||||
|
"reason": "此 action 已對外發布並由既有 workflow 使用 oauth input;直接更名會造成破壞性變更,先保留相容介面與描述文字。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "Dockerfile:4",
|
||||||
|
"role": "Rogue",
|
||||||
|
"original_finding": "在 Dockerfile 中安裝了 git 且未清理,導致映像檔過大。",
|
||||||
|
"reason": "Codex CLI 與 plugin 安裝/執行流程可能需要 git 支援;為避免 runtime 缺少 git 造成 action 失敗,目前保留 git。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "Dockerfile:25",
|
||||||
|
"role": "Rogue",
|
||||||
|
"original_finding": "插件安裝指令採取連續序列執行,導致多次獨立的網路 I/O 等待,嚴重浪費建置時間。",
|
||||||
|
"reason": "codex plugin 指令會修改同一份本機 plugin 狀態,並行安裝可能造成狀態競爭;目前保留循序安裝以確保可預測性。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:22",
|
||||||
|
"role": "Leo",
|
||||||
|
"original_finding": "直接使用 `mktemp` 在 `CODEX_HOME` 目錄下建立臨時檔案,且 `CODEX_HOME` 若未正確隔離,在多個 Action 同時執行時可能會導致檔案名稱衝突。",
|
||||||
|
"reason": "Docker Action 每次執行都有獨立容器與 CODEX_HOME;mktemp 也會建立隨機檔名,實務上不會跨 action 執行衝突。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:16",
|
||||||
|
"role": "Leo",
|
||||||
|
"original_finding": "硬編碼了預設路徑 `/root/.codex`,這使得映像檔的可移植性受限,且如果在非 root 使用者環境下執行此容器,可能會因為權限問題而失敗。",
|
||||||
|
"reason": "Dockerfile 明確以 root 環境執行並設定 CODEX_HOME=/root/.codex;entrypoint 仍允許呼叫端用 CODEX_HOME 覆寫預設路徑。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:6",
|
||||||
|
"role": "Maya",
|
||||||
|
"original_finding": "應在測試案例中模擬空 OAUTH 輸入,並驗證腳本是否正確拋出錯誤並以 exit 1 終止。",
|
||||||
|
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:11",
|
||||||
|
"role": "Maya",
|
||||||
|
"original_finding": "應在測試案例中模擬空 MODEL 輸入,並驗證腳本是否正確拋出錯誤並以 exit 1 終止。",
|
||||||
|
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "Dockerfile:17",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "將安裝腳本邏輯直接寫在 `RUN` 指令中顯得冗長,且使用 `$(mktemp)` 容易產生難以追蹤的臨時檔案。",
|
||||||
|
"reason": "安裝流程需在同一 Docker layer 中下載、驗證 SHA-256、執行並清理暫存腳本;留在 Dockerfile 可讓供應鏈驗證步驟與安裝命令緊鄰,mktemp 產生的檔案也已於同一 RUN 中刪除。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:45",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "使用靜態路徑檢查 `auth.json` 是否存在,若前次執行中斷導致檔案未清除,會導致後續執行失敗(Self-inflicted DoS)。",
|
||||||
|
"reason": "拒絕覆寫既有 auth.json 是刻意的 secret 安全保護,避免覆蓋呼叫端掛載或殘留的認證檔;Docker Action 預期每次執行使用獨立容器,前次中斷造成殘留的風險低於靜默覆寫 secret 的風險。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "tests/entrypoint_test.sh:17",
|
||||||
|
"role": "Bard",
|
||||||
|
"original_finding": "在測試中建立 `codex` 指令時,Here-document 與命令混雜,可讀性較低。",
|
||||||
|
"reason": "測試 helper 需要動態產生可執行的假 codex 指令,here-document 在此處比額外 fixture 檔更直觀且維持測試自含;此項屬風格偏好,不影響行為正確性。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "Dockerfile:10",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "Codex 外掛市集來源指向 `gitea.jsc.idv.tw`,未驗證來源的安全性。若該伺服器遭駭,將導致自動安裝惡意或篡改過的外掛,進而導致供應鏈攻擊。",
|
||||||
|
"reason": "Dockerfile 已使用 `codex plugin marketplace add --ref` 將 doc 與 code-review marketplace 固定到指定 commit SHA;此 action 需要安裝內部 Gitea marketplace 外掛,來源伺服器信任與存取控管屬部署環境治理,不適合在 Dockerfile 內改成其他來源。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "Dockerfile:24",
|
||||||
|
"role": "Leo",
|
||||||
|
"original_finding": "插件 URL 與版本參照 (REF) 硬編碼在 Dockerfile 中,未來若需更新插件或更換來源,需重新編譯整個 Docker 映像檔,維護成本較高。",
|
||||||
|
"reason": "此 Docker Action 需要可重現的建置結果;將 marketplace URL 與 ref 固定在 Dockerfile ARG 中,可讓外掛版本變更必須經由映像檔重建與 code review,避免外部設定在執行時靜默改變供應鏈內容。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "tests/entrypoint_test.sh:65",
|
||||||
|
"role": "Maya",
|
||||||
|
"original_finding": "測試案例對於輸入參數的邊界測試(例如 `PROMPT` 為空字串、極長字串)不足。",
|
||||||
|
"reason": "目前測試已包含 `test_empty_prompt` 與 `test_prompt_with_shell_characters`,覆蓋空字串與含 shell 特殊字元的 PROMPT 傳遞;此 finding 對最新測試內容已不成立。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:49",
|
||||||
|
"role": "Mage",
|
||||||
|
"original_finding": "在執行 install -m 600 時,若 codex 進程已經在嘗試讀取 auth.json,會發生檔案存取競態(Race Condition)。雖然使用了 flock,但這僅在同一個 shell 腳本實例中有效,無法保護跨容器或跨執行環境的檔案存取一致性。",
|
||||||
|
"reason": "最新入口已改為 `entrypoint.sh` 呼叫 `app/main.js`,不存在 `install -m 600`;Node.js 實作會先寫入並 chmod auth.json 後才啟動 codex,且 Docker Action 每次執行為獨立容器,跨容器不共享 CODEX_HOME。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "entrypoint.sh:58",
|
||||||
|
"role": "Mage",
|
||||||
|
"original_finding": "在容器化環境(通常是 ephemeral 的)中,auth.json 寫入後立刻被刪除,這會導致 codex 在後續執行中因找不到驗證檔案而無法運作。另外,trap 的清理機制會導致該檔案在 codex 完成工作前被刪除,這對於長效執行或需要多次存取的應用場景是錯誤的設計。",
|
||||||
|
"reason": "最新 Node.js 實作會等待 `codex exec` 子程序結束並寫入 output 後才執行 cleanup;auth.json 在 codex 執行期間持續存在,不會在工作完成前被刪除。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "Dockerfile:32",
|
||||||
|
"role": "Mage",
|
||||||
|
"original_finding": "在 Dockerfile 中直接使用 RUN 來下載並執行安裝腳本,沒有進行網路連接穩定性的驗證或完整的錯誤恢復機制。一旦網路不穩導致腳本不完整,後續的 sha256sum 檢查會失敗,但 Dockerfile 層疊技術可能會導致中間層殘留損壞的檔案。",
|
||||||
|
"reason": "Dockerfile 已在同一個 RUN 中完成下載、SHA-256 驗證與安裝,並使用 `curl --retry 3 --retry-delay 2 --max-time 120`;checksum 失敗會使該 layer 建置失敗,不會產生可用的損壞中間成果。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/main.js:106",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "在執行 `codex` 子行程時,使用了 `--dangerously-bypass-approvals-and-sandbox` 參數。這會完全繞過沙盒機制與審核流程,如果 `prompt` 內容受到攻擊者控制,該 CLI 工具將獲得在容器中執行任意代碼的權限。",
|
||||||
|
"reason": "此 action 的用途是在隔離的短生命週期 CI 容器中代替使用者執行 Codex,必須允許 Codex 非互動式修改 workspace;workflow 呼叫端需以 trusted prompt/secret 使用,本次已在 `runCodex` 前加入註解明確標示此安全取捨。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/main.js:143",
|
||||||
|
"role": "Assassin",
|
||||||
|
"original_finding": "將 `OAUTH` 環境變數內容解碼並直接寫入 `auth.json`。雖然有檢查 base64 格式與 JSON 結構,但若解碼後的 JSON 內容包含惡意配置(如惡意插件路徑或偽造的 API 憑證),可能導致後續 `codex` CLI 在執行時被劫持或洩漏資料。",
|
||||||
|
"reason": "`OAUTH` 是呼叫端提供給 Codex CLI 的 auth.json secret;action 只能驗證 base64、JSON object 與檔案權限,憑證真偽與欄位語意需由 Codex CLI/上游認證機制處理,action 不應猜測或拒絕未來相容欄位。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/main.js:20",
|
||||||
|
"role": "Leo",
|
||||||
|
"original_finding": "建議至少加上 `console.error` 或在開發/除錯模式下將錯誤拋出,以便在清除失敗時能收到警示。",
|
||||||
|
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/main.js:125",
|
||||||
|
"role": "Mage",
|
||||||
|
"original_finding": "這裡直接使用 `spawn` 執行 `codex` 命令,且參數 `prompt` 是直接從 `process.env.PROMPT` 讀取並傳入的。如果 CI 環境的 `PROMPT` 被惡意竄改,雖使用陣列傳遞參數避免了 shell injection,但 `codex exec` 的邏輯若沒有妥善限制(例如限制可執行指令類型),可能導致攻擊者在 CI Runner 環境執行任意指令。",
|
||||||
|
"reason": "此 action 的目的就是讓呼叫 workflow 以 `PROMPT` 指定 Codex 任務並在隔離 CI 容器內非互動執行;prompt 語意必須由 workflow 與 secret 的信任邊界控管。程式已用 `spawn` 參數陣列避免 shell injection,不適合在 action 內以白名單改寫或限制使用者 prompt。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/main.js:134",
|
||||||
|
"role": "Maya",
|
||||||
|
"original_finding": "對於 codex 執行失敗的各種細節(權限不足、找不到 binary 等)都統一處理為 status: 1,測試未驗證具體錯誤來源。",
|
||||||
|
"reason": "此 GitHub Action 對外輸出以 `status=completed|failed` 與 `output` 訊息表達成功或失敗;process exit code 只需反映 Codex 子程序結果或通用啟動失敗。細分 ENOENT、EACCES 等 Action exit code 會改變既有介面且對 workflow 判斷價值有限,本次已用簡潔 output 訊息與測試覆蓋具體錯誤來源。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/main.js:142",
|
||||||
|
"role": "Rogue",
|
||||||
|
"original_finding": "移除這些無謂的重新編碼比較,直接嘗試解碼。",
|
||||||
|
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"level": "info",
|
||||||
|
"role": "Bard",
|
||||||
|
"problem": "setupAuth 函式職責過於繁雜。",
|
||||||
|
"suggestion": "將鎖定機制與驗證機制拆分為獨立輔助函式。",
|
||||||
|
"location": "app/main.js:283",
|
||||||
|
"is_new": false
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
jobs:
|
||||||
|
version:
|
||||||
|
name: 計算版本號
|
||||||
|
runs-on: ubuntu
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: 計算版本號
|
||||||
|
id: version
|
||||||
|
uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }}
|
||||||
|
with:
|
||||||
|
IS_BETA: true
|
||||||
|
- name: 標註版本號
|
||||||
|
uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }}
|
||||||
|
with:
|
||||||
|
name: codex v${{ steps.version.outputs.version }}
|
||||||
|
tag_name: v${{ steps.version.outputs.version }}
|
||||||
|
target_commitish: ${{ github.head_ref }}
|
||||||
|
test:
|
||||||
|
name: 工具測試
|
||||||
|
runs-on: ubuntu
|
||||||
|
needs: [version]
|
||||||
|
outputs:
|
||||||
|
status: ${{ steps.test.outputs.status }}
|
||||||
|
output: ${{ steps.test.outputs.output }}
|
||||||
|
steps:
|
||||||
|
- name: 工具測試
|
||||||
|
id: test
|
||||||
|
uses: https://gitea.jsc.idv.tw/actions/codex@v${{ needs.version.outputs.version }}
|
||||||
|
with:
|
||||||
|
oauth: ${{ secrets.CODEX_OAUTH }}
|
||||||
|
model: gpt-5.4-mini
|
||||||
|
- name: 測試失敗
|
||||||
|
if: ${{ steps.test.outputs.status != 'completed' }}
|
||||||
|
run: exit 1
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: AI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches-ignore:
|
||||||
|
- master
|
||||||
|
types: [opened, synchronize]
|
||||||
|
jobs:
|
||||||
|
code-review:
|
||||||
|
name: Code Review
|
||||||
|
runs-on: ubuntu
|
||||||
|
steps:
|
||||||
|
- name: AI Code Review
|
||||||
|
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
|
||||||
|
with:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
|
||||||
|
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
OPENCODE_BASE_URL: ${{ vars.OPENCODE_BASE_URL }}
|
||||||
|
OPENCODE_PROVIDER: ${{ vars.OPENCODE_PROVIDER }}
|
||||||
|
OPENCODE_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
@@ -1,10 +1,35 @@
|
|||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
# 安裝必要的工具
|
# 設定安裝用的環境變數
|
||||||
RUN apk add --no-cache --no-check-certificate bash
|
ENV CODEX_NON_INTERACTIVE=1
|
||||||
|
ENV CODEX_INSTALL_DIR=/usr/local/bin
|
||||||
|
ENV CODEX_HOME=/root/.codex
|
||||||
|
ARG CODEX_INSTALL_SHA256=73eb367137d151eabe89590a1d1a08a9c2fe2800a656bffac3c5707b62b42683
|
||||||
|
ARG CODEX_DOC_MARKETPLACE_REF=f8da961328a85f267b4402566e127310370169da
|
||||||
|
ARG CODEX_CODE_REVIEW_MARKETPLACE_REF=9e016edff016d58f4d64e0a5468220d35a0f657b
|
||||||
|
|
||||||
|
Ghost marked this conversation as resolved
|
|||||||
|
# 安裝必要的工具
|
||||||
|
RUN apk add --no-cache --no-check-certificate bash ca-certificates curl git jq nodejs
|
||||||
|
|
||||||
|
# 安裝 Codex CLI 工具
|
||||||
|
RUN install_script="$(mktemp)" \
|
||||||
|
&& trap 'rm -f "$install_script"' EXIT \
|
||||||
|
&& curl -fsSL --retry 3 --retry-delay 2 --max-time 120 \
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:在 Dockerfile 中直接使用 `curl` 下載並執行安裝腳本 (`sh "$install_script"`) 存在潛在的安全風險與不確定性,且沒有檢查腳本的完整性或簽章。若腳本內容在未來變更,可能導致映像檔建置失敗或植入非預期的內容,增加維護與安全風險。
**建議**:建議將安裝腳本改為明確的版本化下載,或者如果可能,將安裝邏輯整合進 Dockerfile 自身,以確保建置過程的冪等性與安全性。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:在 Dockerfile 中直接使用 `curl` 下載並執行安裝腳本存在潛在的安全風險與不確定性,且沒有檢查腳本的完整性或簽章。若腳本內容在未來變更,可能導致映像檔建置失敗或植入非預期的內容。
**建議**:建議將安裝腳本改為明確的版本化下載,或者將安裝邏輯整合進 Dockerfile 自身。
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Assassin
**問題**:Dockerfile 直接從外部連結下載並執行腳本,且未驗證其雜湊值,若來源遭攔截或篡改,將導致任意程式碼執行。
**建議**:下載指令碼後,務必使用 sha256sum 或其他雜湊函數驗證其完整性,確保與預期內容一致後再執行。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:將安裝腳本邏輯直接寫在 `RUN` 指令中顯得冗長,且使用 `$(mktemp)` 容易產生難以追蹤的臨時檔案。
**建議**:考慮將安裝邏輯封裝成一個獨立的 script 檔案,讓 `Dockerfile` 更簡潔優雅。
|
|||||||
|
https://chatgpt.com/codex/install.sh \
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:若 sh "$install_script" 安裝過程失敗,rm -f 由於被 && 連接,不會被執行,導致 build 過程中殘留不必要的暫存檔案,雖然 Docker Layer 會自動清除,但仍屬不佳的清理實作。
**建議**:建議改用 trap 進行清理,或確保 rm -f 指令在 failure 情境下也能執行,例如:sh "$install_script" || { rm -f "$install_script"; exit 1; }
|
|||||||
|
-o "$install_script" \
|
||||||
|
&& echo "${CODEX_INSTALL_SHA256} ${install_script}" | sha256sum -c - \
|
||||||
|
&& sh "$install_script" \
|
||||||
|
&& codex --version
|
||||||
|
|
||||||
|
# 安裝技能
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:插件 URL 與版本參照 (REF) 硬編碼在 Dockerfile 中,未來若需更新插件或更換來源,需重新編譯整個 Docker 映像檔,維護成本較高。
**建議**:建議將這些插件清單與版本資訊移至外部設定檔 (如 plugins.json),並在 Dockerfile 中讀取該檔案進行安裝,增加彈性。
|
|||||||
|
RUN codex plugin marketplace add "https://gitea.jsc.idv.tw/plugins/doc.git" --ref "$CODEX_DOC_MARKETPLACE_REF" \
|
||||||
|
&& codex plugin marketplace add "https://gitea.jsc.idv.tw/plugins/code-review.git" --ref "$CODEX_CODE_REVIEW_MARKETPLACE_REF" \
|
||||||
|
&& codex plugin add "jsc@doc" \
|
||||||
|
&& codex plugin add "jsc@code-review"
|
||||||
|
|
||||||
|
COPY app /app
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 Dockerfile 中直接使用 RUN 來下載並執行安裝腳本,沒有進行網路連接穩定性的驗證或完整的錯誤恢復機制。一旦網路不穩導致腳本不完整,後續的 sha256sum 檢查會失敗,但 Dockerfile 層疊技術可能會導致中間層殘留損壞的檔案。
**建議**:將下載、SHA256 驗證與安裝合併在同一個 RUN 指令中,並加入重試機制(如已有的 --retry),確保每一層的原子性。
|
|||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh /app/main.js
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
name: 'Docker Action Template'
|
name: 'Codex CLI'
|
||||||
description: 'Docker Action 範本'
|
description: 'Codex CLI Action'
|
||||||
author: 'Jeffery'
|
author: 'Jeffery'
|
||||||
inputs:
|
inputs:
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:輸入參數 `oauth` 實質為 base64 編碼的 json,命名易生誤解。
**建議**:建議更名為 `auth_config` 或 `encoded_auth_json`。
|
|||||||
runner_token:
|
oauth:
|
||||||
description: 'Gitea Runner Token'
|
description: 'Base64 encoded Codex auth.json'
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:將 Base64 編碼的認證資訊設為 Action input,導致明文暴露在 Workflow 配置中,難以維護。
**建議**:應改為直接使用 Action 的 Secrets 機制(如 `secrets.CODEX_AUTH`),將認證資訊與設定分離。
|
|||||||
required: true
|
required: true
|
||||||
text:
|
model:
|
||||||
description: '輸入的文字'
|
description: 'Codex model name'
|
||||||
default: "Hello, World!"
|
required: true
|
||||||
|
prompt:
|
||||||
|
description: 'Prompt for codex exec'
|
||||||
|
required: false
|
||||||
|
default: '請自我介紹'
|
||||||
outputs:
|
outputs:
|
||||||
text:
|
status:
|
||||||
description: '輸出的文字'
|
description: 'Execution result status'
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:輸出欄位命名為 `text`,但賦值內容為「執行狀態(status)」,語義不符。
**建議**:將欄位名稱改為 `status`,以保持命名與意圖一致。
|
|||||||
|
output:
|
||||||
|
description: 'Codex execution output'
|
||||||
runs:
|
runs:
|
||||||
using: 'docker'
|
using: 'docker'
|
||||||
image: 'Dockerfile'
|
image: 'Dockerfile'
|
||||||
env:
|
env:
|
||||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
OAUTH: ${{ inputs.oauth }}
|
||||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
MODEL: ${{ inputs.model }}
|
||||||
RUNNER_TOKEN: ${{ inputs.runner_token || secrets.GITEA_TOKEN || secrets.RUNNER_TOKEN }}
|
PROMPT: ${{ inputs.prompt }}
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
|
||||||
|
const DEFAULT_PROMPT = "請自我介紹";
|
||||||
|
const FILE_MODE_PRIVATE = 0o600;
|
||||||
|
const DIR_MODE_PRIVATE = 0o700;
|
||||||
|
const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000;
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Bard
**問題**:全域變數 `createdPaths` 在檔案層級被宣告,讓函式產生強依賴,缺乏封裝性,讀起來不夠優雅。
**建議**:將臨時檔案管理邏輯封裝成一個類別(如 `TempFileRegistry`),讓狀態更具備物件導向的封裝性。
|
|||||||
|
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
||||||
|
const TEMP_DIR_PREFIX = ".codex-action-";
|
||||||
|
const OUTPUT_DELIMITER_PREFIX = "CODEX_OUTPUT_";
|
||||||
|
|
||||||
|
class TempFileRegistry {
|
||||||
|
constructor() {
|
||||||
|
this.files = new Set();
|
||||||
|
this.dirs = new Set();
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:在 `removeIfCreated` 函式中靜默捕捉錯誤 (`catch { ... }`),這會遮蔽潛在的權限或檔案系統問題,使除錯困難。
**建議**:建議至少加上 `console.error` 或在開發/除錯模式下將錯誤拋出,以便在清除失敗時能收到警示。
|
|||||||
|
|
||||||
|
trackFile(filePath) {
|
||||||
|
this.files.add(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackDir(dirPath) {
|
||||||
|
this.dirs.add(dirPath);
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:在 `makeTempFile` 中,使用了 `Math.random().toString(16).slice(2)` 來生成隨機檔名。對於高頻率呼叫的場景,這會產生不必要的計算開銷與效能損耗。
**建議**:建議使用 Node.js 內建的 `crypto.randomBytes` 或 `crypto.randomUUID`,雖然效能略有差異但更具安全性與標準化,且能減少字串轉換次數。
|
|||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Bard
**問題**:隨機檔案名稱的生成邏輯過於冗長且複雜,破壞了程式碼的簡潔美感。
**建議**:建議使用 Node.js 原生的 `crypto` 模組,例如 `crypto.randomBytes(16).toString('hex')`,讓產生的字串更優雅、清晰。
|
|||||||
|
|
||||||
|
removeFile(filePath) {
|
||||||
|
if (!filePath || !this.files.has(filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `makeTempFile` 中使用了 `fs.openSync(filePath, "wx", 0o600)`。如果在 `fs.closeSync(fd)` 之前程式因例外或強制終止(SIGKILL),該檔案會留在硬碟上直到下次清理或手動刪除,且其檔案描述子會持續開啟直到 process 結束。
**建議**:建議使用 `fs.mkdtempSync` 建立獨立目錄,將所有臨時檔案放入該目錄,並在 `cleanup` 時直接移除整個目錄,以確保清理的原子性與完整性。
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Bard
**問題**:檔案權限(如 `0o600`, `0o700`)以數字字面量多次出現,散落在程式碼中,降低了可讀性與一致性。
**建議**:在檔案上方定義權限常數(例如 `const FILE_MODE_PRIVATE = 0o600;`),讓語義更清晰。
|
|||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
this.files.delete(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unable to remove temporary file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
for (const filePath of this.files) {
|
||||||
|
this.removeFile(filePath);
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Rogue
**問題**:在 `TempFileRegistry` 的 `cleanup` 方法中,每次呼叫都使用 `Array.from` 將 Set 轉換為陣列,這在頻繁清理時會產生無謂的記憶體開銷。
**建議**:若無強烈反向迭代的需求,可考慮直接使用 `forEach` 遍歷 Set。若有嚴格順序需求,建議改用其他結構管理,避免每次 cleanup 都額外配置陣列。
|
|||||||
|
|
||||||
|
for (const dirPath of this.dirs) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(dirPath, { force: true, recursive: true });
|
||||||
|
this.dirs.delete(dirPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unable to remove temporary directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempFiles = new TempFileRegistry();
|
||||||
|
|
||||||
|
class OutputCollector {
|
||||||
|
constructor(maxBytes) {
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:在 `validateAuth` 中,為了驗證 base64 字串是否符合 base64 格式,進行了多次正規表達式替換與編解碼運算(如 `encodedAuth.replace`、`Buffer.from`、`decoded.toString('base64')` 等),這在每次執行都會發生的情況下,浪費了不必要的 CPU 週期。
**建議**:如果目的只是驗證結構,建議盡量簡化邏輯。可以直接將字串嘗試轉換為 Buffer 並檢查 `toString('base64')` 是否匹配,避免多重正規表達式替換。
|
|||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.chunks = [];
|
||||||
|
this.size = 0;
|
||||||
|
this.truncated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(chunk) {
|
||||||
|
const available = this.maxBytes - this.size;
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:Base64 的驗證過程充滿了複雜的字串正規化與取代操作,讀起來像是在解迷宮,而非驗證身分。
**建議**:將驗證邏輯拆解或簡化,明確劃分「解碼」、「正規化」與「比較」三個步驟,提升程式碼的可讀性與可維護性。
|
|||||||
|
|
||||||
|
if (available <= 0) {
|
||||||
|
this.truncated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedChunk = chunk.length > available ? chunk.subarray(0, available) : chunk;
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `validateAuth` 中,僅透過 `JSON.parse` 檢查 JSON 格式,但未針對 Codex 預期的 auth.json 結構(如必要的欄位)進行 Schema 驗證。如果傳入的 JSON 格式正確但內容無效,可能會導致 `codex exec` 在後續執行時失敗。
**建議**:建議加入對 JSON 內容的簡單結構驗證(例如確認是否有 `token` 或必要的連線設定欄位)。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `appendGithubOutput` 函式中,當 `output` 內容極大時,此處會將整個 `output` 字串在記憶體中進行檢查(`output.includes(delimiter)`)與多次複製。這可能導致在處理極端長度輸出時發生記憶體不足的問題。
**建議**:建議限制 `delimiter` 嘗試次數,或在檢查時避免讀取整個 `output` 字串,改用串流處理方式。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:OutputCollector 的截斷機制未被測試。
**建議**:增加測試案例模擬輸出超過 DEFAULT_OUTPUT_LIMIT_BYTES,驗證截斷提示。
|
|||||||
|
this.chunks.push(storedChunk);
|
||||||
|
this.size += storedChunk.length;
|
||||||
|
|
||||||
|
if (storedChunk.length < chunk.length) {
|
||||||
|
this.truncated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Rogue
**問題**:在 `runCodex` 中,使用 `new Promise` 封裝 `child_process.spawn` 並手動監聽 data 事件來拼接輸出。在高輸出量的場景下,不斷字串拼接(`output += chunk.toString()`)會導致大量記憶體配置與 garbage collection 壓力。
**建議**:如果預期輸出量大,建議將 stdout/stderr 直接寫入檔案流或使用 `Buffer` 陣列收集後最後合併,減少中間字串變更帶來的記憶體浪費。
|
|||||||
|
|
||||||
|
toString() {
|
||||||
|
const truncationMessage = this.truncated ? "\n[Output truncated]\n" : "";
|
||||||
|
return `${Buffer.concat(this.chunks, this.size).toString()}${truncationMessage}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
tempFiles.cleanup();
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Bard
**問題**:makeTempDir 內部使用了硬編碼的 '.codex-action-' 前綴。
**建議**:將前綴提取為常數或設定檔參數。
|
|||||||
|
}
|
||||||
|
|
||||||
|
function makeTempDir(dir) {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(dir, TEMP_DIR_PREFIX));
|
||||||
|
fs.chmodSync(tempDir, DIR_MODE_PRIVATE);
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:在 `runCodex` 中使用了 `--dangerously-bypass-approvals-and-sandbox`,這類高風險標記若缺乏適當的說明,未來的維護者可能不清楚其安全意義而誤用或引發風險。
**建議**:建議在 `spawn` 呼叫前加上明確的註解,詳細說明為何在此環境中必須繞過沙盒,以及相關的安全考量。
|
|||||||
|
tempFiles.trackDir(tempDir);
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:在執行外部指令時沒有設定逾時(timeout),若 Codex CLI 發生無預期的掛起(hang),Action 將會永久卡住而不會自動終止。
**建議**:建議在 `spawn` 的選項中加入 `timeout` 機制,或是主動在啟動後設置一個計時器,當執行時間過長時強制終止子行程。
|
|||||||
|
function makeTempFile(dir, prefix) {
|
||||||
|
const random = crypto.randomBytes(16).toString("hex");
|
||||||
|
const filePath = path.join(dir, `${prefix}.${random}`);
|
||||||
|
const fd = fs.openSync(filePath, "wx", FILE_MODE_PRIVATE);
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Assassin
**問題**:在執行 `codex` 子行程時,使用了 `--dangerously-bypass-approvals-and-sandbox` 參數。這會完全繞過沙盒機制與審核流程,如果 `prompt` 內容受到攻擊者控制,該 CLI 工具將獲得在容器中執行任意代碼的權限。
**建議**:移除該標記。如果必須使用,請確保 `prompt` 來源完全可信,並將執行權限嚴格限制在最小範圍內。應考慮透過其他機制進行必要的操作,而非直接繞過安全保護。
|
|||||||
|
fs.closeSync(fd);
|
||||||
|
tempFiles.trackFile(filePath);
|
||||||
|
return filePath;
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Rogue
**問題**:`runCodex` 函數中的 `spawn` 使用 `{ stdio: ["ignore", "pipe", "pipe"] }`,這會導致 node 程式在輸出流被填滿時阻塞等待,即便透過 `stdout.on('data')` 監聽,在高輸出的情境下仍可能因為緩衝區管理不當而浪費不必要的 CPU 週期。
**建議**:如果預期輸出量很大,建議改用 `child.stdout.pipe(process.stdout)` 直接導向,而非透過 node 的事件迴圈在兩者間搬運資料。
|
|||||||
|
}
|
||||||
|
|
||||||
|
function appendGithubOutput(status, output) {
|
||||||
|
const outputFile = process.env.GITHUB_OUTPUT;
|
||||||
|
if (!outputFile) {
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `runCodex` 中使用 `spawn` 時,沒有設定 `cwd`。如果 `codex` 工具依賴於當前工作目錄(例如需要編輯當前專案),這在 CI 環境中可能存在風險,雖然目前 CI 通常會設定好目錄,但這是一個隱含的契約。
**建議**:建議明確設定 `cwd` 為 `/github/workspace` 或 CI 定義的專案根目錄,確保 `codex` 運作在預期的上下文中。
|
|||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delimiter;
|
||||||
|
do {
|
||||||
|
delimiter = `${OUTPUT_DELIMITER_PREFIX}${crypto.randomBytes(12).toString("hex")}`;
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:validateAuth 函式中對於 Base64 的正規化與驗證邏輯混雜在一起,使用了大量的取代與判斷,讀起來節奏凌亂,缺乏優雅感。
**建議**:將驗證邏輯與基礎轉換邏輯抽離,建議提取一個輔助函式專門負責 Base64 格式檢查,使主要流程清晰明瞭。
|
|||||||
|
} while (output.includes(delimiter));
|
||||||
|
|
||||||
|
fs.appendFileSync(
|
||||||
|
outputFile,
|
||||||
|
`status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`,
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:直接將所有輸出串接在 `outputChunks` 中,若 CLI 輸出過大的日誌,可能會導致記憶體耗盡(OOM)。
**建議**:建議針對 output 大小設定上限,超過限制時截斷輸出,或是改用串流寫入暫存檔以避免將所有內容存於記憶體。
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Mage
**問題**:這裡直接使用 `spawn` 執行 `codex` 命令,且參數 `prompt` 是直接從 `process.env.PROMPT` 讀取並傳入的。如果 CI 環境的 `PROMPT` 被惡意竄改,雖使用陣列傳遞參數避免了 shell injection,但 `codex exec` 的邏輯若沒有妥善限制(例如限制可執行指令類型),可能導致攻擊者在 CI Runner 環境執行任意指令。
**建議**:在 `runCodex` 函式中,除了已經加入的 `--dangerously-bypass-approvals-and-sandbox` 外,必須確保對 `prompt` 進行強力的白名單過濾,或改為使用非 `exec` 的子指令來限制權限。
|
|||||||
|
{ encoding: "utf8", mode: FILE_MODE_PRIVATE },
|
||||||
|
);
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:在處理輸出區塊時,使用 `chunks.reduce` 重複計算陣列大小,隨著資料量增加,這會造成不必要的 O(n²) 運算瓶頸,浪費 CPU 週期。
**建議**:應在 closure 中維護一個 `currentSize` 變數來追蹤當前總大小,避免每次有新資料時都重新遍歷整個區塊陣列。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:處理輸出區塊時使用 chunks.reduce 重複計算陣列大小,造成 O(n²) 運算瓶頸。
**建議**:在 closure 中維護 currentSize 變數追蹤總大小,避免遍歷。
|
|||||||
|
}
|
||||||
|
|
||||||
|
function fail(message, code = 1) {
|
||||||
|
console.error(message);
|
||||||
|
appendGithubOutput("failed", message);
|
||||||
|
cleanup();
|
||||||
|
process.exit(code);
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:測試檔案 `tests/entrypoint_test.sh` 有測試 `missing_codex_command`,這很好。但實作中對於 `codex` 執行失敗的各種細節(如權限不足、找不到 binary 等)都統一處理為 `status: 1` 和簡單的訊息,測試僅驗證了 failure 狀態,未驗證具體錯誤來源。
**建議**:考慮在 `main.js` 中根據不同的錯誤類型回傳更細緻的 status code,並在測試中驗證這些 code,能更精確地協助 CI 使用者除錯。
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:對於 codex 執行失敗的各種細節(權限不足、找不到 binary 等)都統一處理為 status: 1,測試未驗證具體錯誤來源。
**建議**:根據錯誤類型回傳細緻 status code,並在測試中驗證這些 code。
|
|||||||
|
}
|
||||||
|
|
||||||
|
function compactBase64(value) {
|
||||||
|
return value.replace(/\s+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBase64(value) {
|
||||||
|
const normalized = compactBase64(value);
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:validateAuth 中重複執行 decoded.toString('base64') 並進行 normalizedBase64 處理,極度浪費資源。
**建議**:移除這些無謂的重新編碼比較,直接嘗試解碼。
|
|||||||
|
const paddingIndex = normalized.indexOf("=");
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Assassin
**問題**:將 `OAUTH` 環境變數內容解碼並直接寫入 `auth.json`。雖然有檢查 base64 格式與 JSON 結構,但若解碼後的 JSON 內容包含惡意配置(如惡意插件路徑或偽造的 API 憑證),可能導致後續 `codex` CLI 在執行時被劫持或洩漏資料。
**建議**:除了驗證 JSON 結構外,應進一步驗證 `auth.json` 內的欄位是否符合預期格式,並限制其檔案權限為 `600`(已做),確保容器內其他行程無法讀取。
|
|||||||
|
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Leo
**問題**:`main` 函式過於龐大且職責過多,它同時負責了訊號處理、路徑創建、檔案鎖定、認證驗證以及執行核心邏輯,這降低了程式碼的可讀性與單元測試的困難度。
**建議**:建議將 `main` 拆分為 `validateInput`、`setupAuth`、`runCodexAction` 與 `cleanup` 等子函式,讓職責分離。
|
|||||||
|
if (!normalized || normalized.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:在 `runCodex` 函數中雖然有處理 `child.on('error', ...)`,但若 `codex` 指令本身不存在(spawn ENOENT),這裡捕捉到的 error stack trace 可能會包含完整的系統路徑資訊,這在 CI 環境中屬於資訊洩漏風險。
**建議**:建議在錯誤處理中,針對 `error.code === 'ENOENT'` 做明確判斷,回傳簡潔的錯誤訊息(例如「找不到 codex 指令」),而非直接回傳完整的 `error.message`。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:spawn ENOENT 錯誤處理可能洩漏系統路徑資訊。
**建議**:針對 error.code === 'ENOENT' 做明確判斷,回傳簡潔錯誤訊息而非完整 stack trace。
|
|||||||
|
|
||||||
|
return paddingIndex === -1 || /^=+$/.test(normalized.slice(paddingIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAuth(encodedAuth, authFile) {
|
||||||
|
if (!isBase64(encodedAuth)) {
|
||||||
|
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = Buffer.from(compactBase64(encodedAuth), "base64");
|
||||||
|
|
||||||
|
fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE });
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(decoded.toString("utf8"));
|
||||||
|
} catch {
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:當 `child.on('close', ...)` 觸發時,若 `code` 為 null,預設回傳 1。雖然這處理了非預期終止,但缺少對 signal 終止(例如 SIGKILL)的具體紀錄,只知道失敗,無法區分是指令執行錯誤還是被系統殺掉。
**建議**:在 `close` 事件中,若 `code` 為 null,可以檢查 `signal` 參數(若有),並在 output 中加入被哪個 signal 終止的資訊,增加除錯便利性。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:close 事件缺少對 signal 終止(如 SIGKILL)的具體紀錄。
**建議**:檢查 signal 參數,並在 output 中加入被哪個 signal 終止的資訊。
|
|||||||
|
fail("Decoded OAUTH must be a JSON object.");
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:runCodex 函式過於臃腫,包含了執行、超時處理、輸出截斷與錯誤捕捉等多重責任,這段旋律太過冗長且複雜。
**建議**:建議將輸出處理 (Output truncation logic) 與超時設定分離為獨立函式,以提升函式的可讀性與維護性。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:在 `runCodex` 的輸出處理中,每收到一塊資料就進行 `Buffer.concat` 與 `toString`,若資料量大或封包碎,會產生大量不必要的記憶體配置與垃圾回收 (GC) 壓力。
**建議**:只在輸出完成、達到限制或必須輸出結果時才進行合併與轉型,不要在處理每一塊資料時都執行。
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:runCodex 函式過於臃腫,包含了過多職責。
**建議**:建議將輸出處理與超時設定分離為獨立函式。
|
|||||||
|
|
||||||
|
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Maya
**問題**:parsePositiveInteger 的輸入回退機制未經測試。
**建議**:針對設定變數傳入無效數字或非法格式場景,驗證預設值套用。
|
|||||||
|
fail("Decoded OAUTH must be a JSON object.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInteger(value, fallback) {
|
||||||
|
const parsed = Number.parseInt(value || "", 10);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `child.on('close', ...)` 事件中,使用 `Buffer.concat(outputChunks).toString()` 將所有輸出轉為單一字串。如果 `outputChunks` 總大小接近 `DEFAULT_OUTPUT_LIMIT_BYTES` (1MB),這會導致瞬間記憶體使用量增加,且對於極大輸出,字串轉換本身亦有潛在的負載。
**建議**:考慮使用 `Buffer` 處理後續輸出,或在達到 `outputLimitBytes` 時,僅保存 `Buffer` 片段即可,不必轉為大字串。
|
|||||||
|
}
|
||||||
|
|
||||||
|
function readExecutionConfig() {
|
||||||
|
return {
|
||||||
|
timeoutMs: parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS),
|
||||||
|
outputLimitBytes: parsePositiveInteger(process.env.CODEX_OUTPUT_LIMIT_BYTES, DEFAULT_OUTPUT_LIMIT_BYTES),
|
||||||
|
workspace: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||||
|
};
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:spawn 函式參數陣列過長且散亂,閱讀性較差。
**建議**:將參數拆分為數組變數並展開傳遞。
|
|||||||
|
}
|
||||||
|
|
||||||
|
function codexExecArgs(model, prompt) {
|
||||||
|
return [
|
||||||
|
"exec",
|
||||||
|
"--dangerously-bypass-approvals-and-sandbox",
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Mage
**問題**:在 `setupAuth` 中,`lockPath` 使用 `os.tmpdir()`。在共享環境中,如果 `CODEX_HOME` 字串相同,會導致所有 process 競爭同一個鎖檔,且如果其他無關的 process 也剛好在 `os.tmpdir()` 中建立相同名稱的檔案,會導致誤判或鎖定失敗。
**建議**:應在 `CODEX_HOME` 內部建立鎖檔,而非使用全域的 `os.tmpdir()`,或者包含更具唯一性的識別碼(如 PID 或更長的路徑雜湊)以確保鎖的隔離性。
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Maya
**問題**:Codex 超時處理路徑未經測試,無法確保 SIGTERM 能成功發送與訊息正確產出。
**建議**:增加測試案例模擬長期睡眠(如 sleep 10),驗證超時機制與輸出訊息。
|
|||||||
|
"--skip-git-repo-check",
|
||||||
|
"--model",
|
||||||
|
model,
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Rogue
**問題**:在 setupAuth 中,`lockPath` 檔名產生使用了 `Buffer.from(codexHome).toString("hex")`。如果 `codexHome` 非常長,這個檔名可能會超過作業系統的檔案名稱長度限制(通常為 255 bytes),導致鎖定失敗,進而阻斷整個流程。
**建議**:改用 `crypto.createHash('sha256').update(codexHome).digest('hex')` 來產生固定長度的雜湊值作為檔名的一部分,既安全又保證長度可控。
|
|||||||
|
prompt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCodex(model, prompt) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { timeoutMs, outputLimitBytes, workspace } = readExecutionConfig();
|
||||||
|
const args = codexExecArgs(model, prompt);
|
||||||
|
|
||||||
|
// This Docker Action runs inside an ephemeral CI container where Codex must be
|
||||||
|
// able to edit the checked-out workspace without interactive approvals.
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Mage
**問題**:在 `setupAuth` 中,在 `fs.copyFileSync(authFile, authPath)` 後立即 `removeIfCreated(authFile)`,但在這期間如果發生 process 中斷,auth.json 可能會以不安全的權限(預設)或不完整的狀態寫入。
**建議**:建議使用 `fs.renameSync` 或在完成寫入與權限設定後再進行清理,並確保寫入過程中發生異常時能正確刪除該部分寫入的檔案。
|
|||||||
|
const child = spawn("codex", args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] });
|
||||||
|
|
||||||
|
const output = new OutputCollector(outputLimitBytes);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
output.append(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
child.stderr.pipe(process.stdout);
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
output.append(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
output.append(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
const message = error.code === "ENOENT" ? "Unable to find codex command.\n" : `${error.message}\n`;
|
||||||
|
output.append(Buffer.from(message));
|
||||||
|
resolve({ status: 1, output: output.toString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code, signal) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
if (code === null && signal) {
|
||||||
|
output.append(Buffer.from(`Codex process terminated by signal ${signal}.\n`));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ status: code ?? 1, output: output.toString() });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerCleanupHandlers() {
|
||||||
|
process.on("exit", cleanup);
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
cleanup();
|
||||||
|
process.exit(130);
|
||||||
|
});
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
cleanup();
|
||||||
|
process.exit(143);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfig() {
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔴 嚴重 **嚴重等級**:🔴 嚴重
**審查員**:Mage
**問題**:在 `setupAuth` 中,使用了 `fs.renameSync` 來確保原子性。然而在某些檔案系統中,若 `authFile` 與 `authPath` 不在同一個分區,`renameSync` 可能會失敗。此外,如果 `codexHome` 已存在且權限配置錯誤,`fs.mkdirSync` 可能會被忽略但後續存取失敗。
**建議**:建議確保 `authFile` 與 `codexHome` 處於相同掛載點,並增加對 `fs.mkdirSync` 後權限檢查的驗證。
|
|||||||
|
const oauth = process.env.OAUTH || "";
|
||||||
|
const model = process.env.MODEL || "";
|
||||||
|
const codexHome = process.env.CODEX_HOME || "/root/.codex";
|
||||||
|
const prompt = process.env.PROMPT || DEFAULT_PROMPT;
|
||||||
|
|
||||||
|
if (!oauth) {
|
||||||
|
fail("OAUTH is required: provide base64 encoded Codex auth.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
fail("MODEL is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { oauth, model, codexHome, prompt };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAuthLock(codexHome) {
|
||||||
|
const lockName = crypto.createHash("sha256").update(codexHome).digest("hex");
|
||||||
|
const lockPath = path.join(codexHome, `.codex-auth-${lockName}.lock`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lockHandle = fs.openSync(lockPath, "wx", FILE_MODE_PRIVATE);
|
||||||
|
tempFiles.trackFile(lockPath);
|
||||||
|
return lockHandle;
|
||||||
|
} catch {
|
||||||
|
fail("Unable to lock Codex auth.json.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Bard
**問題**:setupAuth 函式職責過於繁雜。
**建議**:將鎖定機制與驗證機制拆分為獨立輔助函式。
|
|||||||
|
|
||||||
|
function setupAuth(oauth, codexHome) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
|
||||||
|
fs.chmodSync(codexHome, DIR_MODE_PRIVATE);
|
||||||
|
fs.accessSync(codexHome, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
|
||||||
|
} catch {
|
||||||
|
fail("Unable to create CODEX_HOME.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDir = makeTempDir(codexHome);
|
||||||
|
const authFile = makeTempFile(tempDir, "auth");
|
||||||
|
const authPath = path.join(codexHome, "auth.json");
|
||||||
|
const lockHandle = createAuthLock(codexHome);
|
||||||
|
|
||||||
|
validateAuth(oauth, authFile);
|
||||||
|
|
||||||
|
if (fs.existsSync(authPath)) {
|
||||||
|
fail("Refusing to overwrite existing Codex auth.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(authFile, authPath);
|
||||||
|
fs.chmodSync(authPath, FILE_MODE_PRIVATE);
|
||||||
|
tempFiles.trackFile(authPath);
|
||||||
|
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCodexAction({ oauth, model, codexHome, prompt }) {
|
||||||
|
const lockHandle = setupAuth(oauth, codexHome);
|
||||||
|
const result = await runCodex(model, prompt);
|
||||||
|
appendGithubOutput(result.status === 0 ? "completed" : "failed", result.output);
|
||||||
|
|
||||||
|
if (lockHandle !== undefined) {
|
||||||
|
fs.closeSync(lockHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
process.exit(result.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
registerCleanupHandlers();
|
||||||
|
await runCodexAction(readConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
fail(error instanceof Error ? error.message : String(error));
|
||||||
|
});
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "Gitea Server Url: $GITEA_SERVER_URL"
|
set -euo pipefail
|
||||||
|
|
||||||
echo "Gitea Repository: $GITEA_REPOSITORY"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
echo "Gitea Runner Token: $RUNNER_TOKEN"
|
exec node "$SCRIPT_DIR/app/main.js"
|
||||||
|
|
||||||
echo "Input Text: $INPUT_TEXT"
|
|
||||||
|
|
||||||
echo "text=$INPUT_TEXT" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
IMAGE_NAME="codex-action-smoke:local"
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "SKIP: docker is not available"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "SKIP: docker daemon is not available"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker build -t "$IMAGE_NAME" "$ROOT_DIR"
|
||||||
|
|
||||||
|
docker run --rm --entrypoint codex "$IMAGE_NAME" --version
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:目前的 Docker 冒煙測試僅驗證了 CLI 二進位檔是否存在,但尚未驗證其在容器內執行時是否能正常存取與寫入 `CODEX_HOME` 環境設定的目錄。
**建議**:建議在 `docker_image_test.sh` 中增加一個測試案例,執行 `codex --version` 之外的指令,驗證容器權限與目錄環境變數設定是否正確。
|
|||||||
|
docker run --rm --entrypoint sh "$IMAGE_NAME" -c 'mkdir -p "$CODEX_HOME" && test -w "$CODEX_HOME" && codex --version >/dev/null'
|
||||||
|
docker run --rm --entrypoint codex "$IMAGE_NAME" plugin list | grep -q "jsc@doc"
|
||||||
|
docker run --rm --entrypoint codex "$IMAGE_NAME" plugin list | grep -q "jsc@code-review"
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "FAIL: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fake_codex() {
|
||||||
|
local dir="$1"
|
||||||
|
local exit_code="$2"
|
||||||
|
local message="$3"
|
||||||
|
|
||||||
|
cat > "$dir/codex" <<SH
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🟡 警告 **嚴重等級**:🟡 警告
**審查員**:Bard
**問題**:在測試中建立 `codex` 指令時,Here-document 與命令混雜,可讀性較低。
**建議**:建議使用更整齊的縮排格式,或將其抽離為獨立的測試輔助檔案。
|
|||||||
|
#!/bin/sh
|
||||||
|
printf '%s\n' "\$*" > "\${CODEX_FAKE_ARGS:-/dev/null}"
|
||||||
|
echo "$message"
|
||||||
|
exit $exit_code
|
||||||
|
SH
|
||||||
|
chmod +x "$dir/codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fake_sleeping_codex() {
|
||||||
|
local dir="$1"
|
||||||
|
|
||||||
|
cat > "$dir/codex" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
while true; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
SH
|
||||||
|
chmod +x "$dir/codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fake_signaled_codex() {
|
||||||
|
local dir="$1"
|
||||||
|
|
||||||
|
cat > "$dir/codex" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
kill -TERM $$
|
||||||
|
SH
|
||||||
|
chmod +x "$dir/codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fake_large_output_codex() {
|
||||||
|
local dir="$1"
|
||||||
|
|
||||||
|
cat > "$dir/codex" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
printf '0123456789'
|
||||||
|
SH
|
||||||
|
chmod +x "$dir/codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded_json() {
|
||||||
|
printf '%s' "$1" | base64 | tr -d '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
run_entrypoint() {
|
||||||
|
local tmpdir="$1"
|
||||||
|
shift
|
||||||
|
Ghost marked this conversation as resolved
gitea-actions
commented
嚴重等級:🔵 建議 **嚴重等級**:🔵 建議
**審查員**:Maya
**問題**:測試案例對於輸入參數的邊界測試(例如 `PROMPT` 為空字串、極長字串)不足。
**建議**:增加針對 `PROMPT` 輸入為空字串或是包含特殊 Shell 跳脫字元的測試案例。
|
|||||||
|
|
||||||
|
env \
|
||||||
|
PATH="$tmpdir:$PATH" \
|
||||||
|
CODEX_HOME="$tmpdir/codex-home" \
|
||||||
|
GITHUB_OUTPUT="$tmpdir/github-output" \
|
||||||
|
"$@" \
|
||||||
|
bash "$ROOT_DIR/entrypoint.sh" > "$tmpdir/stdout" 2> "$tmpdir/stderr"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_status() {
|
||||||
|
local actual="$1"
|
||||||
|
local expected="$2"
|
||||||
|
|
||||||
|
[[ "$actual" -eq "$expected" ]] || die "expected status $expected, got $actual"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_missing_oauth() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 0 "unused"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 1
|
||||||
|
grep -q "OAUTH is required" "$tmpdir/stderr" || die "missing OAUTH error"
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "missing OAUTH failed status output"
|
||||||
|
grep -q "OAUTH is required" "$tmpdir/github-output" || die "missing OAUTH output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_missing_model() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 0 "unused"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 1
|
||||||
|
grep -q "MODEL is required" "$tmpdir/stderr" || die "missing MODEL error"
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "missing MODEL failed status output"
|
||||||
|
grep -q "MODEL is required" "$tmpdir/github-output" || die "missing MODEL output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_invalid_base64() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 0 "unused"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="not-base64" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 1
|
||||||
|
grep -q "valid base64" "$tmpdir/stderr" || die "invalid base64 error"
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "invalid base64 failed status output"
|
||||||
|
grep -q "valid base64" "$tmpdir/github-output" || die "invalid base64 output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_non_object_json() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 0 "unused"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '[]')" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 1
|
||||||
|
grep -q "JSON object" "$tmpdir/stderr" || die "non-object JSON error"
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "non-object JSON failed status output"
|
||||||
|
grep -q "JSON object" "$tmpdir/github-output" || die "non-object JSON output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_success_output() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 0 "codex ok"
|
||||||
|
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
|
||||||
|
grep -q "status=completed" "$tmpdir/github-output" || die "completed status output"
|
||||||
|
grep -q "codex ok" "$tmpdir/github-output" || die "codex output"
|
||||||
|
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_empty_prompt() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 0 "codex ok"
|
||||||
|
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT=""
|
||||||
|
|
||||||
|
grep -q "codex ok" "$tmpdir/github-output" || die "empty prompt output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_prompt_with_shell_characters() {
|
||||||
|
local tmpdir
|
||||||
|
local prompt
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
prompt='hello; rm -rf / $(echo bad) "quoted"'
|
||||||
|
make_fake_codex "$tmpdir" 0 "codex ok"
|
||||||
|
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="$prompt" CODEX_FAKE_ARGS="$tmpdir/codex-args"
|
||||||
|
|
||||||
|
grep -qF "$prompt" "$tmpdir/codex-args" || die "prompt was not passed as one argument"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_failure_output() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_codex "$tmpdir" 7 "codex failed"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 7
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "failed status output"
|
||||||
|
grep -q "codex failed" "$tmpdir/github-output" || die "failed codex output"
|
||||||
|
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up after failure"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_missing_codex_command_output() {
|
||||||
|
local tmpdir
|
||||||
|
local bindir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
bindir="$tmpdir/bin"
|
||||||
|
mkdir -p "$bindir"
|
||||||
|
ln -s "$(command -v bash)" "$bindir/bash"
|
||||||
|
ln -s "$(command -v dirname)" "$bindir/dirname"
|
||||||
|
ln -s "$(command -v node)" "$bindir/node"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" PATH="$bindir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 1
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "missing codex command failed status output"
|
||||||
|
grep -q "Unable to find codex command" "$tmpdir/github-output" || die "missing codex command error output"
|
||||||
|
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up after missing codex"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_codex_timeout_output() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_sleeping_codex "$tmpdir"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello" CODEX_TIMEOUT_MS="100"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 143
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "timeout failed status output"
|
||||||
|
grep -q "Codex execution timed out after 100 ms" "$tmpdir/github-output" || die "timeout message output"
|
||||||
|
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up after timeout"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_codex_signal_output() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_signaled_codex "$tmpdir"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||||
|
local status="$?"
|
||||||
|
set -e
|
||||||
|
|
||||||
|
assert_status "$status" 1
|
||||||
|
grep -q "status=failed" "$tmpdir/github-output" || die "signal failed status output"
|
||||||
|
grep -q "Codex process terminated by signal SIGTERM" "$tmpdir/github-output" || die "signal message output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_codex_output_truncation() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_large_output_codex "$tmpdir"
|
||||||
|
|
||||||
|
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello" CODEX_OUTPUT_LIMIT_BYTES="5"
|
||||||
|
|
||||||
|
grep -q "status=completed" "$tmpdir/github-output" || die "truncated output completed status"
|
||||||
|
grep -q "01234" "$tmpdir/github-output" || die "truncated output prefix"
|
||||||
|
grep -q "\\[Output truncated\\]" "$tmpdir/github-output" || die "truncation message output"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_invalid_execution_config_uses_defaults() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
make_fake_large_output_codex "$tmpdir"
|
||||||
|
|
||||||
|
run_entrypoint "$tmpdir" \
|
||||||
|
OAUTH="$(encoded_json '{}')" \
|
||||||
|
MODEL="gpt-test" \
|
||||||
|
PROMPT="hello" \
|
||||||
|
CODEX_TIMEOUT_MS="invalid" \
|
||||||
|
CODEX_OUTPUT_LIMIT_BYTES="invalid"
|
||||||
|
|
||||||
|
grep -q "status=completed" "$tmpdir/github-output" || die "invalid config completed status"
|
||||||
|
grep -q "0123456789" "$tmpdir/github-output" || die "invalid output limit did not use default"
|
||||||
|
! grep -q "\\[Output truncated\\]" "$tmpdir/github-output" || die "invalid output limit unexpectedly truncated"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_missing_oauth
|
||||||
|
test_missing_model
|
||||||
|
test_invalid_base64
|
||||||
|
test_non_object_json
|
||||||
|
test_success_output
|
||||||
|
test_empty_prompt
|
||||||
|
test_prompt_with_shell_characters
|
||||||
|
test_failure_output
|
||||||
|
test_missing_codex_command_output
|
||||||
|
test_codex_timeout_output
|
||||||
|
test_codex_signal_output
|
||||||
|
test_codex_output_truncation
|
||||||
|
test_invalid_execution_config_uses_defaults
|
||||||
|
|
||||||
|
echo "entrypoint tests passed"
|
||||||
嚴重等級:🟡 警告
審查員:Assassin
問題:Codex 外掛市集來源指向
gitea.jsc.idv.tw,未驗證來源的安全性。若該伺服器遭駭,將導致自動安裝惡意或篡改過的外掛,進而導致供應鏈攻擊。建議:若可能,請將外掛來源固定在受信任的內部儲存庫或使用經簽署的外掛版本。確保來源伺服器具有嚴格的存取控管與安全性掃描。
嚴重等級:🟡 警告
審查員:Leo
問題:環境變數使用以空格分隔的字串,若未來名稱中包含空格將導致 shell 展開錯誤,且難以維護。
建議:建議改用換行符號(
)分隔,並在安裝迴圈中使用
IFS=$' '處理,以提高 shell 指令的健壯性。