kobako 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +22 -0
- data/Cargo.lock +1 -1
- data/README.md +89 -204
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/lib/kobako/catalog/namespaces.rb +5 -4
- data/lib/kobako/codec/utils.rb +27 -0
- data/lib/kobako/sandbox.rb +6 -1
- data/lib/kobako/transport/dispatcher.rb +1 -1
- data/lib/kobako/transport/request.rb +6 -5
- data/lib/kobako/transport/yielder.rb +23 -4
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +8 -1
- data/sig/kobako/codec/utils.rbs +2 -0
- data/sig/kobako/transport/yielder.rbs +4 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65375590ffd16758a33fee3ef16cfd3d1e99e439c378e9652da453a601b53f8a
|
|
4
|
+
data.tar.gz: 62c134b7035649fcde436816d95618287768f7a8a09d59f330cb5e785eae8024
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc4ad901e03f7de534c41423e3e7795a410d7201caf7a4d730b93829aceefc8ff7518320c2874a80e34978a9e38d6a430b5e03edbcd2e6868cb987c1d8ed3c91
|
|
7
|
+
data.tar.gz: 0b594e9b033b3a2d27b9f330b119ddf3f246b50f462cfb8914173d4074064ce17c3f12fb20851331f1e7c50390c39f64722820564618148de37fa06a4f603beb
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.6.1"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.1](https://github.com/elct9620/kobako/compare/v0.6.0...v0.6.1) (2026-05-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **loader:** try Ruby-ABI subdir before bare path ([51017eb](https://github.com/elct9620/kobako/commit/51017eb5ee40fa722ec962ce3b9a5d016b128d41))
|
|
9
|
+
|
|
10
|
+
## [0.6.0](https://github.com/elct9620/kobako/compare/v0.5.0...v0.6.0) (2026-05-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **bench:** gate against a committed anchor baseline ([ed8b30e](https://github.com/elct9620/kobako/commit/ed8b30e0940736cbabcca18227590d07c3bf94d3))
|
|
16
|
+
* **handle:** restore guest-returned Capability Handles to host objects (B-37) ([092815d](https://github.com/elct9620/kobako/commit/092815d610d3595db82b406d4b67880c84f11900))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **bench:** harden the gate guards and split judgment from the runner ([d6eaae2](https://github.com/elct9620/kobako/commit/d6eaae2de44d14a4735fbb544da712c659144a86))
|
|
22
|
+
* **ci:** chain release.yml from release-please via workflow_call ([711665d](https://github.com/elct9620/kobako/commit/711665d29a8c8445b1e26ca08e4b0efc5b24982c))
|
|
23
|
+
* **handle:** don't restore a Handle broken out of a guest block (B-37) ([ea25ab9](https://github.com/elct9620/kobako/commit/ea25ab9793f376f15e8d668077ad58f8d67e5a63))
|
|
24
|
+
|
|
3
25
|
## [0.5.0](https://github.com/elct9620/kobako/compare/v0.4.0...v0.5.0) (2026-05-27)
|
|
4
26
|
|
|
5
27
|
|
data/Cargo.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# Kobako
|
|
2
2
|
|
|
3
|
+
[](https://deepwiki.com/elct9620/kobako)
|
|
4
|
+
|
|
3
5
|
Kobako is a Ruby gem that embeds a Wasm-isolated mruby interpreter inside your application, so you can execute untrusted Ruby scripts (LLM-generated code, user formulas, student submissions, third-party plugins) in-process without giving them access to host memory, files, network, or credentials.
|
|
4
6
|
|
|
5
|
-
The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby and
|
|
7
|
+
The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby and a Transport proxy. The only way a guest script can reach the outside world is through Host App-declared **Services** — named Ruby objects you explicitly inject into the sandbox; the guest sees each one as a proxy that forwards calls back to the host over the Transport wire.
|
|
6
8
|
|
|
7
9
|
```
|
|
8
10
|
Host process Wasm guest
|
|
9
11
|
┌──────────────────────┐ ┌──────────────────────┐
|
|
10
12
|
│ Kobako::Sandbox │ ─eval─▶ │ mruby interpreter │
|
|
11
13
|
│ │ ─run──▶ │ │
|
|
12
|
-
│ Services │
|
|
14
|
+
│ Services │ ◀─call─ │ KV::Lookup.call(k) │
|
|
13
15
|
│ KV::Lookup │ ─resp─▶ │ │
|
|
14
16
|
│ │ │ │
|
|
15
17
|
│ stdout / stderr buf │ ◀─pipe─ │ puts / warn │
|
|
@@ -19,21 +21,6 @@ The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby an
|
|
|
19
21
|
trusted untrusted
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
## Features
|
|
23
|
-
|
|
24
|
-
| Feature | Description |
|
|
25
|
-
|---|---|
|
|
26
|
-
| In-process Wasm sandbox | No subprocess, no container. Both invocation verbs (`Sandbox#eval` for ad-hoc source, `Sandbox#run` for entrypoint dispatch) are synchronous Ruby calls. |
|
|
27
|
-
| Per-invocation caps | Every invocation enforces a wall-clock `timeout` (default 60 s) and a per-invocation linear-memory `memory_limit` (default 1 MiB); exhaustion raises `Kobako::TimeoutError` / `Kobako::MemoryLimitError`. |
|
|
28
|
-
| Capability injection via Services | Guest scripts can only call Ruby objects you explicitly `bind` under a two-level `Namespace::Member` path. |
|
|
29
|
-
| Preloaded snippets | `Sandbox#preload` registers source or RITE bytecode for setup-once dispatch via `Sandbox#run(:Entrypoint, *args, **kwargs)`. |
|
|
30
|
-
| Capability Handles | Services may return stateful host objects; the guest receives an opaque `Kobako::Handle` proxy it can use as the target of follow-up RPC calls, with no way to dereference it. `Sandbox#run` also accepts non-wire-representable Ruby objects as args and auto-wraps them into Handles, so the guest can use any host object the script needs. |
|
|
31
|
-
| Three-class error taxonomy | Every failure is exactly one of `TrapError`, `SandboxError`, or `ServiceError`, so you can route errors without inspecting messages. |
|
|
32
|
-
| Per-invocation state reset | Handles issued during one invocation are invalidated before the next; Service bindings and preloaded snippets remain. |
|
|
33
|
-
| Separated stdout / stderr capture | Guest writes to `$stdout` / `$stderr` are buffered per-channel (1 MiB default cap, configurable); overflow is clipped and reported by `#stdout_truncated?` / `#stderr_truncated?`. |
|
|
34
|
-
| Per-invocation usage readout | `Sandbox#usage` returns the most recent invocation's `wall_time` (Float seconds spent inside the wasm guest) and `memory_peak` (high-water `memory.grow` delta in bytes), populated on every outcome including `TrapError`, for budget diagnostics. |
|
|
35
|
-
| Curated mruby stdlib | Core extensions plus `mruby-onig-regexp` for full Onigmo `Regexp` support; no mrbgem with I/O, network, or syscall access is bundled. |
|
|
36
|
-
|
|
37
24
|
## Requirements
|
|
38
25
|
|
|
39
26
|
- **Ruby ≥ 3.3.0**
|
|
@@ -61,88 +48,75 @@ result = sandbox.eval(<<~RUBY)
|
|
|
61
48
|
1 + 2
|
|
62
49
|
RUBY
|
|
63
50
|
|
|
64
|
-
result
|
|
65
|
-
sandbox.stdout # => ""
|
|
51
|
+
result # => 3
|
|
66
52
|
```
|
|
67
53
|
|
|
68
54
|
The script executes inside the Wasm guest. It cannot read your filesystem, open sockets, or touch your `ENV`.
|
|
69
55
|
|
|
70
|
-
##
|
|
56
|
+
## Usage
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
### Injecting Services
|
|
59
|
+
|
|
60
|
+
Declare a Namespace, then `bind` any Ruby object as a Member; the guest reaches it as a `<Namespace>::<Member>` proxy and invokes its public methods through the Transport wire. See [`docs/behavior.md`](docs/behavior.md) B-07..B-12.
|
|
73
61
|
|
|
74
62
|
```ruby
|
|
75
|
-
|
|
63
|
+
class User
|
|
64
|
+
attr_reader :name
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
def initialize(name:)
|
|
67
|
+
@name = name
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sandbox.define(:Project).bind(:User, User.new(name: "alice"))
|
|
72
|
+
sandbox.define(:KV) .bind(:Lookup, ->(key) { redis.get(key) })
|
|
79
73
|
|
|
80
74
|
sandbox.eval(<<~RUBY)
|
|
81
|
-
|
|
82
|
-
KV::Lookup.call("user_42")
|
|
75
|
+
Project::User.name # => "alice"
|
|
76
|
+
KV::Lookup.call("user_42") # => "..."
|
|
83
77
|
RUBY
|
|
84
|
-
# => "..." (the redis value)
|
|
85
78
|
```
|
|
86
79
|
|
|
87
|
-
Names must match
|
|
80
|
+
Names must match `/\A[A-Z]\w*\z/`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation; later `#define` raises `ArgumentError`.
|
|
88
81
|
|
|
89
|
-
###
|
|
82
|
+
### Yielding to guest blocks
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch. See [`docs/behavior.md`](docs/behavior.md) B-23..B-30.
|
|
92
85
|
|
|
93
86
|
```ruby
|
|
94
|
-
sandbox.define(:
|
|
87
|
+
sandbox.define(:Seq).bind(:Map, ->(items, &blk) { items.map(&blk) })
|
|
95
88
|
|
|
96
|
-
sandbox.eval('
|
|
97
|
-
# =>
|
|
89
|
+
sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }')
|
|
90
|
+
# => [2, 4, 6]
|
|
98
91
|
```
|
|
99
92
|
|
|
100
|
-
|
|
93
|
+
### Per-invocation caps
|
|
101
94
|
|
|
102
|
-
Each
|
|
95
|
+
Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Sandbox#usage`](lib/kobako/sandbox.rb) after the call — populated on every outcome including traps — for actual consumption ([`docs/behavior.md`](docs/behavior.md) B-35).
|
|
103
96
|
|
|
104
97
|
```ruby
|
|
105
98
|
sandbox = Kobako::Sandbox.new(
|
|
106
|
-
timeout: 5.0,
|
|
107
|
-
memory_limit: 10 * 1024 * 1024, # bytes,
|
|
108
|
-
stdout_limit: 64 * 1024,
|
|
99
|
+
timeout: 5.0, # seconds, default 60.0
|
|
100
|
+
memory_limit: 10 * 1024 * 1024, # bytes, default 1 MiB
|
|
101
|
+
stdout_limit: 64 * 1024, # bytes, default 1 MiB
|
|
109
102
|
stderr_limit: 64 * 1024
|
|
110
103
|
)
|
|
111
104
|
```
|
|
112
105
|
|
|
113
|
-
| Cap | Raises
|
|
114
|
-
|
|
115
|
-
| `timeout` | `Kobako::TimeoutError`
|
|
116
|
-
| `memory_limit` | `Kobako::MemoryLimitError`
|
|
117
|
-
| `stdout_limit` | output
|
|
118
|
-
| `stderr_limit` | output
|
|
119
|
-
|
|
120
|
-
The timeout deadline is absolute wall-clock from invocation entry and is checked at guest Wasm safepoints. Long-running host Service callbacks still consume wall-clock time but do not themselves trap — the next guest safepoint will trap immediately on return if the deadline has passed.
|
|
121
|
-
|
|
122
|
-
`memory_limit` is scoped to the **per-invocation linear-memory delta** — the budget covers how much the current `#eval` / `#run` may grow `memory.grow` past the size observed at invocation entry. The mruby image's initial allocation and prior invocations' high-water mark are folded into that entry baseline, so a Sandbox reused across many invocations does not silently accumulate against a global budget.
|
|
106
|
+
| Cap | Raises | Default |
|
|
107
|
+
|----------------|----------------------------|---------|
|
|
108
|
+
| `timeout` | `Kobako::TimeoutError` | 60.0 s |
|
|
109
|
+
| `memory_limit` | `Kobako::MemoryLimitError` | 1 MiB |
|
|
110
|
+
| `stdout_limit` | output clipped (no raise) | 1 MiB |
|
|
111
|
+
| `stderr_limit` | output clipped (no raise) | 1 MiB |
|
|
123
112
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
To see how much of the cap an invocation actually consumed, read `Sandbox#usage` after the call. It returns a `Kobako::Usage` value object with `wall_time` (Float seconds the guest export call spent inside wasmtime, aligned with the `timeout` accounting) and `memory_peak` (Integer high-water `memory.grow` delta in bytes, aligned with the `memory_limit` accounting). The fields are populated on every outcome, including the `TrapError` branches, so you can read them after rescuing a trap to diagnose which budget the failing invocation chewed through.
|
|
127
|
-
|
|
128
|
-
```ruby
|
|
129
|
-
sandbox = Kobako::Sandbox.new(timeout: 1.0, memory_limit: 4 * 1024 * 1024)
|
|
130
|
-
|
|
131
|
-
begin
|
|
132
|
-
sandbox.eval("'x' * 5_000_000")
|
|
133
|
-
rescue Kobako::MemoryLimitError
|
|
134
|
-
sandbox.usage.memory_peak # => the largest delta accepted before the trap
|
|
135
|
-
sandbox.usage.wall_time # => seconds spent before the cap fired
|
|
136
|
-
end
|
|
137
|
-
```
|
|
113
|
+
`memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget.
|
|
138
114
|
|
|
139
|
-
|
|
115
|
+
### Capturing stdout / stderr
|
|
140
116
|
|
|
141
|
-
Guest
|
|
117
|
+
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and exposed independently of the return value ([`docs/behavior.md`](docs/behavior.md) B-04). Buffers clear at the start of each invocation; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
|
|
142
118
|
|
|
143
119
|
```ruby
|
|
144
|
-
sandbox = Kobako::Sandbox.new
|
|
145
|
-
|
|
146
120
|
result = sandbox.eval(<<~RUBY)
|
|
147
121
|
puts "hello"
|
|
148
122
|
warn "be careful"
|
|
@@ -154,49 +128,34 @@ sandbox.stdout # => "hello\n"
|
|
|
154
128
|
sandbox.stderr # => "be careful\n"
|
|
155
129
|
```
|
|
156
130
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```ruby
|
|
160
|
-
sandbox = Kobako::Sandbox.new(stdout_limit: 64 * 1024)
|
|
161
|
-
sandbox.eval('puts "a" * 100_000')
|
|
162
|
-
sandbox.stdout.bytesize # => 65_536
|
|
163
|
-
sandbox.stdout_truncated? # => true
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Error handling
|
|
131
|
+
### Error handling
|
|
167
132
|
|
|
168
|
-
Every invocation
|
|
133
|
+
Every invocation either returns a value or raises exactly one of three classes, so you can route faults without inspecting messages. The full taxonomy lives in [`lib/kobako/errors.rb`](lib/kobako/errors.rb).
|
|
169
134
|
|
|
170
135
|
```ruby
|
|
171
136
|
begin
|
|
172
137
|
sandbox.eval(script)
|
|
173
|
-
rescue Kobako::TrapError
|
|
174
|
-
# Wasm engine fault
|
|
175
|
-
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
# The
|
|
179
|
-
rescue Kobako::ServiceError => e
|
|
180
|
-
# A Service call failed and the script did not rescue it.
|
|
181
|
-
# Treat like any other downstream-service failure in your app.
|
|
182
|
-
rescue Kobako::SandboxError => e
|
|
183
|
-
# The script itself raised, failed to compile, or produced an
|
|
184
|
-
# unrepresentable value. A script-level fault, not infrastructure.
|
|
138
|
+
rescue Kobako::TrapError
|
|
139
|
+
# Wasm engine fault or cap exhaustion. Discard the Sandbox.
|
|
140
|
+
rescue Kobako::ServiceError
|
|
141
|
+
# A host Service call failed and the script did not rescue it.
|
|
142
|
+
rescue Kobako::SandboxError
|
|
143
|
+
# The script raised, failed to compile, or returned an unrepresentable value.
|
|
185
144
|
end
|
|
186
145
|
```
|
|
187
146
|
|
|
188
|
-
|
|
147
|
+
| Class | Parent | Trigger |
|
|
148
|
+
|---------------------------------|----------------|------------------------------------------------------|
|
|
149
|
+
| `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
|
|
150
|
+
| `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
|
|
151
|
+
| `Kobako::HandlerExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
|
|
152
|
+
| `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
|
|
189
153
|
|
|
190
|
-
|
|
191
|
-
|----------------------------------------|--------------------|------------------------------------------------------------------------------------------|
|
|
192
|
-
| `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
|
|
193
|
-
| `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
|
|
194
|
-
| `Kobako::HandleTableExhausted` | `SandboxError` | Per-invocation Handle counter reached its 2³¹ − 1 cap |
|
|
195
|
-
| `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` payload failed RITE structural validation at first invocation replay |
|
|
154
|
+
`SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` / `details` fields when the guest produced a panic envelope.
|
|
196
155
|
|
|
197
|
-
|
|
156
|
+
### Capability Handles
|
|
198
157
|
|
|
199
|
-
|
|
158
|
+
A non-wire-representable host object — returned from a Service (B-14), passed to `#run` (B-34), or handed back from the guest (B-37) — crosses the boundary as an opaque `Kobako::Handle` proxy and is restored to the original object before host code sees it; any other unrepresentable value raises `Kobako::SandboxError`. Handles are scoped to a single invocation ([`docs/behavior.md`](docs/behavior.md) B-13..B-21, B-34, B-37).
|
|
200
159
|
|
|
201
160
|
```ruby
|
|
202
161
|
class Greeter
|
|
@@ -206,30 +165,15 @@ end
|
|
|
206
165
|
|
|
207
166
|
sandbox.define(:Factory).bind(:Make, ->(name) { Greeter.new(name) })
|
|
208
167
|
|
|
209
|
-
sandbox.eval(
|
|
210
|
-
|
|
211
|
-
g.greet # second RPC, routed to the Greeter
|
|
212
|
-
RUBY
|
|
213
|
-
# => "hi, Bob"
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
`Sandbox#run` accepts non-wire-representable host objects as args / kwargs values too: the host walks the argument tree, wraps every non-wire leaf through the same Handle path, and the guest sees a `Kobako::Handle` proxy in its place. This lets you pass framework objects (a Rack `env` Hash containing an `IO`-like body, an active record, an enumerator) into the entrypoint without first marshalling them into primitives.
|
|
217
|
-
|
|
218
|
-
```ruby
|
|
219
|
-
require "stringio"
|
|
220
|
-
|
|
221
|
-
sandbox = Kobako::Sandbox.new
|
|
222
|
-
sandbox.preload(code: "Echo = ->(body) { body.read.upcase }", name: :Echo)
|
|
223
|
-
|
|
224
|
-
sandbox.run(:Echo, StringIO.new("hello world"))
|
|
225
|
-
# => "HELLO WORLD"
|
|
168
|
+
sandbox.eval('Factory::Make.call("Bob").greet') # => "hi, Bob" (Handle round-trip inside guest)
|
|
169
|
+
sandbox.eval('Factory::Make.call("Bob")') # => #<Greeter @name="Bob"> (B-37 restoration)
|
|
226
170
|
```
|
|
227
171
|
|
|
228
|
-
|
|
172
|
+
A `break` value from a guest block is the one exception: it unwinds back to the guest Member call rather than to host code, so a Handle in it stays a Handle — restoring would just re-wrap the same object into a new id on the return trip.
|
|
229
173
|
|
|
230
|
-
|
|
174
|
+
### Setup-once, run-many
|
|
231
175
|
|
|
232
|
-
|
|
176
|
+
One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; capability state (Handles, stdout, stderr, memory delta) resets between them.
|
|
233
177
|
|
|
234
178
|
```
|
|
235
179
|
───────────── setup phase (mutable) ─────────────
|
|
@@ -270,40 +214,21 @@ A single Sandbox can serve many invocations. Service bindings and preloaded snip
|
|
|
270
214
|
Services + snippets persist; invocation N+1 repeats.
|
|
271
215
|
```
|
|
272
216
|
|
|
273
|
-
|
|
274
|
-
sandbox = Kobako::Sandbox.new
|
|
275
|
-
sandbox.define(:Data).bind(:Fetch, ->(id) { records[id] })
|
|
276
|
-
|
|
277
|
-
sandbox.eval('Data::Fetch.call("a")') # => "..."
|
|
278
|
-
sandbox.eval('Data::Fetch.call("b")') # => "..." (same bindings, fresh state)
|
|
279
|
-
```
|
|
217
|
+
For workloads that must be isolated from each other (one Sandbox per tenant, per student submission, per agent session), construct a fresh `Kobako::Sandbox` per scope — wasmtime's Engine and the compiled Module are cached at process scope, so additional Sandboxes amortize cold-start cost automatically.
|
|
280
218
|
|
|
281
|
-
|
|
219
|
+
### Preloaded snippets and entrypoint dispatch
|
|
282
220
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`Sandbox#preload` registers named mruby snippets that replay against the fresh `mrb_state` before every invocation; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets and returns the value of `Target.call(*args, **kwargs)`. Together they cover setup-once / dispatch-many workloads where the same logic is exercised across many requests.
|
|
221
|
+
`Sandbox#preload` registers named mruby snippets that replay against the fresh `mrb_state` before every invocation; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets ([`docs/behavior.md`](docs/behavior.md) B-31..B-33).
|
|
286
222
|
|
|
287
223
|
```ruby
|
|
288
224
|
sandbox = Kobako::Sandbox.new
|
|
289
|
-
sandbox.preload(code: "Adder
|
|
225
|
+
sandbox.preload(code: "Adder = ->(a, b) { a + b }", name: :Adder)
|
|
290
226
|
sandbox.preload(code: 'Greeter = ->(name:) { "hello, #{name}" }', name: :Greeter)
|
|
291
227
|
|
|
292
|
-
sandbox.run(:Adder, 2, 3)
|
|
293
|
-
sandbox.run(:Greeter, name: "world")
|
|
228
|
+
sandbox.run(:Adder, 2, 3) # => 5
|
|
229
|
+
sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
294
230
|
```
|
|
295
231
|
|
|
296
|
-
`#preload` accepts two payload forms:
|
|
297
|
-
|
|
298
|
-
| Form | Signature | Snippet name source | Validation timing |
|
|
299
|
-
|----------|----------------------------------------|-------------------------------------|------------------------------------------------------------------------------------------|
|
|
300
|
-
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword | Trial-compiled at preload time; compile errors raise immediately |
|
|
301
|
-
| Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` | Structural validation runs at first invocation; failure raises `Kobako::BytecodeError` |
|
|
302
|
-
|
|
303
|
-
The source form trial-compiles each snippet against a fresh `mrb_state` at preload time, so compile errors surface immediately at the `#preload` call. The bytecode form treats `binary:` as opaque bytes and defers RITE version / body validation to the first invocation's replay, because that is when the payload loads into a fresh `mrb_state`. Bytecode compiled without `debug_info` (`mrbc` without `-g`) is still accepted — only its backtrace frames are omitted, while exception class, message, and `origin` attribution are preserved.
|
|
304
|
-
|
|
305
|
-
Snippets replay in insertion order, so later snippets can reference constants defined by earlier ones. The snippet table is sealed by the first invocation alongside Service registration; additional `#preload` calls after the first `#eval` or `#run` raise `ArgumentError`.
|
|
306
|
-
|
|
307
232
|
```
|
|
308
233
|
per-invocation replay (every #eval / #run, snippets in insertion order):
|
|
309
234
|
|
|
@@ -319,65 +244,33 @@ Snippets replay in insertion order, so later snippets can reference constants de
|
|
|
319
244
|
return value, then mrb_state discarded
|
|
320
245
|
```
|
|
321
246
|
|
|
322
|
-
`#
|
|
323
|
-
|
|
324
|
-
### Choosing between source and bytecode
|
|
325
|
-
|
|
326
|
-
Use the **source form** when snippets are authored in your repo or generated at boot — compile errors land at the `#preload` call so a misbehaving snippet fails fast at setup time, and no separate `mrbc` toolchain is needed. The trial-compile happens once per snippet (~2.5 µs per snippet) and is paid at preload, not on the request hot path.
|
|
247
|
+
`#preload` accepts two payload forms:
|
|
327
248
|
|
|
328
|
-
|
|
249
|
+
| Form | Signature | Snippet name source | Validation timing |
|
|
250
|
+
|----------|--------------------------------------|---------------------------------------|----------------------------------------------------------------------------|
|
|
251
|
+
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword | Trial-compiled at preload; compile errors raise immediately |
|
|
252
|
+
| Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` | Deferred to first invocation; failure raises `Kobako::BytecodeError` |
|
|
329
253
|
|
|
330
|
-
|
|
254
|
+
Use the source form for snippets authored in your repo (compile errors fail fast at `#preload`); use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Both replay through the same per-invocation path.
|
|
331
255
|
|
|
332
256
|
## Performance
|
|
333
257
|
|
|
334
|
-
Order-of-magnitude figures
|
|
335
|
-
|
|
336
|
-
### Lifecycle costs
|
|
337
|
-
|
|
338
|
-
| Phase | Cost |
|
|
339
|
-
|-------------------------------------------------------------|-------------------------------------------------|
|
|
340
|
-
| First `Sandbox.new` in a fresh process (Engine + Module JIT) | ~600 ms one-time |
|
|
341
|
-
| Subsequent `Sandbox.new` (Engine cache warm) | ~130 µs |
|
|
342
|
-
| Reusing a Sandbox for one `#eval("nil")` | ~135 µs |
|
|
343
|
-
| Fresh `Sandbox.new` per request | ~275 µs (≈ +140 µs vs reuse) |
|
|
344
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~165 µs |
|
|
345
|
-
| Per-RPC cost amortized inside one invocation | ~6.6 µs (1 000 RPCs in one `#eval` ≈ 6.6 ms) |
|
|
346
|
-
| 100 000-iteration integer XOR loop in mruby | ~43 ms |
|
|
347
|
-
| 1 000 Onigmo `Regexp =~` matches | ~3 µs each |
|
|
258
|
+
Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values vary by hardware but ratios are stable across machines. Full numbers, methodology, and the +10%-regression gate live in [`benchmark/README.md`](benchmark/README.md).
|
|
348
259
|
|
|
349
|
-
|
|
260
|
+
| Phase | Cost |
|
|
261
|
+
|--------------------------------------------------------------|-----------------------|
|
|
262
|
+
| First `Sandbox.new` in a fresh process (Engine + Module JIT) | ~600 ms one-time |
|
|
263
|
+
| Subsequent `Sandbox.new` (Engine cache warm) | ~125 µs |
|
|
264
|
+
| Warm `#eval("nil")` on a reused Sandbox | ~135 µs |
|
|
265
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~165 µs |
|
|
266
|
+
| Service call amortized inside one invocation | ~6.7 µs |
|
|
267
|
+
| Snippet replay per invocation | ~7-9 µs each |
|
|
268
|
+
| Per additional Sandbox (RSS) | ~570 KB |
|
|
350
269
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
| Allocation | Cost |
|
|
354
|
-
|---------------------------------------------|----------------------------------------------------------------------------|
|
|
355
|
-
| Process RSS after first `Sandbox.new` | ~165-195 MB (one-time engine + module + first instance) |
|
|
356
|
-
| Per additional Sandbox | ~580 KB (Wasm instance + linear memory + WASI capture pipes) |
|
|
357
|
-
| 1 000 isolated tenants in one process | ~765 MB total |
|
|
358
|
-
|
|
359
|
-
Use these as upper-bound budgets for capacity planning, not lower bounds — actual RSS shifts ~30% with host process load and macOS allocator state.
|
|
360
|
-
|
|
361
|
-
### Choosing your pattern
|
|
362
|
-
|
|
363
|
-
When the script is ad-hoc (LLM-generated, untrusted user input) and only runs once, use `Sandbox#eval(source)`. Per-invocation cost is ~135 µs of setup plus the script's own runtime; mruby parses the source on every call.
|
|
364
|
-
|
|
365
|
-
When you have a fixed set of entrypoints exercised many times — a stable AI Agent tool-call protocol, a plug-in registry loaded at boot, a small library of host-side commands — preload the entrypoints via `Sandbox#preload(code:, name:)` once at setup and dispatch via `Sandbox#run(:Target, *args, **kwargs)`. The mruby source compile (~2.5 µs per snippet) lands once at preload, not on every request, and warm dispatch costs ~165 µs.
|
|
366
|
-
|
|
367
|
-
Mind the snippet replay cost. Every preloaded snippet replays into a fresh `mrb_state` before **every** invocation, whether the invocation is `#eval` or `#run`, at ~7-9 µs per snippet per invocation. Preloading 8 helpers adds ~60 µs to every subsequent invocation; preloading 64 helpers adds ~565 µs. Keep the snippet count proportionate to how often the helpers are actually used — preloading rarely-touched helpers is more expensive than inlining or re-eval'ing them.
|
|
368
|
-
|
|
369
|
-
For tenant isolation between mutually untrusted scopes, construct a fresh `Kobako::Sandbox` per scope. Per-request construction costs ~140 µs over reuse plus ~580 KB of RSS — comfortably affordable for 1 000+ isolated tenants in one Sidekiq / Puma worker. Reuse a Sandbox when all requests share one trust scope; isolate when scripts come from many.
|
|
370
|
-
|
|
371
|
-
### Concurrency
|
|
372
|
-
|
|
373
|
-
`ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput across N Threads stays around 7-8k `#eval`/s regardless of N. Ruby-side `#eval` setup can still overlap, so a short `#eval` running while another Thread is in a long `#eval` is slowed by ~2× (not 10×) — host-side synchronization yields the GVL and the contending Thread interleaves. Mixed short / long workloads in one process do not deadlock.
|
|
374
|
-
|
|
375
|
-
### Regression gate
|
|
376
|
-
|
|
377
|
-
A +10% regression on any of the five SPEC-mandated benchmarks (cold_start, RPC roundtrip, codec, mruby VM, HandleTable) blocks release. Full per-suite breakdown in [`benchmark/README.md`](benchmark/README.md).
|
|
270
|
+
Construct one Sandbox at boot so the ~600 ms JIT cost lands off the request hot path. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 7-8k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. A +10% regression on any of the six SPEC-mandated benchmarks blocks release.
|
|
378
271
|
|
|
379
272
|
```bash
|
|
380
|
-
bundle exec rake bench
|
|
273
|
+
bundle exec rake bench # six gated regression benchmarks (~5-8 min)
|
|
381
274
|
```
|
|
382
275
|
|
|
383
276
|
## Development
|
|
@@ -385,19 +278,11 @@ bundle exec rake bench # five gated regression benchmarks (~5-8 min, ≤ 1 MiB
|
|
|
385
278
|
After checking out the repo:
|
|
386
279
|
|
|
387
280
|
```bash
|
|
388
|
-
bin/setup
|
|
389
|
-
bundle exec rake
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
Building from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain. The first compile walks the full vendor / mruby / wasm chain:
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
bundle exec rake compile # build the native extension
|
|
396
|
-
bundle exec rake wasm:build # rebuild data/kobako.wasm
|
|
397
|
-
bundle exec rake test # run the Ruby test suite
|
|
281
|
+
bin/setup # install dependencies
|
|
282
|
+
bundle exec rake # default: compile + test + rubocop + steep
|
|
398
283
|
```
|
|
399
284
|
|
|
400
|
-
`bin/console` opens an IRB session with the gem preloaded
|
|
285
|
+
Building from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain; the first compile walks the full vendor / mruby / wasm chain. See [`CLAUDE.md`](CLAUDE.md) for the rake task map and pipeline layout. `bin/console` opens an IRB session with the gem preloaded; `bundle exec rake install` installs the local checkout as a gem.
|
|
401
286
|
|
|
402
287
|
## Contributing
|
|
403
288
|
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/ext/kobako/Cargo.toml
CHANGED
|
@@ -59,10 +59,11 @@ module Kobako
|
|
|
59
59
|
@namespaces[name_str] ||= Namespace.new(name_str)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# Resolve a +target+ path of the form +"Namespace
|
|
63
|
-
# bound Host object. +target+ is a
|
|
64
|
-
# separator. Returns the bound Host
|
|
65
|
-
# namespace or the member is not
|
|
62
|
+
# Resolve a +target+ path of the form +"<Namespace>::<Member>"+
|
|
63
|
+
# (e.g. +"MyService::KV"+) to the bound Host object. +target+ is a
|
|
64
|
+
# two-level path using the +::+ separator. Returns the bound Host
|
|
65
|
+
# object. Raises +KeyError+ when the namespace or the member is not
|
|
66
|
+
# bound.
|
|
66
67
|
def lookup(target)
|
|
67
68
|
namespace_name, member_name = target.to_s.split("::", 2)
|
|
68
69
|
namespace = @namespaces[namespace_name]
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -116,6 +116,33 @@ module Kobako
|
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
120
|
+
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
121
|
+
# it to ({docs/behavior.md B-37}[link:../../../docs/behavior.md]).
|
|
122
|
+
# The symmetric inverse of {deep_wrap}: that walk allocates objects
|
|
123
|
+
# into Handles on the host→guest argument path; this walk resolves
|
|
124
|
+
# Handles back to their objects on every guest→host value path — the
|
|
125
|
+
# +#eval+ / +#run+ result and the yield-block result alike. The walk
|
|
126
|
+
# descends through Array elements and Hash keys and values one
|
|
127
|
+
# structural level at a time; any non-Handle leaf passes through
|
|
128
|
+
# unchanged.
|
|
129
|
+
#
|
|
130
|
+
# +value+ is a decoded Ruby value (a Handle here is a wire-decoded
|
|
131
|
+
# +Kobako::Handle+, never a guest-forged one — B-20); +handler+ must
|
|
132
|
+
# respond to +#fetch(id) -> object+ (a host-side
|
|
133
|
+
# +Kobako::Catalog::Handles+). +handler.fetch+ raises
|
|
134
|
+
# +Kobako::SandboxError+ for an id with no live binding, which is the
|
|
135
|
+
# corrupted-runtime fallback B-37 specifies.
|
|
136
|
+
def deep_restore(value, handler)
|
|
137
|
+
case value
|
|
138
|
+
when ::Array then value.map { |element| Utils.deep_restore(element, handler) }
|
|
139
|
+
when ::Hash
|
|
140
|
+
value.to_h { |key, val| [Utils.deep_restore(key, handler), Utils.deep_restore(val, handler)] }
|
|
141
|
+
when Kobako::Handle then handler.fetch(value.id)
|
|
142
|
+
else value
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
119
146
|
# Predicate split out of {representable?} for cyclomatic
|
|
120
147
|
# budget — the closed-set non-container branch. Returns +true+ for
|
|
121
148
|
# the scalar leaves and an existing Handle. Not part of the
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "forwardable"
|
|
4
4
|
|
|
5
5
|
require_relative "capture"
|
|
6
|
+
require_relative "codec"
|
|
6
7
|
require_relative "errors"
|
|
7
8
|
require_relative "outcome"
|
|
8
9
|
require_relative "sandbox_options"
|
|
@@ -304,7 +305,11 @@ module Kobako
|
|
|
304
305
|
snapshot = yield
|
|
305
306
|
@stdout_capture = snapshot.stdout
|
|
306
307
|
@stderr_capture = snapshot.stderr
|
|
307
|
-
|
|
308
|
+
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
309
|
+
# token; restore it to the host object the guest referenced before
|
|
310
|
+
# handing the value to the Host App (B-37). @handler still holds this
|
|
311
|
+
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
312
|
+
Codec::Utils.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
|
|
308
313
|
rescue Kobako::TrapError => e
|
|
309
314
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
310
315
|
ensure
|
|
@@ -59,7 +59,7 @@ module Kobako
|
|
|
59
59
|
request = Kobako::Transport::Request.decode(request_bytes)
|
|
60
60
|
target = resolve_target(request.target, namespaces, handler)
|
|
61
61
|
args, kwargs = resolve_call_args(request, handler)
|
|
62
|
-
yielder = Yielder.new(yield_to_guest, BREAK_THROW) if request.block_given
|
|
62
|
+
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given
|
|
63
63
|
value = catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }
|
|
64
64
|
encode_ok(value, handler)
|
|
65
65
|
rescue StandardError => e
|
|
@@ -20,11 +20,12 @@ module Kobako
|
|
|
20
20
|
#
|
|
21
21
|
# 5-element msgpack array:
|
|
22
22
|
# +[target, method_name, args, kwargs, block_given]+. +target+ is
|
|
23
|
-
# either a +String+ (+"Namespace
|
|
24
|
-
# +kwargs+ map keys to ext 0x00 Symbol;
|
|
25
|
-
# the Value Object is the single source of
|
|
26
|
-
# Boolean signalling whether the guest call
|
|
27
|
-
# (B-23); the block body itself never crosses the
|
|
23
|
+
# either a +String+ (+"<Namespace>::<Member>"+, e.g. +"MyService::KV"+)
|
|
24
|
+
# or a {Handle}. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
|
|
25
|
+
# enforced at construction so the Value Object is the single source of
|
|
26
|
+
# truth. +block_given+ is a Boolean signalling whether the guest call
|
|
27
|
+
# site supplied a block (B-23); the block body itself never crosses the
|
|
28
|
+
# wire.
|
|
28
29
|
#
|
|
29
30
|
# Built on the +class X < Data.define(...)+ subclass form so the
|
|
30
31
|
# class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
|
|
@@ -36,21 +36,29 @@ module Kobako
|
|
|
36
36
|
# +Runtime#yield_to_active_invocation+ bound through a lambda) that
|
|
37
37
|
# {#yield} invokes to re-enter the guest; +break_tag+ is the +catch+
|
|
38
38
|
# throw tag the Dispatcher matches against to unwind the Service on
|
|
39
|
-
# +tag 0x02+.
|
|
40
|
-
|
|
39
|
+
# +tag 0x02+. +handler+ is the Sandbox's +Kobako::Catalog::Handles+,
|
|
40
|
+
# used to restore a Capability Handle in the block's ok value back to
|
|
41
|
+
# its host object before it reaches the Service +yield+ site
|
|
42
|
+
# ({docs/behavior.md B-37}[link:../../../docs/behavior.md]).
|
|
43
|
+
def initialize(yield_to_guest, break_tag, handler)
|
|
41
44
|
@yield_to_guest = yield_to_guest
|
|
42
45
|
@break_tag = break_tag
|
|
46
|
+
@handler = handler
|
|
43
47
|
@active = true
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# Re-enter the guest with +args+ and reify the YieldResponse into
|
|
47
51
|
# Ruby control flow. Raises +LocalJumpError+ if called after
|
|
48
|
-
# {#invalidate!} (E-23).
|
|
52
|
+
# {#invalidate!} (E-23). The ok value is consumed by the host Service
|
|
53
|
+
# method, so a Capability Handle in it is restored to its host object
|
|
54
|
+
# (B-37). The break value unwinds past the Service back to the guest
|
|
55
|
+
# Member call (B-25), so it passes through verbatim — a Handle stays a
|
|
56
|
+
# Handle and rides back on the same id rather than churning a new one.
|
|
49
57
|
def yield(*args)
|
|
50
58
|
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
51
59
|
|
|
52
60
|
response = Kobako::Transport::Yield.decode(@yield_to_guest.call(Kobako::Codec::Encoder.encode(args)))
|
|
53
|
-
return response.value if response.ok?
|
|
61
|
+
return restore(response.value) if response.ok?
|
|
54
62
|
|
|
55
63
|
throw @break_tag, response.value if response.break?
|
|
56
64
|
|
|
@@ -72,6 +80,17 @@ module Kobako
|
|
|
72
80
|
|
|
73
81
|
private
|
|
74
82
|
|
|
83
|
+
# Restore any Capability Handle in a block's ok value to its host
|
|
84
|
+
# object via the injected +Catalog::Handles+
|
|
85
|
+
# ({docs/behavior.md B-37}[link:../../../docs/behavior.md]). Only the
|
|
86
|
+
# ok path calls this — host code consumes the ok value, whereas a
|
|
87
|
+
# break value returns to the guest and stays a Handle. Walks nested
|
|
88
|
+
# Array / Hash one level at a time; a plain value passes through
|
|
89
|
+
# unchanged.
|
|
90
|
+
def restore(value)
|
|
91
|
+
Kobako::Codec::Utils.deep_restore(value, @handler)
|
|
92
|
+
end
|
|
93
|
+
|
|
75
94
|
# Reify a +YieldResponse+ tag 0x04 payload into a +RuntimeError+ the
|
|
76
95
|
# Service method observes at its +yield+ site. The +{class, message,
|
|
77
96
|
# backtrace}+ shape mirrors the +Kobako::Transport::Yield+ tag 0x04
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "kobako/version"
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
|
7
|
+
require "kobako/#{Regexp.last_match(1)}/kobako"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
require "kobako/kobako"
|
|
10
|
+
end
|
|
11
|
+
|
|
5
12
|
require_relative "kobako/errors"
|
|
6
13
|
require_relative "kobako/transport"
|
|
7
14
|
require_relative "kobako/catalog"
|
data/sig/kobako/codec/utils.rbs
CHANGED
|
@@ -11,6 +11,8 @@ module Kobako
|
|
|
11
11
|
|
|
12
12
|
def self?.deep_wrap: (untyped value, Kobako::Catalog::Handles handler) -> untyped
|
|
13
13
|
|
|
14
|
+
def self?.deep_restore: (untyped value, Kobako::Catalog::Handles handler) -> untyped
|
|
15
|
+
|
|
14
16
|
def self?.primitive_type?: (untyped value) -> bool
|
|
15
17
|
|
|
16
18
|
def self?.container_representable?: (untyped value) -> bool
|
|
@@ -3,9 +3,10 @@ module Kobako
|
|
|
3
3
|
class Yielder
|
|
4
4
|
@yield_to_guest: ^(String) -> String
|
|
5
5
|
@break_tag: Symbol
|
|
6
|
+
@handler: Kobako::Catalog::Handles
|
|
6
7
|
@active: bool
|
|
7
8
|
|
|
8
|
-
def initialize: (^(String) -> String yield_to_guest, Symbol break_tag) -> void
|
|
9
|
+
def initialize: (^(String) -> String yield_to_guest, Symbol break_tag, Kobako::Catalog::Handles handler) -> void
|
|
9
10
|
|
|
10
11
|
def yield: (*untyped args) -> untyped
|
|
11
12
|
|
|
@@ -15,6 +16,8 @@ module Kobako
|
|
|
15
16
|
|
|
16
17
|
private
|
|
17
18
|
|
|
19
|
+
def restore: (untyped value) -> untyped
|
|
20
|
+
|
|
18
21
|
def yield_failure: (untyped payload, default: String) -> RuntimeError
|
|
19
22
|
end
|
|
20
23
|
end
|