Skip to Content

command line

command 把組合式的多表格指令納入 IaC 管理。與其他所有具備 state 的 kind 不同,command 是 scope 層級,不是資料表子資源:它沒有 table 欄位,state ref 是保留形式 command:{ref},而不是 {table}.{ref}。

JSONL Line

{"kind":"command","ref":"close_appointment","spec":{"name":"Close appointment","description":"Mark one appointment closed","definition":{"version":2,"inputs":[{"name":"code","type":"string","required":true}],"steps":[{"kind":"update","name":"close","table":"appointments","set":{"status":"closed"},"match":{"code":"$input.code"},"max_rows":1}],"outputs":{}}}}

欄位契約

頂層對應 IacCommandLine:

欄位必填精確契約
kind是固定 "command"
ref是裸的 authored ref,符合 ^[a-z0-9][a-z0-9_-]{0,63}$
state否"present" 或 "absent",預設 "present"
spec條件式IacCommandSpec;present 必填、absent 禁止

本 kind 沒有 renamed_from,因此沒有純改名例外:present 行一律需要 spec,而且該 guard 的訊息沒有括號補述。

spec 只接受:

欄位必填精確契約
name是非空 string,最多 64 字元;在 scope 內唯一
description否string,預設 "",最多 1024 字元
agent_enabled否boolean,預設 true——聊天室 agent 是否看得到並能呼叫這個 command
visibility否"public"(預設)或 "private"——private 指令只為建立者綁定成 agent 工具,REST 的 get/list/invoke 與執行紀錄讀取則限建立者或管理全部被引用資料表的呼叫者(restricted 的指令不看它)
tag_id否同 scope 的 tag ID,1–36 字元;省略時改綁文件 system tag。它是原始、依環境而異的 UUID,沒有 ref 或 token 形式(見 Tag 綁定)
execute_policy否"open"(預設)或 "restricted"——誰可以執行這個 command;見執行權限。Export 在預設值時省略這個 key
execute_grants否最多 100 個 {principal_type, principal_id} 項目:principal_type 是 user、department、chatroom 或 client;principal_id 接受原始平台 id 或相符的可攜身分 token,schema 長度上限 160 字元。Plan/apply 會把解析後的 native principal 正規化、去重並排序。宣告式;export 在清單為空時省略此 key,名稱無歧義時反轉為可攜 token
definition是Command DSL object,version 為 1 或 2;也可以帶 authority、table_access 與 exclude_requester(執行權限)

執行權限

execute_policy: "restricted" 搭配 execute_grants,以及宣告 authority: "definition" 的 definition,會讓 command 納入 action policy:

{"kind":"command","ref":"approve_refund","spec":{"name":"Approve refund","execute_policy":"restricted","execute_grants":[{"principal_type":"department","principal_id":"$dept:Finance"}],"definition":{"version":2,"authority":"definition","table_access":{"refunds":{"read":"all","update":true}},"inputs":[{"name":"refund_no","type":"string","required":true}],"steps":[{"kind":"update","name":"approve","table":"refunds","set":{"status":"approved"},"match":{"refund_no":"$input.refund_no"},"max_rows":1}],"outputs":{}}}}

在文件中,table_access 以本文件或本 scope 的資料表 ref 為鍵——與 steps[].table 使用的是同一種 ref——apply 會把它們解析為資料表 id;未知的 ref 會回報 unknown table ref '{ref}' in table_access。authority: "definition" 而沒有 execute_policy: "restricted",會在 parse 階段被拒絕(definition_requires_restricted;見錯誤目錄),沒有 authority: "definition" 卻帶 table_access 也一樣。Apply 會把 definer owner 設為套用的操作者,而他必須管理每一張被引用的資料表。Grant 以宣告式套用:apply 建立缺少的、撤銷多出的。Principal 必須在文件所屬公司內解析;未知、有歧義或 kind 不相符的參照以 iac_command_grant_reference 失敗。範例需要同公司既有的 Finance 部門。可攜 grant token 為 $user:、$dept:、$room:(含部門限定名稱)與 $smc:,各自配對 principal type。Token 綁定納入 plan fingerprint;native grant read 仍回傳 ID,而 export 在可攜名稱有歧義或懸空時退回原始 ID。

Export 對每個 key 都是「等於預設值就省略」:只有受限的 command 才輸出 execute_policy,只有非空的 grant 清單才輸出 execute_grants,definer command 則輸出 authority,其 table_access 會轉回 ref;authority: "invoker" 與 exclude_requester: false 絕不會被寫出。指向 write_policy 為 commands_only 之資料表的 record 行,會在 plan 時被拒絕,所以這類資料表是由像這樣的 command 來改,而不是由種子資料列來改。

省略 execute_policy 代表 open,省略 execute_grants 代表沒有任何 grant——不是「不要動它」。用早於這些 key 的舊文件重新套用,會規劃出一個重新開放 restricted command、並撤銷其所有 grant 的 update,與 visibility、agent_enabled 的行為相同。請在每次 apply 都帶上這兩個 key,或改從新的 export 開始。

對 agent 隱藏一個 command

agent_enabled: false 不是停用。在通過一般權限與 visibility 閘門的前提下,該 command 仍可由 REST、IaC 與 trigger 呼叫,只是永遠不會被載入成 agent 工具——所有 agent 端的 command 查詢都帶 agent_enabled IS TRUE 過濾。適合用在「可以被呼叫,但不希望被建議」的破壞性或維運專用 command。

這個欄位和 name、description 一樣,是三方比對的一等成員:

  • 在文件中翻轉它會規劃成 update,changes 內帶 {"field": "agent_enabled", "from": true, "to": false}。
  • 在 REST 端被翻轉會以 agent_enabled 出現在 drifted_fields。
  • Apply 在 create 與 update 兩條路徑上都會寫入它。

Export 是預設值即省略:只有被隱藏的 command 才會輸出 "agent_enabled": false;預設可見的 command 完全不輸出這個鍵,讓此欄位存在之前寫的文件維持 byte-stable。

省略這個鍵不代表「不要動它」,而是代表 true。用早於這個欄位的舊文件重新 author 一個已隱藏的 command,會規劃出一個把它重新公開的 update。要讓 command 保持隱藏,就在每次 apply 都帶上 "agent_enabled": false,或改從新的 export 開始。

可見性走的是同一套三方比對

visibility 在 differ 裡的行為與 agent_enabled 完全相同:在文件中翻轉它會規劃成 update,changes 內帶 {"field": "visibility", "from": "public", "to": "private"};在 REST 端外部翻轉則會出現在 drifted_fields 的 visibility;apply 在 create 與 update 兩條分支都會寫入。

匯出是預設值就省略:只有 private 指令才會寫出 "visibility": "private",所以在這個欄位出現之前寫成的文件維持位元穩定。省略這個 key 代表 public,不是「不要動它」——用舊文件重新套用一個 private 指令,會規劃成把它改回 public 的 update。

IaC 這條通道本身不會依可見性過濾。Export、plan 與 apply 是 chatroom-manage lane,會讀到該 scope 內的每一個指令,private 的也一樣。這是刻意的:若依操作者過濾 differ 看到的線上狀態,plan 會變得具破壞性——操作者看不到的 private 指令會被判讀成「不存在」而規劃刪除,或在名稱上相撞。能對一個 scope 跑 IaC,就能看到並管理其中的每一個指令。

Definition 以 ref 撰寫

REST 的 definition 不能原封不動貼進文件,反之亦然。在 IaC 形式中:

  • 步驟的資料表值——寫入步驟的 table,以及 select 步驟的 from.table 與 joins[].table——是IaC 資料表 ref,對本文件或 IaC state 解析,絕不是即時資料表 UUID。
  • v1 row 的 data、match 與 where,以及 v2 寫入步驟的 values、set 與 match,其欄位鍵可以是 column ref、顯示名稱或內部 key:ref 會被翻譯,其餘由 REST 撰寫邊界正規化為 col_<hex>。
  • 運算式中的 $row.{alias}.{column} 與 $current./$new./$old.{column} 必須指名 column ref,或 metadata 名稱 id、version、created_at、updated_at、sort_order、created_by、created_by_ai、created_by_client 與 table_id 之一;在這裡寫顯示名稱會以 unknown column ref '{table}.{name}' 失敗。明確的 {"$column": {"source": "{alias}", "ref": "{table}.{column}"}} 節點接受含資料表的 column ref;alias 指向資料表時也可只寫 column ref。
  • 在 v2 insert step 的 on_conflict 中,rule 是 unique 規則的名稱,set 的 key 是 column ref,$existing.{column} 與 $current. 一樣使用 column ref;匯出時 rule 寫成規則名稱、set 的 key 寫成 column ref,$existing.{column} 則寫回等價的 current 欄位節點,重新 plan 仍是 noop(見 upsert)。
  • 行內的 $rollup、$lookup 與 $formula 運算式使用與 column 行相同的 ref——link_field、target_column、source_table 與 filter[].column——在其 record alias 所屬的資料表上解析。
  • Callback 若帶 secret_ref,必須等於 callback:{該 callback 的名稱}。它不會被保存:它標記該 callback 依賴 live command 的 secret,因此和 "<REDACTED>" 值一樣,只能讓該行更新既有的受管 command,永遠不能建立新的。自 backend 5.21.0 起,這個 key 只保留給 definition.callbacks 的各個 entry:名為 secret_ref 的業務欄位,在 step 的 values、set、match、data 或 where 中就是一般的欄位名稱,不會再被 callback secret_ref must address its callback name 拒絕,也不會從 definition 中被丟掉。
  • Definition 不帶身分 token。$user: / $smc: token 屬於 trigger invoke_command action 的 inputs。
  • Definition 不能帶 attachment_copies grant。自後端 5.22.0 起,definition 含有這個 key 的行會在 apply 時以 attachment_copies are authored over the Commands API only 失敗,是 200 apply 回應內的單行錯誤,沒有 code,也沒有 command '<ref>': 前綴。請改走 Commands REST API 撰寫 grant(附件複製)。Export 不會把這個 key 寫回,而更新 command 的 apply 會用文件取代整份已儲存的 definition,所以這次更新會移除透過 REST 加上的 grant:之後請再加回去。
  • decimal 輸入要宣告 precision、scale 與 allow_negative,與 decimal 欄位完全相同,其值為 decimal 字串;其他輸入型別帶這三個欄位會被拒絕。

secret_ref 就是把憑證留在文件之外的做法。相對地也請注意:你以字面值寫下的任何 callback header,都會照原樣儲存,並回吐給所有讀得到該 command definition 的人——見 callback 的形狀。

運算式預算由 REST 使用的同一個 compiler 執行:每個 row 運算式 256 個節點,每個 where 深度 12/128 個節點,case 為 8/64,rule 為 8/128,整份 definition 8192 個節點,JSON 深度 64/16384 個節點。Parse 會先檢查預算與 secret_ref 規則;兩者都來自丟出一般 ValueError 的 validator,所以違反時回的是可讀的 parse 400,帶 typed value_error issue(見 Validator 訊息類別)。Parse 接著把每個 $column ref 換成佔位 id,再驗證完整的 DSL model;這一步的結構錯誤是一般的 parse 400,會列出巢狀 issue(version-2 step 的 kind 給 list 或 object 是例外:它會拋出未型別化的 TypeError,回 500)。凡是需要 ref 或 live 資料表的檢查,都以 invalid command definition: {reason} 回報(見錯誤目錄)。另外 mode: "write" 以「等於預設就省略」的方式儲存,所以 write definition 永遠不會把這個鍵回吐——不要寫它然後期待在 export 看到。完整 DSL 契約請見 commands API 參考。

Command line 中的 effect identity

版本 2 的定義可以帶 effect_identity 區塊。它不需要 ref:裡面只有 step 名稱、欄位名稱與有型別的 literal——沒有資料表或欄位 id——所以它在 definition 裡的寫法與 REST 完全一樣。

{"kind":"command","ref":"post_receipt","spec":{"name":"Post goods receipt","agent_enabled":false,"definition":{"version":2,"inputs":[{"name":"receipt_no","type":"string","required":true}],"steps":[{"kind":"select","name":"receipt","from":{"table":"receipts","as":"r"},"select":{"receipt_id":"$row.r.id","qty":"$row.r.qty"},"where":{"$eq":["$row.r.receipt_no","$input.receipt_no"]},"max_rows":1},{"kind":"insert","name":"movement","table":"movements","values":{"qty":{"$single":{"relation":"$rel.receipt","expr":"$item.qty"}}},"returning":{"movement_id":"$new.id"}}],"outputs":{"movement":{"relation":"$rel.movement","select":["movement_id"],"max_rows":1}},"effect_identity":{"namespace":"erp.inventory","kind":"goods_receipt_post","key":[{"$single":{"relation":"$rel.receipt","expr":"$item.receipt_id"}}]}}}}

這個區塊是 differ 比較的定義的一部分,匯出時會原樣寫回,所以宣告 → apply → 匯出 → 再匯入會規劃為 noop。一旦 command 已記錄過業務事件,變更或移除區塊的行會在 apply 時以 IacLineError.code effect_identity_locked 失敗——這是 REST 409 在 IaC 的對應——detail 為 command '<ref>': effect_identity_locked — This Command has recorded business events; its effect identity (namespace, kind, key terms and their source rows) cannot change or be removed。這個代碼不可重試,該行會 rollback,其他行仍會套用。違反建立規則的區塊,會在 apply 時讓該行以 invalid command definition: <reason> 失敗,reason 用的是定義版本 2裡的那些訊息。

版本:寫 v1、匯出 v2

Command 的 IaC plan 會感知 DSL 版本。

  • 對可安全升級的 v1 程式,export 會輸出保持行為的 v2 形式。
  • 若升級會失真,則原樣保留 v1:帶隱含 body 的舊式 callback、產生的 capture 名稱超過 32 個字元,或輸入名稱與產生的步驟名稱相撞。
  • 比較時會把 v1 與 v2 正規化,因此 export 的文件重新匯入是 noop。

你以 v1 撰寫的 command,在 export 時看到 version: 2 是預期行為,不是一次編輯。不要把它「還原」成 v1;正規化器已經把兩者視為相等。

dependency_contract 是伺服器建立的即時狀態,會在正規化比較前被剝除,所以 export 後重新匯入不會看起來像 definition 被改過。

Ref 與身分規則

State 存的是 command:{ref},resource_id 為 command UUID。這個形式與 table、child ref 共用文件層級的重複 ref 檢查,因此兩行 command 宣告同一個 slug 會碰撞:

duplicate ref 'command:close_appointment' (already declared in this document)

以 . 切割 state ref 來推導父資料表的工具,會在 command(command:<ref>)與 insight selection(insight.<chatroom uuid>)上出錯。兩者都不是資料表子資源。

認領需要完全相符

認領是以 scope 內的名稱進行,而且不像 table 那樣有「先認領再更新」。若同名的 live command 存在但 spec 不同,該行會直接失敗,而不是認領後修正:

command adoption requires the live command to match the declared spec; export the live command or use a distinct name ambiguous adoption: 2 live commands named 'Close appointment'

第一種是 validate 階段錯誤:它擋下整份文件,apply 回 422,什麼都不執行。第二種是 diff 階段錯誤:apply 回傳 200,只略過該行。第一種的處理方式是先把 live command export 出來,以那份文字為起點。

Tag 綁定

明確的 tag_id 必須是本 scope 的 tag,而且必須涵蓋 definition 引用的每一張表。省略 tag_id 時,apply 會把 command 綁到文件的 system tag,並把每張被引用的表指派給它——這需要非空的 header.system。因此,沒有 system(system: "")的文件無法在不提供 tag_id 的情況下宣告 command。

tag_id 沒有 ref 或 token 形式。只要 command 綁定的不是其文件的 system tag,export 就會原樣寫出它,所以這種行只能在來源 scope 套用;在其他地方會以 command '{ref}': tag not found in this scope 失敗。把文件套用到別處之前,請替換該 id,或移除這個鍵改綁 system tag。

權限

具備 scope 存取權並不足夠。在認領以及每一次 present apply 時,操作者都必須是該 command 相依集合中每一張表的 table manager,而且那些表都必須在本 scope 可解析。自 2026-07-28 版起,刪除(state: "absent")是例外:table-manager 檢查失敗時——綁定的表在本 scope 已解析不到活表,或你不是它們的 manager——刪除分支會改看 command 自身 scope 錨點的權威(聊天室建立者或管理員、部門管理員、或租戶錨定的公司管理員),鏡射 REST 的殭屍 command 復原。所有失敗都回傳同一則刻意不透露細節的訊息:

command 'close_appointment': table no longer resolves live

它不區分「表不存在」與「表不屬於你」。這是刻意的,與 REST 的 command 撰寫路徑一致。

生命週期與規劃

Present 行可規劃 create、adopt、update 或 noop;state: "absent" 規劃 delete,會軟刪除受管理的 command。沒有 move——本 kind 沒有 renamed_from。

被遮罩的 callback 值無法用來建立 command。Export 的 callback 密鑰素材會以 <REDACTED> 回來並單獨計算指紋,因此帶遮罩的行可以更新既有的受管理 command,卻永遠無法建立一個:

redacted callback values require an existing managed command; replace them before recreating it redacted callback values require an existing managed command; replace them before creating in a fresh scope

Live callback 密鑰變動會以 definition 出現在 drifted_fields,這也是為什麼有人在 REST 改了 callback URL 或 header 之後,一個完全沒被改動的 command 仍會回報 definition drift。

以 Custom API tool 為目標的 callback,匯出形式是 {"name": …, "api_tool_id": …},原本有 body 時再加上一個被遮蔽的 body:tool 的參照會完整往返,body 樣板不會;而 tool 自己的 url 與 headers 本來就不在定義裡。Callback 密鑰 digest 也涵蓋 api_tool_id,所以把 callback 指向另一個 tool 算是密鑰變動,會以 definition drift 呈現。

支援的 dependency maintenance 不分 DSL version。IaC column 與 rule 行使用一般政策:先選 mutation 前 stored contract 仍 valid 的 commands,在同一 commit 刷新 mutation 後仍可編譯者;若 mutation 使某 command 失效,mutation 優先,該 command 保持 stale。非資料庫 prepare/compile/closure/concurrent-edit failure 會降級或略過,不 veto column/rule line;DB lock/deadlock error 仍可能中止。IaC trigger 行使用更強政策:candidate graph 與每個 selected command refresh 都必須成功,trigger、history、IaC state 與 refreshed definitions 一起提交或一起回滾。兩種政策都讓原本 stale 的 command 維持 stale。

在同一次 IaC apply 中,成功的 column、rule 或 trigger maintenance 都可能在較後面的 command 行執行前推進 dependency digest。Executor 只接受這些精確、由伺服器擁有的 digest 推進;任何外部或其他非預期 before-fingerprint drift 仍會產生 iac_plan_stale。

Apply 階段錯誤

以下錯誤出現在 200 apply 回應的 per-line 錯誤中;其他行仍會執行。

command 'close_appointment': table no longer resolves live command 'close_appointment' has pending staged executions command 'close_appointment': tag not found in this scope command 'close_appointment': tables outside tag: <table id>, <table id> command 'close_appointment': document system tag is required command 'close_appointment': adoption target no longer resolves command 'close_appointment' references no table: a command must operate on at least one custom table in this scope command 'close_appointment': approval_trigger_conflict — this command is invoked by a table trigger and cannot add a reference to table '<name>' (<table id>), which requires approval (rule <rule id>) command 'close_appointment': effect_identity_locked — This Command has recorded business events; its effect identity (namespace, kind, key terms and their source rows) cannot change or be removed command 'close_appointment': system tag sync failed: <exception> command 'close_appointment': unknown table ref '<ref>' a command named 'Close appointment' already exists in this scope command changed after planning; run plan again command 'close_appointment' is referenced by table triggers (command_referenced_by_trigger): table '預約' (<table id>) trigger '完成通知' (<trigger id>) Concurrent write conflict (lock); please retry the request.

當該行的 before-fingerprint(resource、state、tag、dependency digests)於 plan 與 apply 之間變動時,command changed after planning; run plan again 會帶 IacLineError.code == "iac_plan_stale"。若套用該行時遇到 MySQL 1205 或 1213,則改帶 code == "retryable_lock_conflict",detail 是淨化後的 Concurrent write conflict (lock); please retry the request.。command '<ref>': effect_identity_locked — … 帶第三個代碼 code == "effect_identity_locked"(見 Command line 中的 effect identity),重試無法修復它。上面其他 diagnostics 的 code 不設值。三者都是 200 內的 per-line 訊號,不是文件層級的 409;前兩者的任何重試都要從新的 plan 與 apply 開始。請見 plan 與 apply。

a command named '<name>' already exists in this scope 現在只計活列,與 REST 一致——軟刪的 command 立即釋放名字(command 沒有還原機制),所以「先一行 delete、之後再用同名 create」是合法順序。

trigger 引用守衛在 state: "absent" 時照樣生效,殭屍 command(表已消失)也不例外——仍被活表(或垃圾桶中可還原之表)trigger 呼叫的 command 是唯一擋住刪除的東西,對應 REST 的 409 command_referenced_by_trigger。

具備核准機制且有 staged execution 進行中的 command,會擋住自己的 IaC 行,直到該 staged 變更被處理完。

驗證錯誤

state=absent lines carry no spec (table lines are the sole adopt-then-delete exception) state=present lines require a spec unknown table ref 'appointments' duplicate ref 'command:close_appointment' (already declared in this document)

工作台會檢查生命週期 guard、spec 欄位契約,以及 definition.steps[].table 指名的資料表 ref。它看不到 IaC state,所以指向由先前 apply 管理之資料表的步驟,在本機會回報成 unknown table ref,必須以伺服器 plan 確認。其餘所有 DSL 規則——運算式預算、步驟形狀、參照解析——都由伺服器 compiler 判定。

動手試試

在 IaC 工作台宣告一張表、一個欄位與上面的範例 command。把 definition.steps[0].table 改成未宣告的 ref 觀察本機 validate 錯誤,修正後對測試 scope 執行唯讀 plan,並確認 action ref 顯示為 command:close_appointment。

Last updated on