sandbox_start
為一個 goal 開新箱子:宣告 repo 對外的服務(各有自己的名字),也可以選環境、秘密、大小與工具鏈。回箱子 id、它的網址與收到的服務。
| 輸入 | 輸出 |
|---|---|
name、goal、services[{name, port, targetPort, web, fromBox, fromPort, version, containerPort, env}]、externalBaseUrl、environment、secrets(名稱清單)、size、toolchains、orientation、idleTimeoutMin、restore、waitForCapacitySec(goal 必填,其他選填) |
id、arch、sceneUrl、webUrl、takeoverUrl、status、orientation、orientationNote(只在退成橫向時有)、startedFrom(snapshot)、size、vcpus、memoryGb、收到的 services(web 服務各附 url)、收到的 secrets、toolchains 與 idleTimeoutMin、toolchainsNote(只在 toolchains 寫了本來就有的語言時有)、environment、帳號設了 IP 白名單時的 boxUrlAccess、next |
name:這個箱子要做什麼,用人看得懂的語言寫("會員中心退款流程")。app 用它當箱子的標題;沒給的話人只看得到箱子 id。goal(必填):這個箱子為什麼存在、做到怎樣算完成,用人讀的語言寫一兩句("讓會員中心的退款按鈕把退款記進帳本;在瀏覽器裡從頭到尾退款成功就算完成")。app 把它顯示在箱子的卡片與頁面上,sandbox_status與sandbox_list也會回它,這個對話被關掉時,別的對話才接得下去(接手別的對話留下的箱子)。最多 500 個字元,超過的截到 500。沒給或只有空白時會失敗並回goal is required: one or two sentences, in the language the person reads, on why this box exists and what done looks like. The person sees it on the box's card in their app, and whoever picks the box up if this conversation is closed reads it in sandbox_status. Call sandbox_start again with goal set.走 REST(POST /v1/boxes)時goal可省略。orientation:AI 畫面與交件截圖使用portrait(390 × 844)或landscape(1280 × 800)。沒給時沿用帳號偏好,預設直向。設定屬於這個箱子;之後改帳號偏好不會改變已開箱子的畫面(跑著的箱子用 REST 的PUT /v1/boxes/{id}/orientation切換)。要在螢幕上看桌面版面就要landscape;Chromium 視窗最窄 500 像素,在直向螢幕上剛好鋪滿的參數見sandbox_exec。要直向(或帳號偏好是直向)、但箱子的映像轉不了螢幕時,箱子改以橫向開起來,結果多一個orientationNote說明:頁面在 1280 × 800 的視窗裡畫,要手機大小的螢幕得等映像更新後開新的箱子。restore:閒置規則收掉、凍結副本還留著的箱子 id,副本在箱子停掉後留 24 小時(sandbox_status的restorableUntil,或sandbox_list帶all: true)。它把同一個箱子照凍結那一刻拿回來:/work、跑著的程式、id 與網址都一樣,不開新箱子,next也會這樣說。goal照常給;其他設定沿用箱子原本的,給了也不管。接著的手機不會留,要重新 attach。只有閒置規則收掉的箱子留凍結副本:用sandbox_stop停掉的拿不回來(回 409,並說怎麼重開)。拿回來跟開新箱子一樣要方案的箱子上限內有空位、也要有點數;那一刻沒有主機有空位的話,箱子以凍結狀態回來,下一個會動到它的呼叫再叫醒。size:1(預設)、2、4 或 8 個單位(每單位 2 vCPU、8 GB 記憶體、40 GB 磁碟),整個箱子的生命週期固定,扣點照比例。每個箱子都是 x86_64(amd64)的 Debian 12,結果以arch: "amd64"註明;arm64 的 image 見sandbox_exec。1 適合網頁前端與大多數 Node、Python、Go 的工作;Android、Gradle 或吃重的 docker compose(例如在一個箱子裡把好幾個服務 build 成 image)用 2 以上。toolchains:java-17、java-21、android、dotnet、rust、flutter(網頁版與 Android,會一併啟用android)、esp32(ESP32 的 PlatformIO,Arduino 與 ESP-IDF,只能編譯)、python-3.12或python-3.13(那一版 Python 排在 PATH 最前面,當python、python3與pip;不選的話python3是 Debian 的 3.11,裝好了 Pillow、numpy 與 boto3),每個命令都會啟用。Go、Node、Python 3.11、PHP 與 Composer、MySQL、Postgres 與 S3 測試服務(psbx-testdb)、psql、redis-cli、mysql、ImageMagick、PowerShell 7、Azure 與 GitHub CLI、clang、cmake 不用選,一直都在;在toolchains寫了其中的語言(go、node、python)會被略過,結果的toolchainsNote會說明。services:你的 repo 在箱子上開出來的服務,每個有name、呼叫端連的port,以及選填的targetPort與web。- 箱子一開起來,每個名字在箱子裡就有自己的位址(取自
198.18.0.0/15),箱子的 DNS 用這個位址回答這個名字,程式與容器都一樣;不需要接線。name:port連到你的程式聽的targetPort,沒給就跟port一樣:呼叫端照樣連name:port,程式聽的是targetPort。預設 bridge、自訂的 Docker 網路與docker compose裡的容器都用同樣的方式連到這些名字:箱子的規則同時套在箱子自己的程式與每個容器網路上,箱子裡 Docker 的 DNS 也轉給箱子的解析器。 - 程式要聽
0.0.0.0:只聽127.0.0.1的程式從它的網址(見下面的sceneUrl)連得到,用名字連不到。容器要把targetPort發布出來(compose 的ports:、docker run的-p <targetPort>:<容器裡的 port>)。 - 共用同一個
targetPort的名字是同一個程式,在sandbox_wire裡一起切。每個名字都有自己的位址,所以port相同、targetPort不同的名字是不同的服務:api.acme.internal:8080與billing.acme.internal:8080可以分別連到聽 8080 與 8081 的兩個程式。 - 箱子裡的 port 80 與 9095 屬於 boxd(80 是場景入口,9095 是它的 API),任何程式都聽不了。呼叫端用 port 80 連的服務要宣告
targetPort:{ "name": "api.acme.internal", "port": 80, "targetPort": 8080 }時,呼叫端連api.acme.internal:80(或http://api.acme.internal/),程式聽 8080。沒給targetPort會回 400:port 80 in the box is boxd's scene entry; add targetPort, the port your process listens on — callers keep dialing <name>:80。port 9095 也一樣,targetPort本身也不能是 80 或 9095。其他 port 都可以用,443 等特權 port 也行(命令以 root 執行)。只有箱子裡的服務在 port 80 要targetPort:列在 port 80 的環境位址(內部 load balancer)與宣告在 port 80 的 link({ "name": "api.test.internal", "port": 80, "fromBox": "<A>", "fromPort": 8080 })照樣能用,因為每個名字在箱子裡都有自己的位址,碰不到箱子自己的 port 80。沒宣告、也沒列在 80 的名字(宣告在別的 port 的服務或 link,或只列在別的 port 的環境主機)用 port 80 連,會拿到 502nothing listens on <name>:80 in this box: that name is declared on another port. Dial the port it is declared on, or declare it on port 80 with targetPort, the port your process listens on,箱子裡的程式與容器都一樣;localhost:80仍是 boxd 的場景入口,跟sceneUrl一樣連到第一個宣告的服務。2026-09-24 20:30 UTC 之前開的箱子,這種名字用 80 連還是會連到第一個宣告的服務。其他沒人宣告的 port 則是被拒絕,或連到箱子裡聽那個 port 的程式;名字的 port 9095 連到的是 boxd 的 API,會回 404。 - 名字是主機名(小寫英數、
-與.),或10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、100.64.0.0/10之內的私有 IPv4 位址或範圍(例如172.16.0.0/16):箱子裡(含容器)往這個範圍的那個 port 的連線,都會轉到箱子裡這個服務的targetPort(external模式時轉到通往externalBaseUrl的轉發器)。範圍是給「不用名字、從自己的註冊表拿 IP 直接連」的呼叫端用的,沒有自己的位址,只能在這裡宣告。 web: true標出人可以用瀏覽器打開、直接用產品的每個服務;API、資料庫與只會顯示 JSON 的東西不要標。每個 web 服務都有自己的網址,app 的「使用」按鈕打開其中第一個(見下面的webUrl)。fromBox(加上選填的fromPort)讓這一項變成連到你另一個箱子的 link,而不是這個箱子裡的服務:這個箱子裡的name:port連到箱子fromBox的fromPort。見連到別的箱子。version(加上選填的containerPort)讓這個名字跑一個已發布的版本,而不是你自己的程式:箱子會拉 image、在背景把它跑起來。見跑發布好的版本。
- 箱子一開起來,每個名字在箱子裡就有自己的位址(取自
externalBaseUrl:沒改的 HTTP 服務住在哪裡,http(s)://host[/path],不能帶 query 或帳密,例如你的 staging 閘道。箱子有它時,宣告的每個服務一開始都是external模式:程式與容器連name:port的每個 HTTP 請求都轉到externalBaseUrl,路徑保留、Host 換掉;宣告在 port 80 的服務也一樣。直接跑在箱子上的程式連localhost:<port>與127.0.0.1:<port>也會到轉發器,但 80 與 9095 除外(boxd 自己的;localhost:80是場景入口),box模式服務的程式正在聽的 port 也除外。箱子自己的 IP 與172.17.0.1的那個 port 到不了轉發器:請用名字連。boxd 不佔服務的 port,所以你的程式隨時都能在它的targetPort起來,再用sandbox_wire把名字切過去。選填。environment:你的環境(sandbox_environments列得出來)。箱子連得到它的內網位址(資料庫、Redis、內部服務,經你內網裡的連接器),拿到每個服務的設定檔/work/.sbx/env/<服務>.env與.sh,沒給externalBaseUrl就用環境的,一樣以external開始。你在services宣告的主機名若也是環境的位址(不論 port),就改指到箱子、不列在reachable;落在宣告範圍內、同 port 的環境 IP 位址也一樣。環境的每個主機名在箱子裡也各有自己的位址,所以 port 80 的位址(例如內部 load balancer)跟其他位址一樣能用,你的程式也可以聽跟環境位址同一個 port 號。回傳的environment每個服務列keysCount與notable(最該先看的連線、模式、權限設定;完整的keys在sandbox_environments與 env 檔),每條連線的rttMs,以及activeBoxes:同一個環境上你其他還活著的箱子。沒帶environment、帳號卻有環境時,next會列出它們,因為箱子開了之後就接不上環境。見環境。secrets:從sandbox_secrets的名稱裡挑要注入的,認領箱子時變成環境變數。沒列的不注入;之後可以用sandbox_secrets再加。idleTimeoutMin:多少分鐘沒有使用(會動到箱子的工具呼叫或喚醒,見箱子狀態)就把箱子永久停掉(/work跟著消失),凍結中的也一樣;給了它就取代預設的期限。讓箱子保持醒著的東西(用background: true起的命令,最多到最後一次使用後 1 小時;有人接手;別的箱子經 link 連著)會把停掉的時間往後延,直到它結束。這個時間從最後一次使用開始算:有人在用箱子的網址不會把它歸零。他們的請求最多讓箱子在最後一次使用後醒著 2 小時;之後箱子凍結,因為已經超過期限,會馬上永久停掉。載入頁面把凍結的箱子喚醒算一次使用,會重新開始算。0 或不給:凍結的箱子在最後一次使用後 24 小時停掉。sandbox_status與sandbox_list回stopsAt,是這些規則在沒有人再用它的情況下永久停掉它的時間;離那時 30 分鐘內,這個箱子的每個工具結果後面都會附一段預警(箱子狀態)。idleTimeoutMin最多設 10080 分鐘(7 天);超過會被限制為 10080,並在回傳值附上說明。凍結的箱子在這段期間仍算在帳號的同時箱數裡。不論哪一種,箱子閒置 10 分鐘都會凍結(見箱子狀態)。一直有人在用的箱子沒有最長存活時間。startedFrom:snapshot:箱子在 microVM 主機上從快照恢復,幾秒就好。每個箱子都跑在 microVM 上(sandbox_status的placement: "microvm"),閒置會凍結。sceneUrl:https://<id>-<key>.box.parallelsandbox.com。它打開第一個宣告的服務,不論有沒有標web:box模式時是箱子裡的127.0.0.1:<targetPort>,external模式時是externalBaseUrl;沒宣告任何服務時回 503。箱子停掉之前都有效,凍結不會改變它。services[].url:每個宣告了web: true的服務都有自己的網址,用同樣的方式打開那個服務,箱子停掉之前都有效。第一個宣告的服務是web時,它的url就是sceneUrl本身,https://<id>-<key>.box.parallelsandbox.com;其他每個 web 服務是https://<id>-<key>-<targetPort>.box.parallelsandbox.com。沒標web的服務沒有url。sandbox_status與 REST 的箱子清單(GET /v1/boxes)回同樣的網址。boxd 比這個功能舊的箱子(在它上線前開的)一個都不回,只有sceneUrl能用。webUrl:第一個標了web的服務的url,沒有就不回;箱子停掉之前都有效。所以第一個宣告的服務是web時它就是sceneUrl,不是的話是第一個web服務的-<targetPort>網址。app 的「使用」按鈕打開的是它,所以跟services的順序無關。sandbox_review的卡片打開的是那次呼叫用open指定的入口,不是webUrl。從箱子外面只連得到標了
web的服務,加上經sceneUrl的第一個宣告的服務。-<port>的 port 屬於沒標web的服務時回 404,跟那個 port 沒有服務時一樣,所以換著 port 試也找不出沒開放的服務。這些網址帶著隨機的 key,只知道箱子 id 組不出來:一律照
sandbox_start或sandbox_status回的原樣使用。key 不對的網址跟不存在的箱子一樣回 404。這些網址前面沒有任何登入:網址裡的 key 是唯一的保護。拿到網址的人就能用那個服務,也能透過它用到它連得到的一切:接了環境的箱子裡的前端,會替打開它的人呼叫你 dev 的資料庫與內部服務。只分享給該有這些權限的人。聊天 app 會去抓貼進對話的網址來做預覽:預覽服務拿得到頁面,這次抓取算箱子的使用,要 HTML 的抓取也會像載入頁面一樣喚醒凍結的箱子。箱子停掉後網址就失效(503
box is terminated),凍結期間回 503。每個箱子的網址都有自己的 host,沒辦法事先登記在身分提供者(IdP)那裡。要在箱子上登入(SSO、OAuth),就讓箱子裡的那份用團隊在本機用的登入方式,或在箱子跑著的期間把它的確切網址加進 dev client 允許的 redirect URI。絕對不要登記
https://*.box.parallelsandbox.com/*這種萬用字元:每個帳號的箱子都在這個網域底下。見在箱子網址上登入。經這些網址進來的請求到程式時是
Host: 127.0.0.1:<targetPort>,公開的 host 在X-Forwarded-Host,scheme(https)在X-Forwarded-Proto,用戶端在X-Forwarded-For。相對路徑的轉址照常可用;用Host組絕對網址或 cookie 網域的程式要改成信任 forwarded 標頭(Express:app.set('trust proxy', true))。經這些網址打開的頁面跑在人的瀏覽器裡,在箱子外面。頁面的程式呼叫別的 origin 上的 API 時,只有那個 API 的服務標了
web(頁面還得拿到那個服務的url)、API 也對頁面的 origin 回應 CORS,請求才會到箱子裡的 API。比較簡單的做法是在前端的 dev server 設同源 proxy,轉給 API 在箱子裡的名字,例如 Vite 的server: { proxy: { '/api': 'http://api.acme.internal:8080' } }:瀏覽器呼叫頁面自己 origin 上的/api/...,跑在箱子裡的 dev server 再轉給api.acme.internal:8080,/api前綴照帶。前綴要留要拿掉,照 dev 的做法(團隊設定)。next:一句下一步該做什麼的提示。
同時箱數:Free 3、Pro 5、Max 20。凍結的箱子也算在內,而且這是整個帳號的上限,所有連線共用。到了上限,sandbox_start 會失敗並回 429 plan <plan> allows <n> boxes at once and <m> are running or frozen; frozen boxes still hold a slot, so list them with sandbox_list (GET /v1/boxes) and sandbox_stop one you no longer need.,後面接著講其中幾個是凍結的、沒人再用的話閒置規則最先停掉哪一個、在什麼時候。sandbox_list 看得到是哪些箱子佔著名額。
沒有任何主機放得下新箱子時,sandbox_start 會失敗並回 503 no box capacity right now: every host is full and more is being started. Try sandbox_start again in about 5 minutes; a box of size 4 or 8 can take a few minutes longer. No box was created by this call. 另一台主機會立刻開起來;除非你本來就想要,不必把 size 調小。ParallelSandbox 剛換過箱子映像後的約 3 分鐘內,訊息改說 the hosts are switching to a new box image, which takes about 3 minutes。
waitForCapacitySec(0 到 1800):放不下時在這次呼叫裡等,不馬上失敗。主機都滿、主機在換映像、或方案的同時箱數滿了,sandbox_start每 15 秒再試一次並送進度(No room for the box yet: … Waited 45s of 5m0s; trying again in 15s.),一開出來就回那個箱子;在那之前沒有任何箱子。最多等多久還受呼叫端宣告的上限限制:stdio adapter 照它所在 host 的工具逾時宣告(PSBX_TOOL_TIMEOUT_SEC,預設 60 秒,可以等 25 秒;怎麼調高見sandbox_review),沒宣告的連線最多等 25 秒。等完還是沒有,錯誤會說等了多久、為什麼停。- 呼叫的結果沒回到你手上(連線斷了,或 client 取消了這次呼叫、畫面寫「被中斷」)時,stdio adapter 會去找箱子:結果沒回來的,回應直接說這個對話剛用這個
name與goal開了哪一個箱子,或是沒開成。被中斷之後,下一次用同樣name與goal呼叫sandbox_start,回的是被中斷那次開出來的箱子,不會再開第二個(真的要再開一個就再呼叫一次);其他工具的結果後面也會附一句講那個箱子。
所有工具與主題都列在工具參考。