沙盒
每次聊天和應用階段都運行在 沙盒 中:一台擁有獨立檔案系統、行程和網路的臨時電腦,用完即丟,絕不碰到你的機器或其他使用者。
pip install、執行 shell 命令、寫檔案、嘗試各種會失敗的東西」,因為爆炸半徑只有一個容器, 執行結束就會銷毀。三種 Provider
Aitroop 把執行時抽象在統一的 ISandbox 介面背後,並提供三種實作。具體某次執行使用哪一種, 取決於工作區設定、方案以及任務類型。代理人的程式碼感知不到差異,但運行特性差別很大。
| Provider | 運行位置 | 適用情境 | 限制 |
|---|---|---|---|
host 本機 | 直接運行在 Aitroop 伺服器上,使用每個使用者各自的暫存目錄。 | 自架的開發安裝、冒煙測試,以及那種「啟動一台遠端虛擬機太重」的輕量階段。 | 使用者之間沒有真正的隔離,host 模式僅適用於單租戶 / 開發環境。 |
e2b 預設雲端 | 由 E2B 依需求配置的全新 Firecracker microVM。 | 絕大多數聊天和應用執行。啟動時間 <1 秒,最長存活 7 天,完整支援 claude-code 範本。 | 每個沙盒的 RAM/CPU 由你的方案決定。閒置 5 分鐘後會被暫停。 |
daytona 重型 | 託管在 Daytona 上的開發容器。 | 長時間運行的工作、大型程式碼庫,以及階段之間需要保留快取的執行。 | 冷啟動比 E2B 慢。更適合那種能把啟動成本攤平的階段。 |
代理人在沙盒裡能做什麼
沙盒介面暴露兩個面,命令和檔案:以及幾個生命週期掛鉤:
命令
sandbox.commands.run({ cmd: "node", args: ["build.js"] })
// returns: { exitCode, stdout, stderr }
- Shell 命令以沙盒使用者身分(非 root)在
projectDir下執行。 - 你可以安裝套件,在 E2B 與 Daytona 上可使用
pip install、npm install、apt-get;在 host 模式下則受限於你自己的權限。 - 標準輸出與標準錯誤會串流回傳到執行記錄;命令完成時代理人就能看到。
- 單條命令的逾時由階段的
timeout_ms限定。
檔案
sandbox.files.read(path) // utf-8 by default
sandbox.files.list(dir)
sandbox.files.stat(path)
sandbox.files.remove(path)
sandbox.files.rename(from, to)
sandbox.files.mkdir(path)
sandbox.files.copy(from, to)
檔案位於 projectDir 之下(E2B 上通常是 /home/user/project)。該目錄在同一次 執行的多條命令之間會保留,但在沙盒銷毀時一併清空。
生命週期掛鉤
sandbox.pause():主動釋放沙盒。下一次呼叫會啟動一個全新的沙盒。sandbox.keepAlive(timeoutMs):為長時間運行的階段延長閒置逾時。
你應該了解的幾個逾時
每次執行都由三個計時器掌控:
| 常數 | 預設值 | 控制的內容 |
|---|---|---|
SANDBOX_IDLE_TIMEOUT_MS | 5 分鐘 | 未使用的沙盒在被暫停前會保留多久。 |
AGENT_RUN_TIMEOUT_MS | 7 天 | 單次執行的硬上限,不受階段逾時影響。 |
stage.timeout_ms | 3 分鐘 | 單階段的牆鐘預算。可在應用定義中覆寫。 |
timeout_ms: 600000(10 分鐘)。什麼會保留,什麼不會
單次執行內
單次應用執行的所有階段共用同一個沙盒。所以:
- 階段 1 寫入的檔案,階段 2 可以讀取。
- 階段 1 透過
pip install裝的套件,階段 2 可以使用。 - 階段 1 設定的環境變數會延續到階段 2。
這就是為什麼 「階段 1 寫報告,階段 2 轉成 PDF」 這種用法天然可行,階段 2 在階段 1 放下報告的同一位置 就能找到它。
跨執行
執行之間什麼都不會保留。 即便是同一個應用,每次執行都會得到一個全新的沙盒。 如果你的應用需要在兩次執行之間記住一些東西,已處理項的清單、計數器、快取,請存到外部:
- 作為 產物,下次執行時再透過 Connect(Drive、S3、Notion)讀回來。
- 透過 webhook 寫入你自己的狀態儲存。
- 透過排程把一個應用的輸出串接成另一個應用的輸入。
跨聊天
同樣的規則:每個聊天都有自己的沙盒。兩個並排打開的聊天,檔案系統完全獨立。 一個聊天裡的代理人看不到另一個聊天裡的代理人在做什麼。
網路與 Connects
沙盒擁有完整的對外網路存取權限。代理人可以抓取 URL、呼叫公開 API、從 npm / pypi 安裝套件等等。
對於 Connects(OAuth 整合),Aitroop 會在執行開始時注入憑證,但不是把 token 寫到沙盒 檔案系統裡,而是透過內部 proxy 呼叫,從不暴露原始 token。如果代理人執行 gh repo list 或呼叫 Gmail API,請求會經過一個 proxy,由 proxy 用你的 OAuth token 簽署。token 在沙盒內部完全不可見。
script_code 是惡意的, 它也無法外洩你的 Connect token,這些 token 既不在檔案系統裡,也不在環境變數裡。最壞的情況是腳本利用 Connect 做一些對外可見的動作(寄送郵件、建立紀錄),這些動作會像任何其他操作一樣出現在你的服務商稽核日誌裡。選擇 Provider
如果你使用託管方案
你不需要選擇,由平台決定。99% 的情境下平台會選 e2b。對於特定類型的階段,例如需要跑完整 IDE 負載的階段,可能會選 daytona。
如果你是自架
在 config.yaml 中或透過環境變數設定 provider:
SANDBOX_PROVIDER=host
# Use E2B (recommended; needs E2B_API_KEY)
SANDBOX_PROVIDER=e2b
E2B_API_KEY=e2b_...
E2B_TEMPLATE=claude-code
# Use Daytona (needs DAYTONA_API_KEY)
SANDBOX_PROVIDER=daytona
DAYTONA_API_KEY=...
DAYTONA_TARGET=...
混合 provider 路由,「輕量用 host,重型用 E2B」,已在路線圖上,但還不是一等的設定項。目前每個部署 只能挑一個 provider。
UI 裡你能看到什麼
沙盒基本是隱形的,這正是設計目的,但有兩個介面讓你可以一探究竟:
執行記錄
應用執行期間,右側面板會顯示代理人的每一次工具呼叫。每一條 shell 命令和檔案操作都會連同參數、結束碼 以及(截斷後的)輸出一起被記錄下來:
→ exit 0 · 8.2s
Successfully installed pandas-2.3.1 numpy-2.1.0
$ python analyze.py
→ exit 0 · 4.1s
Wrote 1,247 rows to /home/user/project/output.csv
檔案樹(適用於程式碼型執行)
當某個階段產出 code 類型的產物時,預覽面板會顯示執行結束時沙盒內的完整專案樹。 你可以點開任何檔案查看,然後下載或分享。
生命週期:沙盒是如何取得與持有的
Aitroop 不會為每個請求都建立一個沙盒。每個使用者最多只有一個暖的沙盒,記錄在 sb_container 表的一列裡,以其內部使用者名稱為鍵。連續的聊天與應用執行會重用同一個沙盒,這正是把冷啟動成本挪出 常見路徑的關鍵。生命週期狀態全部存放在那一列:
sb_container 欄位 | 含義 |
|---|---|
username | 主鍵。使用者穩定的內部識別碼。 |
sandbox_id | Provider 端的識別碼(E2B sandbox ID、Daytona workspace ID、host PID 前綴)。建立中時為空。 |
status | 可用時為 ready;當 worker 正在配置新的沙盒時為 creating。 |
paused | Provider 因閒置自動暫停沙盒後為 true;恢復並暖起來後為 false。 |
type | 所屬的 provider(e2b / daytona / local)。 |
lock_version | 樂觀鎖計數器。每次寫入都會自增;版本過期的並行寫入會被拒絕。 |
total_usage_seconds | 沙盒累計活躍的牆鐘時間,計費指標。 |
last_activity_at | SDK 最近一次成功觸達的時間。驅動 5 分鐘閒置暫停計時器。 |
帶樂觀鎖的並行取得
沙盒可能被同時觸達的兩種原因:聊天回覆和已排程的應用在同一瞬間啟動;或者兩台伺服器實例同時處理同一使用者的流量。 平台透過對 lock_version 的 CAS 來序列化:
- 呼叫端讀取目前那一列,包括
lock_version。 - 用剛剛讀到的版本作為 UPDATE 的條件來搶鎖,並原子地把版本號 +1。
- 成功 → 該呼叫端擁有沙盒;其他人在 ~100 ms 後重讀重試。
- 失敗 → 別人先一步搶到。重新讀取再試一次。
如果一個 worker 在取得途中崩潰,會讓 status = 'creating' 一直掛著。為了避免被無限卡死, 任何在 creating 狀態停留超過 ~25 秒的列都被視為過期:下一個取得者會重設它、推高版本號, 然後繼續。這就是過期鎖的恢復路徑,正常時悄無聲息,觸發時會在日誌裡以一行警告出現。
暫停 / 恢復
每一次對底層 provider 的 SDK 呼叫都會重設 5 分鐘閒置計時器。如果 5 分鐘內都沒有任何觸達,provider 會凍結 沙盒:列紀錄保留,paused 翻為 true,容器不再計費。下一次取得會把它喚醒,E2B 上的冷恢復大約 1 秒。為了在代理人長時間思考(LLM 輪次之間沒有 SDK 呼叫)時保持沙盒暖著, runner 每 30 秒發送一次 keepAlive ping。這就是為什麼叢集常數是這樣對齊的:
SANDBOX_IDLE_TIMEOUT_MS = 5 min (12× the keepalive)
AGENT_RUN_TIMEOUT_MS = 7 days (hard ceiling on a single turn)
已死亡的沙盒與 withSandbox
沙盒可能在平台視線之外死亡,provider 把底層虛擬機刪了,host 行程被 OOM 殺掉。runner 透過把對應錯誤 歸類為 sandbox_not_found 來識別這種情況(參見 runner 錯誤分類)。大多數伺服端程式碼都使用一個包裝輔助函式:
await sb.commands.run('pip install pandas');
await sb.files.write('script.py', code);
});
如果內部函式拋錯且錯誤符合「沙盒已不存在」,wrapper 會清掉記憶體快取,呼叫 createOrResume 取得一個新的沙盒,並把函式再跑一次。只重試一次,就這樣;第二次失敗會向外拋出。其他暫時性錯誤 (網路、忙碌)會立刻向外傳播;它們不屬於 wrapper 的職責範圍。
Host 模式的密鑰剝離
Host 模式是僅供開發者使用的 provider,所謂的「沙盒」就是 Aitroop 伺服器上的一個行程。如果不做防護, 代理人運行的任何程式碼都會繼承伺服器的環境,包括資料庫連線字串、JWT 簽署金鑰和 S3 憑證。因此 host provider 會在 spawn 之前從子行程環境裡剝除一組固定的變數:DATABASE_URL、JWT_SECRET、 E2B_API_KEY、DAYTONA_API_KEY、各種 OAuth client secret、S3 access/secret key, 以及任何 Composio 端的金鑰。代理人拿到的是一個乾淨的環境;伺服器自己的密鑰得以保全。
沙盒使用量與費用
每一秒沙盒時間都會被計量。計費表(buy_balance)以美元記錄累計儲值與累計消費; 每次執行的明細出現在 設定 → 使用量。
- 沙盒時間:只在沙盒實際運行時計入。被暫停(閒置 > 5 分鐘)的沙盒不產生費用。
- LLM token:依階段個別計費,因模型而異。預設是 Sonnet-4.6。
- 儲存:S3/R2 中的產物依保留的 GB·月計費。在你方案額度內免費。
sb_container 表透過 total_usage_seconds 追蹤每個沙盒實例, sb_usage_log 則依原因記錄每段時長,代理人執行、檔案操作等等。
FAQ 與疑難排解
我的階段以「command timed out」失敗了。怎麼回事?
可能的原因:
- 階段的
timeout_ms對於實際工作量太小了(預設 3 分鐘)。把它調大。 - 命令本身卡住了,一個等不到 stdin 的行程、對無回應主機的 curl,或者在公司 proxy 後面的
npm install。 - 命令之間觸發了沙盒閒置計時器。如果一個階段在代理人兩次決策之間停留 >5 分鐘(罕見),沙盒會被暫停。 下一條命令會自動恢復但會增加延遲。
解決方式:打開失敗的執行,點 Debug in chat,查閱執行記錄, 找出哪條命令一直沒有回傳。多數情況下,把那個階段的 timeout_ms 調大就行, 例如 "timeout_ms": 600000 提供 10 分鐘預算。
為什麼我的階段 2 看不到階段 1 寫的檔案?
可能原因:階段 1 寫到了 projectDir 之外的絕對路徑(例如 /tmp/foo.csv),而階段 2 在 projectDir 之下查找。或者這個應用被拆成了兩次 執行(每次都會拿到自己的沙盒)。
解決方式:永遠寫到 projectDir(代理人的工作目錄)之下。執行開始時該路徑會 作為 $AITROOP_PROJECT_DIR 暴露給代理人。
我可以 SSH 進沙盒裡看看嗎?
不能直接連。沙盒在設計上就是封閉的。如果你需要查看執行過程中的狀況,對失敗的執行使用 Debug in chat:聊天會帶著同一份沙盒快照打開,你可以互動式地執行任何命令。
我要怎麼安裝系統套件(apt-get)?
在 e2b 和 daytona 上,代理人擁有 sudo 權限來安裝套件。 在階段目標裡告訴它:「先執行 apt-get install -y ffmpeg,然後……」。 在 host 模式下,代理人只擁有 host 行程的權限,通常沒有 sudo,所以 apt-get 無法使用。
為什麼我的 E2B 沙盒今天比昨天慢?
冷啟動會因地區與時段而有所變動。多數時候 <1 秒。如果你看到了 10 秒的冷啟動,那是代理人在等 provider 配置一台新的虛擬機。這是正常現象,會自行恢復。如果系統性地發生,請查看 設定 → Workspace 裡的狀態橫幅。
兩個階段可以並行執行嗎?
不行。階段嚴格依序執行:階段 n 必須等到階段 n−1 完成、其產物已儲存後才會啟動。 如果你需要在階段內部並行,代理人可以透過 commands.run 衍生子行程,但那是它的選擇,不是你能控制的。
沙盒銷毀後我的檔案會怎樣?
它們沒了。任何你想保留的東西都必須在執行結束之前儲存為產物(存到 S3/R2 的 app_artifact.s3_key)。產物在沙盒銷毀後仍會保留。
沙盒是真的虛擬機還是容器?
取決於 provider。E2B 使用 Firecracker microVM,完整的核心隔離。Daytona 使用開發容器。 Host 模式只是伺服器上屬於你帳號的一個行程。從功能上看代理人的程式碼在三種實作裡都一樣; 從安全性上看 E2B 最強。