Skip to Content

審批

當 command 的計畫觸及一張 require_approval 規則命中的資料表時,什麼都不會套用。該計畫會被凍結成型別為 command_plan 的 staged change,執行紀錄轉為 staged,呼叫者收到 409 approval_required。本頁完整說明這條路徑。

預掃描針對真正的計畫

這個決定不是從定義做出的。Command 會先在一個 savepoint 內執行,接著把跑出來的具體、已完成綁定的 actions,連同真實的前後影像,逐一比對每張被引用資料表的審批規則。這封住了以定義層級掃描會留下的前影像缺口。

已執行計畫中受控管的資料表數結果
保留 savepoint,寫入正常 commit。
恰好一張Savepoint 回滾、計畫被凍結、執行轉為 staged,呼叫者收到 409 approval_required
超過一張422 {"error":"multiple_approval_gated_tables","tables":[…]}
{ "detail": { "error": "approval_required", "process_id": "ffffffff-6666-4666-8666-ffffffffffff", "rule_id": "rule_ab12cd34", "rule_label": "Orders over 10k", "staged_change_id": "99999999-7777-4777-8777-999999999999" } }

兩張受控管的資料表是永久性的 422,不是排隊。一次執行永遠不能橫跨兩個審批對象,因此這種 command 根本不可能執行成功——解法是把它拆成兩個 command,而不是去批准任何東西。

「凍結」是什麼意思

Staged change 儲存的是已經跑過的計畫,不是能產生它的程式。具體來說包含:解析後的 action 清單與其綁定好的欄位值、為新增列預先鑄好的 id、被消耗的紀錄版本與 link 集合、每個 action 讀過的欄位、它綁定的變數與 relation、宣告的 output 契約、staging 當下擷取的授權上限,以及 callback 生命週期的接線。

因為計畫被凍結而 command 沒有,所以在它底下修改或刪除 command 是被擋住的。只要該 command 還有 staged 狀態的執行,PUTDELETE 都回 409 {"error":"pending_staged_executions","staged_change_ids":[…]}。這個檢查在 command 這一列被鎖住時進行,所以新的 staged 計畫無法在檢查與變更之間插隊。請先處理完審批。

即使來源仍然漂移了,release 也會 fail closed 而不是套用一半舊的計畫:409 frozen_manifest_changed409 source_version_changed409 source_link_changed 就是凍結計畫版本的 source_authority_changed

Staged command plan 共用模組既有的審批預算:每個 actor 在每張表最多 500 筆待審變更(另有整張表最多 50 筆 pending create 的上限)、套用嘗試最多 5 次(超過即視為毒列),已儲存的回應快照上限 65 536 bytes——過大的 body 會被換成帶 truncated: true 的替身。佇列拒絕是 structured 429 staged_cap_exceeded;前端應顯示回傳的 scopelimit,不要內嵌固定上限。

審核者看到什麼

Staged command plan 是刻意去識別化的。Moderator 絕不會看到自己無法讀取的資料表中的列:那些 action 會依(資料表, 動作)對收斂成單一的 {table_id, action, row_count} 替身。版本 2 的計畫更進一步,要求審核者對某個 action 的每一張**來源(provenance)**資料表——也就是該 action 的值所衍生自的那些表——都有讀取權,才會顯示它的細節。

除了計畫本身,版本 2 的 staged change 還提供一份安全的審核 manifest,讓審核者不必看到資料也能判斷意圖:

  • command id 與其定義的 sha256;
  • invoker 的 identity 參照;
  • rule 名稱與 phase;
  • 資料表 trigger 的事件與型別計數
  • 每個 callback 的目的地、header 名稱(絕不含值)、去識別化的 body 形狀,以及它引用的 token;
  • 宣告的 output 契約;
  • private_planreview_manifest 與語意 sha256 摘要。

Staged change 的 change_type 就是字面上的 command_plan。要篩選 staged change 佇列的 moderator UI 需要這個字串。

Release 全有或全無,而且會重新檢查一切

批准一個計畫並不等於重放一次權限授予。Release 在單一 transaction 內完成,並且會:

  1. 重放凍結的計畫;
  2. 即時重新載入 invoker——已刪除、已停用或已被踢出聊天室的 invoker 會讓 release 失敗;
  3. 先對凍結的 ACL 上限做 dry run,再獨立解析並鎖定當下的 ACL、SCP 與成員資格;
  4. 把 staged change 從 pendingapplyingapplied
  5. 同一列執行紀錄轉為 succeeded,並回填 result_refsresponse_body

任何例外都會整個回滾、寫入終態的 apply_failed(審核者拒絕時則是 discarded)、把執行紀錄翻成 failed,並派送 failed 生命週期 callback——版本 1 的 release 通道有兩個例外。

**鎖錯誤會把計畫重新排回待審,而不是丟掉。**Release 期間的 InnoDB 鎖等待逾時(1205)或死結(1213)過去是終態:一個已經被人批准的計畫,因為一次暫時性錯誤就被永久丟棄。現在它會回滾、讓執行紀錄維持 staged,並把 staged change 重新排回審批 sweep,受既有的 5 次套用預算限制。儲存的原因是正規化 JSON 信封

{"details":{},"error":"command_execution_failed","message":"Approved command release should be retried","phase":"release","retryable":true}

**失敗原因會被消毒。**SQLAlchemy 的 DBAPIError 字串化後會帶 [SQL: …] [parameters: {…}],而套用迴圈過去把它原樣塞進 staged_change.error 與呼叫者讀得到的執行快照。那些綁定參數可能包含凍結計畫時重新注入的 ACL 隱藏欄位值,於是讀不到隱藏欄位的請求者,可以從一次失敗的已批准更新裡把它撈回來。現在只要錯誤的 orig__cause__ 鏈上任何一層有 SQLAlchemy 成因,原因就會換成安全版本;本來就安全的 command 錯誤則保留自己的訊息。

審批凍結的是授權上限,不是授權本身。凍結的 ACL 界定計畫最多能做什麼;當下的 ACL 仍然必須允許它。因此已批准的計畫仍可能在 release 階段以 apply_failed 失敗——審核者的批准是必要條件,不是充分條件。

輪詢的用戶端看到什麼

執行紀錄是整個生命週期中唯一的事實來源,而且從 staging 到終態始終保有同一個 execution_id

階段執行 status用原本的 idempotency key 重放
已 staged、等待審核staged重新拋出同一個 409 approval_required body
已批准並套用succeeded原樣回傳回填後的 response_body
被拒絕或 release 失敗failedFailed 紀錄會被忽略,新的呼叫會重新執行

因此保存了 idempotency key 的用戶端,只要用同一份請求 body 輪詢,就能看到終態結果而不會重新執行 command。它永遠不需要猜審批到底有沒有落地。

重播會原樣回傳儲存的快照;被裁切到 65 536 bytes 稽核上限的快照,回來時會帶頂層的 truncated: true,而且沒有任何宣告的 outputs 與步驟資料列。在斷定「這次執行什麼都沒產出」之前請先讀這個旗標——兩種 body 在其他方面完全一樣。truncated 只屬於 execute 通道:執行紀錄路由以固定欄位清單組出 body,而那份清單沒有它,所以 GET .../executionsGET .../executions/{execution_id} 就算對被裁切的執行也不會帶這個欄位,標記只會留在巢狀的 response_body 裡。

已經確實 commit 的 succeededstaged 執行紀錄,事後也不會再被改寫成 failed。過去只要 commit 落地但確認訊息遺失,或 commit 之後的 refresh 拋錯,失敗標記路徑就會覆寫那筆已提交的資料列——連帶毀掉重播快照,於是用戶端用同一把 key 重試時會重新執行 command,而不是重播終態結果。現在標記前會重新載入該列,狀態已是終態就原封不動地回傳。

審批與其他呼叫通道

資料表 trigger 不能指向會寫入審批控管資料表的 command——那在 trigger 存檔時就是 409 {"error":"approval_trigger_conflict","command_id","target_table_id","target_table_name","rule_id"}。無人值守的 staging 在設計上就不可能發生:背景觸發永遠不會在別人的審核佇列裡留下一個待審計畫。

這兩道守衛使用的參照集合不同,而這個不對稱是真正的陷阱。Trigger 端的守衛只走 insert/update/delete 分支,所以「僅讀取」控管資料表的 command 是可以接上 trigger 的。REST PUT 的守衛走的是完整 dependency contract,所以在已被 trigger 引用的 command 上,就算只是新增一個唯讀的控管資料表參照,也會得到同一個 409。

延伸閱讀

Last updated on