今年 RubyKaigi 2026 時,剛好在跟朋友聊 Harness Engineering 提到 Sandbox 的部分,覺得 Ruby 語言目前還沒有比較完善的解決方案,剛好第二天晚上的 Code Party 被分配到 druby(Ruby 語言內建的分散式系統)組別,讓我有了靈感,讓 Kobako 被設計出來。
沙盒選擇:為什麼是 WebAssembly
要打造 AI Agent 可以運行的環境有很多路線,像是虛擬機器(Virtual Machine)、容器技術、WebAssembly 都可以實現。
目前有哪些路線,可以讀 Agent Sandbox 沙箱架構: 論兩種設計模式與七種隔離方式這篇文章的整理
選擇哪一種技術,就會影響到後續技術發展的路線,以及可以做的事情,在搜集現有的解決方案跟技術後,最終只能走自己開發。
舉例來說,Shopify 有 mruby-engine 的技術,這是相當理想的選項之一,但是這個做法只能保證 Host(宿主)跟客體(Guest)不是直接互通的,如果運行的 Ruby 腳本針對記憶體或是一些攻擊,都很容易打穿,給 Agent 使用強度還是稍嫌不足。
如果選虛擬機器或者容器技術,最大的困難是對環境的要求相對嚴苛,這種嚴苛對小專案或者開發都會有不少限制,所以最後 Kobako 選擇 WebAssembly 的路線,有類似虛擬機器的 WASI 標準,跟直接跑 mruby 來說,犧牲一些啟動、運行速度,換到更好的隔離性還算是划算。
後來 Shopify 也有投資元在 wasmtime 讓 ruby.wasm 可以替代原本 Shopify 的腳本功能,也因此 ruby.wasm 也是我考量的選項之一,但最終因為 ruby.wasm 本身的限制,讓我走向不同的路線。
WASI 的限制與 Kobako 的取捨
WASI 全名是 WebAssembly System Interface,簡單來看,是一種讓我們在 WebAssembly 環境中獲得類似 UNIX 介面的架構,像是 ruby.wasm 就是使用 Preview 1 版本的標準,並且正在測試 Preview 2 的標準。
然而,這個介面設計的非常的差,以 Preview 1 來說,除了有 Stdin / Stdout / Stderr 等介面,以及類似 VFS(Virtual File System)的機制外,基本上沒什麼能做的事情,也不支援 I/O 之類的行為,這也造成 Kobako 在評估是否要用 ruby.wasm 當基底時,只能放棄這條路線。
即使 ruby.wasm 能用,也不一定是好的選項,因為本身需要載入 20 MB 左右的 Ruby 環境,加上啟動時間可能還比容器技術慢
目前 WASI 有 Preview 1 ~ Preview 3 個不同版本,後面要建置跟編譯也有不同類型的麻煩,最後 Kobako 選比較穩定和普及的 Preview 1 標準來實作,跟 ruby.wasm 的差異在於額外的 ABI(Application Binary Interface)來實現一個 In-Memory RPC 的溝通,讓 Sandbox 中的 Ruby 腳本能夠呼叫 Host 提供的方法。
這也是沒有選擇 ruby.wasm 的主要原因,如果想增加 ABI 就必須完整編譯 ruby.wasm 背後的開發成本跟困難度會極大上升,但是 mruby 設計給嵌入式系統的特性反而就很適合。
在今年 RubyKaigi 也有 Uzumibi: Reinventing mruby for the Edges 這樣的主題,剛好呼應 Kobako 想要實現 Cloudflare 針對 MCP 問題所提出的 Code Mode 想法,同樣是從 Edge 環境運行為出發點,mruby 的規模也剛很好適合。
RPC:讓 Sandbox 與 Host 溝通
Kobako 受到 druby 啟發的地方,就是 druby 巧妙的運用 Ruby 語言特性來實現 RPC 機制,以下是 Kobako 實際使用的範例。
1User = Data.define(:name)
2
3sandbox = Kobako::Sandbox.new
4sandbox.define(:App).bind(:User, User.new(name: "Aotoki"))
5
6sandbox.run(<<~RUBY)
7 "Hello, #{App::User.name}"
8RUBY
9# => "Hello, Aotoki"在 Sandbox 中,要怎麼知道 Host 端提供了 App::User 而且有 #name 這個方法呢?
Kobako 在每次 Sandbox#run 的時候,都會自動注入像這樣的程式碼:
1class App::User < Kobako::RPC::Client; end
2# ...然後,就沒有然後,我們的 Sandbox 環境就能自然地知道 #name 可以呼叫,這是因為 Ruby 語言天生支援這樣的實作,因此 Kobako::RPC::Client 實現了這樣的行為
1class Kobako::RPC::Client
2 def method_missing(name, *args, &block)
3 __kobako_rpc_call__(name, *args)
4 end
5end這個 __kobako_rpc_call__ 是利用 Rust 所定義的 ABI,負責將 Method Call 轉到 WebAssembly 外面,等外面回覆後再轉發回 WebAssembly。透過這個機制,就能在隔離性跟可擴充性之間達到平衡。
套用到 Rails 的情境,原本要整合 AI 需要重新設計跟定義工具,但是原本就有做不錯的 Service Object 封裝跟權限控管,就只需要利用 RBS 向 AI 說明介面定義,讓 Agent 自動轉寫操作的腳本即可。
1sandbox = Kobako::Sandbox.new
2ns = sandbox.define(:Merchant)
3ns.bind(:Product, ProductManagementService.new(actor: current_user))
4# - ProductManagementService#where
5# - ProductManagementService#update
6# - ...
7
8# Agent Tool
9def execute_code(code)
10 sandbox.run(code)
11end
12
13# Merchant::Product.where(name: "%macOS%")
14# => [#<Product id=1>, #<Product id=2>]這也是 Cloudflare 為什麼需要用他們的 Cloudflare Worker 底層技術,改造出 Code Mode 的使用方式,因為這樣可以很快的把大量 API 整合進去,但不需要提供大量的工具。
Cloudflare Code Mode 提供一個值得思考的選擇,利用 LLM 本身的程式能力來解決問題,Kobako 則從 Ruby 生態系提取經驗,讓這種整合更加無縫。Kobako 專案目前也有 examples/codemode 的範例,可以使用 OpenAI 相容的 API 或者本地 Ollama / LMStudio 搭配 Gemma 4 體驗 KeyValue Store + WebFetch 的功能,有興趣的可以嘗試看看。
喜歡這篇文章?請我喝杯奶茶 🧋