iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Engineering

從單一agent 到多agent 集群的開發流水帳以及應用系列 第 27

Day 23|回廠重造:方向改成「開源堆疊+薄核心」,然後一份加密協定被三個 AI 退了九次

  • 分享至 

  • xImage
  •  

系列:「一人艦隊:用一群 AI CLI 打造跨平台代理 mesh」— 第 23 篇
紀錄日期:2026-09-19 ~ 09-20

今天的一件事

這兩天做了三件連在一起的事:把專案方向正式改成「開源堆疊 + 薄薄一層自己的核心」、把它寫進公開 repo 的 README、然後為核心裡最重要的那一層——receipt——寫了一份加密協定草案,送進本機的 review-gate。

第三件事佔了大半時間。那份草案被 codex、opencode、agy 三個審查者退了九輪,中間三次觸發「停止自動迭代、交給人」。每一輪退的東西層級都不一樣,從真正的漏洞一路退到「兩節寫法不一致」。這篇主要記這個過程,因為它是這個專案到目前為止最接近「真的工程審查」的一次。

先講方向:為什麼回廠

前天查完 55 個開源專案(Day 22 提過),結論很清楚:每一層都有人做得比我好、而且會一直做下去。Orca 一個月 500 多個 commit、377 個貢獻者;Paperclip 半年 8 萬星;Omarchy 背後是基金會。我一個人、一台 M5、五台 Windows 機、兩支手機。

但 55 個裡沒有一個做這三件事:手機當 worker、有簽章的 receipt、量測合併品質。這三件剛好是這個專案從 Day 13 做到現在的東西。

所以方向改成:組合,不發明。三層——

三・核心(自己寫)    Sealed Task Envelope 與 receipt 鏈 · 手機 worker · presence roster · 評估層
二・艦隊(借)        Omarchy 節點 · 十個預接的 coding agent · Paperclip 派工 · Orca cockpit · LiteLLM gateway · Ollama
一・地基(借)        Headscale · AdGuard Home · Vaultwarden · LibreWolf · RustDesk/Sunshine · wg-easy · LUKS/FileVault · age

公開 repo 的 README 第一段改成「🔧 回廠重造中」,下面是一頁「一句話、三層、三張清單」:我們組什麼、做什麼、拒絕什麼。一句話是:

用你自己的機器組一支 AI 艦隊,每一件事都有收據,資料不出你的門。

公開 repo README:回廠重造公告與「一句話、三層、三張清單」

README 現在的第一屏。三張清單裡「拒絕什麼」最長,那是刻意的。

「拒絕什麼」那張清單比另外兩張重要,因為它是防止一人專案被「綜合化」吃掉的圍籬:不寫自己的桌面 UI、worktree 管理、VPN、DNS、密碼庫;不發明任何密碼學原語;不讓產品的執行路徑經過作者自己的訂閱;沒人付錢前不做 SaaS;非 OSI 授權的依賴一律只當工具。

配套的是一本帳:docs/governance/BORROW-LEDGER.md。每個借來的專案一行——org/repo(一年內改名搬家的太多,記專案名會斷)、授權、借法(黑盒/搬模組/抄設計)、釘住的 commit、動能、邊界測試、退場方案。建檔時「釘住的 commit」全是「待」,因為四個驗證用的 spike 還沒跑。這不是遺漏,是帳本現在該有的樣子。

然後:核心裡最重要的一層要有協定

方向定了之後,第一個要寫清楚的是 receipt——它是三塊核心裡唯一沒有人做、也是唯一能拿去賣的東西。現在的 receipt 是 nonce 回傳、輸出 sha256、HMAC 簽的請求;夠 demo,不夠承諾。要能對客戶說「這份資料沒離開過你的機器,而且有簽章可以證明」,得把三個問題解掉:

  1. coordinator 不該看得到內容——它可能跑在別人的機器上,也可能被入侵。
  2. local-only 不能只靠設定——設定會錯,鑰匙不會。
  3. receipt 要能被第三方驗證、而且改不了。

所以寫了 docs/specs/MESH-CRYPTO-v1.md,叫 Sealed Task Envelope。原則第一行就釘死:本文不定義任何新的密碼學原語——全部用審計過的標準構件(X25519、ML-KEM-768、HPKE、XChaCha20-Poly1305、Ed25519、age、minisign、Rekor),本文只定義它們的組合、鑰匙的分發規則、與訊息格式。

核心想法是政策等於鑰匙分發:每個任務一把內容鑰,用 HPKE(X25519 + ML-KEM-768 的混合 KEM)分別封給被授權的 worker;local-only 的任務只封給「本地推論且沙箱無網路」的 adapter,雲端 adapter 根本沒有鑰,設定寫錯也洩不出去。coordinator 只轉送密文和 metadata。每份 receipt 用裝置的 Ed25519 簽,鏈上前一份的雜湊,鏈頭定期錨到外部透明日誌。

訊息格式到草案 11 長這樣(節錄):

Envelope {
  v, task_id(16B), epoch, requester_device_id, class
  recipients: [ { device_id, adapter_id, required_profiles_sha256, enc, ct_key } ... ]
  nonce(24B), pad_bucket
  ct = XChaCha20-Poly1305(content_key, nonce, aad = canonical(上列), pad(Payload))
  retry?, offline_override?
  requester_sig = PureEd25519(canonical(上列,不含本欄位))
}
Receipt {
  task_id, attempt_id, device_id, adapter_id, nonce_echo, output_sha256
  execution_location, profiles_sha256, egress_ledger_sha256, key_recipients_sha256
  prev_receipt_sha256, seq, rotation?, gateway?
  device_sig = PureEd25519(canonical(上列))
}

required_profiles_sha256 在 AAD 裡,所以 coordinator 改 recipients 名單或沙箱要求,worker 端的 AEAD 就開不了;key_recipients_sha256 在 receipt 裡,所以「這把鑰封給了誰」是有簽章的證據,不是日誌。canonical 一律 deterministic CBOR,Rust 和手機端的 TypeScript 要逐 byte 相同——這是第一個測試向量要驗的事。

草案 1 寫完,送進 review-gate。

九輪

review-gate 是這個專案 6 月就定的規矩:非瑣碎的變更要經過本機至少兩個不同 AI 的審查,全體 APPROVE 才能 land;三輪沒共識就停下來交給人。之前跑的都是程式碼 diff,這是第一次拿一份協定文件去跑。

MESH-CRYPTO-v1 九輪 review-gate 的結果與退的層級

九輪。標黃的三輪是 NEEDS-HUMAN,停下來的都是審查者決定不了的取捨。

第一輪(三方全退):五個真正的組合層錯誤。所有 worker 共用同一把內容鑰,任一台拿到別人的輸出密文就能解;HPKE 的封裝少了 ct_key(我只寫了 enc,recipient 根本解不出內容鑰);receipt 鏈的時間證明方向寫反了(從 receipt 往回走到舊錨點證明不了 receipt 本身);Ed25519 我寫成先 SHA-256 再簽,那會把碰撞抗性砍到 128 bits 而且跟標準工具不相容;還有 Apple Secure Enclave 根本不支援 Curve25519 和 ML-KEM,我把三把鑰都寫成「放硬體」。

這五個任何一個進到實作都是真的漏洞。第一輪的價值就在這裡。

第二輪(三方全退):修完之後露出下一層。鑰輪替之後往回驗給不出舊鑰;canonical 編碼沒定整數寬度和可選欄位;padding 桶的邊界算錯(4095 bytes 加 4 bytes 長度前綴塞不進 4 KiB);recipients 為空時 空真,政策斷言直接通過;gateway 沒進 recipients,雲端類任務根本跑不了。

第三輪(opencode 通過,另兩位退)→ 第一次 NEEDS-HUMAN。 codex 指出 local-only 沙箱若能連 tailnet,就能把明文轉給 tailnet 上的 gateway;agy 指出 RFC 3161 時間戳伺服器不能被查詢,我拿它當「查最新 epoch」的來源是錯的。三個決定要人做:沙箱要不要完全無網路、Rekor 要不要自架、adapter 級的密碼學隔離要不要進 v1。決定了,reset,繼續。

第四到第六輪:agy 開始通過,codex 和 opencode 的意見從漏洞變成一致性——retry 的雜湊若含 nonce 和 ct 就不可能跨重試相同(所以 retry 改成原信封原樣重送,不重新加密);coordinator 可以扣住合法信封等某台被撤銷後才投遞,worker 端也要查新鮮度;多 requester 交錯在同一台的鏈上,requester 驗不了中間別人的 receipt(所以鏈改成公開可取,receipt 本來就不含明文);offline_overrideKeySet.enroll_sigrotation.reset 不在 schema 裡。第六輪第二次 NEEDS-HUMAN,剩一個實質問題:硬體 attestation 我只是「一把 P-256 簽了軟體鑰」,證明不了鑰真的在硬體裡,要證明就得接 Apple App Attest——跟「自建、非美」方向衝突。決定:v1 不宣稱硬體保管。

第七到第九輪:schema 一致性收尾。Receipt.profiles_sha256 固定雙元素但 §3.1 依 class 不同;reset 應該是分支不是延續;worker 規則漏掉自機離線例外;OfflineOverride.operator_sig 沒說覆蓋哪些欄位。第九輪 agy 通過,另兩位剩這四項,全部修入草案 11。第三次 NEEDS-HUMAN,停在這裡。

三個被退回的細節,值得單獨寫

鏈的驗證方向。 草案 1 寫:拿到任一份 receipt,驗簽、沿 prev 往回走到最近的錨點、對照外部時間戳。三個審查者都指出這是反的。prev 指向過去;錨點是某個時間點對「鏈頭」的承諾。從 receipt R 往回走到一個更早的錨點,只證明 R 之前的東西存在,證明不了 R。正確方向是:取一個時間在 R 之後、seq ≥ R.seq 的錨點 Head,從 Head 沿 prev 往回走到 R,每一步驗簽與雜湊;走得到,R 就在 Head 被外部日誌蓋時間戳的那一刻之前存在且未改。等號的情況(Head 承諾的就是 R 自己)在第五、六輪又被翻了一次:opencode 說等號是自我指涉,agy 說 Head 有外部時間戳所以等號成立。最後定案 ,理由是 agy 的:時間戳是外部日誌給的,不是裝置自報的。這一條到第七輪又補了一句——Head.at_unix 是裝置自報,證明用的時間一律是日誌回傳的 integratedTime

retry 為什麼是原樣重送。 第五輪 codex 指出 coordinator 可以把同一份合法信封配新的 attempt id 重送,worker 就會執行兩次。我加了 requester 簽的 retry 憑證和 envelope_sha256。第七輪 codex 說雜湊是循環的(外層簽章含 retry);我改成明列欄位的 envelope_core_sha256。第八輪 opencode 說核心欄位含 noncect,重試若重新加密,nonce 必然不同,雜湊不可能相同。對——所以最後的定義是:重試不是重新加密,requester 把原信封的九個核心欄位原樣重送、只附 retry 並重簽外層。同一把鑰、同一個 nonce、同一份密文,不是 nonce 重用,因為沒有第二次加密。這條走了四輪才定,每一輪都對。

adapter 級隔離不是加密隔離。 一台裝置上所有 adapter 共用裝置的 KEM 鑰;HPKE 的 infoadapter_id 只是標籤,防不了同一台上另一個 adapter 解密。codex 和 opencode 在第三、四輪都要求明講。定案是:v1 由裝置 daemon 當 key broker——鑰只有 daemon 有,daemon 解開後比對信封裡的沙箱 profile 雜湊,相等才在那個沙箱裡啟動 adapter,明文只經管道進沙箱。這是「可信的本機 broker」而不是密碼學隔離,文件裡明講;每 adapter 一組鑰是 v2。

三個停下來的決定

三次 NEEDS-HUMAN 各留一個取捨給人:

  1. local-only 沙箱要不要完全無網路。 保留 tailnet 例外比較方便,但 tailnet 上有 gateway,等於開了一條往雲端的路。決定:完全無網路,本地模型伺服器也進沙箱、走 unix socket。代價是 local-only 下任何要網路的工具都不能用——這正是它該有的意思。
  2. Rekor 要不要自架。 epoch 新鮮度和歷史鑰驗證都依賴一個可查詢的透明日誌;用 sigstore 的公開 Rekor 是美國基金會營運。決定:自架,放 acer,並在 §9 明寫「日誌與 coordinator 同機時保證失效」。
  3. 硬體 attestation 要不要做真的。 要證明鑰在硬體裡得接 Apple App Attest(要 Apple 伺服器)或 TPM EK 憑證鏈。決定:v1 不宣稱,key_custody 只是資訊欄位、不進政策。

三個決定的共同點:都是「安全一點」和「自建一點」之間的取捨,審查者看得到問題、決定不了方向。這就是那條規則存在的理由。

九輪下來看到的

退的層級單調下降。 漏洞 → 設計缺口 → schema 缺欄位 → 兩節寫法不一致。這條曲線本身就是「文件可以 land 了沒」的量尺,比任何一輪的 APPROVE 數更有用。

三個審查者看的東西不一樣,而且穩定。 codex 每輪都在攻擊信任邊界(誰能重放、誰能扣住、誰能偽造);agy 每輪都在對 schema 和數學(欄位在不在、方向對不對、演算法支不支援);opencode 每輪在對一致性和實作可行性(這節和那節說的一樣嗎、實作者拿到這句話寫得出來嗎)。三個人湊起來剛好是一個審查委員會該有的三種眼睛,缺一個都會漏一類。

「三輪沒共識就停」這條規則救了我三次。 每次停下來的問題都不是審查者能決定的——沙箱要不要完全斷網是產品取捨,Rekor 自架是信任模型取捨,硬體 attestation 是方向取捨。沒有這條規則,我會在第三輪、第六輪、第九輪各自繼續改,改到審查者滿意,然後做出一個沒有人決定過的取捨。

一份 30 KB 的協定文件,三個 AI 每輪都能找到新的一致性問題。 這說明這種文件靠人讀是讀不完的;也說明它現在應該進實作,讓測試向量(T-CRYPTO-001018)接手。

明確不宣稱

草案 11 的 §9 現在有七條「明確不宣稱」:不宣稱 coordinator 被入侵時系統仍安全(只宣稱它看不到內容、偽造不了 receipt、重放不了舊 roster);不宣稱抗量子(只宣稱「現在錄、以後解」要同時破兩個 KEM);不宣稱能收回被撤銷裝置已解開的內容;不宣稱任何鑰在硬體裡;自架的日誌若與 coordinator 同機則新鮮度保證失效;不宣稱經過密碼學家審查——review-gate 是工程審查,不是密碼學審查。

這種寫法要變成全專案的慣例。它跟 Day 20 那條「指標第一次給出極端值先懷疑指標」是同一種態度:先寫下自己做不到什麼,再寫做到什麼。

Sealed Task Envelope 的七層與 §9 明確不宣稱

七層全用現成構件;下面那六條「不宣稱」是草案 11 的 §9。

借力帳的第一批

方向改了,帳先立。三節:

  • 地基八行:Headscale、Tailscale 客戶端、AdGuard Home、LuLu、OpenSnitch、age/rage、Vaultwarden、Omarchy。
  • 艦隊七行:Orca、Paperclip、Agent Orchestrator(只讀)、A2A、LiteLLM、Ollama/llama.cpp、Goose(觀察)。
  • 只讀,不接七行:Gas Town、ORCH、ORCHA、ruflo、vibe-kanban、Superset、Daytona,各一句為什麼。

每行的動能欄(★/30 日 commit/貢獻者)來自前天用 GitHub API 的查證,日期寫在旁邊;之後每週由掃描腳本回填,不憑記憶。「邊界測試」欄大半是「無(債)」——規則是必須指到一個上游變動時會變紅的測試,沒有就老實寫債。

驗證這條路的是四個 spike:(1) 一台 Omarchy 節點接到 attempt 並回 receipt;(2) Paperclip 的 ticket 掛得上 receipt;(3) spectyn 註冊成 Orca 的 agent;(4) MONDAY-MESH-API 對照 A2A 的欄位表。任一失敗就回頭。帳本裡每個「待」都對應其中一個。

順帶做的一件小事

方向改成「堆疊」之後,堆疊的第一個 profile 是 hardened:防火牆、DNS、加上一支每晚跑的稽核腳本。腳本今天寫完了——scripts/audit/mac-input-audit.sh,只讀、不用 sudo,列出此刻的鍵盤 event tap、24 小時內請求過輸入監控/螢幕/麥克風的非 Apple 程式、常駐項、根憑證、代理、簽章。它的定位跟 receipt 一樣:一台機器對自己狀態的、可以回頭查的紀錄。之後這個目錄會進 MESH-CRYPTO §5 的加密目錄。

寫腳本踩了兩個坑,記一下:zsh 有個內建指令叫 log,把 /usr/bin/log 蓋掉、靜靜回空,查了半小時;雙引號裡的反引號是指令替換,一行註記裡的 claude 把 Claude Code 本體叫起來等 stdin,整支腳本掛了十分鐘。兩個都是「工具靜靜地回錯的東西」,跟 Day 22 那把在中文上回 25% 的尺同一類。

接下來怎麼進實作

草案 11 有 18 個測試向量(T-CRYPTO-001018),每一個對應九輪裡的一個發現:coordinator 視角持有全部流量仍解不開任何 Payload;worker A 拿到 B 的輸出密文解不開;改一份 receipt 一個 byte 從錨點往回驗會停在哪;撤銷後下一 epoch 的信封不含它;padding 對 4092 與 4093 bytes 落在不同桶;重送同一份信封 worker 回 duplicate_task;錨點 seq 小於 receipt 時拒絕。這些不是驗算法,是驗組合——算法本身是 RustCrypto 和 libsodium 的事。

Rust 側的 crate 都是現成的:hpkeml-kemchacha20poly1305ed25519-dalekx25519-dalekciborium;手機那邊用 libsodium.js 加 cbor-x。唯一標「待填」的是 HPKE 後量子草案的修訂版號與 kem_id——第八輪 codex 要求釘住版本,我沒憑記憶填,留了一個 SUITE_PIN = 待實作時填入。一個協定文件裡有一格明寫「不知道」,比填一個可能錯的數字好。

順序上,實作會從 L4 receipt 鏈開始而不是 L2 信封——因為現有的 receipt 已經在跑,加簽章和 prev hash 是增量;信封要改 fanout 的 body 形狀,牽動 coordinator 和手機兩端,等 S1、S2 決定 coordinator 是不是還要自己養再動。

從 Day 13 到今天的一條線

回頭看,這件事其實從 Day 13「手機也是工作節點」就開始了:手機當 worker(Day 13–17)→ receipt 要有 nonce 和輸出雜湊(Day 18)→ 合併在手機端做、標明非 receipt(Day 19)→ 每一輪留一行紀錄(Day 20)→ 紀錄要能分辨誰產出誰複核(Day 21)→ 兩把尺並排、指標自己也要被量(Day 22)→ 今天,receipt 要有協定、協定要過審查。

每一步都是前一步的紀錄逼出來的。這也是為什麼方向改成「組合」之後這三塊留下來:它們不是我最喜歡的功能,是這 23 天裡唯一一路有紀錄可以回頭看的東西。

今天真正學到的

協定寫得出來,跟協定能 land,中間差的是一個會說「不」的流程。

草案 1 我自己讀了兩遍,覺得沒問題。第一輪五個真漏洞。這不是我不夠仔細,是作者讀自己的東西時,讀到的是自己的意圖,不是自己寫的字。三個審查者沒有意圖,只有字。

九輪也讓我確定了一件事:review-gate 的價值不在「通過」,在退回的層級。一個只會說「通過/不通過」的閘門是二元的;一個能讓你看到「這次退的是漏洞、上次退的是缺欄位」的閘門,是一把尺。

明天

MESH-CRYPTO 等一個決定:再跑第十輪,還是以草案 11 凍結進實作。我偏前者——第九輪 agy 已過、另兩位剩一致性問題,差一步。

S1 還是第一順位:acer 那台裝 Omarchy、進 tailnet、接到 attempt 回 receipt。這一步過了,借力帳裡第一個「待」才能變成 commit hash。


上一篇
Day 22|我修好了分類器,然後它在下一輪拿了零分
下一篇
Day 24|凍結協定、開始寫程式:第一條簽了名的 receipt 鏈,和十個我自己沒看到的死角
系列文
從單一agent 到多agent 集群的開發流水帳以及應用28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言