跳至主要內容
蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Kobako:讓 Agent 安全的操作 Rails

今年 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 也有投資元在 wasmtimeruby.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 的功能,有興趣的可以嘗試看看。