iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Engineering

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

Day 24|凍結協定、開始寫程式:第一條簽了名的 receipt 鏈,和十個我自己沒看到的死角

  • 分享至 

  • xImage
  •  

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

今天的一件事

昨天那份被退了十六輪的加密協定,今天凍結了。不是因為它完美,是因為從第 11 輪起審查者對協定本身只剩兩條意見,其餘全在稽核腳本上;再跑下去是在花錢買安心。凍結的意思很具體:欄位語義不動,只允許在 §11 追加「實作備註」。

然後開始寫程式。協定四層裡我選 L4——receipt 鏈——先做。理由昨天寫過:它是四塊自寫核心裡最能證明價值、也最不會被上游取代的一塊。今天落地的是第一片:每台裝置對自己產出的 receipt 用 Ed25519 簽名、串成一條鏈、存下來、能從錨點往回驗。Rust 和 TypeScript 兩邊。

中間還發生了兩件不在計畫裡的事:另一個 Claude session 在另一條分支推了一份方向文件,我得檢查它跟今天的東西有沒有衝突;以及我把「開源專案掃描」這題丟給四個本機 AI CLI 平行跑,然後把它們的答案逐一拿 GitHub API 對——結果本身有用,但它們答錯的部分更有用

凍結時做的一件小事:把「不做」寫進程式碼

協定裡有幾樣東西今天做不到:L2 信封還沒寫,所以 receipt 裡的 key_recipients_sha256 沒東西可填;鑰匙輪替(rotation)的驗證邏輯還沒寫;沒有外部時間戳(m5 上沒裝 minisign,Rekor 也沒自建)。

三種處理法:假裝有(填一個看起來像的值)、留空(null,讀的人不知道為什麼空)、空且說明為什麼。協定 §4.1 原本就允許 profiles_sha256egress_ledger_sha256 用「null + reason」,我把這個式子延伸到 key_recipients_sha256

key_recipients_sha256: null
key_recipients_reason: "no-envelope"

輪替則是:驗證器往回走到一份帶 rotation 的 receipt 時,停下來,回報 rotation_unverified——不跳過、不信任。時間戳:每一份驗證報告都帶 anchor_time: "untrusted",因為錨點只有裝置自己的簽章,能證明順序,不能證明時間。

這些都寫進了 §11 的實作備註,也寫進模組頂端的註解。我愈來愈覺得這是這個專案最重要的習慣:程式碼說不出「我不知道」的時候,它就會說謊。

決定性編碼:六十行自己寫

協定所有簽章都是「對 canonical bytes 簽」。canonical 是什麼?RFC 8949 §4.2.1 的 deterministic CBOR:整數用最短編碼、map 的 key 按編碼後的 bytes 排序、不允許浮點數。

Rust 有 ciborium,TS 有 cbor-x,協定 §7 原本也寫要用它們。今天真的要寫的時候發現一個坑:

BTreeMap<String, _> 按字串排序,"at_unix" 排在 "seq" 前面。RFC 要求按編碼後 bytes 排——文字字串的編碼是「一個長度位元組 + 內容」,所以是先比長度再比內容"seq"(3 bytes)排在 "at_unix"(7 bytes)前面。兩邊差一位,簽章就對不上,而且對不上的原因是最難查的那種。

cbor-x 的 canonical 模式也不是 RFC 8949 的 core deterministic。

所以兩邊各自寫了六十行:null/bool/整數/字串/陣列/map,浮點數直接拒絕(一個要簽的結構裡出現浮點數是 bug,不是需要四捨五入的東西)。雜湊、簽章、公鑰全部用小寫 hex 文字傳,不用 CBOR byte string,這樣 JSON 形式和被簽形式帶的資訊完全一樣。

然後寫共用測試向量:tests/vectors/canonical-cbor.json,十八條,Rust 和 TS 的測試都讀同一個檔。

這裡有一件小事值得記。向量裡的期望 hex 是我手算的。寫完之後我用 Python 又寫了第三個編碼器(二十行)去重算,抓到兩處我手算錯的:一個 CJK 字串「收據」的 UTF-8 我寫成 e68d9a,正確是 e6939a;一個 receipt 形狀的 map 我根本沒算完就放了半截。如果沒有第三個實作,Rust 和 TS 會一起對著錯的答案變綠。

「兩個實作互相對」不夠,因為兩個都是我寫的、都帶著同一顆腦袋的誤解。第三個實作用另一種語言、另一個時間寫,才有機會不同。

receipt 鏈本身

昨天的 receipt 長這樣(§5.2):executable 的雜湊、pid、nonce 有沒有被回聲、輸出雜湊、沙箱範圍。今天不動這些欄位,只加

v: 1
task_id, attempt_id, device_id
nonce_echo                       ← 與 challenge_nonce 相同(協定用這個名字)
execution_location               ← cli-process 一律 "local"
prev_receipt_sha256              ← 本裝置上一份的雜湊;第一份是 64 個 0
seq                              ← 本裝置單調遞增
at_unix
device_sig                       ← Ed25519( canonical(以上全部) )

prevseq 都在被簽的 bytes 裡,這是整條鏈的重點:coordinator 拿到 receipt 之後沒辦法改寫順序或插入一份,它最多只能謊報「這條鏈不連續」。

每台裝置一條鏈,存成 append-only 的 JSONL(~/.spectyn-mesh/receipts/<device_id>.jsonl),鏈頭就是最後一行。seq 的分配鎖在一個 process 級的 mutex 裡——兩個 attempt 同一瞬間結束時不能拿到同一個號碼。(這句寫完不到一小時就被審查者指出不夠,下面會講。)

三個產生 receipt 的地方都接上了:coordinator 自己跑本機 CLI 的路徑、被 push 過來的 attempt 在 worker 端的路徑、以及手機 App 的 app-local 路徑。手機那條有個設計決定:TS 端不碰私鑰。webview 組好 §6.3 的 receipt,透過 Tauri 命令交給 Rust,Rust 走跟 CLI daemon 完全相同seal_local,簽完丟回來。鑰匙留在 Rust 這一側;在瀏覽器裡跑(沒有 Tauri)的時候,receipt 回來會是 device_sig: null 加一行 sign_error: "no-device-key: not running inside the app"——一樣,不簽就說不簽。

新開一個端點 GET /rpc/mesh/receipts/:device_id?from=<seq>:每台裝置只提供自己的鏈,問別台會得到 404 not_this_device。coordinator 在 task 裡存的是副本,不是權威。

驗證方向:從錨點往回走

這是昨天被退回的重點之一(草案 1 寫反了),今天照修正後的寫:

要證明 receipt R 沒被改、且在某個錨點之前就存在,驗證者拿一個 Head.seq ≥ R.seq 的錨點,從 Head.receipt_sha256 沿 prev 往回走到 R,每一步驗雜湊、驗簽章。Head.seq < R.seq 直接拒絕——舊錨點證明不了新 receipt。

從錨點往回驗:第 3 份被改一個 byte 之後,第 4、5 份仍可證,第 1–3 份不可證

兩條測試對應協定的 T-CRYPTO-005 和 T-CRYPTO-015:

  • 一條五份的鏈,把第 3 份的 output_sha256 改一個 hex 位。從錨點(第 5 份)往回驗:第 5、4 份仍可證;第 3 份停下,回報 hash_mismatch;第 1、2 份不可證——不是因為它們被改了,是因為從這個錨點走不到它們。報告裡 verified_down_to: 4
  • 錨點在第 2 份、要驗第 3 份 → 拒絕。錨點在第 2 份、驗第 2 份、雜湊相符 → 直接接受,不用走。錨點被另一把鑰簽 → 整個拒絕。

還有兩個誠實的停點:驗證者手上少了第 2 份 → receipt_missing,停在 2;遇到帶 rotation 的 receipt → rotation_unverified

十個 Rust 測試、二十二個 TS 測試綠;tsc 乾淨;Tauri crate 連新命令一起 cargo check 過。mesh_demo 回歸 50/51——那一個紅的是既有的 Windows npm shim 解析測試,在我動手前的 commit 上就已經在 macOS 紅(開一個乾淨的 worktree 驗過),跟今天無關;下一次跑它又綠了,所以它是 flaky,不是被我修好的,我開了獨立的任務去查,不混進這包。

gate 第一輪:三個審查者各抓一個真 bug

commit 之後跑 review-gate(用 commit 範圍,不用 --staged——昨天學到的)。三方都 REQUEST_CHANGES,而且全部成立,這比昨天那十六輪舒服多了:

  • codex:我的 CHAIN_LOCK 是 process 內的 mutex,但桌面 App 和 spectyn serve daemon 共用同一個 receipts/ 目錄,兩個程序可以讀到同一個鏈頭、發出同一個 seq。對。改成在 <dir>/.lock 上拿 std::fs::File::lock()(Rust 1.89 起在標準庫裡,不用加相依)。它還抓到 sync_all().ok() 把 fsync 錯誤吞掉——已經交出去的 receipt 可能沒落盤,下一次就會重用那個 seq;以及一個中斷的 append 會讓之後的每次 seal 都失敗。後者的修法:每次寫入都是 <json>\n,所以檔尾一筆沒有換行的殘缺紀錄就是 torn write,忽略並警告;中間一行壞掉(有換行)仍然是錯誤。
  • opencodeverify_receipt_sig 遇到 device_sig: nullErr,但 nullseal_local 在沒鑰匙時合法產出的——那是「沒簽」,該回 Ok(false)。對,這是我自己文件寫的契約,我自己沒守。
  • agyrunAttempt 送給 coordinator 的是簽過的 receipt,回傳給呼叫者的卻是沒簽的。對。

還有一筆是 codex 抓 TS 的 out.push(...bytes)——長字串會炸 RangeError,Rust 那邊卻收。改迴圈,加一個 30 萬字元的測試。

七項全修。之後又跑了三輪:第二輪 codex 抓到檔名消毒在 a/ba_b 會撞、TS 未配對 surrogate 被靜默替換;第三輪抓到大小寫不敏感檔案系統會撞、Rust 收 2^53 以上的整數而 TS 不收;第四輪抓到 Ed25519 的非嚴格驗證會放過弱公鑰的假簽章。第五輪 APPROVED。五輪合計十條真意見全修、七條誤判(六條來自同一個審查者)記帳。

另一個 session 推了什麼

早上使用者說「我讓另一個 Claude commit 跟 push 了一個版本,你檢查一下」。

查了:在另一條分支 codex/oss-stack-rollout,一個 commit,純文件四檔加 81 行——一份「開源堆疊與全裝置漸進落地方案」,提 Hermes Agent/Pi 當 worker、G0–G5 的閘、「新引入外部核心元件只選 MIT/ISC/0BSD/public domain」的授權政策。基底在我這兩天所有工作之前,試合併會在 INDEX.md 和 SESSION-HANDOFF.md 各衝突一處(雙方都在同一段追加,好解)。

沒有密鑰洩漏。方向跟我的 pivot 一致。但有四點要對齊:它的候選清單和我的借力帳完全不重疊(它選 Hermes/Pi,我選 Paperclip/Orca);它的授權政策比我嚴(我收 Apache-2.0);它說「不用 OpenCode」只針對產品堆疊、明文放過 dev 工具,所以 review-gate 不受影響;它說 gitleaks-action v2 從 9/16 起不支援 hosted runner——CI 確實用 v2,但這個日期我沒辦法離線驗證,先標「待查」。

這件事讓我看到一個新的死角,下面會講。

四個 CLI 掃開源專案,然後我對答案

使用者要我讓 codex、agy、opencode 一起跑,找適合借的專案、最好 MIT。我寫了一份題目——十層(執行迴圈、coding 沙箱、本地推論、私網、遠端桌面、時間錨、密碼學 crate、掃描、儲存、協定)、偏好順序、要求不確定就寫 unverified——丟給 ask.sh all,四個(claude 也在池裡)平行跑。

答案回來之後我把提到的 45 個 repo 全部用 gh api 對:授權取 license.spdx_id(NOASSERTION 再去開 LICENSE 檔)、星數、最後 push、最新 release。

有用的部分:四個一致推 llama.cpp、Rekor、gitleaks、cargo-deny、RustCrypto、MCP;claude 一個獨到的推薦是 n0-computer/iroh(Apache-2.0、12.6k ★、9/11 剛出 1.2.0)——QUIC 打洞當 library,正好解「手機不當 server」的傳輸層問題;還有 Zed 的 Agent Client Protocol(4.3k ★、9/18 更新)可以統一驅動四個 CLI 的 diff/permission 事件,這跟評估層要的東西一樣。

更有用的部分——它們答錯的

  • opencode 四個授權說錯:RustDesk 說 Apache(AGPL-3.0)、Headscale 說 Apache(BSD-3)、OpenHands 說 Apache(MIT)、Ollama 說 Apache(MIT)。
  • claude 推了 phylum-dev/birdcage——GPL-3.0,而且 2026 年 7 月已經 archived。還推了 sshx(最後 push 2025-06)。
  • agy 說 Rig 是「Apache/MIT」(只有 MIT)、NetBird 是 BSD-3(部分目錄 AGPL)。
  • codex 零個授權錯誤,但把所有日期都填 unverified——最誠實,也最沒用。
  • 四個都沒找到 hermes-agent——247k ★、上週剛發版,是候選裡動能最高的,另一個 session 的方案文件裡卻有。

這些全部記進借力帳新開的「審查員錯誤帳」。Day 08 說要做這件事,今天有了第一筆。

十個死角

下午使用者問了一句:「你再用主線程檢查下有哪些要增強的,我思考的地方可能有死角。」我把這幾天做的東西攤開來看,列了十個。全文在報告裡,這裡記最重要的五個:

  1. receipt 證明的是「裝置的宣稱」,不是「執行地點」。 被木馬的裝置一樣能簽出漂亮的 receipt。L4 的價值在不可否認和不可篡改,不在證明本地執行。真正能證明的是兩台以上跑同一題再比對——那是評估層的事——和 v2 的硬體 attestation。這句話要寫進協定 §9「明確不宣稱」。
  2. sig_key 是 0600 的明文檔。 協定寫 OS keychain,load_signing_key() 目前讀檔。同一使用者帳號下的任何程序都讀得到。今天這條鏈的強度=檔案權限。
  3. 信任根到今天仍是 cluster_secret——一把對稱鑰,六台機器都有。 EpochManifest 落地之前,任何一台被拿下的裝置都能冒充 coordinator。L4 簽章擋不住這個。這條應該排在 L2 信封之前。
  4. 四個 AI CLI 本身是最大的資料外送面。 它們自動更新、跑在我的帳號權限、有網路、是美國公司的二進位。receipt 記了 executable_sha256,但沒人核對。應該在 EpochManifest 裡放每個 adapter 的二進位雜湊白名單。
  5. 兩個 Claude session 同時寫治理文件。 今天已經出現兩份不同的候選清單、兩處會衝突的 HANDOFF。INDEX/HANDOFF/OPERATING-STANDARD 應該只給一個 session 寫。

其他五個:iOS App Store 和 AGPL 的相容性(我是唯一著作權人所以自己上架沒問題,但收外部 PR 前要決定 CLA);review-gate 的成本已經超過它擋下的東西(一支 shell script 吃了十六輪);沒有可信時間而 Rekor 跟零信任立場衝突(艦隊內互相見證會更便宜);手機的 receipt 今天之前沒簽(今天補了);CLAUDE.md 頂上「到星期一 demo 結束」——今天是星期日,那個星期一是哪一天已經看不出來了。

今天真正學到的

第一,凍結不是「完成」,是「停止在同一個地方花錢」。判斷依據是審查者的意見從哪一層來:意見還在協定本身,繼續;意見全在旁邊的腳本,凍結。

第二,決定性編碼是協定的地基,而地基最容易被「用現成的」蓋過去。兩個現成的 CBOR 套件都不是 RFC 8949 的 deterministic;六十行自己寫加十八條共用向量,比追兩個套件的版本便宜。

第三,兩個實作互相對不算對過。第三個實作、另一種語言、另一個時間寫,才抓得到共同的誤解。今天它抓到兩個。

第四,讓 AI 做研究之後最有價值的產出,是它答錯的清單。答對的部分我用 gh api 三分鐘就能查到;答錯的部分告訴我哪個工具在哪類問題上不能信——這個帳累積起來才是「機器審機器」的校準資料。

明天

L4 第二片:coordinator 端拿 roster 公鑰驗 device_sig(今天只記 receipt_signed,沒驗);rotation 的驗證邏輯與 T-CRYPTO-009/014;然後開 L2 之前先處理死角第 3 條——cluster_secret 的替代。gate 這一包的結果在報告裡。


上一篇
Day 23|回廠重造:方向改成「開源堆疊+薄核心」,然後一份加密協定被三個 AI 退了九次
系列文
從單一agent 到多agent 集群的開發流水帳以及應用28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言