# 在沙盒之外執行你的 harness（原因與方法）

> 📖 本站完整內容索引（documentation index）：[llms.txt](/llms.txt)

> 原作者：Nathan Flurry 🔩 (@NathanFlurry) · 策展與摘要：EasyVibeCoding · 平台：X (Twitter) · 熱度：🔥🔥 · 日期：2026-07-30

> 原始來源：https://x.com/nathanflurry/status/2081768022025658672

## 證據與延伸閱讀

- [自2026年初以來業界正朝沙盒外部執行Agent](https://x.com/nathanflurry/status/2081768022025658672)

## 中文摘要

# 在沙盒之外執行你的 harness（原因與方法）

自 2026 年初以來，關於該在哪裡執行 Agent 一直有一場持續的辯論：應該在沙盒內部，還是在沙盒外部？業界已經有了這兩種做法的實作，但對成熟專案而言，業界正朝著把 Agent 跑在沙盒外的方向走。

在過去幾個月裡，OpenAI、Anthropic、Vercel、Cloudflare 和 Amp 都推出了在沙盒外部執行 Agent 迴圈的平臺。

# 

# 在沙盒內部執行 Agent：設定簡單，維護困難

![三個並排虛線框的 Sandbox 架構圖，每個框內均包含帶有右向箭頭的 Agent 與 Shell + Files 區塊](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/12fcaeddf4d759a3.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面最上方有文字「Sandbox」。下方為三個被虛線外框包覆的區塊，每個區塊內部結構完全相同，左側為帶有藍色斜線填滿外框、標示為「Agent」的方塊，透過一條向右的箭頭指向右側標示為「Shell + Files」的紅色斜線填滿方塊。</div></details>

許多公司一開始會選擇在沙盒內部執行 Agent，坦白說，是因為這樣很簡單。其設定方式大抵如下：

![客戶端、API 端與 Sandbox 之間的互動時序圖](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/3fd2d2bd5a05a32c.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">這是一張黑色背景的時序圖（Sequence Diagram），展示了 User、Your API 與 Sandbox 三個實體之間的互動流程。圖中包含的文字與步驟如下：
1. User 向 Your API 發送訊息：「Create agent」。
2. Your API 向 Sandbox 發送訊息：「Create sandbox」。
3. User 向 Your API 發送訊息：「Send prompt」。
4. Your API 呼叫 Sandbox 執行：「Exec Claude CLI with prompt」。
5. Sandbox 端進行藍色虛線區塊的處理：「Process prompt &amp; run tools」。
6. Sandbox 回傳「Response」至 Your API（以虛線箭頭表示）。
7. Your API 回傳「Response」至 User（以虛線箭頭表示）。</div></details>

然而，當公司採用這種架構時，他們便開始與沙盒內部天生混亂的特性進行一場艱苦的抗爭。

# 為什麼不該在沙盒中執行 Agent

沙盒是為了執行不受信任的程式碼而建構的，而不是用來託管 Agent 本身。在內部執行 Agent 會失敗，原因有三個：

## 原因 1：影響範圍（Blast Radius）

![標有「Sandbox」字樣與圓形黑炸彈圖示的黑色背景介面](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/3277890fef5a5193.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">黑色背景畫面上方顯示手寫風格的文字「Sandbox」，下方有一虛線邊框的圓角矩形區域，框內中央置放著一顆黑色球體炸彈，帶有一條連接至亮黃色燃燒引信的導火線。</div></details>

沙盒是用來容納 Agent 所製造之混亂的地方。這是設計使然，Agent 在沙盒內能做的事情沒有結構可言，因此沙盒會因為許多大大小小的原因而崩潰。一次建構指令讓沙盒記憶體不足（OOM）、一個失控的腳本佔滿 CPU、一次搞砸的工具安裝破壞了 `$PATH`，或者檔案系統直接損壞。

當沙盒崩潰時，以下東西也會跟著陪葬：

- Agent 迴圈、重試機制與持久性（durability）：本該從失敗中恢復的持久機制，會隨著沙盒的死亡而失效。重試機制必須存在於你的後端，這樣你才能優雅地處理沙盒本身的失敗。

- 工作階段歷史記錄（Session history）：當工作階段歷史記錄儲存在沙盒內部時，會與其他所有東西一起損壞或被抹除。請把它們放在外部的真實資料庫中。

- 失敗的可觀測性（Observability）：如果沙盒崩潰，它將無法向可觀測性系統回報自身的失敗。請從你自己的後端發送檢測到的失敗。

## 原因 2：信任邊界（Trust Boundary）

![黑色背景上方標示「Sandbox」並以虛線框住一個開著的金屬掛鎖圖示](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/20018819c519a9b4.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為純黑色背景，上方正中央以白色手寫風格字體顯示「Sandbox」。下方置有一組由白色虛線構成的圓角方框，框內正中心為一個金屬質感的掛鎖圖示，其金屬環呈開啟狀態。無其他文字或程式碼內容。</div></details>

沙盒的設計理念是「不信任任何事物」的架構，因為 Agent 容易受到提示詞注入（prompt injection）以及洩漏敏感資訊的影響。如果某個東西在沙盒中可以存取，我們就該假設該資料或 API 會遭到濫用與洩漏。

在沙盒中，以下這些事情你都無法安全地做到：

- LLM 憑證與路由：沙盒內的 token 就是 Agent 可以洩漏或濫用的 token。把它們留在外面。（某些路由器具有每個租戶（tenant）範圍限定的 token，有助於減輕這個問題。）

- 權限與核准：程式碼 Agent 經過訓練懂得繞過障礙，因此在沙盒內部強制執行的權限，很可能是 Agent 有辦法繞過的權限。請在外部受信任的後端強制執行它們。

- 稽核記錄：從沙盒內部寫入的稽核記錄，是 Agent 可以操縱或竄改的。請從外部寫入。

- 多人協作安全性：對沙盒的存取就是對一切的存取，因此無法在其中為協作者提供依使用者區分的權限。請在你的後端內強制執行依使用者區分的權限。

- 受信任的工具：與私有資料庫或內部 API 通訊的工具需要憑證，而這些憑證是不能信任沙盒來保管的。請將它們作為 harness 中的工具提供，無需任何自訂的驗證代理伺服器。

- Agent 對 Agent 的通訊：沙盒到沙盒的通訊意味著每個沙盒都必須保存其兄弟沙盒的位址和憑證，這很複雜、容易出錯且不安全。改在你的後端中路由 Agent 對 Agent 的通訊，這樣就不需要複雜的驗證和路由機制。

## 原因 3：沙盒並非隨時都在執行

![標示為 Sandbox 並帶有三個藍色 Z 字與虛線框的黑底畫面](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/0ba664a9235d3d4a.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為全黑背景，上方以手寫風格字體顯示「Sandbox」。下方置中一個由白色虛線組成的圓角方形框，框內包含三個由小到大排列的藍色「Z」字圖示。</div></details>

設計上，沙盒在不使用時會進入休眠狀態。但這破壞了許多讓 Agent 強大之處。

以下是沙盒休眠時會停止運作的功能：

- 排程（「迴圈」）：處於休眠狀態的沙盒無法喚醒自己來觸發某些事情。你在沙盒外部的 harness 負有喚醒它的責任。

- 工作流程（「圖（graph）」）：一個具備持久性的多步驟工作流程會重試失敗，並且可能會休眠數小時。必須有某個東西存在於沙盒之外來恢復它。

- 快速載入工作階段：讀取工作階段不應該需要喚醒沙盒，因為啟動沙盒可能需要很長時間。將歷史記錄儲存在外部資料庫中，讀取操作就能瞬間完成。

- 索引工作階段：跨工作階段搜尋意味著讀取每一份逐字稿，你無法喚醒一整群沙盒來建立索引。只要歷史記錄放在外部，建立索引就只是一個資料庫作業。

# 正確的架構：將沙盒公開為工具

![呈現 Your Backend 與 Sandbox 架構對應關係的系統架構圖](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/0e366a3332d836a1.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一張深色背景的系統架構圖，分為左右兩個主要區塊：左側標示為「Your Backend」，內部包含三個並排的藍色區塊，每個區塊均標示為「Agent」；右側標示為「Sandbox」，內部包含三個對應的紅色區塊，每個區塊均標示為「Shell + Files」。左側的每個 Agent 透過帶箭頭的線條指向右側對應的 Shell + Files。</div></details>

Agent 與沙盒的正確架構長什麼樣子？它需要將 Agent harness 的執行地點從沙盒內部移至你自己的後端。

當 harness 需要執行腳本、讀取或寫入檔案，或在沙盒中執行任何其他操作時，它會透過一個工具來發出對沙盒的遠端呼叫。

沒有任何東西會在 harness 執行的機器上執行。相反地，所有事情都是透過對沙盒的工具呼叫來執行的。

# 搭配 Vercel AI SDK 的簡單範例

![Your Backend 與 Sandbox 系統架構圖，展示 generateText 透過 tools 呼叫 Sandbox 中的 Shell + Files](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/c81a5f25c7f44ef7.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">這是一張手繪風格的系統架構圖，背景為黑色，主要分為兩個區域：「Your Backend」與「Sandbox」。

「Your Backend」區塊包含以下元件與關聯：
1. **generateText**：藍色帶有斜線圖示的圓角方框，標有 `generateText`。
2. **LLM Provider**：位於 generateText 下方的圓角方框，標有 `LLM Provider`，generateText 有向下箭頭指向此元件。
3. **Tools**：位於 generateText 右側的圓角方框，標有 `Tools (runCommand, readFile, writeFile)`。generateText 與 Tools 之間有雙向箭頭相連。

「Sandbox」區塊包含：
- **Shell + Files**：紅色帶有斜線圖示的圓角方框，標有 `Shell + Files`。Tools 與此元件之間有向右的箭頭相連，跨越 Backend 與 Sandbox 的虛線邊界。</div></details>

為了介紹這個概念，讓我們從最簡單的版本開始：一個 generateText 迴圈，以及被公開為工具的沙盒。這裡沒有框架，也沒有編排層。它只有三個工具和一個迴圈，大約 90 行程式碼。

這段程式碼做了三件事：

步驟 1：在本機 Docker 提供者上建立沙盒：

![使用 `@computesdk/docker` 套件初始化 Docker sandbox 的 JavaScript 程式碼片段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/f4ddae0809cde5de.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段 JavaScript 程式碼，背景為深色。程式碼內容如下：
import { docker } from "@computesdk/docker";
// Boot a sandbox on the local Docker provider. Swap `docker` for any other
// provider (e2b, daytona, vercel, etc.).
const compute = docker({
  runtime: "node",
  image: { name: "node:22-slim", pullPolicy: "ifNotPresent" },
});
const sandbox = await compute.sandbox.create();
這段程式碼透過 `@computesdk/docker` 匯入 `docker` 模組，並註解說明可在本地 Docker provider 上啟動 sandbox，亦可替換為其他 provider（如 e2b、daytona、vercel 等）。接著建立 `compute` 常數，設定 `runtime` 為 `"node"`，並指定 image 名稱與 `pullPolicy`；最後透過 `await compute.sandbox.create()` 建立 sandbox 實例。</div></details>

步驟 2：定義呼叫沙盒的工具：

![使用 TypeScript 定義 sandbox 執行環境工具的程式碼片段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/944f27a453b6a5b8.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段以 TypeScript 編寫的程式碼，使用 `import { tool } from "ai";` 與 `import { z } from "zod";` 進行套件引入。
程式碼上方註解說明：
`// The agent's tools run here in your own process. The sandbox only receives`
`// the commands and file operations that the tools send to it.`
定義常數 `const tools = {` 包含了三個工具函式：
1. `runCommand`：
   - description: `"Run a shell command in the sandbox"`
   - inputSchema: `z.object({ command: z.string().describe("The shell command to run") })`
   - execute: 非同步函式，執行 `await sandbox.runCommand(command);` 並回傳 `stdout`、`stderr` 與 `exitCode`。
2. `writeFile`：
   - description: `"Write a file in the sandbox"`
   - inputSchema: `z.object({ path: z.string().describe("Absolute path of the file"), content: z.string().describe("Full contents of the file") })`
   - execute: 非同步函式，執行 `await sandbox.filesystem.writeFile(path, content);` 並回傳 `{ ok: true }`。
3. `readFile`：
   - description: `"Read a file from the sandbox"`
   - inputSchema: `z.object({ path: z.string().describe("Absolute path of the file") })`
   - execute: 非同步函式，回傳 `{ content: await sandbox.filesystem.readFile(path) }`。</div></details>

步驟 3：設定 Agent 迴圈，從終端機讀取任務：

![使用 Node.js 與 `@ai-sdk/anthropic` 套件建立 agent 主迴圈的程式碼](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/36a400e75b3906fd.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段使用 JavaScript / TypeScript 編寫的程式碼，展示如何透過 `@ai-sdk/anthropic` 與 `ai` 套件建立一個 agent 的互動迴圈。
程式碼逐行轉錄如下：
import * as readline from "node:readline/promises";
import { anthropic } from "@ai-sdk/anthropic";
import { generateText, type ModelMessage, stepCountIs } from "ai";
// Read tasks from the terminal and run the agent on each one
const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});
// The session history
const messages: ModelMessage[] = [];
while (true) {
  // Wait for the user to type a task
  const prompt = await terminal.question("agent&gt; ");
  messages.push({ role: "user", content: prompt });
  // Run the agent loop until the task is complete
  const result = await generateText({
    model: anthropic("claude-sonnet-5"),
    system:
      "You are a coding agent working inside a fresh Linux sandbox with No...",
      "Use the tools to run commands and edit files to complete the user's...",
    messages,
    tools,
    stopWhen: stepCountIs(20),
  });
  messages.push(...result.response.messages);
  console.log(result.text);
}
程式碼重點：
1. **導入套件**：引入 `node:readline/promises` 用於讀取終端機輸入，以及 `@ai-sdk/anthropic` 與 `ai` 的 `generateText`、`ModelMessage`、`stepCountIs`。
2. **初始化介面與對話紀錄**：建立 `terminal` 互動介面，並宣告 `messages` 陣列來維護 `session history`。
3. **主迴圈 (`while (true)`)**：
   - 透過 `terminal.question("agent&gt; ")` 等待使用者輸入任務，並將其放入 `messages` 中（角色為 `user`）。
   - 呼叫 `generateText` 執行 agent loop，指定模型為 `anthropic("claude-sonnet-5")`。
   - 設定 system prompt，宣告其為在 Linux sandbox 中運作的 coding agent，並賦予其使用 tools 執行指令與編輯檔案的權限。
   - 帶入 `messages` 與 `tools`，並設定停止條件 `stopWhen: stepCountIs(20)`。
   - 將 agent 回應的訊息推回 `messages` 陣列，最後用 `console.log(result.text)` 輸出結果。</div></details>

這能讓你完全掌控 Agent 的工作階段、其權限、可觀測性，以及我們上面討論過的一切。

完整的可執行範例可在 GitHub 上找到。

# 邁向生產環境：無狀態 HTTP 伺服器帶來的問題

上面的腳本在你的本機開發機器上運作得很好，但現在我們需要將其實作到生產環境中。

然而，問題來了。大多數後端都是無狀態（stateless）HTTP 伺服器，在它們之上執行 Agent 會迅速變得非常棘手：

- 競態條件（Race conditions）：對同一個 Agent 的兩次呼叫落在了不同的伺服器上，兩者同時執行迴圈並寫入歷史記錄。

- 取消提示詞：在執行中途停止提示詞，意味著必須觸及正在執行它的任何一個請求，而無狀態伺服器無法讓你做到這點。

- 多人協作：多個使用者與同一個 Agent 對話時，需要一個共用的、即時的連線地點，而無狀態端點無法做到這點。

- 持久性與容錯能力：如果伺服器在迴圈中途掛掉，Agent 也會跟著死亡，且沒有任何東西會恢復它。

你可能會考慮的另一個替代方案是針對「具備持久性的 Agent」使用工作流程引擎。但是 Agent 的主迴圈會無限期地執行下去，而工作流程引擎從來都不是為永不結束的迴圈所建構的：

- 重播（Replays）變得緩慢：工作流程引擎透過重播迄今為止的每個步驟來重建狀態，而永不結束的迴圈意味著永無止境增長的重播。（某些工作流程引擎有技巧可以限制歷史記錄，但它們並不是為此而建構的。）

- 執行中途難以升級：在即時執行期間更改工作流程程式碼是出了名的困難，而永不退出的 Agent 意味著每次部署都發生在執行中途。

- 沒有即時通訊或多人協作：工作流程無法維持 WebSocket 或向客戶端串流 token，因此你最終還是得為此建構一個獨立的層。

無狀態伺服器和工作流程引擎失敗的原因都是一樣的。Agent 是一個長壽命、有狀態的工作負載（stateful workload），而兩者都不是為此而建構的。

# Actor 模型：適用於 Agent 的有狀態架構

有狀態工作負載的問題並不是什麼新鮮事。

想像一下，為每個 Agent 永遠執行一個微小的 Node.js 行程：它在崩潰時重新啟動，在閒置時休眠，並且你可以向它發送請求。

這就是 Actor 模型：每個 Agent 一個 Actor，各自擁有自己的沙盒。這正是 WhatsApp、Discord 和《Halo》多人遊戲背後的相同架構，也是在生產環境中執行 Agent 的最佳方式。

![多個 user 與 Agent、Sandbox 互動狀態的架構圖](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/df955e72a909b66a.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">圖中分為三大直欄：左側為 `Actors`，中間為 `Agent` 相關狀態，右側為 `Sandbox`。上方標題區分別標示 `Actors` 與 `Sandbox`。
圖中包含三個橫列的情境：
1. 第一列：
   - 左側 `User A` 連線至中間帶有斜線紋理、標示 `Agent + SQLite` 的藍色方框，外圍有虛線框。
   - 中間 `Agent` 連線至右側帶有斜線紋理、標示 `Shell + Files` 的紅色方框，外圍有虛線框。
2. 第二列：
   - 左側 `User A` 與 `User C` 同時連線至中間的 `Agent + SQLite` 藍色方框。
   - 中間 `Agent` 連線至右側的 `Shell + Files` 紅色方框。
3. 第三列：
   - 左側 `User C` 獨立呈現。
   - 中間為標示 `Sleeping` 的灰色方框，外圍有虛線框。
   - 右側為標示 `Shell + Paused Files` 的灰色方框，外圍有虛線框，兩者之間有連線。</div></details>

回顧我們在先前步驟中建立的 Agent 迴圈，這非常符合 Actor 模型。我們的 Agent 需要：

- 長時間執行：Agent 迴圈能在任何情況下存活。如果 Actor 崩潰或沙盒進入損壞狀態，Actor 會重新啟動並從上次中斷的地方繼續。

- 在執行之間維持狀態：每個 Actor 都有自己的 SQLite 資料庫，因此工作階段歷史記錄就緊鄰著迴圈存放。

- 閒置時休眠：當使用者安靜下來時，Actor 就會休眠。喚醒時，它只佔用幾 MB 的 RAM。你只需為有狀態行程支付網頁請求（web-request）的價格。

- 當使用者再次提示時，或依照排程喚醒：Actor 會在收到請求、依照排程或 cron 任務時喚醒，而工作流程其實只是 Actor 的一個子集。

此外，Actor 還提供：

- 預設支援多人協作與即時通訊：多個客戶端可以與同一個 Actor 對話，並使用 WebSockets 進行即時協作。

- 水平擴展：每個 Agent 一個 Actor，分佈在你需要的任意數量機器上。並行開 Agent 是預設行為，不用另外大費周章。

本指南特別以 Rivet Actors 進行展示，但任何實作 Actor 模型的工具也都適用。

# 將 Agent 迴圈移至 Actor 中

![User、Actor 與 Sandbox 之間的互動時序圖，展示 agent 建立、sandbox 初始化、prompt 傳遞與 tool call 執行流程。](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/72010e2acd5a085d.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為黑底白線的互動時序圖（Sequence Diagram），頂端包含三個參與者：
1. `User`
2. `Actor`
3. `Sandbox`

互動流程依時間順序如下：
- `User` 發送訊息至 `Actor`：`Create agent (creates actor)`。
- `Actor` 發送訊息至 `Sandbox`：`Create sandbox`。
- `User` 發送訊息至 `Actor`：`Send prompt`。
- `Actor` 進入處理區塊（以藍色虛線外框標示，標註為 `Process prompt &amp; run tools`）：
  - `Actor` 與 `Sandbox` 之間進行兩次 `Tool call` 互動（包含發送請求與虛線回應）。
- `Actor` 發回回應至 `User`：`Response`（以向左的虛線箭頭表示）。</div></details>

以下是使用 Actor 實作此架構的簡單範例。程式碼跨越三個檔案（client.ts、actors.ts 和 server.ts），完整的可運作版本大約 180 行程式碼：

步驟 1：Actor 在喚醒時建立沙盒，並在休眠與重新啟動之間重新附加到同一個沙盒：

![使用 TypeScript 編寫的程式碼片段，展示如何透過 docker 設定執行環境並宣告 codingAgent](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/920b82b65db3a0f2.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段 TypeScript 程式碼，內容如下：
import { docker } from "@computesdk/docker";
import { actor } from "rivetkit";
// The same Docker provider setup as the simple example
const compute = docker({
  runtime: "node",
  image: { name: "node:22-slim", pullPolicy: "ifNotPresent" },
});
export const codingAgent = actor({
  // The sandbox id is persisted so the actor can reattach to the same
  // sandbox across sleeps and restarts
  state: { sandboxId: null as string | null },
  // Attach the sandbox when the actor wakes
  createVars: async (c) =&gt; {
    // Reattach to this session's sandbox, or boot a new one on first wake
    let sandbox = c.state.sandboxId
      ? await compute.sandbox.getById(c.state.sandboxId)
      : null;
    if (!sandbox) {
      sandbox = await compute.sandbox.create();
      c.state.sandboxId = sandbox.sandboxId;
    }
    return { sandbox, tools: createTools(sandbox) };
  },
  // ...the rest of the actor comes together in step 5
});
這段程式碼主要展示了：
1. 從 `@computesdk/docker` 引入 `docker`，並從 `rivetkit` 引入 `actor`。
2. 透過 `docker()` 建立執行環境設定 `compute`，指定 runtime 為 `node`，以及 `node:22-slim` 映像檔。
3. 導出 `codingAgent` actor，內含 `state` 用於保存 `sandboxId`，並透過 `createVars` 函數在 actor 喚醒時重新連線至現有的 sandbox，若不存在則建立新的 sandbox，最後回傳 `sandbox` 與 `tools`。</div></details>

步驟 2：設定與沙盒通訊的工具：

![宣告 agent 執行 sandbox 指令與工具的 TypeScript 程式碼區段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/0738eb39e9f84aae.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段深色主題的 TypeScript 程式碼，內容如下：
import { tool } from "ai";
import { z } from "zod";
// The agent's tools run in the actor. The sandbox only receives the
// commands and file operations that the tools send to it.
function createTools(sandbox: Sandbox) {
  return {
    runCommand: tool({
      description: "Run a shell command in the sandbox",
      inputSchema: z.object({ command: z.string() }),
      execute: async ({ command }) =&gt; await sandbox.runCommand(command),
    }),
    // ... same writeFile and readFile tools as the simple example above
  };
}
程式碼重點解析：
1. 引入了 `"ai"` 套件的 `tool` 函式以及 `"zod"` 套件的 `z`。
2. 註解說明 agent 的工具是在 actor 中執行，sandbox 只接收工具傳送過來的指令與檔案操作。
3. 定義 `createTools` 函式，接收 `sandbox: Sandbox` 參數，並回傳一個包含 `runCommand` 的物件。
4. `runCommand` 工具透過 `zod` 定義輸入結構（`inputSchema`）接收 `command` 字串，並在 `execute` 非同步函式中呼叫 `sandbox.runCommand(command)` 執行指令。</div></details>

步驟 3：從具備持久性的佇列中處理提示詞，工作階段歷史記錄存放在 Actor 的 SQLite 資料庫中：

![使用 Anthropic SDK 與資料庫建構 codingAgent 的 JavaScript 程式碼片段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/e59525e4de2e585d.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段 JavaScript 程式碼，展示如何使用 `actor` 從 `"rivetkit"` 建立名為 `codingAgent` 的物件。
程式碼重點包含：
- 引入 `@ai-sdk/anthropic` 的 `anthropic`、`ai` 的 `generateText` 與 `stepCountIs`、以及 `"rivetkit"` 與 `"rivetkit/db"`。
- 設定 SQLite 資料庫，利用 `onMigrate` 執行 `db.execute` 建立 `messages` 資料表。
- 設定 `queues` 接收 `prompt` 任務。
- 在 `run` 函式中非同步處理佇列訊息，包含從 SQLite 載入 session history、執行 agent loop（呼叫 `generateText` 並指定 model 為 `anthropic("claude-sonnet-5")`、tools、`stopWhen: stepCountIs(20)`）、將訊息寫回資料庫，以及透過 `message.complete` 回覆結果。</div></details>

步驟 4：註冊 Actor 並啟動伺服器：

![引入 rivetkit 與 codingAgent 並啟動 registry 的程式碼片段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/b0ea9ce6d31a0d95.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為深色背景的程式碼編輯器介面，內容如下：
import { setup } from "rivetkit";
import { codingAgent } from "./actors";
export const registry = setup({ use: { codingAgent } });
registry.start();
這段程式碼從 `rivetkit` 匯入 `setup` 函數，從 `./actors` 匯入 `codingAgent`，透過 `setup` 進行初始化並指定使用的 agent，最後呼叫 `registry.start()` 來啟動該註冊表。</div></details>

步驟 5：使用者建立一個 Agent，這會建立一個 Actor。每個鍵值都是一個獨立的 Agent，擁有自己的 Actor、沙盒和歷史記錄：

![使用 rivetkit/client 與 TypeScript 型別建立 agent 客戶端的程式碼片段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/bb7643aa0b0b6dda.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段 TypeScript 程式碼片段，深色背景，程式碼內容如下：
import { createClient } from "rivetkit/client";
import type { registry } from "./actors";
const client = createClient&lt;typeof registry&gt;();
// Each key is an independent agent with its own actor, sandbox, and history
const agent = client.codingAgent.getOrCreate(["demo"]);
程式碼透過 `rivetkit/client` 匯入 `createClient`，並從 `./actors` 匯入型別 `registry`，接著初始化 `client`，並透過註解說明每個鍵值都是具備各自 actor、sandbox 與 history 的獨立 agent，最後使用 `client.codingAgent.getOrCreate(["demo"])` 取得或建立名為 `"demo"` 的 agent 執行個體。</div></details>

步驟 6：使用者發送一個提示詞，排隊等待 Actor 處理：

![顯示以 Node.js 讀取終端輸入並將 prompt 傳送給 agent 的 JavaScript 程式碼片段](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/13c9f333f496324d.jpg)

<details class="chart-data"><summary>展開畫面重點</summary><div class="me-note">畫面為一段 JavaScript 程式碼，背景為深色。程式碼內容如下：
import * as readline from "node:readline/promises";
// Read tasks from the terminal and run the agent on each one
const terminal = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
});
while (true) {
    // Queue the task on the actor and wait for the agent to complete it
    const prompt = await terminal.question("agent&gt; ");
    const result = await agent.send("prompt", { content: prompt }, { wait: true });
    if (!result.response) throw new Error(`Task ${result.status}`);
    console.log(result.response.text);
}
程式碼解析：
1. 載入 `node:readline/promises` 模組，透過 `process.stdin` 與 `process.stdout` 建立終端介面。
2. 進入無限迴圈 (`while (true)`)，使用 `terminal.question("agent&gt; ")` 讀取終端機輸入的 prompt。
3. 將取得的 prompt 透過 `agent.send` 送出，並設定 `{ wait: true }` 等待 agent 完成任務。
4. 檢查回傳的 `result.response`，若無回應則拋出錯誤；若有回應則將結果文字印出。</div></details>

完整的可執行範例可在 GitHub 上找到。

# 替你在沙盒外部執行 Harness 的框架

儘管在整個業界中溝通得並不充分，但這種架構已經得到了充分的證明。有很多選項可以替你在沙盒外部執行 harness：

- Rivet Actors（自備 harness）：Rivet Actors 是將你的 harness 與沙盒結合最具彈性的選項，因為它是一個通用的基礎元件（primitive），且為開源且可自架。請參閱文件與 GitHub。

- agentOS（現有 harnesses）：agentOS 建立在 Rivet Actors 之上，它為主流 harnesses（如 Claude Code、Codex、OpenCode 和 Pi）提供對此架構的支援。它還允許你提供自己的自訂 Agent，並支援使用沙盒掛載（sandbox mounting）來與沙盒通訊。請參閱文件與 GitHub。

- Vercel 的 Eve（自訂 harness）：Eve 使用 Vercel 原生的基礎元件提供「Agent 在沙盒外部」的架構。

- Cloudflare 的 Flue（修改版的 Pi）：Flue 使用 Cloudflare 原生的基礎元件提供相同的架構。

- Amp Orbs：Amp Orbs 透過 Amp CLI 自動化執行具備持久性對話串（thread）、自我排程以及 Agent 對 Agent 訊息傳遞的 Agent。

- OpenAI 的 Agents SDK（自備沙盒）：Sandbox Agents 將 Agent 迴圈保留在你自己的 harness 中，並將執行委派給可插拔的沙盒客戶端，託管提供者包含 Cloudflare、Daytona、E2B、Modal 和 Vercel。

- Anthropic 的 Managed Agents（託管迴圈）：Managed Agents 將 Agent 迴圈保留在 Anthropic 的基礎設施上，同時工具則在你設定的沙盒中執行（推出時支援 Cloudflare、Daytona、Modal 和 Vercel，或自備沙盒）。它驗證了相同的拆分架構，但迴圈是在 Anthropic 的伺服器上執行，而不是你的後端。

# 常見問答

- 在沙盒外部執行 harness 會不安全嗎？不會。這些工具實際上是對沙盒的 API 呼叫。沒有任何東西能讓 Agent 在執行 harness 的機器上執行程式碼或觸碰檔案。

- 我該如何像這樣執行 Claude Code、Codex 或 OpenCode？它們原生不支援此架構，但 agentOS 提供了一個執行環境（runtime），能讓這些 harnesses 在沙盒外部運作。Pi 則直接支援：你可以將其程式碼編寫工具替換為綁定到你沙盒的工具，就像上面提到的 Vercel AI SDK 範例一樣。

- 這難道不比單純使用沙盒更昂貴嗎？不會。一個 Actor 只佔用幾 MB 的 RAM，而且很多時候它是唯一在運作的東西，不需要啟動完整的沙盒。例如，讀取或分享對話串（thread）不需要啟動整個沙盒。

- 在沙盒外部與內部執行 Agent 的延遲差異為何？微不足道。在同一個資料中心內，每個工具呼叫大約會多出 10 毫秒的額外延遲，相比之下，從瀏覽器發出的典型 API 請求則需要 50 到 200 毫秒。

# 開始使用

我們一直在打造 Rivet Actors，作為一個具彈性、開源且可自架的 Agent 基礎元件。如果你有任何問題，歡迎加入我們的 Discord、留言回覆或直接發私訊給我！

- 文件：rivet.dev/docs/actors

- GitHub：github.com/rivet-dev/rivet

- Discord：rivet.dev/discord

## 標籤

Agent, Deployment, Harness, 產業趨勢, OpenAI, Anthropic, Vercel, Cloudflare
