Secret 與授權
任務版本可以宣告需要的 secret slot(secret_slot_declarations / secret_slot_names)。實際的 secret 值透過獨立的 secret API 綁定、端對端傳輸加密,值永遠不回傳、不記錄。
兩種 API
Secret binding(自己的任務綁值)
把一個 secret 值綁到 (consumer_scope, consumer_id, task_id, slot_name):
POST /secrets:建立值。POST /secrets/rotate:輪替值。POST /secrets/revoke:撤銷值。GET /secrets/{binding_id}:讀遮蔽後的 binding(只有has_provider_binding: bool、state∈pending/active/revoking/revoked、generation…,沒有值)。
規則:
- 需要 consumer-scope manager 權限。
- 建立/輪替/撤銷都要帶
Idempotency-Keyheader,缺了回 422idempotency_key_required。 - 值只透過
encrypted_value(混合傳輸信封)傳遞——見下面的傳輸加密。明文value一律拒絕。 - 解密後的值:4 UTF-8 bytes..64 KiB,不可含 NUL,永不回顯。
- secret adapter / pepper 不可用時回 503。
Secret approval(借用他人分享的任務)
當你執行的是別人分享的任務,且該任務需要 secret,必須由consumer-scope manager(借用方的聊天室/部門/公司管理員——不是任務擁有者)核准該 consumer 能用哪些 slot:
POST /secret-approvals(或別名POST /shared-task-secret-approvals):對精確的任務版本 digest核准一組slot_names。POST /secret-approvals/{approval_id}/revoke:撤銷。
規則:
SandboxSecretApprovalCreateRequest要帶task_version_digest(sha256:...)與risk_accepted: true(必須剛好是 true)。- digest 與已發布版本不符 → 409
task_version_digest_mismatch。 - 撤銷不改寫歷史,只作廢後續使用。
啟用同意與驗證(v5.10.0)
房間管理員的 Enable/Update 可替自己管理、且解析為 borrower 的 secret scope 記錄精確版本同意,請見同意規則;這不代表能替其他 scope 的值核准。GET /secrets?consumer_scope=chatroom&consumer_id=... 可查找遮罩 binding,回應也有 consumer_name;task scoped secret/approval 清單請見能力與管理查詢。
Create/rotate 的解密後值至少需 4 個 UTF-8 bytes,上限 64 KiB,不可含 NUL。這是位元組數:一個三位元組中文字仍太短。值過短回 422,detail[].type = sandbox_value_too_short,與加密信封錯誤不同;已存值不會被這個寫入下限改寫。
明確核准要求已發布版本、相符 digest、risk_accepted: true,以及非空、合法且正規化的 slot 名稱集合。Draft 版本回 409 task_version_not_published;digest 不符回 409 task_version_digest_mismatch。Route 不計算實際 borrower-resolved 子集,claim 仍要求核准集合與解析後的 pins 精確相符;核准不會授予超出 consumer 管理權限的值。
傳輸加密(寫入路徑)
POST /secrets 與 POST /secrets/rotate 只接受 encrypted_value 信封。明文 value 一律拒絕(422)——即使這個部署根本沒設定傳輸金鑰對也一樣。沒有明文備援,永遠沒有(owner 決策 2026-08-30)。
Client 端流程:
GET /public/info/model_key/public_key→{ public_key_pem, algorithm }(PEM,SubjectPublicKeyInfo)。與 model-catalogencrypted_api_key流程共用同一組金鑰對/端點。這裡回 503 代表伺服器沒設定金鑰對——加密寫入仍會 422,不會退回明文。- 產生隨機 AES-256 金鑰與新的 12-byte IV。用 AES-256-GCM 加密 secret 值(GCM tag 附加在 ciphertext 後面)。
- 用抓到的 PEM 以 RSA-OAEP-SHA256 包住 AES 金鑰。
- 三段都用標準字母表 base64 編碼後送出:
{
"encrypted_value": {
"encrypted_key": "<base64,RSA-OAEP-SHA256 包住的 AES-256 金鑰>",
"iv": "<base64,剛好 16 字元——12-byte 的 IV 大小正確就不需要補 padding>",
"ciphertext": "<base64 AES-256-GCM ciphertext,GCM tag 附加在後>"
}
}解密後的明文仍會檢查一般的 4 UTF-8 bytes..64 KiB 上限。四種可分辨的 422 錯誤代碼(在 detail[].type;線上的 msg 就是精確的 validation_error:<slug>——input/ctx 一律剝除、不外洩):
| Slug | 原因 |
|---|---|
sandbox_plaintext_value_rejected | 送了非空的明文 value 欄位(不論有沒有一起送 encrypted_value);空字串或 null 的 value 會被忽略,不算明文 |
sandbox_encrypted_value_required | encrypted_value 缺失或為 null |
sandbox_transit_keypair_missing | 伺服器沒設定解密金鑰對 |
sandbox_encrypted_value_undecryptable | 信封形狀合法但解密失敗(金鑰不對/ciphertext 損毀/IV 不對/明文非 UTF-8) |
解密後,secret 值會以環境變數的形式送進 runner,在 work script 開始前就匯出——不會跟執行 input 一樣寫進磁碟。
slot 命名
slot_name 必須符合 ^[A-Za-z_][A-Za-z0-9_]{0,127}$(與 ENV_NAME_PATTERN 相同),且不能是保留環境變數名(PATH、HOME、任何 TEAMSYNC_/GOOGLE_/K_SERVICE 前綴……)。保留名與 ordinary_env 鍵一樣會被拒絕。任務版本/選單回應只給 secret_slot_names(名稱清單),不給值或 provider 路徑。
沒有 binding 或 approval 的列表端點。若之後要 GET /secrets/{id} 或撤銷 approval,請自己留下建立時回的 id。
若必填 slot 沒有活著的 binding(或分享任務沒有活著的 approval),提交仍會建 run,但 runner 解不到 secret 時執行會失敗——在提交前就把選單上的 secret_slot_names 與 secret_use_risk_warning 秀出來。
共享 secret slot 政策(borrower / owner / owner_overridable)
分享 grant(POST /tasks/{id}/shares)可以帶一個逐 slot 的憑證來源政策,只能在建立時設定——要改就是撤銷再重新分享(開新的 grant generation),跟其他 grant 異動一樣:
{ "target_kind": "chatroom", "target_id": "...", "secret_slot_policies": { "SLOT_NAME": "owner_overridable" } }| 政策 | 意義 |
|---|---|
borrower | Map 裡沒列到的 slot 預設用這個。借用方自己的 binding 必須存在且已核准(見上面的 Secret approval)。這個 slot 永遠不會用 owner 的 binding。 |
owner | Owner 的 binding 借給借用方——這個 slot 不需要借用方自己的 binding 或 approval。 |
owner_overridable | 借用方有綁定就用借用方的,否則退回用 owner 的。 |
Owner 出借只有跨 scope 才觀察得到。 effective_source 只有在 consumer chain(借用房間的聊天室 → 部門 → 公司)跟任務的擁有部門不同時才會真的解析成 "owner"——也就是部門擁有的任務分享到不同部門的房間。當 consumer 剛好跟 owner 重疊時(例如公司擁有任務上的公司範圍 binding,同一個 binding 同時落在 consumer 與 owner 兩條鏈上),即使政策是 owner_overridable,解析結果也會保守地降級成 "borrower"——這種情況下一個 slot 永遠不會被歸類成 "owner"。
一次 run 在提交時會凍結它所依據的那筆 grant。之後有兩道獨立的存活檢查保護 owner 出借的 pin,防止 grant 之後失效:
- 認領時:run 第一次被認領去執行時,會重新驗證那筆 grant 是否還活著(state、active slot、generation、task id)。
- Manifest 揭露時:每次 runner 的 secret manifest 被(重新)讀取——包括重試/重放——平台都會再檢查一次 grant,外加目標的 authority 有沒有變動過。這刻意是比認領時檢查更嚴的超集,因為部門改組(或重新分享、或撤銷)可能發生在認領與揭露之間的空檔。Borrower 解析的 pin 不帶 grant 依賴,完全不用付這個成本。
選單(GET .../menu)與已授予任務(GET .../granted-jobs)會露出逐 slot 的履行狀態:
"secret_slots": [
{ "name": "SLOT_NAME", "policy": "owner_overridable", "borrower_bound": false, "owner_bound": true, "effective_source": "owner" }
]Run 詳情(GET .../runs/{run_id})會露出一份遮蔽過的快照,記錄每個 slot 實際怎麼解析、在 run 的 secret pin 於認領時發布(publish)那一刻凍結——絕不含 fingerprint、binding id 或 provider 路徑:
"secret_slot_provenance": {
"SLOT_NAME": { "resolved_via": "owner", "consumer_scope": "chatroom" }
}Run 沒有留存快照時 secret_slot_provenance 是 null;格式不對的項目會逐筆跳過,不會整包報錯。
A-012 核准在 claim 時必須符合 borrower-resolved slot 子集。POST 會檢查非空且正規化的 slot 集合、consumer 管理權、已發布版本與 digest,但不判斷哪些 slot 實際解析為 borrower;claim 時不相符仍是 secret_approval_required,不要要求另行核准 owner 出借的 slot。
逐任務的 secret 路徑
同一個 consumer(同一組 consumer_scope/consumer_id)底下,兩個不同任務用同一個 slot_name 不會再互相碰撞——底層 provider secret 路徑現在按任務分了命名空間。這之前就已經有已確認 provider secret 的 binding,會永遠留在舊的(不分命名空間的)路徑上;只有還沒確認過的 binding 才會採用新的逐任務路徑。前端兩種情況都看不到 provider 路徑,所以這件事是透明的——只有你之前在繞開碰撞問題時才會在意。
確切請求/回應欄位見 API 端點目錄。