任務、分享與啟用
建立與管理目錄任務走 owner scope。房間啟用走 目標聊天室管理者階梯。這不是「一律 company manager」。受眾規則只寫在一處:任務受眾。受限 API 金鑰在 tasks/shares/granted-jobs/bindings 一律 404。
步驟
- 建立任務 —
POST /tasks(SandboxTaskCreateRequest):name1–256,description≤2048。owner_scope:只有department或company。chatroom→ 422。owner_id:部門 id 或公司 id(company 必須等於company_id)。agent_enabled(預設 false)是舊旗標。Agent 能否看到任務看房間啟用開關。
- 建立草稿版本 —
POST /tasks/{task_id}/versions:environment_version_id:一個ready的環境版本(自家或策展)。startup_script/work_script(各 ≤1 MiB)。ordinary_env:名稱數與 secret slot 合計 ≤100;每個NAME=VALUE條目 ≤64 KiB,所有 ordinary 條目加總也只有 64 KiB(ordinary+secret 合計 ≤256 KiB);名稱須符合^[A-Za-z_][A-Za-z0-9_]{0,127}$,且不能是保留名(TEAMSYNC_、AWS_、PATH…)。注意:草稿建立/PATCH與發布完全不檢查這些限制——超標的版本照樣發布;它們只在 secret claim/manifest reveal 時被強制,而且只對有宣告 secret slot 的版本生效——沒宣告 slot 的超標版本會一路通過後端不被擋。input_instructions/input_example(≤64 KiB)。input_schema(≤64 KiB,可選):JSON Schema(draft 2020-12),驗證每次 run 的input。設定後,input_example必須符合它(就在這次草稿建立/更新呼叫時檢查,不符合是 422job_contract_invalid——發布不會重新驗證),而提交的 run 若input違反它,會在派工前被拒絕,回 422input_schema_violation。work_command(≤4096 UTF-8 位元組,可選):work 步驟的 driver 指令列,例如python3 /workspace/work.sh——work_script永遠落地在/workspace/work.sh,上傳檔案的原始檔名不會保留,所以指令要指向那個固定路徑,不是你上傳時的檔名(沒有work.py這種檔)。Runner 會把它原封不動寫進/workspace/driver.sh,在startup.sh之後 source 它(絕不是 argv)。空白/省略 →. /workspace/work.sh(此時 work_script 必須是 shell;填了直譯器就可以是任何語言)。含 NUL 或超長一樣是 422job_contract_invalid。見下面的腳本檔案與 runtime 契約。task_bundle_upload_id(可選):任務包——先用POST /tasks/{task_id}/bundle-uploads(multipart 欄位file,zip / tar / tar.gz,壓縮後 ≤20 MiB)上傳的封存檔。工作步驟開始前會解壓到/workspace,所以一個任務可以帶任意多個檔案(程式、資料、設定),以絕對路徑讀取。見下方任務包。timeout_seconds(預設 1800,ge=1,le≈7 天,也受公司 ceiling 限制)。secret_slot_declarations:{ "name": "SLOT" }或純字串。見 Secrets。requires_confirmation(預設 false)。自訂表格 trigger 不能對準true。output_policy(可選,有型別,以kind區分的聯集):表格 writeback{ "kind": "custom_table_writeback", "table_id": "...", "allowed_ops": "create" | "create,update" },或 Command 輸出{ "kind": "custom_table_command", "command_id": "...", "chatroom_id": "...", "input_schema": [ … ] }(v5.21.0——見任務輸出交給自訂表格 Command)。只允許部門擁有的任務。公司擁有的任務發布 → 409output_policy_owner_scope_unsupported。不是不透明 JSON。
- 改草稿(可選)—
PATCH /tasks/{task_id}/versions/{vid}(只有草稿)。跟建立同一組欄位,含input_schema/work_command。 - 發布 —
POST /tasks/{task_id}/versions/{vid}/publish→state: "published"。需要已發布版本的是啟用與執行,不是分享:分享是任務層級的(POST /tasks/{task_id}/shares不帶版本,任務連一個已發布版本都還沒有也能先分享);啟用一定釘一個 published 且 content active 的版本;執行時版本非 published/active → 409version_not_runnable(已 scrub 的版本則是 409content_scrubbed_irreversible)。 - 分享 —
POST /tasks/{task_id}/shares(target_kind+target_id,可選逐 slot 的secret_slot_policies——見 Secrets)。見 分享對象。撤銷:.../shares/{grant_id}/revoke。分享不是 Agent 啟用。 - 房間啟用 —
POST /chatrooms/{chatroom_id}/granted-jobs/{task_id}/enable(可選task_version_id;省略 → 目前已發布版)。釘版本並放進GET .../menu。當 live binding 已經釘在你要的版本(或你沒送task_version_id)時,enable是冪等的。送不同的task_version_id會重新釘版(後端 ≥ #1140):既有 binding 會像disable一樣被撤銷(舊版本上排隊中的 run 會被取消),再建立新的 binding;回應的pinned_task_version_id一定是你要求的版本。POST /bindings/{binding_id}/accept-version仍是原地升版的方式。若任務沒有分享給這個聊天室(或其部門/公司),enable 會回 409task_not_shared_to_room,訊息會寫出該呼叫哪條分享 API(#1140 之前是單純的 404)。停用:.../disable(share 還在)。
POST /bindings 是同一個釘選,只是必須帶明確版本。產品路徑請用 granted-jobs。
任務包(多檔案任務)
POST /tasks/{task_id}/bundle-uploads # multipart:file=<job.zip>(zip / tar / tar.gz,壓縮後 ≤20 MiB)
# → { id, filename, archive_format, compressed_byte_size, extracted_byte_size, canonical_byte_size, file_count, sha256, manifest_sha256, expires_at }
POST /tasks/{task_id}/versions # { environment_version_id, task_bundle_upload_id: <id>, work_script | work_command, ... }- 封存檔根目錄就是
/workspace:zip 裡的app/main.py在執行期是/workspace/app/main.py。一律用絕對路徑讀取包內檔案。 - 保留的根目錄檔名:
startup.sh、work.sh、driver.sh、input.json屬於 runner。封存檔根目錄含有其中任一個會被拒絕:422bundle_path_forbidden,訊息會寫出是哪個檔案(runner-reserved root path: work.sh (rename it; …),後端 ≥ #1140)。把進入點腳本放到子目錄(例如app/run.sh)再用work_command指過去,或把進入點寫在 inline 的work_script。 - 已發布版本帶
task_bundle摘要(sha256、manifest_sha256、file_count、各種位元組大小);上傳本身若沒有版本採用會在expires_at後過期。 - 可用的模式(2026-09-03 實機驗證):一個環境裝好各 runtime,三個任務掛在同一環境版本上,每個任務包 =
app/<程式>+data/params.json,work_script在工作階段安裝該任務的套件(pip install --target /tmp/pylib …、/tmp/app下npm install …、/tmp/gomod下go get …)再執行程式,程式讀取包內資料檔與$TEAMSYNC_INPUTS_FILE。參考實作:teamsync-backend的sandbox/e2e-js/。
腳本檔案與 runtime 契約
腳本檔案有兩條 owner-manager 路由:
-
POST /tasks/{task_id}/script-uploads(multipartfile,一個 UTF-8 文字檔 ≤1 MiB)——版本還沒建立就能用。立即驗證腳本並回一個綁在任務上、可重用的 handle(SandboxScriptUploadResponse:id、filename、byte_size、sha256、expires_at——24 小時)。把id當work_script_id或startup_script_id帶進POST /tasks/{id}/versions或PATCH .../versions/{vid};內容會複製進版本,handle 在過期前可重複使用。建版本表單讓使用者選檔案時,走的就是這條。 -
POST /tasks/{task_id}/versions/{vid}/script-file?target=startup|work(multipart,一個 UTF-8 文字檔,≤1 MiB;只限草稿)——直接把檔案內容上傳進startup_script或work_script,語意跟把該欄位PATCH成字串完全一樣。檔案有問題回 422script_too_large/script_not_utf8/script_contains_nul。已發布版本 409(不可變——跟PATCH一樣)。
另外還有一條公司裡任何非受限成員都能打的路由,草稿或已發布版本都行(沒有 owner-manager 閘、也沒有限草稿——它用的讀取權限跟 GET /tasks/{id}/versions/{vid} 一樣):
POST /tasks/{task_id}/versions/{vid}/input-preview({ "input": <任意 JSON> })——不會派工的 dry run。回傳會真正落地到容器裡的精確 canonical 位元組(input_file_content)、其 digest(input_digest)、runtime_contract(見下)、以及input_schema的驗證結果(schema_valid+schema_errors[])。用這個讓作者在發布前先測input_schema,或讓任何呼叫者(owner 或 borrower)看看自己的input實際落地會長什麼樣子。Borrower 的預覽一樣會把runtime_contract.driver遮蔽成預設 fallback——只要呼叫者不是這個任務的管理者,handler 在組出runtime_contract前就會把work_command設為 null。
每個 run 都在一份固定的 runtime 契約下執行:
invocation = ". /workspace/startup.sh && . /workspace/driver.sh"
driver = 你的 work_command,空白時是 ". /workspace/work.sh"
working_directory = runner 以 cwd=/workspace 啟動 sh,但契約不保證——一律寫絕對路徑
network = 公司設定 runtime_egress_enabled 為 true 時可對外連線(GET /settings;目前預設開啟)——pip / npm / go get 在工作階段可用;對外流量會計量(measured_egress_bytes)
inputs_file_env = "TEAMSYNC_INPUTS_FILE" # 存放 input JSON 檔案路徑的環境變數
output_file_env = "TEAMSYNC_OUTPUT_FILE" # 存放結果該寫去哪裡的環境變數
artifacts_dir_env = "TEAMSYNC_ARTIFACTS_DIR" # /workspace/artifacts——留在這裡的檔案會變成可下載的產出物提交的 input 會在腳本啟動前寫進 $TEAMSYNC_INPUTS_FILE 指的那個檔案(用任何語言讀都行,例如 json.load(open(os.environ["TEAMSYNC_INPUTS_FILE"])));宣告的 secret slot 會以環境變數匯出,不會寫進磁碟。
腳本必須知道的執行期事實(皆為實機觀察):
PATH就是映像設定的ENV PATH(會驗證:只接受絕對路徑條目);映像沒設時採慣用的/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin(runner 版本 ≥ #1145/stage image7abca452…;之前 work shell 根本沒有PATH,只靠 shell 內建的預設搜尋路徑)。裝在這些目錄之外的工具鏈仍要在work.sh裡export PATH=/usr/local/go/bin:$PATH。runtime_contract.path會寫明這件事。exit 127(python3: not found)代表直譯器的目錄不在生效的 PATH 上。- 工作 shell 的環境變數只有
PATH、你的ordinary_env、宣告的 secret slot 與三個TEAMSYNC_*檔案路徑。映像裡烘焙的其他ENV(JAVA_HOME、LANG、PYTHON_VERSION……)不會繼承——需要的請在startup.sh自行 export。地端部署自 v5.10.11 起行為相同(之前會漏出映像的ENV)。 - 託管雲端開啟對外網路的 run(預設)若使用以 v5.10.11 原始碼建置的 runner release,shell 會成為自己的 user+PID 命名空間的 PID 1;主機允許私有
/proc設定時,$$為1,ps只看得到這個任務的行程。短命的孤兒行程(例如 BusyBoxwget https://…分出的ssl_client)在巢狀設定啟用時不再讓 run 以 exit 137 被砍;shell 結束時仍在執行的行程會一併被終止。主機允許 id 對應時id -u為0,否則是溢位 uid65534且沒有任何 capability;兩種情況/workspace都仍可寫。若主機拒絕命名空間,runner 使用直接 shell fallback。地端工作使用 Docker executor 的直接 shell 路徑,不進入這個 Go gate。Runner 會在/workspace/.teamsync/下寫自己的紀錄——不要使用這個名稱。版本會沿用建置時的 runner(runner_release)。 - 只有
/workspace與/tmp可寫。快取與安裝放/tmp(HOME=/tmp、GOPATH=/tmp/go、npm_config_cache=/tmp/npm-cache)。 python -m venv需要能透過PATH找到python3(找不到時會出現 “Unable to determine path to the running Python interpreter”)。沿用映像 PATH 之後,官方python:*映像上可正常使用;python3 -m pip install --target /tmp/pylib …搭配PYTHONPATH=/tmp/pylib仍是較輕量的替代作法。- 執行之間不保留任何東西:每次執行都重新安裝。
借用者看不到真正的 work_command/driver(會遮蔽成預設的 fallback)——但看得到真正的 input_schema,因為那描述的是呼叫者自己 input 的形狀,不是 owner 的實作細節。
版本升級(binding)
- 啟用會釘住精確版本。新發布會把 binding/granted-job/選單項目的
update_available設為 true。 - 升級:
POST /bindings/{binding_id}/accept-version(task_version_id)— 從不自動升。 - 撤銷釘選:granted-jobs
disable,或POST /bindings/{id}/revoke。兩者都有副作用:該 binding 底下所有 runner 尚未 claim 的 run(狀態queued)會被立刻標成cancelled(error_code="binding_revoked"),待確認的 Agent proposal 也會被取消;已 claim 的 run(starting/running…)保留。撤銷 share(.../shares/{grant_id}/revoke)同樣會把該任務在 grant 涵蓋的每個房間裡未 claim 的 run 取消(error_code="share_revoked")。對已終態的 binding 再 revoke → 409binding_terminal。
擁有者 vs 借用者
- 你管理的任務/版本:回應含 scripts、
work_command、ordinary_env、secret_slot_declarations、output_policy,以及會回顯真正指令的runtime_contract.driver。 - 分享給你的任務:那些欄位會省略(
work_command是null、runtime_contract.driver只顯示預設 fallback)。借用者永遠看不到output_policy(裡面嵌了擁有者的 table id,或 Command 與聊天室的 id)。借用者看得到真正的input_schema——那描述的是自己input的形狀,不是 owner 的實作。
常見錯誤
- 422:
owner_scope=chatroom(enum)、欄位驗證、未知欄位。 - 422
job_contract_invalid:在草稿建立/更新時(發布不會出現這個)——input_example不是合法 JSON、不符合input_schema,或work_command含 NUL/超過 4096 UTF-8 位元組。 - 422
input_schema_violation:提交時,run 的input不符合版本的input_schema(派工前就拒絕;只有存好的 schema 本身解析不出來時才會 fail-open——放行並記警告)。 - 422
script_too_large/script_not_utf8/script_contains_nul:script-file上傳的檔案有問題。 - 403
owner_scope_forbidden:呼叫者不能擁有的 department/company scope。 - 403
output_policy_author_denied:表格 writeback——發布者對目標表沒有不受限寫入權。Command 輸出——撰寫者、發布者或提交者沒有通過身分、grant 或範圍檢查,該 Command 不再是帶effect_identity的 restricted definition-authority Command(發布時是下面的 409),或提交 run 的方式不符合任務輸出交給自訂表格 Command的規則。 - 409
output_policy_owner_scope_unsupported:公司擁有的任務帶 writeback 或 Command 輸出 policy。 - 409
output_policy_unsatisfiable:表格 writeback,在發布時,以及提交 run 且真的鑄出憑證時——表已不在、有 channel rule,或是commands_only。Command 輸出:在發布時,三項 Command 檢查(Command 存在、輸入與模式相符、restricted definition authority 且有effect_identity)任何一項失敗;只有對發布者與已記錄撰寫者的身分、grant 與範圍檢查仍是 403。 - 422
output_policy_governed_table:草稿建立/更新——表格 writeback policy 指向write_policy為commands_only的表(它的紀錄只能透過 Command 變更)。 - 422
output_policy_command_not_found/output_policy_schema_mismatch:Command 輸出,草稿建立/更新時,以及提交 run 時再次檢查——policy 指的不是你公司裡一個活著的 Command,或input_schema與該 Command 的inputs不同(或該 Command 不是寫入型)。 - 422
secret_slot_policies_invalid:分享建立時的secret_slot_policiesmap 格式不對。 - 409:
already_published/version_not_draft(重複發布)、already_archived/version_not_published(封存)、environment_version_not_ready(發布時環境版本非 ready)、published_version_immutable(PATCH或 script-file 打在非草稿版本——已發布或已封存都算)、task_not_active(在非 active 的任務上開草稿)、content_scrubbed_irreversible(對已 scrub 的內容發布/PATCH)、share_already_live(同一 task+target 已有 live grant)、binding_already_live(同一 room+task 已有 live binding——POST /bindings會撞到;enable對仍適用的 live binding 直接回傳同一個 binding,但 binding 已不適用時也可能出現)、binding_terminal(對已終態的 binding 再 accept-version/revoke)。 - 422
bundle_path_forbidden:任務包根目錄含 runner 保留檔名(startup.sh、work.sh、driver.sh、input.json)。 enable回 404 “Task not found”:任務還沒分享給該房間(或其部門/公司)——先POST /tasks/{task_id}/shares。- 404:未授權/跨公司分享對象/受限金鑰。
下一步:執行與取回結果。