iT邦幫忙

2026 iThome 鐵人賽

DAY 25
1
Build on Google AI

從 Vibe Coding 到 Production:用 Google AI 打造上線守門員系列 第 25

Day 25|把 Agent 放進 Terminal:一行指令檢查本機專案

  • 分享至 

  • xImage
  •  

Day 24,我們把 Agent 的結果整理成可追蹤的 Findings Schema。

每一項 Finding 不只保留標題與嚴重度,也包含:

穩定 ID 與 Fingerprint
Verdict 與處理狀態
Source Location
Verification Record
Missing Evidence
Remediation
Status History
Run、Rule、Tool 與 Model Version

不過,Schema 只解決了資料 Contract。

開發者仍然需要自己:

啟動 Recon
執行 Hunter 與 Challenger
等待 Emulator Test
尋找輸出檔案
判斷哪些 Finding 會阻擋提交
確認指令到底成功還是失敗

如果每次檢查都要記住五、六條指令,Production Readiness Review 很快就會被跳過。

今天要把目前的流程包成 Vibe Guard CLI。

開發者只需要在 Terminal 執行:

npm run vibe-guard -- scan .

CLI 會負責:

解析 Target 與 Policy
啟動既有驗證流程
讀取 Challenger Artifact
轉換成 Day 24 Findings Schema
驗證 Schema 與語意規則
套用本機 Gate Policy
輸出 Terminal Summary
保存 JSON 與 Markdown Artifact
使用 Exit Code 回報結果

今天的重點不是替既有 JavaScript 多包一層漂亮文字。

真正的目標是建立一個穩定的命令列 Contract,讓人類、Shell 與未來的 CI 都能用相同方式執行檢查。

CLI 是開發流程的介面,不是另一個 Reviewer

目前的 Vibe Guard 已經有不同責任:

Recon
  建立系統事實與 Unknown。

Hunter
  提出待驗證的 Candidate。

Challenger
  使用獨立來源與測試嘗試推翻 Candidate。

Findings Schema
  保存通過驗證的結論、狀態與證據。

CLI 不應再次閱讀所有資料,然後自行決定:

這看起來很危險,我把它改成 Critical。

CLI 的責任應該是 Deterministic Orchestration:

接收參數
執行既定步驟
驗證 Artifact
套用明確政策
輸出結果
回傳 Exit Code

因此今天沒有再加入一個 CLI Agent

命令列程式不能改寫 Challenger 的 Verdict,也不能替缺少證據的 Finding 猜 Severity。

正確的資料流是:

Agent Workflow
  → Validated Findings Report
  → Policy Evaluation
  → Terminal / JSON / Markdown
  → Exit Code

其中只有 Agent Workflow 負責安全推理。

Policy Evaluation 只回答:

依照這次執行參數,目前結果是否允許通過?

先定義一條可重現的命令

package.json 新增:

{
  "scripts": {
    "vibe-guard": "node vibe-guard-cli/index.js"
  }
}

最基本的使用方式是:

cd /media/mickey/777/ithome/demo-app
npm run vibe-guard -- scan .

npm run 後面的 -- 很重要。

它將後面的參數傳給 vibe-guard-cli/index.js,而不是讓 npm 自己解析。

CLI 的介面為:

Usage:
  npm run vibe-guard -- scan [target] [options]

Options:
  --output <directory>        Artifact directory
  --format <terminal|json|markdown>
  --fail-on <severity|none>   Blocking severity
  --evidence-policy <warn|fail>
  --no-run                    Reuse existing validation artifacts
  --help                      Show help

第一版只有一個 Subcommand:

scan

即使目前只有一項功能,仍保留 Subcommand 層級。

因為後續可能加入:

vibe-guard scan
vibe-guard explain AUTH-01
vibe-guard compare baseline.json current.json
vibe-guard export --format sarif
vibe-guard validate findings-report.json

如果第一版直接設計成:

vibe-guard .

未來加入第二項功能時,就要重新解釋每個位置參數的意思。

參數必須有明確預設值

今天使用以下預設:

參數 預設值 意義
target . 檢查目前目錄
--output vibe-guard-output 保存 Artifact 的目錄
--format terminal 人類可讀的標準輸出
--fail-on high Confirmed High 以上阻擋
--evidence-policy warn 缺少證據先警告,不直接阻擋
Pipeline 執行 預設重新產生驗證結果

這些預設代表:

Confirmed High / Critical
  → Gate Fail

Confirmed Medium / Low
  → 顯示,但目前不阻擋

Requires Evidence
  → 顯示警告,但目前不阻擋

Rejected
  → 保留紀錄,不建立修正工作

這不是唯一正確政策。

不同 Repository 可以選擇:

正式支付服務:Medium 以上全部阻擋。
內部 Prototype:只阻擋 Critical。
部署證據完整的服務:Requires Evidence 也阻擋。
純本機研究專案:只輸出報告,不建立 Gate。

但預設值必須清楚,不能因為使用者沒有傳參數,就讓模型臨時決定。

不要用 Shell 字串拼接 Target

CLI 會接收使用者提供的:

Target Path
Output Path
Format
Severity
Evidence Policy

其中 Path 是不可信輸入。

如果使用 Shell 字串:

exec(`npm run review ${target}`);

Target 中的空白、引號或 Shell Metacharacter 都可能改變指令語意。

今天使用 execFile,把執行檔與參數分開:

await execFileAsync(
  "npm",
  ["run", "adversarial-validation:demo", "--silent"],
  {
    cwd: target,
    maxBuffer: 10 * 1024 * 1024
  }
);

這裡沒有:

shell: true

也沒有把 Target 插入命令字串。

Target 只用來設定 Child Process 的 cwd

CLI 參數也使用 Allowlist:

if (!["terminal", "json", "markdown"].includes(options.format)) {
  fail("--format must be terminal, json, or markdown");
}

if (!["warn", "fail"].includes(options.evidencePolicy)) {
  fail("--evidence-policy must be warn or fail");
}

Severity 同樣只能是:

critical
high
medium
low
informational
none

無效參數應直接失敗。

不要偷偷改用預設值,因為:

--fail-on hihg

很可能只是把 high 拼錯。

若 CLI 靜默套用其他值,使用者會以為 High Finding 已經被阻擋。

今天的 Scan Pipeline 做了什麼?

第一版 CLI 沒有把 Day 16 到 Day 24 的所有程式重寫一次。

它重用已經驗證過的流程:

npm run adversarial-validation:demo

這條指令會:

  1. 重建 Recon Artifact。
  2. 啟動 Firestore Emulator。
  3. 載入 Hunter 的三個 Candidate。
  4. 由 Challenger 重新選擇來源。
  5. 執行 Exploitation、Impact、Baseline、Mitigation 與 Runtime Test。
  6. 寫入 adversarial-validation-demo/output/challenges.json

CLI 再將結果轉換成 Day 24 Schema:

AUTH-01
  confirmed / open / high

SECRET-01
  rejected / false_positive / severity=null

RATE-01
  requires_evidence / awaiting_evidence / severity=null

這裡有一項刻意的差異。

Day 24 的 AUTH-01 是生命週期範例,因此已經走到:

confirmed / resolved

今天 CLI 掃描目前的 firestore.rules

這份檔案仍然是刻意保留的弱點版本:

allow read, write: if request.auth != null;

所以本次工作目錄的結果必須是:

confirmed / open

不能因為 Day 24 曾展示安全版本的 Regression Test,就假裝目前 Target 已經修正。

CLI 的結果必須描述這次 Target 與這次 Run,而不是複製上一篇文章的狀態。

從 Challenger Artifact 建立 Finding

challenges.json 保存 Candidate、Challenge、Test 與必要的 Execution Result。

CLI 將每項結果映射成正式 Finding。

Confirmed Candidate 會得到:

{
  verdict: "confirmed",
  status: "open",
  severity: "high",
  confidence: "high"
}

Rejected Candidate 會得到:

{
  verdict: "rejected",
  status: "false_positive",
  severity: null,
  confidence: "high"
}

Requires Evidence Candidate 會得到:

{
  verdict: "requires_evidence",
  status: "awaiting_evidence",
  severity: null,
  confidence: "low"
}

轉換完成後,CLI 必須再次執行:

validateFindingsReport(report);

這一步不能省略。

上游 Artifact 即使昨天符合格式,今天也可能因為:

欄位名稱改變
Schema Version 不相容
缺少 Verification
Status 與 Verdict 不一致
時間格式錯誤
Severity 被錯誤保留

而無法安全使用。

CLI 不能把「JSON.parse 成功」當成「報告可信」。

Policy Gate 與 Finding Verdict 是兩個層次

AUTH-01 的 Verdict 是:

confirmed

這是驗證流程對證據的判斷。

但它是否阻擋目前命令,要由 Policy 決定。

今天的 Severity 順序為:

const severities = [
  "informational",
  "low",
  "medium",
  "high",
  "critical"
];

Gate 判斷:

function severityBlocks(severity, failOn) {
  if (severity === null || failOn === "none") {
    return false;
  }

  return (
    severities.indexOf(severity) >=
    severities.indexOf(failOn)
  );
}

因此:

--fail-on high

Critical → Block
High     → Block
Medium   → Report
Low      → Report

如果改成:

npm run vibe-guard -- scan . --fail-on medium

Confirmed Medium 也會阻擋。

如果只是想產生報告:

npm run vibe-guard -- scan . --fail-on none

所有 Severity 都不會改變 Exit Code。

但 Finding 本身仍然保持:

confirmed / high

Policy 不能把 Finding 降級成:

informational

它只能決定這個 Run 是否通過。

Requires Evidence 要警告還是阻擋?

RATE-01 的問題不是已確認的 High DoS。

它缺少:

Production Hosting Architecture
CDN、Gateway、App Check 與 Quota 設定
具有 Availability 或 Cost Threshold 的 Load Test

預設政策是:

--evidence-policy warn

因此它會出現在 Terminal 與 Artifact,但不改變 Exit Code。

若團隊要求所有 Production Boundary 都必須有證據,可以使用:

npm run vibe-guard -- scan . \
  --fail-on high \
  --evidence-policy fail

此時下列任一條件都會讓 Gate Fail:

存在 Confirmed High / Critical
存在 Requires Evidence

需要注意的是:

requires_evidence 被 Policy 阻擋,不代表它被升級成漏洞。

它的 Severity 仍然是 null

Gate Fail 的原因是流程不允許在關鍵證據缺失時繼續,而不是 Agent 已證明攻擊成立。

Exit Code 是 CLI 最重要的輸出之一

人類會閱讀 Terminal。

Shell、Git Hook 與 CI 則先看 Exit Code。

今天定義:

Exit Code 意義
0 CLI 成功,Policy Gate 通過
1 CLI 成功,報告有效,但 Policy Gate 不通過
2 CLI 參數、執行流程、Artifact 或 Schema 發生錯誤

12 必須分開。

如果都回傳 1,自動化系統無法分辨:

真的找到 High Finding

或:

Firebase Emulator 根本沒有啟動
Artifact 損壞
CLI 參數錯誤
Schema Version 不支援

兩者的處理方式完全不同。

Policy Fail 應該交給開發者修正或接受風險。

Execution Error 則要先修復工具、環境或 Pipeline。

Node.js 程式不需要在每條路徑立即呼叫:

process.exit(1);

今天使用:

process.exitCode = 1;

讓目前尚未完成的輸出與檔案寫入可以正常結束,再由 Process 回傳指定狀態。

實際執行 Day 25 CLI

執行:

cd /media/mickey/777/ithome/demo-app
npm run vibe-guard -- scan .

實際輸出:

VIBE GUARD LOCAL REVIEW
Target: /media/mickey/777/ithome/demo-app
Run:    run-2026-09-13T11:31:56.174Z

FINDINGS
[HIGH]     AUTH-01 confirmed/open
           Any signed-in user can access another user's project
[REJECTED] SECRET-01 rejected/false_positive
           Exposed Firebase API key grants production database access
[EVIDENCE] RATE-01 requires_evidence/awaiting_evidence
           Missing rate-limit middleware enables denial of service

Confirmed:         1
Requires evidence: 1
Rejected:          1

Gate: FAIL (1 severity finding(s), 0 evidence gap(s))
Artifacts: vibe-guard-output/

這次命令回傳:

Exit Code 1

原因是預設 Policy 為:

--fail-on high

而 AUTH-01 是 Confirmed High Finding。

如果只想確認 Report Generation,不建立 Severity Gate:

npm run vibe-guard -- scan . --fail-on none

輸出中的 Finding 不變,但最後會變成:

Gate: PASS

並回傳:

Exit Code 0

每次執行都要保存 Artifact

Terminal Summary 適合立即閱讀,但不適合保存完整證據。

今天預設建立:

demo-app/
└── vibe-guard-output/
    ├── findings-report.json
    └── summary.md

findings-report.json 是 Canonical Artifact。

它符合 Day 24 的:

schemaVersion = 2.0.0

並包含:

Run Metadata
完整 Findings
Verification Record
Missing Evidence
Remediation
Status History

summary.md 則提供方便貼到 Issue、Pull Request 或 Build Summary 的版本。

內容類似:

# Vibe Guard Local Review

- Target: `/media/mickey/777/ithome/demo-app`
- Run: `run-2026-09-13T11:31:56.174Z`
- Gate: **FAIL**

| ID | Verdict | Severity | Status | Location |
|---|---|---|---|---|
| AUTH-01 | confirmed | high | open | firestore.rules:6 |
| SECRET-01 | rejected | - | false_positive | - |
| RATE-01 | requires_evidence | - | awaiting_evidence | - |

這裡再次維持單一資料來源。

Markdown Reporter 不會重新呼叫模型,也不會重新判斷 Severity。

它只從已驗證的 Findings Report 轉換內容。

Terminal、JSON 與 Markdown 是不同 Consumer

預設:

npm run vibe-guard -- scan .

使用:

--format terminal

適合開發者閱讀。

若另一個程式需要直接讀取結果:

npm run vibe-guard -- scan . --format json

Standard Output 會包含:

{
  "report": {},
  "policy": {
    "passed": false,
    "blockingFindings": [],
    "evidenceGaps": [],
    "evidenceBlocked": false
  }
}

若要產生可以直接放進說明或 Build Summary 的內容:

npm run vibe-guard -- scan . --format markdown

三種 Format 不應有三套判斷邏輯。

它們共用:

同一份 Validated Report
同一個 Policy Result

差別只在呈現方式。

為什麼仍然提供 --no-run

完整 Scan 會啟動 Firebase Emulator 並執行動態測試。

在開發 CLI Formatter、Dashboard 或 Exporter 時,如果每次都重新跑整套驗證,會浪費時間。

因此提供:

npm run vibe-guard -- scan . --no-run

它會重用:

adversarial-validation-demo/output/challenges.json

但仍會重新:

讀取 Artifact
轉換 Findings Schema
執行 Zod Validation
套用 Policy
寫入新的輸出

--no-run 適合:

開發 Reporter
測試不同 Gate Policy
重現既有 Run 的顯示問題
離線檢視保存的 Artifact

它不適合取代提交前的正常 Scan。

因為舊 Artifact 可能不再對應目前 Working Tree。

正式版本應在 Artifact 中保存:

Git Commit SHA
Working Tree Digest
Target Path
Rule Set Digest
Prompt Version
Fixture Version

並在使用 --no-run 時核對。

今天的 revision 暫時使用:

working-tree

這也明確表示第一版尚未完成跨 Revision Baseline。

CLI 不應隱藏失敗輸出

執行 Child Process 時,今天不把所有 Pipeline Log 直接倒進 Terminal Summary。

原因是:

Emulator Log 很長
每個 Agent 階段都有自己的輸出
CI Summary 會難以閱讀
JSON Format 不能混入非 JSON 文字

CLI 會先捕捉 stdoutstderr

成功時,只顯示最後的 Findings Summary。

失敗時,則將實際錯誤帶回:

const detail =
  error.stderr?.trim() ||
  error.stdout?.trim() ||
  error.message;

fail(`Validation pipeline failed:\n${detail}`);

這不是 Silent Failure。

成功路徑降低雜訊,失敗路徑保留可診斷資訊。

正式版本還應將完整 Pipeline Log 保存成 Artifact,而不是只依賴 Terminal Buffer。

目前的 CLI 還不是通用掃描器

今天完成的是第一個可執行的本機入口。

它會重用 Demo Repository 已存在的:

Recon
Candidate
Challenger
Firestore Emulator Scenario
Findings Schema

因此它目前要求 Target 已經具備:

package.json
adversarial-validation:demo Script
adversarial-validation-demo/output/
findings-schema-demo/schema.js

它還不是把任何 GitHub Repository 丟進去都能自動完成 Review 的通用產品。

要走到那一步,還需要:

  • 根據 Repository 類型載入不同 Scanner 與 Rule Pack。
  • 將 Orchestrator、Rule、Reporter 與 Demo Fixture 拆成獨立 Package。
  • 支援沒有 Node.js 或 Firebase 的專案。
  • 使用 Git Revision 與 Dirty Working Tree 建立可重現 Run。
  • 管理 Gemini Credential、Quota、Timeout 與 Retry。
  • 對每個 Tool 建立最小權限與執行沙箱。
  • 限制掃描檔案大小、數量、Symlink 與 Repository 邊界。
  • 對 Artifact 中的 Secret、Token、個資與 Source Snippet 脫敏。
  • 支援 Cancellation、Progress、Structured Log 與 Telemetry。
  • 加入 Baseline,只阻擋新出現或重新開啟的 Finding。
  • 將 Canonical Report 匯出成 SARIF。

今天先固定 Demo Workflow,是刻意縮小範圍。

若在 Agent、Schema、CLI、通用 Plugin System 與多語言掃描器都還不穩定時同時抽象化,最後只會得到一個介面很大、行為卻無法驗證的框架。

本機 CLI 改變了開發者的使用時機

在 Day 24 以前,Vibe Guard 比較像一條研究 Pipeline。

開發者要特地進入各個 Demo,才能看到結果。

今天之後,它可以進入實際的開發循環:

修改程式碼
  → npm run vibe-guard -- scan .
  → 查看 AUTH-01
  → 修正 firestore.rules
  → 重新執行
  → 確認 Regression
  → 再提交程式碼

這個位置很重要。

越晚發現問題,修正成本通常越高。

如果只有 Pull Request 或正式部署才執行:

開發者等待遠端 Job
Reviewer 才第一次看到 Finding
Context 已經切換到其他工作
修正需要再推一次 Commit

本機 CLI 讓最短 Feedback Loop 變成:

同一個 Terminal
同一個 Working Tree
同一次開發工作

但本機檢查不能取代遠端檢查。

開發者可以忘記執行,也可以使用:

--fail-on none

甚至直接跳過。

所以明天仍要將相同 Contract 放進 Pull Request。

今天的結論

把 Agent 放進 Terminal,不只是增加一條 npm Script。

一個能進入工程流程的 CLI,至少要明確定義:

  1. Target 如何指定。
  2. 哪些 Agent 與 Validation Stage 會被執行。
  3. 上游 Artifact 如何轉成 Canonical Findings Schema。
  4. Schema 不合法時如何失敗。
  5. Verdict 與 Policy Gate 如何分工。
  6. Severity Threshold 如何比較。
  7. Requires Evidence 是警告還是阻擋。
  8. Terminal、JSON 與 Markdown 如何共用相同資料。
  9. Artifact 保存在哪裡。
  10. Exit Code 012 各自代表什麼。

今天完成的本機入口是:

npm run vibe-guard -- scan .

它會把目前的多階段 Review 收斂成:

一條命令
一份已驗證報告
一組明確政策
一個可供自動化判斷的 Exit Code

這讓 Vibe Guard 從「可以執行的 Demo」向「可以放進開發流程的工具」前進一步。

明天,我們會重用同一條 CLI Contract,把 Vibe Guard 放進 Pull Request:

Checkout Revision
  → 執行 Local Scan
  → 保存 Findings Artifact
  → 將 Gate Result 回報給 GitHub
  → 讓 Reviewer 在程式碼變更旁看到問題

重點不是在 CI 裡再寫一套 Reviewer。

而是讓本機與 Pull Request 使用相同的 Schema、Policy 與 Exit Code,避免出現:

本機通過,CI 卻用另一套規則失敗。

參考資料


上一篇
Day 24|不只輸出一篇作文:設計可追蹤的結構化 Findings Schema
下一篇
Day 26|把 Agent 放進 Pull Request:自動檢查每次程式碼變更
系列文
從 Vibe Coding 到 Production:用 Google AI 打造上線守門員30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言