---
title: "Kobako: Handle Mechanism"
date: 2026-10-14T00:00:00+08:00
publishDate: 2026-10-14T00:00:00+08:00
lastmod: 2026-10-12T21:44:58+08:00
tags: ["LLM","AI","Experience","Ruby","WebAssembly","Gem","mruby"]
series: "kobako"
toc: true
aiTranslated: true
permalink: "https://blog.aotoki.me/en/posts/2026/10/14/kobako-handle-mechanism/"
language: "en"
---


In [Kobako: Script Execution](https://blog.aotoki.me/en/posts/2026/09/23/kobako-script-execution/) we walked through how a complete run is designed, but the current design still has some inconvenient spots. When we want to call a Ruby object from inside the sandbox, it's hard to actually make use of the values it returns.


<!--more-->

## Limits of Binding{#binding-limitation}

For example, say we want an Agent to use [Code Mode](https://developers.cloudflare.com/agents/tools/codemode/) to query a Rails database directly. The implementation might look like this.

```ruby
s = Sandbox.new
s.bind("User", User)

# Agent's tool
s.eval("User.find_by(email: 'user@example.com')")
# => ?
```

We'll find that this design can't be implemented. `User.find_by` returns a User object, but the mruby inside the sandbox has no idea what `User` is, so it can't construct one, and therefore can't return it.

This is because MessagePack only supports basic value types, and covering every kind of object is nearly impossible. The usual workaround is to try `#to_msgpack` and see whether the object defines how to convert itself into basic values like Hash, Number, or String, but then we end up spending a lot of effort converting data between the host and the guest.

And it's likely unnecessary, since the User object just goes in and comes back out, which is quite wasteful.

## Pointers{#pointer}

There's already a well-known mechanism to borrow from for this problem: the idea of a "pointer." Many mechanisms in software work by "pointing to some location in memory," which fits our needs nicely.

In Kobako's design, when we pass something into the guest for mruby to use, it first checks whether the object can be converted. If it can't, Kobako allocates an ID for it and turns the passed-in object into a `Kobako::Handle` object.

```ruby
def deep_wrap(value, handle_table)
  case value
  when ::Array then value.map { |element| Utils.deep_wrap(element, handle_table) }
  when ::Hash  then value.transform_values { |val| Utils.deep_wrap(val, handle_table) }
  else
    wire_representable?(value) ? value : Kobako::Handle.new(handle_table.alloc(value))
  end
end
```

Since objects may also hide inside an Array or a Hash, the check runs recursively, making sure every object is accounted for before wrapping it into something MessagePack can fully parse.

This borrows MessagePack's Extension feature: Kobako tells MessagePack that whenever it encounters a `Kobako::Handle` object, it only needs to pass along the ID.

This way, the original User object is passed in as a `#<Handle id=1>`. When a return value contains `#<Handle id=1>`, we just ask Kobako again what object ID 1 refers to, and we get the User object back.

## Easier Communication{#communication-easier}

This design works because Kobako maintains two tables. With these two tables, both the host and the guest can communicate and interact with each other easily.

Binding provides known objects to the guest, so it can use the host's capabilities.

| Binding       | Class       |
| ------------- | ----------- |
| `"User"`      | `User`      |
| `"Workspace"` | `Workspace` |

The Handle mechanism covers return values. It isn't limited to known bindings, and it works while keeping the objects opaque.

| Handle | Object              |
| ------ | ------------------- |
| 1      | `#<User id=1>`      |
| 2      | `#<Workspace id=5>` |

Both bindings and handles come with the `#method_missing` mechanism Kobako designed, so calls on them can be sent back to the host to use the host's capabilities. Combined, they greatly expand what can be done inside the sandbox.

```ruby
s = Sandbox.new
s.bind("User", User)

s.eval("User.find_by(email: 'user@example.com')")
# => #<User id=1>

s.eval("User.where(role: :admin).pluck(:id)")
# => [1, 5, 9]
```

Even ActiveRecord's method chaining works with ease, but this also becomes one of Kobako's major challenges. Once we've worked out most of the features and come back to look at this mechanism, we'll see that the more useful a mechanism is, the more dangerous it becomes.

