Skip to main content
Aotokitsuruya
Aotokitsuruya
Senior Software Developer
Published at

Kobako: Handle Mechanism

This article is translated by AI, if have any corrections please let me know.
This article is part of Kobako series.

In 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.

Limits of Binding

For example, say we want an Agent to use Code Mode to query a Rails database directly. The implementation might look like this.

1s = Sandbox.new
2s.bind("User", User)
3
4# Agent's tool
5s.eval("User.find_by(email: 'user@example.com')")
6# => ?

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

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.

1def deep_wrap(value, handle_table)
2  case value
3  when ::Array then value.map { |element| Utils.deep_wrap(element, handle_table) }
4  when ::Hash  then value.transform_values { |val| Utils.deep_wrap(val, handle_table) }
5  else
6    wire_representable?(value) ? value : Kobako::Handle.new(handle_table.alloc(value))
7  end
8end

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

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.

BindingClass
"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.

HandleObject
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.

1s = Sandbox.new
2s.bind("User", User)
3
4s.eval("User.find_by(email: 'user@example.com')")
5# => #<User id=1>
6
7s.eval("User.where(role: :admin).pluck(:id)")
8# => [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.