執行權限
預設情況下,任何能讀取 Command 所引用的每一張資料表、並通過它的 visibility 的人都能執行它,而它讀寫時使用的是呼叫者自己的資料表權限。action policy 的一部分同時改變這兩半,讓一張資料表可以透過一個 Command 來改,而執行的人不需要持有資料表上的任何 grant:
| 問題 | 控制項 | 預設 |
|---|---|---|
| 誰可以執行? | execute_policy 與 execute grant | open |
| 它用誰的資料表權限? | definition.authority 與它的 table_access ceiling | invoker |
| 呼叫者拿回什麼? | status-only 結果,definer authority 下自動啟用 | 完整 |
誰能執行 Command
execute_policy 設定在 Command 的建立與更新 body 上(REST 與 IaC):
open(建立時省略的預設值)維持既有規則:讀取每一張被引用的資料表,並通過visibility。restricted只接納每一張被引用資料表的現任 manager,以及持有有效 execute grant 的主體。資料表讀取權限打不開它、沒有建立者後門,而且移除最後一個 grant 也絕不會重新打開它。visibility不會被參考,所以private的受限 Command 對每一位 grantee 都會列出、也都能執行。Manager 身分是依呼叫者自己的 user id 判定:client principal(外部或 Agent 平台)永遠不算 manager,即使它所委派的人類管理每一張被引用的資料表也一樣,所以它需要 grant,而且從 definer Command 得到的永遠是 status-only 結果。
REST PUT 時省略 execute_policy 會保留目前的值,所以整份覆寫的更新絕不會悄悄重新打開受限 Command;IaC 的 command 行則相反,省略這個 key 代表 open(見以 IaC 撰寫)。明確設成 open 在兩條通道上都會重新打開,除非 definition 宣告了 authority: definition,它要求 restricted(REST 為 422 definition_requires_restricted,IaC 行則是 parse 階段的 400)。
這道 gate 在每一條會執行 Command 的通道上都即時檢查——REST execute、agent、MCP、trigger 的 invoke_command 與審批 release——所以撤銷 grant 或 manager 角色立即生效,連已經暫存的計畫也一樣。閘外的呼叫者會得到帶 program-error 信封的 403,error: "command_execute_denied"(This Command is restricted and the caller holds no execute grant、phase: "execute"、details: {};MCP 工具改回 not_found)。受限 Command 對不能執行它的呼叫者也是隱藏的:它不會出現在列表中、讀取時回一般的 Command 404,也不在 agent 的 roster 內。
Execute grant
一個 grant 指名一個可以執行一個受限 Command 的主體。Grant 只在 Command 為 restricted 時才有意義。
| Route | 用途 |
|---|---|
GET …/commands/{command_id}/grants | 該 Command 的 grant {items, total} |
POST …/commands/{command_id}/grants | 授予一個主體。具冪等性:已存在的 grant 會原樣回傳 |
DELETE …/commands/{command_id}/grants/{grant_id} | 撤銷,立即生效;回傳被刪除的 grant |
這些 route 存在於三種 scope({scopeWithId}),而且每一個都要求即時管理該 Command 引用的每一張資料表——與編輯它的規則相同,不看 created_by。
{ "principal_type": "department", "principal_id": "33333333-3333-4333-8333-333333333333" }{
"id": "5d1c0f0e-8a0a-4a53-9a63-0c8d2a8c9a11",
"command_id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
"principal_type": "department",
"principal_id": "33333333-3333-4333-8333-333333333333",
"created_by": "99999999-9999-4999-8999-999999999999",
"created_at": "2026-10-01T09:00:00"
}principal_id 最長 36 個字元,目標必須是該 Command 所屬公司中存活的使用者、部門或聊天室,或是房間屬於該公司的既有 client:其他一律是統一的 404 Grant principal not found,所以 grant 請求絕不能用來探測別家公司的 id。不存在的 grant id 是 404 Grant not found。Grant 是對實際主體比對的:
principal_type | 符合 |
|---|---|
user | 主體自己的使用者,或 Agent 平台 client 所委派的已驗證人類 |
department | 已驗證人類現在所屬的部門——絕不是發出呼叫的部門 |
chatroom | 屬於該存活聊天室、且為存活成員的已驗證人類。綁定房間的呼叫(agent;呼叫聊天室範圍 command 的 trigger)只計算 acting room 的 grant,而且仍需要存活的成員資格,因為非成員也可以為某個房間鑄造出 Agent 平台 client。REST 或 MCP 對聊天室範圍之 version-2 command 的呼叫,其審批 release 同樣綁定該 command 所屬的房間,所以在那裡只計算它自己房間的 grant |
client | 外部 client id(外部社群媒體 client 需要明確的 grant;房間 grant 絕不會接納它) |
已驗證人類是存活且不是服務帳號的使用者。對人而言,該 scope 自己的門檻仍然適用:REST、MCP 與 trigger 只讓聊天室範圍 command 所屬房間的存活成員執行它:透過 REST,非成員會得到房間門檻的 403 Insufficient permissions, not a member …,通過門檻的 manager 則得到 403 Command principal is not authorized;MCP 回 not_found;trigger run 則會失敗。綁定到該房間的 client 不檢查成員資格:外部 client 靠 client grant 執行受限 Command,為該房間鑄造的 Agent 平台 client 則靠 user、department 或 client grant 執行,即使它背後的人類不是成員;只有 chatroom grant 會檢查成員資格。
Execute-only 呼叫者看到什麼
可以執行受限 Command、但不管理它引用的每一張資料表的呼叫者,拿到的是介面,不是程式。GET 與列表回傳的 Command 是 definition: null、definition_redacted: true、lifecycle_redacted: true 與 interface: {inputs, outputs, mode}——宣告的 inputs、output 名稱與 mode("query",寫入型 command 則是 null),絕不含 steps、ceiling、rules、callbacks 或 triggers。Manager 看到的仍是完整的 definition。
定義者權限
definition.authority: "definition" 讓 Command 用它自己宣告的 ceiling 執行,而不是用呼叫者的 grant。它是為 execute-only Command 設計的:呼叫者不需要任何資料表權限,只需要執行它的權利。帶有這種 authority 的 Command 不能帶 attachment_copies grant:儲存時就會被拒絕,因為 grant 的來源 select 必須以呼叫者自己的讀取權執行。
{
"version": 2,
"authority": "definition",
"table_access": {
"11111111-aaaa-4aaa-8aaa-111111111111": { "read": "all", "update": true },
"22222222-bbbb-4bbb-8bbb-222222222222": { "read": "filtered", "read_filter": { "column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa", "op": "eq", "value": "open" }, "insert": true }
},
"inputs": [ … ],
"steps": [ … ]
}| 規則 | 契約 |
|---|---|
| 前提 | execute_policy 必須是 restricted(建立與 PUT 時為 422 definition_requires_restricted;/validate 不會跑這項檢查),而且 Command 必須是寫入型 command(authority `definition` is not supported for query-mode commands) |
table_access | 從資料表 id 對應到 {read, read_filter, insert, update, delete} 的 map,最多 20 個項目。read 是 none(預設)、all 或 filtered,read_filter 是以內部 col_<hex> 欄位 key 為鍵的 row policy,恰在 read 為 filtered 時必填(儲存時只檢查它是否存在),insert、update 與 delete 預設為 false。REST 對格式錯誤的組合回 model 的訊息:authority `definition` requires a non-empty table_access、table_access is only allowed with authority `definition` 、table_access.read `filtered` requires read_filter 或 table_access.read_filter is only allowed with read `filtered` |
| 必須與程式吻合 | 它必須恰好指名 definition 引用的資料表(寫入目標、select 與 join 來源與相依資料表):透過 REST 時,缺少或多出的 id 在建立與更新是 422 definition_ceiling_invalid,帶 missing_table_ids 與 extra_table_ids;在 /validate 報告中則是 status: 422 的 errors[] 項目(HTTP 200、valid: false)。IaC apply 不會做這項檢查:缺少的項目要到 Command 執行時才以 403 definition_ceiling_missing 浮現,多出的項目雖然會被接受,但必須是同租戶、且 owner 有管理權的資料表,否則 Command 會以 403 definition_authority_invalid 失敗。作者本來就管理每一張被引用的資料表,所以指名這些 id 不會變成存在性 oracle |
| 逐項操作 | read、insert、update 與 delete 各自獨立強制,兩種 definition 版本皆然 |
| 絕不是 manager | ceiling 取代呼叫者的 grant,但 definer 不是 manager:row filter 與資料表 column_acl 的隱藏欄位仍然生效,租戶永遠有約束力。綁定房間、在部門資料表上的呼叫,會再受 acting room 為該呼叫者 audience 所設的 grant 資料列限制(沒有資料列就什麼都沒有) |
| Owner | 伺服器把儲存這一版的 manager 記為 authority_owner_id;client 永遠不能指定,而且每次由另一位 manager 重新儲存都會重新推導 |
Owner 必須保持有效。在每一次執行、冪等 replay、trigger 執行與審批 release 時,伺服器都會檢查 owner 是存活、屬於該 Command 的公司、不是服務帳號、而且仍是每一張 table_access 資料表的 manager,並檢查每張被引用的資料表都有 ceiling 項目;否則呼叫會 fail closed,回 403 definition_authority_invalid(訊息會請一位管理每一張資料表的 manager 重新儲存這個 Command)或 403 definition_ceiling_missing。Owner 會隨每次執行一起記錄、並凍結進暫存的計畫,release 時若發現它變了,會以 source_authority_changed、field 為 authority_owner_id 結束(見審批)。
Status-only 結果
在 definer authority 之下,每一個呼叫者看得到的結果——即時 body、冪等 replay、錯誤——除非呼叫者管理每一張被引用的資料表,否則都是 status-only receipt:
{ "execution_id": "ffffffff-ffff-4fff-8fff-ffffffffffff", "status": "succeeded", "steps": [], "redacted": true, "executed_at": "2026-10-01T09:00:00Z", "outputs": {} }- Receipt 不帶資料列、outputs、result ref 或 response body,唯獨作者選擇公開的公開結果除外。
executed_at仍保留(冪等 key 的 replay 只顯示原本的execution_id與executed_at),完整 body 有 outputs 而沒有任何公開項目時outputs是{},effect-identity 的 replay 仍標示"replayed": true。 - 不在一份固定清單上的每個錯誤——包括純字串 detail,以及凍結版本之前、指令資料列已不存在時所拋出的錯誤(REST
/execute對未知的 id 就回404加這個 body)——都會換成{"error": "command_failed", "message": "The command could not be completed.", "redacted": true},狀態碼維持原樣。清單上的 25 個錯誤碼會原樣通過,只保留error、phase、message、retryable、details、process_id、rule_id、rule_label、staged_change_id、table_id、operation與surface,所以沒有step與path:approval_required、approval_pending、staged_cap_exceeded、governed_context_required、command_execute_denied、definition_authority_invalid、definition_ceiling_missing、source_authority_changed、schema_dependency_changed、source_version_changed、source_link_changed、frozen_manifest_changed、idempotency_key_reuse、idempotency_in_progress、idempotency_context_conflict、command_lock_conflict、command_contract_mismatch、query_command_requires_query_route、command_timeout、no_eligible_approver、requester_unattributable、invalid_agent_group_scope、ct.effect_identity_conflict、ct.effect_identity_replay_forbidden與effect_identity_unbound。其他任何錯誤碼,例如approval_template_unusable、multiple_approval_gated_tables或command_link_graph_changed,都會變成command_failed,輸入驗證的400/422、assertion_failed(失敗的assertstep 若是公開則例外)與command_rule_failed規則失敗,以及純字串的Read/Insert/Edit access not granted403 也是:execute-only 的呼叫者只會得知狀態碼。 - 該 Command 的 dry run 適用同樣的規則。對這類呼叫者,它的成功 body 本來就是遮蔽後的
{"status": "preview", "outputs": {}, "redacted": true};自後端 5.22.0 起,它的錯誤也和/execute的錯誤一樣被取代:狀態碼相同、command_failedbody 相同,會原樣通過的錯誤碼與公開的assert訊息也相同。5.22.0 之前,dry run 會把完整的引擎錯誤交給 execute-only 的呼叫者,包含錯誤碼、phase、step、path、details、assertion 訊息與輸入驗證文字。 - 執行歷史(
…/commands/{command_id}/executions)會把這種執行顯示為redacted: true、steps: []、沒有 outputs(公開項目除外),錯誤文字是Command execution failed;input snapshot 仍保留,完整紀錄則留在供 manager 查看的稽核資料列中。 - 呼叫 definer Command 的 trigger run 失敗時,只會記錄不含值的錯誤碼,因為 trigger run 可由宿主資料表的 moderator 讀取,而他們不一定管理該 Command 的每一張資料表。
- 投影的判斷依據是實際執行的那一版(replay 則依據原執行的不可變 snapshot)與呼叫者的即時權限,所以並行的重新儲存無法讓已產生的結果解除遮蔽。若無法做出判斷,就一律遮蔽。
所以 agent 回報的是 status-only receipt,不會承諾或重述其中的資料列內容,只會陳述公開 receipt所載的事實。
作者選擇公開的結果
自 backend 5.21.0 起,作者可以讓 execute-only 的呼叫者看到 definer 執行的特定事實,讓呼叫者、Agent 與下一輪對話知道發生了什麼,而不是只知道成功。這是逐項 opt-in:version-2 的純量 output(outputs.<name>)或 assert step 加上 "public": true。未設定時 key 會省略,true 是唯一接受的值。沒有這個旗標、呼叫者管理每一張被引用的資料表,或是 invoker-authority 的 Command,回應都不會有任何變化:旗標只在結果是 status-only 時才有作用。伺服器在儲存 Command 時仍會檢查每個公開項目,不論它的 authority 為何。在 invoker-authority 的 Command 上,依賴 relation 的公開項目(包含排在 select 或資料列寫入 step 之後的公開 assert)會以 command_public_value_unsafe 被拒絕,因為讀取 relation 需要 read: "all" 的 table_access 項目,而只有 definer 的 Command 才能有(見哪些可以公開)。
{
"outputs": {
"note": { "value": "$input.note", "description": "The note you sent", "public": true },
"accepted": { "value": true, "description": "Whether the request was accepted", "public": true }
}
}{ "kind": "assert", "name": "within_limit", "condition": { "$lte": ["$input.qty", 10] }, "message": "At most 10 per request.", "public": true }公開 output 的 description 與公開 assert 的 message 都會顯示給呼叫者,請不要把機密放進去。
呼叫者看到什麼
成功的執行會回覆 status-only receipt,外加公開的值與 public_receipt:
{
"execution_id": "ffffffff-ffff-4fff-8fff-ffffffffffff",
"status": "succeeded",
"steps": [],
"redacted": true,
"executed_at": "2026-10-08T09:00:00Z",
"outputs": { "note": "front desk", "accepted": true },
"public_receipt": {
"name": "Record visit",
"input_summary": [{ "label": "note", "value": "front desk" }],
"outputs": [
{ "label": "The note you sent", "value": "front desk" },
{ "label": "Whether the request was accepted", "value": true }
]
}
}outputs只含目前仍可安全顯示的公開 output,其他 output 一律隱藏。public_receipt.name是 Command 名稱,input_summary以各 input 的description(沒有時用 input 名稱)回顯呼叫者自己送出的 input(identity 型別的 input 顯示為{"label": …, "value_hidden": true}),outputs則以各公開 output 的description(沒有時用名稱)列出。- 任何公開 output 被扣留,或 receipt 會超過 100 個 input 或 4 KiB 的 JSON 時,
public_receipt會變成不含值的{"name": …, "input_summary": [], "outputs": [], "withheld": true};仍然安全的公開 output 照樣留在outputs。 - 停在公開
assert上的執行回422 {"error": "assertion_failed", "message": "<作者寫的 message>", "redacted": true},而不是一般的command_failed。其他失敗,以及不公開的assert失敗,維持上一節的行為。 - 讀回這次執行的每個地方都會帶同樣的項目:限制未變時的同 key replay、
GET …/executions/{execution_id}與執行清單,各自是帶outputs與public_receipt的redacted: true項目。有兩個例外:被截到 65 536 bytes 上限的已儲存回應不再保留outputs,所以該次執行的 replay 與讀取都不帶公開項目(只有第一次回應有);已儲存的結果若帶有讀取者看不到的approval_provenanceenvelope(見審批 provenance),則回傳單純的 status-only receipt。權限變動後的同 key replay 仍是409 idempotency_context_conflict(見 replay)。
哪些可以公開
撰寫(authoring)時,伺服器會從編譯後的運算式——包含 relation 篩選條件及值所依賴的其他來源——證明每個公開值只來自:
- 字面值,以及呼叫者自己送出的純量 input;
- 呼叫本身指名的 context:
$ctx.user_id、$ctx.social_media_client_id、$ctx.company_id與$ctx.chatroom_id(不含由伺服器解析成員資格的$ctx.department_id); - 伺服器時鐘
$now與$today; - 不指向任何 principal 的資料列 metadata:
id、version、created_at、updated_at、sort_order與table_id(不含created_by、created_by_client、created_by_ai); - 沒有
column_acl限制、欄位本身或資料表設定中都沒有secret_ref綁定、且不是formula、rollup或lookup的儲存欄位; - 由以上來源組成的
let值與 relation 結果。這類 relation 讀取或寫入的每張資料表,都必須列在table_access並設為read: "all",且沒有 channel 範圍(SCP)規則,因為 row policy 會依程式從未指名的欄位篩選資料列。公開的assert也會繼承它之前每個 step 的來源,因為執行到它就證明那些 step 都通過了。它不會繼承before_execute規則的來源,所以它的 message 也等於告訴呼叫者那些規則已通過。relation 只有來自沒有for_each的select、insert、update或deletestep、而且不是以字面values為來源的select時才能被證明;公開assert之前只要有一個 step 的 relation 通不過這項檢查,這個assert也會以command_public_value_unsafe失敗。
公開 output 是純量(null、string、text、select、integer、float、decimal、boolean、date、datetime),或宣告了不超過 20 的 max_items 的純量陣列;relation output 不能公開。其他一律在 POST 與 PUT …/commands 被拒絕、並出現在 /validate 報告中,回 typed 422(phase: "author"、retryable: false、details: {},path 指出 outputs.<name> 或 steps[<index>],合計的常數大小上限則只指出 outputs);IaC apply 則以 invalid command definition: 後接同樣文字拒絕該 command 行。
error | 時機 |
|---|---|
command_public_value_unsafe | 受限或有 secret_ref 的欄位、計算欄位、不透明運算子($json_parse、$json_get、$json_stringify、$formula、$rollup、$lookup)、非純量 input、relation output、table_access 的 read 不是 all 或有 channel 範圍規則的資料表上的 relation、上述被排除的 context 或 metadata 來源,或證明無法追蹤的運算式 |
command_public_value_limit_exceeded | 沒有 max_items(上限 20)的陣列 output、超過 512 個呈現字元的常數 output、常數 output 合計超過 4 KiB、單一項目背後超過 1,000 個來源,或證明超過 50,000 個節點 |
每次交付都會重新檢查
每次交付公開項目時——第一次回應、replay、執行讀取與 Agent 暫存的確認結果——都會用目前的狀態重新檢查儲存的證明。Command 儲存之後才受限或加上 secret_ref 綁定的欄位、新加在來源資料表上的 channel 範圍規則、會篩選或縮小 department 資料表的 room grant,或已刪除的資料表,都會讓該項目被扣留;執行不會重跑也不會失敗,Command 也不會被標成過期。交付時同樣套用上限:值是純量或最多 20 個純量,呈現文字(項目以 , 串接)不超過 512 個字元,所有公開 output 合計不超過 4 KiB 的 JSON。失敗的公開 assert 若證明已不成立,則退回 command_failed。
Agent 與聊天 receipt
Agent 的 command 工具在已套用的執行上帶有 public_receipt 物件;公開 assert 失敗時,它的拒絕是 assertion_failed,作者的 message 放在 reason,並帶 "public_assertion": true。在聊天中,已套用 command 的 receipt 會改為陳述業務事實,而不是固定句:
「Record visit」已完成。
- note:「front desk」
- The note you sent:「front desk」
- Whether the request was accepted:是文字值以引號呈現,URL 或像識別碼的值顯示為「系統識別資訊已隱藏」,被隱藏的 identity input 顯示為「已依您指定的對象帶入(識別資訊已隱藏)」。當 receipt 是 withheld、沒有列出任何公開 output、超過 4 KiB,或無法安全轉成文字時,會退回「操作已完成。」(The operation completed successfully.)。在純文字通道(LINE、Messenger 與 Instagram)上,這份 receipt 會接在模型自己的敘述之後,而不是只留下敘述。manager 的執行、invoker-authority 的執行,以及沒有公開項目的執行,仍是固定句(見 receipt)。
以 IaC 撰寫
command 行接受 execute_policy、execute_grants(最多 100 個 {principal_type, principal_id} 項目,宣告式套用:apply 會建立缺少的 grant、撤銷多出的 grant;請依 principal_type、再依 principal_id 排序列出,就像 export 一樣),以及在 definition 內的 authority、table_access(以 IaC 資料表 ref 為鍵,會被解析為 id)與 exclude_requester。authority: definition 而沒有 execute_policy: restricted,是 parse 階段的 400(definition_requires_restricted),也是 apply 的拒絕。Grant 的主體在公司內已無法解析時,該行會以 execute grant principal <type>:<id> not found 失敗。省略 execute_policy 代表 open,省略 execute_grants 代表撤銷所有 grant,就像省略 visibility 代表 public 一樣:請在每次 apply 都帶上這兩個 key,或改從新的 export 開始。Apply 會從套用的操作者重新推導 owner,export 則會省略每個等於預設值的 key。
錯誤
error | 狀態 | 時機 |
|---|---|---|
command_execute_denied | 403 | 受限 Command 被既不管理每一張被引用資料表、也沒有有效 grant 的呼叫者執行 |
definition_authority_invalid | 403 | definer owner 已不存活、不在該公司、是服務帳號,或不再管理每一張 table_access 資料表 |
definition_ceiling_missing | 403 | 某張被引用的資料表沒有 table_access 項目 |
definition_requires_restricted | 422 | authority: definition 用在不是 restricted 的 Command 上 |
definition_ceiling_invalid | 422 | table_access 沒有恰好指名被引用的資料表 |
限制
- Execute access 不是對資料表 manager 隱藏資料的方法:他們可以讀取、編輯並重新儲存這個 Command。
- Definer Command 只能做 ceiling 允許的事,但它是替它接納的每一個人做的。請讓
table_access保持狹窄、優先使用filtered讀取,並只授予你能掌控的部門或房間。 - 這些 gate 都是即時檢查,所以「這個呼叫者現在能不能執行它」的答案,可能在暫存的 proposal 與它的 release 之間改變。