---
title: "Kobako: Letting Blocks Pass Through the Sandbox"
date: 2026-11-04T00:00:00+08:00
publishDate: 2026-11-04T00:00:00+08:00
lastmod: 2026-11-02T22:04:55+08:00
tags: ["LLM","AI","Experience","Ruby","WebAssembly","Gem","mruby","Rust"]
series: "kobako"
toc: true
aiTranslated: true
cover: "https://blog.aotoki.me/posts/2026/11/04/kobako-block-through-sandbox/cover_hu_f8f86b40e3d1952e.webp"
permalink: "https://blog.aotoki.me/en/posts/2026/11/04/kobako-block-through-sandbox/"
language: "en"
---

The [Kobako: Handle Mechanism](https://blog.aotoki.me/en/posts/2026/10/14/kobako-handle-mechanism/) isn’t anything special for projects that support a scripting language, but blocks are a capability unique to Ruby, and it would be a shame if we couldn’t make good use of them inside a sandbox.

But can we really write something like this?

```ruby
sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
sandbox.eval("Seq::Map.call([1, 2, 3]) { |x| x * 2 }").value

# => [2, 4, 6]
```

Surprisingly, a block inside the sandbox can receive calls from the outside.

## Enumerator::Yielder

To make this work, we need a design that “hands the block over” for us, and CRuby happens to have an object that expresses exactly this behavior.

```ruby
y = Enumerator::Yielder.new do |x|
  puts x
end

[1, 2, 3].each(&y)
```

This doesn’t look special, and a lambda seems able to do the same thing, but the most abstract part here is that the block `Enumerator::Yielder.new` receives belongs to whoever uses it.

Let’s look at an `Enumerator` example.

```ruby
e = Enumerator.new do |y|
  pp y # => Enumerator::Yielder
  y << 1
  y.yield 2
end

e.each { |x| puts x }
```

In this case `y` is an `Enumerator::Yielder` object, and the block it receives is the one from `e.each`. With this property, we can turn the block inside the sandbox into a version the outside can use.

## Kobako::Transport::Yielder

When mruby calls a method on an external object with a block, it first makes an outbound call and records a pointer to this internal Proc, waiting for the outside to call it.

```ruby
sandbox.eval("Sequence.map { |x| x * 2 }")
```

At this point, what CRuby sees is a memory address inside mruby, representing the mruby `Proc`, the block `{ |x| x * 2 }`, but it can’t call it.

Kobako wraps this memory address, which CRuby can’t call, in `Kobako::Transport::Yielder`, turning it into the same concept as `Enumerator::Yielder`.

```ruby
sandbox.bind("Sequence", Enumerator.new { |y| y << 1 })
sandbox.eval("Sequence.map { |x| x * 2 }").value # [2]
```

In this case, Kobako cleverly hides the calls between the Host and the Guest, leaving only CRuby’s native Yielder concept, so the block appears usable both inside and outside.

> The example above fails in newer versions of Kobako. To strengthen security, only methods explicitly defined by the user are allowed by default; native `#each` and `#map` on objects like `Enumerator` are rejected, because several escape vulnerabilities exploited exactly this kind of boundary, so newer versions forbid them.

## Language Features

Now that AI can implement features quickly and in large volume, I think mechanisms that make good use of language features are all the more valuable, because they aren’t just piling up features. They come from carefully thinking about how to design so that users get an intuitive, native experience.

This feature is useful in many places. With Rails, for example, we can easily build a design like this.

```ruby
class MetricService
  def initialize(metric)
    @metrics = Metric.where(metric: metric)
  end

  def each(&block) = @metrics.each(&block)
  def map(&block) = @metrics.map(&block)

  def by_day
    @metrics = @metrics.group(:date)
    self
  end

  def count
    @metrics = @metrics.count
    self
  end

  def sum_of(type)
    @metrics = @metrics.sum(type)
    self
  end
end

sandbox.bind("Metric", MetricService.new(:page_view))
sandbox.eval("Metric.by_day.sum_of(:value).map { ... }").value
```

By wrapping specific objects in CRuby, mruby can process the data as if it were native. For an Agent, this is easier than reading the data out and processing it itself, because it can write a piece of code to handle the data directly, without keeping the intermediate steps in the Context and spending extra tokens on them.

This was also Kobako’s original idea: to give the Ruby ecosystem a usable [Code Mode](https://blog.cloudflare.com/code-mode-mcp/) that doesn’t depend on mechanisms like container technology, but is a simple, lightweight option that’s ready to use.
