任務輸出交給自訂表格 Command
任務版本可以用執行一個自訂表格 Command 來收尾。版本宣告一個 kind 為 custom_table_command 的 output_policy;每次 run 都會拿到一把單次 run 憑證,這把憑證只能在某一個聊天室裡呼叫這一個 Command,work script 產出 Command 的輸入後就把它送出去。平台再用與 Commands API 相同的執行器去執行這個 Command。
本頁只寫 Sandbox 契約(v5.21.0 發布)。Command 的輸入、效果、核准與結果的意義,仍留在自訂表格的文件。較早的那種,表格 writeback(custom_table_writeback),契約不變,只多了一項:它現在會拒絕 write_policy 為 commands_only 的表;本頁會標出兩者不同的地方。
1. 在草稿版本宣告
POST /tasks/{task_id}/versions 與 PATCH .../versions/{vid} 上的 output_policy 是以 kind 區分的聯集型別。Command 這一種長這樣:
{
"output_policy": {
"kind": "custom_table_command",
"command_id": "<Command id>",
"chatroom_id": "<room id>",
"input_schema": [
{ "name": "order_id", "type": "string", "required": true }
]
}
}| 欄位 | 說明 |
|---|---|
kind | 字面 custom_table_command。 |
command_id | 這次 run 可以呼叫的 Command。≤36 字元,字元集 [A-Za-z0-9._-]。 |
chatroom_id | Command 執行所在的聊天室(同樣的格式)。此版本的每一次 run 都必須從這個聊天室提交。 |
input_schema | Command 的輸入宣告,≤200 項(每項是自訂表格的 CommandInput:至少 name、type、required)。必須等於該 Command 自己的 inputs(正規化之後比對):順序相同,每個輸入的每個欄位值都相同(包括 description 與 nullable)。 |
未知欄位是 422。這個政策會被雜湊進版本的 canonical_digest,只出現在 owner 視圖(borrower 看到 null)。和表格 writeback 一樣,它屬於部門擁有的任務:公司擁有的任務在發布時回 409 output_policy_owner_scope_unsupported,而且只有部門擁有的任務的 run 能通過 §4。草稿與發布時都不會檢查 chatroom_id 是否為擁有部門的聊天室;若不是,這個版本能發布,但每一次 run 都會因為 borrowed 而被拒。
2. 什麼樣的 Command 合格
草稿建立、草稿 PATCH、發布,以及每一次提交 run 時都會檢查 Command:
| 檢查 | 拒絕方式 |
|---|---|
| Command 存在、沒被刪除,且屬於你的公司 | 422 output_policy_command_not_found |
input_schema 等於 Command 宣告的 inputs,且 Command 是寫入型(mode 為 write) | 422 output_policy_schema_mismatch |
Command 以 definition authority 執行、execute policy 為 restricted,且宣告了 effect_identity | 403 output_policy_author_denied |
在發布時,這三項檢查的任何一項失敗(包括第三項)都會以 409 output_policy_unsatisfiable 回報——和表格 writeback 的表格不見了是同一種回報方式(訊息文字是通用的、提到的是 table;以 code 為準)。只有下一節對發布者與已記錄撰寫者做的身分、grant 與範圍檢查,仍是 403 output_policy_author_denied。在提交時,表中的 code 會原樣回傳,所以 Command 若在發布後被刪除或重新宣告,這次提交就會被拒(什麼都不會排隊),直到 Command 與版本的 policy 又一致為止。
還有一件這些檢查抓不到的事:需要凍結提案的 Command(有 proposal_policy)能通過這三項檢查,但 executor 對 run 發出的每一次呼叫都會拒絕(ct.governed_context_required),所以它不能當作 run 的輸出。
3. 誰能撰寫、發布與提交
撰寫者與提交者必須各自通過同一套 Command 檢查,而且每一步都會重做:
- **身分:**公司內有效(已驗證、未停用、未過期)、非 service account 的使用者,且是
chatroom_id的成員(該聊天室必須還在)。 - **Grant:**該 Command 有效的 execute grant(以
chatroom_id為行動中的聊天室來判斷)。光是管理該 Command 的表、或持有 execute 委派 lease 都不夠,雖然 Commands API 本身對 restricted Command 兩者都會放行。 - **範圍:**部門範圍的 Command,
chatroom_id必須是該部門的聊天室;聊天室範圍的 Command,chatroom_id必須就是那個聊天室。
失敗是 403 output_policy_author_denied。撰寫者是最後寫入草稿的人:草稿建立、每次草稿 PATCH 與每次 POST .../script-file 上傳都檢查呼叫者並讓呼叫者成為撰寫者,發布則同時檢查發布者(平台 root operator 除外)與已記錄的撰寫者。
4. 提交 run
每一條會建立 run 的路徑(手動 POST /chatrooms/{id}/runs、重試、Agent 工具、自訂表格 trigger)都會在同一個交易裡為這次 run 鑄出一把密封憑證。表格 writeback 遇到 borrowed 的 run、或提交者不是以 JWT 驗證的使用者(API key、受限 Sandbox 金鑰或社群媒體 client)時會跳過鑄憑證,run 照樣排隊。Command 輸出的版本則是拒絕這次提交(什麼都不會排隊),回 403 output_policy_author_denied,除非下列條件全部成立(Command 已被刪除或不再相符,則是 §2 的 422):
- 任務是部門擁有,且這次 run 不是 borrowed(房間屬於擁有部門;公司擁有的任務或其他部門的房間都算 borrowed——見任務受眾);
- run 的聊天室就是政策的
chatroom_id; - 提交者是以 JWT 驗證的使用者主體——已登入的使用者、trigger 觸發的 run 中 trigger 的撰寫者,或內部聊天中 Agent 代表的使用者;API key、受限 Sandbox 金鑰、社群媒體 client 都不算;
- 提交者與版本的撰寫者都通過 Command 檢查。
若平台的 writeback 簽章金鑰缺少或太短,提交會回 503 sandbox_writeback_key_unavailable,與表格 writeback 相同。
5. run 裡的憑證
認領時,憑證透過密封 secret 通道送進 work script,用的是與表格 writeback 相同的兩個變數名稱。它們是 runner 保留的 TEAMSYNC_ 名稱,不能設在 ordinary_env:
| 變數 | Command 輸出 run 的值 |
|---|---|
TEAMSYNC_CT_WRITEBACK_URL | {API origin}/public/module/custom_tables/callback/command-output/{credential id} |
TEAMSYNC_CT_WRITEBACK_TOKEN | 這個 URL 的 Bearer secret |
- 每次 run 一把憑證;只儲存 secret 的 SHA-256。
- 它會在下列三個限制中最早到的那個失效:run 離開
queued/starting/running;認領時間 +timeout_seconds+ 120 秒收尾包絡(finalization envelope);以及提交時設下的保底期限,也就是提交時間 + 48 小時與timeout_seconds+ 120 秒兩者中較大的那個(排隊等待也算在內,超過之後才被認領的 run 會在沒有這兩個變數的情況下啟動)。每次進入終態都會撤銷。 - **請檢查這兩個變數是否存在。**若平台在認領時無法揭露憑證——run 排隊期間撰寫者或提交者的 grant 被收回、Command 或版本的 policy 變了、任務或版本被封存、上述保底期限已過、簽章金鑰或公開的 API origin 不可用,或多出來的變數會讓環境超過大小上限——run 仍會啟動,只是沒有這兩個變數。
- work script 必須連得到 API origin:託管雲端需要執行期對外網路(限制);地端部署另外需要主機的
SANDBOX_ONPREM_EGRESS=internet與 operator 對 API origin 的開放(雲端與地端部署)。
6. 送出輸出
POST {TEAMSYNC_CT_WRITEBACK_URL} # /public/module/custom_tables/callback/command-output/{credential id}
Authorization: Bearer {TEAMSYNC_CT_WRITEBACK_TOKEN}
Content-Type: application/json
{ "inputs": { "order_id": "A-1001" } }import json, os, urllib.request
request = urllib.request.Request(
os.environ["TEAMSYNC_CT_WRITEBACK_URL"],
data=json.dumps({"inputs": {"order_id": "A-1001"}}).encode(),
headers={
"Authorization": "Bearer " + os.environ["TEAMSYNC_CT_WRITEBACK_TOKEN"],
"Content-Type": "application/json",
},
method="POST",
)
print(urllib.request.urlopen(request).read().decode())這是公開路由(沒有 /private 前綴、不用租戶登入):Bearer secret 本身就是授權。Body 必須恰好是 { "inputs": { … } }——任何其他頂層欄位都是 422——inputs 會對照 Command 宣告的輸入與它的 JSON 上限驗證。成功時的 body 是 Commands API 的執行回應(CommandExecutionResponse,省略 null 欄位);欄位與狀態請看自訂表格文件。
- Command 以 run 的提交者身分執行(trigger 觸發的 run:trigger 的撰寫者),以
chatroom_id為行動中的聊天室,冪等鍵是sandbox-output:{run_id},所以每次 run 的 Command 最多生效一次。 - **第一份有效輸出勝出。**第一份通過宣告輸入檢查的
inputs會在 Command 執行之前以 digest 釘住(對驗證後的 inputs 取 digest),即使 Command 隨後拒絕它們或執行失敗,這個釘選也不會解除:重試必須送相同的 inputs,改過的 inputs 在這次 run 剩下的時間裡都會得到 409output_command_conflict。再送相同的 inputs 會走 Command 自己的冪等機制:已經成功(或已暫存等待核准)的執行會被重放、不會執行兩次,失敗的執行則可以重試(若呼叫與一個仍在進行中的執行競爭,可能收到 Commands API 自己的 409idempotency_in_progress)。 - 每次呼叫都會重新檢查授權,所以 run 進行中 grant 被收回,呼叫就會變成 403
output_policy_author_denied。 - **核准需要 run 還活著。**若 Command 沒有直接完成、而是暫存了一個核准,放行時會重新解析 run 的憑證;run 一結束憑證就被撤銷,所以在 run 結束之後才放行的核准無法再成功。
| 狀態 | detail.code | 何時發生 |
|---|---|---|
| 404 | output_run_unavailable | 憑證 id 不存在,或 Bearer secret 缺少、超過 256 字元或不對(統一一種回答,不洩漏任何資訊) |
| 409 | output_run_unavailable | 憑證已撤銷或過期,或 run 已不在 queued/starting/running,或它的任務或版本不再是 active 且已發布 |
| 409 | output_command_stale | run 提交之後,版本的政策、Command 的定義或它宣告的輸入變了,或該 Command 已被刪除、或已不再是你公司的寫入型 Command |
| 409 | output_command_conflict | 這次 run 已經接受過另一份不同的有效 inputs |
| 422 | output_schema_violation | inputs 不符合 Command 宣告的輸入 |
| 403 | output_policy_author_denied | 提交者或撰寫者的 Command 授權被收回,或該 Command 不再是帶 effect identity 的 restricted definition-authority Command |
| 422 | (validation 陣列) | body 不是 { "inputs": { … } }、超過 JSON 上限,或路徑裡的憑證 id 不是小寫 UUID |
| 其他 | Command 執行錯誤 | 同一個呼叫在 Commands API 會得到的任何回應,沿用它自己的錯誤形狀(detail.error 物件,或由 {type, msg} 項目組成的 detail[] 清單) |
7. Trigger、Agent 與前端
- 自訂表格 trigger。
submit_sandbox_job可以對準 Command 輸出的版本(表格 writeback 的版本仍然被拒)。run 由 trigger 的撰寫者提交,輸入檔就是渲染後的input,而 §4 的拒絕在觸發時是 trigger-run receipt 上一個失敗的 action(不會有 Sandbox run)。見自訂表格 trigger。 - Agent。
requires_confirmation=false時 Agent 直接提交,不經提案回合;表格 writeback 的版本仍需要兩回合確認。§4 的 403 拒絕,對模型來說是工具錯誤unauthorized;422(Command 不存在或不符)是validation_error;503 是provider_unavailable。見 Agent toolkit。 - **前端。**owner 在版本上看得到
output_policy,borrower 看到null。處理上面的撰寫與提交拒絕。用一般的執行 API觀察 run;Sandbox 的 run 紀錄不帶 Command 的結果。