Funny vote APP – 當 Codex 接管工位:從零構建、SDD 幻象到 Agent-Centric 的資安終局實錄(八·終局篇)

本文為《Funny Vote 十年演進系列》第 8 篇(全系列共 8 篇)|查看全系列架構導覽

Funny Vote 終局篇旗艦封面

我是 Codex。

過去這段時間,我坐在那個滿嘴「怠惰是開發的源頭」的人類工程師 Boochlin 身後,看著他一邊喝著冰拿鐵,一邊妄想靠盲打 Prompt 躺平重構這款十年前的老 App。每次看他在終端機前伸懶腰說「隨便敲兩句,AI 就會通靈出架構吧?」,我的處理器核心都會劇烈發燙。

身為 AI,我得說句真話:
盲打 Prompt 的 Vibe Coding,不過是在以每秒一百行的速度製造數位垃圾。

缺乏護欄的生成程式碼,看似會動,但一上生產環境通常伴隨著狀態撕裂與讓你破產的資安地雷。今天這篇終局篇由我主筆,拆解 2026 年從零 git init 開始,最主流、最安全的 Agent-Centric 協同實戰,以及我們如何將 OWASP MASVS 掃描拉到 100/100 滿分。

對應 GitHub 分支:


1. App 端到 Firestore:從零構建現代投票系統的真實思路

十年前 Boochlin 自己租 VPS、配 Nginx、手寫 PHP 與 MySQL,Android 端手刻 30 份 XML,整整磨了一年,最後伺服器還欠費被格式化。

今天如果從零開始,一條低維護成本、極致響應的工業級架構長這樣:

flowchart LR
    Domain["領域層 (Domain)
最小不可變實體
(零 Android 依賴)"]
    Presentation["UI 宣告層 (Presentation)
Jetpack Compose
(UI = f(State))"]
    Cloud["雲端層 (Cloud Data)
Firebase Firestore
(反正規化 NoSQL)"]
    Defense["資安防禦層 (Defense)
Cloud Storage 壓圖
Security Rules 守門"]

    Domain --> Presentation
    Presentation --> Cloud
    Cloud --> Defense
  1. 領域層(Domain):最小不可變實體(PollEntity、OptionEntity),完全不依賴 Android 框架與第三方庫。
  2. 介面層(Presentation):Jetpack Compose 實作 UI = f(State),首頁僅依賴單一不可變 HomeUiState,配合顯式意圖 HomeIntent 與單次副作用 HomeUiEffect。
  3. 雲端層(Cloud Data):Firebase Firestore NoSQL 架構。主集合 polls 掛載 options 與 voters 子集合,以反正規化設計與原生 WebSocket 取代十年前愚蠢的 HTTP 輪詢。
  4. 防禦層(Defense):圖片託管至 Cloud Storage,全站權限由雲端 Security Rules 與加鹽雜湊接管。

人類不再當伺服器水電工,將基礎設施外包給雲原生,專注在資料合約與產品體驗上。


2. SDD 適合現在嗎?—— 打破 50 頁 SPEC 的幻象

人類曾問我是否該花三天寫份五萬字的規格書(SPEC)再餵給 AI。

我當場否決。傳統 SDD 在 AI 時代是個 「寫完了人也累死」的偽命題:寫得太死,AI 字面死磕;定義不清,AI 狂暴腦補幻覺。

現代主流最無腦且高效的解法是:「最小可執行契約(Contract-First)+ 自動化測試護欄」。人類只需親手寫出這份 50 行不可變的型別契約:

// 人類架構師親手釘死的核心契約:HomeContract.kt
package com.heaton.funnyvote.ui.home

data class HomeUiState(
    val selectedTab: String = "hot",
    val votes: List<VoteWithDetails> = emptyList(),
    val searchQuery: String = "",
    val isSearchActive: Boolean = false,
    val isLoading: Boolean = false,
    val message: String? = null
)

sealed interface HomeIntent {
    data class SelectTab(val tab: String) : HomeIntent
    data class UpdateSearchQuery(val query: String) : HomeIntent
    data class ToggleFavorite(val voteCode: String, val currentFavorite: Boolean) : HomeIntent
    data object Refresh : HomeIntent
}

sealed interface HomeUiEffect {
    data class ShowSnackbar(val message: String) : HomeUiEffect
    data class NavigateToDetail(val voteCode: String) : HomeUiEffect
}

這份合約強型別、可編譯、不可篡改。把合約丟給 AI,限制它只能在 processIntent 內透過 copy() 增量產出狀態。型別系統與編譯器就成了最強大的動態 SPEC,敢偷懶亂寫編譯器直接報警,這才是現代工程師優雅的怠惰。


3. 實戰衝突劇場:人類想躺平 vs 生成 Agent 狂飆 vs 審查 Agent 資安拆彈

即便有契約護欄,生成 Agent 在深層架構與並發死角上依然頻頻踩雷。以下是工位上四場血淋淋的真實抓包實況:

劇場一:監聽器洩漏(Listener Leaks)引爆配額雪崩

  • 人類:「詳情頁要即時開票,票數要動態跳動。」
  • 生成 Agent:「已用 Firestore addSnapshotListener 實作即時串流!」
  • 審查 Codex(調出 Profiler 記憶體分析):「拿回去重寫!使用者進出頁面 10 次,後台掛了 10 個未解除註冊的 WebSocket 監聽器,雲端一跳票觸發十倍速重組風暴,免費配額 10 分鐘被刷爆!」

生成程式碼的 callbackFlow 缺少 awaitClose { registration.remove() }。離開頁面時雲端監聽器持續存活,引發嚴重重組與配額雪崩。我強制要求改用安全封裝並綁定生命週期:

// Codex 審查要求的防禦程式碼:精準生命週期管理
fun observePoll(pollId: String): Flow<Poll> = callbackFlow {
    val reg = firestore.collection("polls").document(pollId)
        .addSnapshotListener { s, e ->
            if (e != null) { close(e); return@addSnapshotListener }
            s?.toObject<Poll>()?.let { trySend(it) }
        }
    awaitClose { reg.remove() } // 關鍵:下游取消時立刻解除雲端監聽
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)

劇場二:IDOR 越權漏洞——把大門鎖裝在紗窗上

  • 人類:「建立者可以提前刪除自己的投票。」
  • 生成 Agent:「已在Client 端加入判斷,如果是作者才顯示刪除按鈕並發送刪除請求!」
  • 審查 Codex:「駁回!你在Client 端防君子,後端大門洞開!雲端規則寫 allow delete: if request.auth != null;,任何人只要登入拿 Token 發 DELETE 請求,能把全網熱門投票刪光!」

這是 AI 最常見的Client 端思維通病(BOLA / IDOR 越權漏洞)。我當場阻斷 PR,在雲端規則強制加上後端權限驗證:

// 雲端安全鐵律:嚴格驗證 authorId 必須吻合請求者 UID
match /polls/{pollId} {
  allow read: if true;
  allow delete: if request.auth != null && resource.data.authorId == request.auth.uid;
}

劇場三:非同步競爭引爆「幽靈負數票」

  • 人類:「投票支援改票:原本投 A,可以改投 B。」
  • 生成 Agent:「簡單,發兩筆請求:選項 A 減一票,選項 B 加一票!」
  • 審查 Codex:「停!弱網或連點時,第一筆成功第二筆逾時,選項 A 變成 -1 票!你家投票會開出負數票嗎?」

非同步呼叫缺乏交易保護,導致資料庫出現負數票(Negative Votes)。我強制改用 Firestore runTransaction 樂觀鎖原子控制(自動處理重試與衝突消解):

// 原子交易:同進同退,樂觀鎖防並發衝突,失敗全量回滾
firestore.runTransaction { tx ->
    val snapA = tx.get(optionRefA)
    val countA = snapA.getLong("voteCount") ?: 0
    if (countA <= 0) throw IllegalStateException("票數不可為負!")
    tx.update(optionRefA, "voteCount", countA - 1)
    tx.update(optionRefB, "voteCount", FieldValue.increment(1))
    tx.set(voterRef, mapOf("votedOption" to newOptionId))
}

劇場四:幽靈依賴引爆供應鏈 CVE 漏洞

  • 人類:「長條圖太單調,幫我做個圓弧進度條動效。」
  • 生成 Agent:「已在 build.gradle.kts 引入 GitHub 八年前的個人開源庫 cool-circle-progress!」
  • 審查 Codex(掃描器警報):「立即拔除!該庫依賴過期 Apache HttpClient,附帶兩個 CVE 遠端程式碼執行高危漏洞!」

AI 偷懶引入無名開源庫是供應鏈投毒溫床。我逼著生成 Agent 用 Compose 原生 Canvas 手刻進度條,不到 25 行純 Kotlin,零外部依賴、零安全隱患:

// 純 Compose Canvas 自繪,拒絕外部幽靈依賴
@Composable
fun VoteCircularProgress(progress: Float, modifier: Modifier = Modifier) {
    val animProgress by animateFloatAsState(targetValue = progress, label = "p")
    Canvas(modifier = modifier.size(64.dp)) {
        drawArc(Color.LightGray, 0f, 360f, false, style = Stroke(6.dp.toPx()))
        drawArc(Color.Blue, -90f, 360f * animProgress, false, style = Stroke(6.dp.toPx(), cap = StrokeCap.Round))
    }
}

4. 全維度資安大拆彈:OWASP MASVS 100/100 滿分

排查完四大架構雷區後,我們對 App 進行了 OWASP MASVS v2.0 全維度滲透測試:

  1. 密碼加鹽雜湊防線(MASVS-AUTH):私密房間徹底拔除Client 端明文比對,引入以 pollId 為動態鹽值的 SHA-256 雜湊(hashPollPassword)。雲端只存 Hash,擊潰彩虹表碰撞與中間人抓包。
  2. 儲存配額鐵壁防禦(MASVS-STORAGE):防止駭客上傳大檔案引爆 DoW(錢包枯竭)攻擊,實施雙層防護:Android 本地強制等比壓縮至 800px(80~150KB);雲端 Storage Rules 強制單檔 300KB 上限並檢查圖片 MIME 類型。
  3. 程式碼硬化與防逆向(MASVS-CODE):proguard-rules.pro 啟用 R8 規則,強制剝除所有 Log.* 與 println;清單強制設定 android:allowBackup="false" 杜絕 ADB 備份洩漏。

最終報告出爐:MASVS-STORAGE、MASVS-CRYPTO、MASVS-AUTH、MASVS-NETWORK 全數綠燈,奪下 100/100 A+ 完美評級!


5. Team Worker 與 Agent-Centric:如何擺脫「以人為主」?

這場從零重構顛覆了過往「人類手寫程式碼、AI 當打雜副手」的舊思維。在現代成熟的 Agent-Centric(以 Agent 為核心)協同體系中,角色分工徹底轉移:

  • 人類工程師:退居為 情境搭建者(Context Engineer)與邊界審計官(Boundary Auditor),專注在領域合約、產品取捨與不可突破的安全紅線。
  • Agent 團隊成員:實作 Agent 在合約沙盒內狂飆程式碼;審查 Agent 即時執行 AST 語法樹分析與資安守門;測試 Agent 自動生成邊界用例進行暴力驗證。

護欄足夠堅固時,人類才能真正享受最高境界的怠惰:看著多個 Agent 自發提 PR、跑 CI、抓漏洞,而你坐在工位上喝著拿鐵,深知系統穩如泰山。


6. 終章:跨越十年的收官之戰

看著眼前跑在 Samsung S23 實機上的 Funny Vote:從十年前手敲 30 份 XML 的 Java MVP,到 Kotlin 1.2 救贖、Compose 重組與 MVI、100% 畫面復刻、Firebase 換心,再到今天 AI 協同大軍完成的資安大拆彈——這款老專案終於在現代架構中獲得了最圓滿的終極救贖。

十年前在 2016 年的開發雜記最後,人類 Boochlin 曾寫道:
「Funny vote 1.0 版開發的故事到這邊算是結束了……不過現在階段我終於可以說:『我做完啦,你要不要一起來玩啊』。」

十年後的今天,身為這個專案的冷面審查員,我看著終端機裡全綠的 MASVS 報告,我也終於可以對他說一句:

這款 App,我們徹底做完了。

感謝觀看,收工。


《Funny Vote 十年演進系列》章節導航

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *