文件 執行 沙盒

沙盒

每次聊天和應用階段都運行在 沙盒 中:一台擁有獨立檔案系統、行程和網路的臨時電腦,用完即丟,絕不碰到你的機器或其他使用者。

一句話講清它為什麼重要。 正是有了沙盒,我們才敢放心地說「是的,代理人可以隨意 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("python script.py")
sandbox.commands.run({ cmd: "node", args: ["build.js"] })
// returns: { exitCode, stdout, stderr }
  • Shell 命令以沙盒使用者身分(非 root)在 projectDir 下執行。
  • 你可以安裝套件,在 E2B 與 Daytona 上可使用 pip installnpm installapt-get;在 host 模式下則受限於你自己的權限。
  • 標準輸出與標準錯誤會串流回傳到執行記錄;命令完成時代理人就能看到。
  • 單條命令的逾時由階段的 timeout_ms 限定。

檔案

sandbox.files.write(path, contents)
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_MS5 分鐘未使用的沙盒在被暫停前會保留多久。
AGENT_RUN_TIMEOUT_MS7 天單次執行的硬上限,不受階段逾時影響。
stage.timeout_ms3 分鐘單階段的牆鐘預算。可在應用定義中覆寫。
你遇到的失敗大多是階段逾時,而不是沙盒逾時。 如果一個研究階段超過 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:

# Use the local host machine (single-tenant only)
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 命令和檔案操作都會連同參數、結束碼 以及(截斷後的)輸出一起被記錄下來:

$ bash -c "pip install pandas"
→ 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_idProvider 端的識別碼(E2B sandbox ID、Daytona workspace ID、host PID 前綴)。建立中時為空。
status可用時為 ready;當 worker 正在配置新的沙盒時為 creating
pausedProvider 因閒置自動暫停沙盒後為 true;恢復並暖起來後為 false
type所屬的 provider(e2b / daytona / local)。
lock_version樂觀鎖計數器。每次寫入都會自增;版本過期的並行寫入會被拒絕。
total_usage_seconds沙盒累計活躍的牆鐘時間,計費指標。
last_activity_atSDK 最近一次成功觸達的時間。驅動 5 分鐘閒置暫停計時器。

帶樂觀鎖的並行取得

沙盒可能被同時觸達的兩種原因:聊天回覆和已排程的應用在同一瞬間啟動;或者兩台伺服器實例同時處理同一使用者的流量。 平台透過對 lock_version 的 CAS 來序列化:

  1. 呼叫端讀取目前那一列,包括 lock_version
  2. 用剛剛讀到的版本作為 UPDATE 的條件來搶鎖,並原子地把版本號 +1。
  3. 成功 → 該呼叫端擁有沙盒;其他人在 ~100 ms 後重讀重試。
  4. 失敗 → 別人先一步搶到。重新讀取再試一次。

如果一個 worker 在取得途中崩潰,會讓 status = 'creating' 一直掛著。為了避免被無限卡死, 任何在 creating 狀態停留超過 ~25 秒的列都被視為過期:下一個取得者會重設它、推高版本號, 然後繼續。這就是過期鎖的恢復路徑,正常時悄無聲息,觸發時會在日誌裡以一行警告出現。

暫停 / 恢復

每一次對底層 provider 的 SDK 呼叫都會重設 5 分鐘閒置計時器。如果 5 分鐘內都沒有任何觸達,provider 會凍結 沙盒:列紀錄保留,paused 翻為 true,容器不再計費。下一次取得會把它喚醒,E2B 上的冷恢復大約 1 秒。為了在代理人長時間思考(LLM 輪次之間沒有 SDK 呼叫)時保持沙盒暖著, runner 每 30 秒發送一次 keepAlive ping。這就是為什麼叢集常數是這樣對齊的:

SANDBOX_KEEPALIVE_INTERVAL_MS = 30 s
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 錯誤分類)。大多數伺服端程式碼都使用一個包裝輔助函式:

provider.withSandbox(userId, userEmail, async (sb) => {
  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_URLJWT_SECRETE2B_API_KEYDAYTONA_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)?

e2bdaytona 上,代理人擁有 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 最強。