kobako 0.23.0 → 0.26.0
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 +89 -0
- data/Cargo.lock +156 -110
- data/README.md +19 -18
- data/ROADMAP.md +12 -12
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/profile.rs +2 -2
- data/crates/kobako-transport/CHANGELOG.md +14 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +2 -0
- data/crates/kobako-transport/src/envelope/bytes.rs +6 -0
- data/crates/kobako-transport/src/envelope/call.rs +9 -0
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -0
- data/crates/kobako-transport/src/envelope/fault.rs +5 -0
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +9 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +11 -0
- data/crates/kobako-transport/src/envelope/reply.rs +9 -0
- data/crates/kobako-transport/src/envelope/run.rs +3 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
- data/crates/kobako-wasmtime/Cargo.toml +5 -5
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/ambient.rs +2 -0
- data/crates/kobako-wasmtime/src/capture.rs +8 -0
- data/crates/kobako-wasmtime/src/frames.rs +3 -1
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -0
- data/crates/kobako-wasmtime/src/invocation.rs +10 -0
- data/crates/kobako-wasmtime/src/trap.rs +6 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/lib/kobako/capture.rb +8 -22
- data/lib/kobako/catalog/handles.rb +33 -9
- data/lib/kobako/catalog/services.rb +9 -7
- data/lib/kobako/codec/encoder.rb +4 -13
- data/lib/kobako/codec/handle_walk.rb +1 -1
- data/lib/kobako/codec/nesting.rb +43 -0
- data/lib/kobako/codec.rb +4 -2
- data/lib/kobako/context.rb +8 -8
- data/lib/kobako/handle.rb +10 -40
- data/lib/kobako/sandbox.rb +5 -0
- data/lib/kobako/transport/dispatcher.rb +29 -26
- data/lib/kobako/transport/exposure.rb +100 -0
- data/lib/kobako/transport/reflection.rb +19 -20
- data/lib/kobako/transport/run.rb +5 -2
- data/lib/kobako/transport/yielder.rb +6 -4
- data/lib/kobako/transport.rb +4 -2
- data/lib/kobako/usage.rb +11 -22
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +1 -1
- data/sig/kobako/catalog/handles.rbs +2 -0
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +9 -0
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +7 -7
- data/sig/kobako/transport/exposure.rbs +43 -0
- data/sig/kobako/transport/reflection.rbs +1 -3
- data/sig/kobako/transport.rbs +11 -1
- metadata +5 -1
data/README.md
CHANGED
|
@@ -151,7 +151,7 @@ Build the crate as a `cdylib` for `wasm32-wasip1`, then bake the canonical boot
|
|
|
151
151
|
|
|
152
152
|
### Services
|
|
153
153
|
|
|
154
|
-
`bind` any Ruby object as a Service at a constant-path name; the guest reaches it as a `MyService::KV` (or top-level `File`) proxy and invokes
|
|
154
|
+
`bind` any Ruby object as a Service at a constant-path name; the guest reaches it as a `MyService::KV` (or top-level `File`) proxy and invokes the public methods its own class defines through the Transport wire. See [`SV-006`](docs/spec/behavior/services.md) and [`SV-007`](docs/spec/behavior/services.md).
|
|
155
155
|
|
|
156
156
|
```ruby
|
|
157
157
|
class User
|
|
@@ -175,7 +175,7 @@ Each `::`-separated path segment must match `/\A[A-Z]\w*\z/`. Symbol kwargs trav
|
|
|
175
175
|
|
|
176
176
|
### Per-Invocation Bindings
|
|
177
177
|
|
|
178
|
-
A setup-time `bind` fixes one object for the Sandbox's life. When the object belongs to a single run instead — the current request, the acting user, a per-tenant store — declare the path at setup and fill it per invocation. `bind(path)` with no object reserves the name as a *fillable*: the guest sees the constant, while an unfilled dispatch fails closed as `Kobako::ServiceError`. The optional `#eval` / `#run` block fills it ([`docs/behavior/
|
|
178
|
+
A setup-time `bind` fixes one object for the Sandbox's life. When the object belongs to a single run instead — the current request, the acting user, a per-tenant store — declare the path at setup and fill it per invocation. `bind(path)` with no object reserves the name as a *fillable*: the guest sees the constant, while an unfilled dispatch fails closed as `Kobako::ServiceError`. The optional `#eval` / `#run` block fills it ([`SV-017`](docs/spec/behavior/services.md), [`SV-024`](docs/spec/behavior/services.md)).
|
|
179
179
|
|
|
180
180
|
```ruby
|
|
181
181
|
sandbox.bind("Req::Current") # declared, unfilled — stands for Kobako::Unresolved
|
|
@@ -187,7 +187,7 @@ sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
|
|
|
187
187
|
|
|
188
188
|
### Output Capture
|
|
189
189
|
|
|
190
|
-
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value` ([`docs/behavior/
|
|
190
|
+
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value` ([`S-023`](docs/spec/behavior/sandbox.md), [`S-028`](docs/spec/behavior/sandbox.md)). Each invocation captures its own; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
|
|
191
191
|
|
|
192
192
|
```ruby
|
|
193
193
|
execution = sandbox.eval(<<~RUBY)
|
|
@@ -232,7 +232,7 @@ Each of these carries the failed run's Execution on `#execution`, so a rescue re
|
|
|
232
232
|
|
|
233
233
|
### Resource Limits
|
|
234
234
|
|
|
235
|
-
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 [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does ([`docs/behavior/
|
|
235
|
+
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 [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does ([`S-058`](docs/spec/behavior/sandbox.md), [`S-061`](docs/spec/behavior/sandbox.md)).
|
|
236
236
|
|
|
237
237
|
```ruby
|
|
238
238
|
sandbox = Kobako::Sandbox.new(
|
|
@@ -256,9 +256,9 @@ Beyond the four caps, `profile:` requests the Sandbox's isolation posture on the
|
|
|
256
256
|
|
|
257
257
|
### Concurrency
|
|
258
258
|
|
|
259
|
-
A Sandbox keeps no state from any run, so concurrent Threads may invoke distinct Sandboxes or share a single one; each invocation owns its Handles, captures, and usage either way ([`docs/behavior/runtime.md`](docs/behavior/runtime.md)
|
|
259
|
+
A Sandbox keeps no state from any run, so concurrent Threads may invoke distinct Sandboxes or share a single one; each invocation owns its Handles, captures, and usage either way ([`RT-001`](docs/spec/behavior/runtime.md), [`RT-002`](docs/spec/behavior/runtime.md)). One Thread still runs one invocation at a time. Sharing a Sandbox adds a single obligation: an object bound once at setup is reached by every Thread and must itself be thread-safe, while an object supplied per invocation — `ctx.bind`, or an Extension `provider:` — carries no such requirement.
|
|
260
260
|
|
|
261
|
-
By default an invocation holds Ruby's GVL for its whole span, so guest execution across Threads serializes. `gvl: :release` drops the GVL for the guest span and re-acquires it for each guest→host dispatch, running guest code in parallel across Threads (
|
|
261
|
+
By default an invocation holds Ruby's GVL for its whole span, so guest execution across Threads serializes. `gvl: :release` drops the GVL for the guest span and re-acquires it for each guest→host dispatch, running guest code in parallel across Threads ([`RT-024`](docs/spec/behavior/runtime.md), [`RT-026`](docs/spec/behavior/runtime.md)).
|
|
262
262
|
|
|
263
263
|
```ruby
|
|
264
264
|
sandbox = Kobako::Sandbox.new(gvl: :release)
|
|
@@ -319,9 +319,9 @@ For workloads that must be isolated from each other (one Sandbox per tenant, per
|
|
|
319
319
|
|
|
320
320
|
### Pooling
|
|
321
321
|
|
|
322
|
-
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`docs/behavior/
|
|
322
|
+
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`PL-003`](docs/spec/behavior/pool.md), [`PL-011`](docs/spec/behavior/pool.md)). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
|
|
323
323
|
|
|
324
|
-
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation.
|
|
324
|
+
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. The build bakes the shared boot state into the artifact ([`mruby.md`](docs/spec/behavior/mruby.md)) and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the host-side `Sandbox.new` — now under 3 µs, an order of magnitude below the invocation that follows it. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a gain worth the coupling. What a Pool buys is warm setup and exclusive checkout, not isolation: a Sandbox holds no state from any run, so Threads sharing one are equally safe (see [Concurrency](#concurrency)).
|
|
325
325
|
|
|
326
326
|
```ruby
|
|
327
327
|
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
@@ -340,7 +340,7 @@ Sandboxes construct lazily on first demand. `#with` yields a Sandbox and returns
|
|
|
340
340
|
|
|
341
341
|
### Service Blocks
|
|
342
342
|
|
|
343
|
-
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/yield.md`](docs/behavior/yield.md)
|
|
343
|
+
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 [`T-085`](docs/spec/behavior/transport-yield.md) and [`T-089`](docs/spec/behavior/transport-yield.md).
|
|
344
344
|
|
|
345
345
|
```ruby
|
|
346
346
|
sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
|
|
@@ -351,7 +351,7 @@ sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
|
|
|
351
351
|
|
|
352
352
|
### Handle Management
|
|
353
353
|
|
|
354
|
-
A non-wire-representable host object — returned from a Service (
|
|
354
|
+
A non-wire-representable host object — returned from a Service ([`T-001`](docs/spec/behavior/transport-dispatch.md)), passed to `#run` ([`T-066`](docs/spec/behavior/transport-dispatch.md)), or handed back from the guest ([`T-054`](docs/spec/behavior/transport-dispatch.md)) — 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 ([`T-038`](docs/spec/behavior/transport-dispatch.md)).
|
|
355
355
|
|
|
356
356
|
```ruby
|
|
357
357
|
class Greeter
|
|
@@ -362,12 +362,12 @@ end
|
|
|
362
362
|
sandbox.bind("Factory::Make", ->(name) { Greeter.new(name) })
|
|
363
363
|
|
|
364
364
|
sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle round-trip inside guest)
|
|
365
|
-
sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (
|
|
365
|
+
sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (Handle restoration)
|
|
366
366
|
```
|
|
367
367
|
|
|
368
368
|
A `break` value from a guest block is the one exception: it unwinds back to the guest Service 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.
|
|
369
369
|
|
|
370
|
-
Each dispatch that hands back a non-wire-representable object allocates a *new* Handle — kobako never deduplicates by object identity (
|
|
370
|
+
Each dispatch that hands back a non-wire-representable object allocates a *new* Handle — kobako never deduplicates by object identity ([`T-007`](docs/spec/behavior/transport-dispatch.md), [`T-009`](docs/spec/behavior/transport-dispatch.md)). This is most visible with fluent / builder APIs. An `ActiveRecord::Relation` chain `spawn`s a fresh relation at each step, so every hop is an independent dispatch that binds its own Handle:
|
|
371
371
|
|
|
372
372
|
```
|
|
373
373
|
guest chain host (Catalog::Handles, one invocation)
|
|
@@ -383,11 +383,11 @@ Each dispatch that hands back a non-wire-representable object allocates a *new*
|
|
|
383
383
|
all stay live until the invocation ends, then reset together
|
|
384
384
|
```
|
|
385
385
|
|
|
386
|
-
This is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and reset between invocations, so even deep chains stay far inside the range. Two consequences are worth keeping in mind: the same host object handed back twice yields two *different* Handles — the guest cannot tell they alias — and every intermediate Handle stays live until the invocation ends, since there is no per-Handle release (
|
|
386
|
+
This is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and reset between invocations, so even deep chains stay far inside the range. Two consequences are worth keeping in mind: the same host object handed back twice yields two *different* Handles — the guest cannot tell they alias — and every intermediate Handle stays live until the invocation ends, since there is no per-Handle release ([`transport-dispatch.md`](docs/spec/behavior/transport-dispatch.md)).
|
|
387
387
|
|
|
388
388
|
### Snippets & Entrypoints
|
|
389
389
|
|
|
390
|
-
`Sandbox#preload` registers named mruby snippets that replay into every invocation's canonical boot state; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets ([`docs/behavior/
|
|
390
|
+
`Sandbox#preload` registers named mruby snippets that replay into every invocation's canonical boot state; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets ([`S-053`](docs/spec/behavior/sandbox.md), [`S-045`](docs/spec/behavior/sandbox.md)).
|
|
391
391
|
|
|
392
392
|
```ruby
|
|
393
393
|
sandbox = Kobako::Sandbox.new
|
|
@@ -428,7 +428,7 @@ Use the source form for snippets authored in your repo; use the bytecode form wh
|
|
|
428
428
|
|
|
429
429
|
### Extensions
|
|
430
430
|
|
|
431
|
-
An Extension teaches the guest a native-style constant by pairing a guest idiom (`source`) with an optional host `backend`. `Sandbox#install` composes the two through the existing `#preload` and `#bind` verbs, adding no wire or Guest Binary surface: pure operations run in-guest with no round-trip, while the rest dispatch to the backend under the same isolation and reflection guarantees as any bound Service ([`docs/extensions.md`](docs/extensions.md), [`docs/behavior/extension.md`](docs/behavior/extension.md)
|
|
431
|
+
An Extension teaches the guest a native-style constant by pairing a guest idiom (`source`) with an optional host `backend`. `Sandbox#install` composes the two through the existing `#preload` and `#bind` verbs, adding no wire or Guest Binary surface: pure operations run in-guest with no round-trip, while the rest dispatch to the backend under the same isolation and reflection guarantees as any bound Service ([`docs/extensions.md`](docs/extensions.md), [`EX-005`](docs/spec/behavior/extension.md), [`EX-006`](docs/spec/behavior/extension.md)).
|
|
432
432
|
|
|
433
433
|
```ruby
|
|
434
434
|
FILE = <<~'MRUBY'
|
|
@@ -466,8 +466,9 @@ kobako ships no concrete Extension; the idiom and backend are yours. The [overla
|
|
|
466
466
|
## Security
|
|
467
467
|
|
|
468
468
|
kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
|
|
469
|
-
exposes
|
|
470
|
-
|
|
469
|
+
exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
|
|
470
|
+
methods stay out of reach, but the class's own surface does not, so bind a purpose-built
|
|
471
|
+
object scoped to the task, not a capable one whose other methods leak more than you intend.
|
|
471
472
|
|
|
472
473
|
```ruby
|
|
473
474
|
class ThemeReader # only #color is reachable; AppConfig.secret_key is not
|
|
@@ -483,7 +484,7 @@ sandbox.eval('Cfg::Settings.color').value # => "#3366ff" — every other metho
|
|
|
483
484
|
When a purpose-built wrapper is more than you need, an object can gate its own surface in
|
|
484
485
|
place: a private `respond_to_guest?(name)` answers, per method, whether the guest may call
|
|
485
486
|
it. Returning `false` for every name makes the object opaque — a credential the guest
|
|
486
|
-
forwards to another Service but never reads — while a named subset
|
|
487
|
+
forwards to another Service but never reads — while permitting a named subset exposes exactly those.
|
|
487
488
|
|
|
488
489
|
Guest code can name any `MyService::KV` path, but a forged name only resolves to
|
|
489
490
|
something you bound — the real authorization gate is this host-side allowlist. Give each
|
data/ROADMAP.md
CHANGED
|
@@ -9,17 +9,17 @@ output capture, and a warm Sandbox pool.
|
|
|
9
9
|
|
|
10
10
|
| Feature | Entry Points | Notes |
|
|
11
11
|
|---------|-------------|-------|
|
|
12
|
-
| ✅ [F-01 Sandbox instantiation](docs/behavior/
|
|
13
|
-
| ✅ [F-02 Service binding](docs/behavior/
|
|
14
|
-
| ✅ [F-04 Synchronous mruby source execution (`#eval`)](docs/behavior/
|
|
15
|
-
| ✅ [F-05 Guest-initiated Transport dispatch](docs/behavior/dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) | — |
|
|
16
|
-
| ✅ [F-06 Capability Handle encoding and referencing](docs/behavior/dispatch.md) | [lib/kobako/catalog/handles.rb](lib/kobako/catalog/handles.rb) | — |
|
|
17
|
-
| ✅ [F-07 Three-class error attribution and raising](docs/behavior/
|
|
18
|
-
| ✅ [F-08 Guest output capture](docs/behavior/
|
|
12
|
+
| ✅ [F-01 Sandbox instantiation](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
|
|
13
|
+
| ✅ [F-02 Service binding](docs/spec/behavior/services.md) | [lib/kobako/catalog/services.rb](lib/kobako/catalog/services.rb) | — |
|
|
14
|
+
| ✅ [F-04 Synchronous mruby source execution (`#eval`)](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
|
|
15
|
+
| ✅ [F-05 Guest-initiated Transport dispatch](docs/spec/behavior/transport-dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) | — |
|
|
16
|
+
| ✅ [F-06 Capability Handle encoding and referencing](docs/spec/behavior/transport-dispatch.md) | [lib/kobako/catalog/handles.rb](lib/kobako/catalog/handles.rb) | — |
|
|
17
|
+
| ✅ [F-07 Three-class error attribution and raising](docs/spec/behavior/outcome.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) | A guest-entry decode failure is exercised only through its payload half ([`CD-003`](docs/spec/behavior/codec.md)); a Run envelope that does not frame is not reachable through the public API |
|
|
18
|
+
| ✅ [F-08 Guest output capture](docs/spec/behavior/sandbox.md) | [lib/kobako/capture.rb](lib/kobako/capture.rb) | — |
|
|
19
19
|
| ✅ [F-09 Host–guest message codec](docs/wire-codec.md) | [crates/kobako-transport/](crates/kobako-transport/) (core envelope + ABI, one implementation), [lib/kobako/codec/](lib/kobako/codec/) (host payload codec) | The payload layer has a second implementation in `crates/kobako-codec`; the envelope layer is pinned by golden vectors instead |
|
|
20
|
-
| ✅ [F-10 Reproducible build pipeline](SPEC.md#code-organization) | [tasks/wasm/build.rake](tasks/wasm/build.rake) | Verified by build-time gates (
|
|
20
|
+
| ✅ [F-10 Reproducible build pipeline](SPEC.md#code-organization) | [tasks/wasm/build.rake](tasks/wasm/build.rake) | Verified by build-time gates (double-bake byte-identity, gemspec whitelist), not `test/` |
|
|
21
21
|
| ✅ [F-11 Multi-layer test and benchmark suite](SPEC.md#testing-style) | [test/](test/) | Benchmarks live in [benchmark/](benchmark/) with the gate in `tasks/bench/`; the anchor baseline advances only by deliberate re-bless |
|
|
22
|
-
| ✅ [F-12 Guest block reception and yield re-entry](docs/behavior/yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) | — |
|
|
23
|
-
| ✅ [F-13 Snippet preloading (`#preload`)](docs/behavior/
|
|
24
|
-
| ✅ [F-14 Synchronous entrypoint dispatch (`#run`)](docs/behavior/
|
|
25
|
-
| ✅ [F-15 Warm Sandbox pool checkout (`Kobako::Pool`)](docs/behavior/
|
|
22
|
+
| ✅ [F-12 Guest block reception and yield re-entry](docs/spec/behavior/transport-yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) | — |
|
|
23
|
+
| ✅ [F-13 Snippet preloading (`#preload`)](docs/spec/behavior/sandbox.md) | [lib/kobako/catalog/snippets.rb](lib/kobako/catalog/snippets.rb) | — |
|
|
24
|
+
| ✅ [F-14 Synchronous entrypoint dispatch (`#run`)](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
|
|
25
|
+
| ✅ [F-15 Warm Sandbox pool checkout (`Kobako::Pool`)](docs/spec/behavior/pool.md) | [lib/kobako/pool.rb](lib/kobako/pool.rb) | — |
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.15.0...kobako-runtime-v0.16.0) (2026-09-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
10
|
+
## [0.15.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.14.0...kobako-runtime-v0.15.0) (2026-09-13)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **spec:** declare what the guest answers when a value will not cross ([886a16f](https://github.com/elct9620/kobako/commit/886a16f173efe44766dd9884ac8bfed61113728a))
|
|
16
|
+
|
|
3
17
|
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.13.1...kobako-runtime-v0.14.0) (2026-08-06)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
[package]
|
|
15
15
|
name = "kobako-runtime"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.16.0"
|
|
17
17
|
edition = "2021"
|
|
18
18
|
description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
|
|
19
19
|
license = "Apache-2.0"
|
|
@@ -27,4 +27,4 @@ categories = ["wasm", "virtualization"]
|
|
|
27
27
|
# in-tree builds (and the Ruby gem, which ships both crates) resolving
|
|
28
28
|
# locally.
|
|
29
29
|
[dependencies]
|
|
30
|
-
kobako-transport = { version = "0.
|
|
30
|
+
kobako-transport = { version = "0.16.0", path = "../kobako-transport" }
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
//! A rung on an ordered ladder: the host application requests the
|
|
5
5
|
//! posture it wants, the runtime builds it and declares the posture it
|
|
6
6
|
//! actually built, and the frontend refuses a declaration below the
|
|
7
|
-
//! request — so the request is also the floor.
|
|
8
|
-
//! lives in the spec corpus (docs/behavior/security.md).
|
|
7
|
+
//! request — so the request is also the floor.
|
|
9
8
|
|
|
10
9
|
/// The ordered isolation ladder a runtime builds one rung of.
|
|
11
10
|
///
|
|
@@ -34,6 +33,7 @@ pub enum Profile {
|
|
|
34
33
|
mod tests {
|
|
35
34
|
use super::*;
|
|
36
35
|
|
|
36
|
+
// @behavior RT-014
|
|
37
37
|
#[test]
|
|
38
38
|
fn a_declaration_satisfies_any_floor_at_or_below_it() {
|
|
39
39
|
assert!(Profile::Hermetic >= Profile::Hermetic);
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.15.0...kobako-transport-v0.16.0) (2026-09-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-transport:** Synchronize kobako crates versions
|
|
9
|
+
|
|
10
|
+
## [0.15.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.14.0...kobako-transport-v0.15.0) (2026-09-13)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **spec:** declare the bytes both sides must already agree on ([3875755](https://github.com/elct9620/kobako/commit/3875755999550511837808dcd24a25086bedc926))
|
|
16
|
+
|
|
3
17
|
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.13.1...kobako-transport-v0.14.0) (2026-08-06)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -59,6 +59,7 @@ pub fn unpack_ptr_len(packed: u64) -> (u32, u32) {
|
|
|
59
59
|
mod tests {
|
|
60
60
|
use super::*;
|
|
61
61
|
|
|
62
|
+
// @behavior WE-001
|
|
62
63
|
#[test]
|
|
63
64
|
fn the_layout_is_high_ptr_low_len() {
|
|
64
65
|
let packed = pack_ptr_len(0xAABB_CCDD, 0x1122_3344);
|
|
@@ -68,6 +69,7 @@ mod tests {
|
|
|
68
69
|
);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// @behavior WE-002
|
|
71
73
|
#[test]
|
|
72
74
|
fn every_pair_round_trips() {
|
|
73
75
|
for &(ptr, len) in &[
|
|
@@ -151,6 +151,7 @@ impl Writer {
|
|
|
151
151
|
mod tests {
|
|
152
152
|
use super::*;
|
|
153
153
|
|
|
154
|
+
// @behavior WE-003
|
|
154
155
|
#[test]
|
|
155
156
|
fn round_trips_every_primitive() {
|
|
156
157
|
let mut w = Writer::new();
|
|
@@ -181,6 +182,7 @@ mod tests {
|
|
|
181
182
|
);
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
// @behavior WE-004
|
|
184
186
|
#[test]
|
|
185
187
|
fn u32_is_big_endian_on_the_wire() {
|
|
186
188
|
let mut w = Writer::new();
|
|
@@ -192,6 +194,7 @@ mod tests {
|
|
|
192
194
|
);
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
// @behavior WE-005
|
|
195
198
|
#[test]
|
|
196
199
|
fn a_length_running_past_the_end_is_refused() {
|
|
197
200
|
// Declares 8 bytes but supplies 2.
|
|
@@ -203,6 +206,7 @@ mod tests {
|
|
|
203
206
|
);
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
// @behavior WE-006
|
|
206
210
|
#[test]
|
|
207
211
|
fn a_count_larger_than_the_message_is_refused() {
|
|
208
212
|
// Declares 0xffff_ffff elements in a 4-byte message.
|
|
@@ -214,6 +218,7 @@ mod tests {
|
|
|
214
218
|
);
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
// @behavior WE-007
|
|
217
222
|
#[test]
|
|
218
223
|
fn non_utf8_in_a_text_field_is_refused() {
|
|
219
224
|
let mut w = Writer::new();
|
|
@@ -226,6 +231,7 @@ mod tests {
|
|
|
226
231
|
);
|
|
227
232
|
}
|
|
228
233
|
|
|
234
|
+
// @behavior WE-008
|
|
229
235
|
#[test]
|
|
230
236
|
fn trailing_bytes_after_a_self_delimiting_field_are_refused() {
|
|
231
237
|
let mut w = Writer::new();
|
|
@@ -82,6 +82,7 @@ impl<'a> Call<'a> {
|
|
|
82
82
|
mod tests {
|
|
83
83
|
use super::*;
|
|
84
84
|
|
|
85
|
+
// @behavior WE-009
|
|
85
86
|
#[test]
|
|
86
87
|
fn a_path_target_round_trips() {
|
|
87
88
|
let call = Call {
|
|
@@ -98,6 +99,7 @@ mod tests {
|
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// @behavior WE-010
|
|
101
103
|
#[test]
|
|
102
104
|
fn a_handle_target_round_trips() {
|
|
103
105
|
let call = Call {
|
|
@@ -114,6 +116,7 @@ mod tests {
|
|
|
114
116
|
);
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
// @behavior WE-011
|
|
117
120
|
#[test]
|
|
118
121
|
fn the_payload_is_borrowed_not_interpreted() {
|
|
119
122
|
// Bytes that are not valid msgpack: the envelope layer must carry
|
|
@@ -133,6 +136,7 @@ mod tests {
|
|
|
133
136
|
);
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
// @behavior WE-012
|
|
136
140
|
#[test]
|
|
137
141
|
fn golden_layout_pins_the_path_kind_and_field_order() {
|
|
138
142
|
let call = Call {
|
|
@@ -155,6 +159,7 @@ mod tests {
|
|
|
155
159
|
);
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
// @behavior WE-013
|
|
158
163
|
#[test]
|
|
159
164
|
fn golden_layout_pins_the_handle_kind_and_its_bare_id() {
|
|
160
165
|
let call = Call {
|
|
@@ -176,6 +181,7 @@ mod tests {
|
|
|
176
181
|
);
|
|
177
182
|
}
|
|
178
183
|
|
|
184
|
+
// @behavior WE-014
|
|
179
185
|
#[test]
|
|
180
186
|
fn an_unknown_kind_is_refused() {
|
|
181
187
|
let bytes = [9u8, 0, 0, 0, 0];
|
|
@@ -185,6 +191,7 @@ mod tests {
|
|
|
185
191
|
);
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
// @behavior WE-015
|
|
188
195
|
#[test]
|
|
189
196
|
fn handle_id_zero_is_refused() {
|
|
190
197
|
let call_bytes = {
|
|
@@ -198,6 +205,7 @@ mod tests {
|
|
|
198
205
|
);
|
|
199
206
|
}
|
|
200
207
|
|
|
208
|
+
// @behavior WE-016
|
|
201
209
|
#[test]
|
|
202
210
|
fn a_non_boolean_block_flag_is_refused() {
|
|
203
211
|
let bytes = {
|
|
@@ -211,6 +219,7 @@ mod tests {
|
|
|
211
219
|
);
|
|
212
220
|
}
|
|
213
221
|
|
|
222
|
+
// @behavior WE-017
|
|
214
223
|
#[test]
|
|
215
224
|
fn a_truncated_call_is_refused() {
|
|
216
225
|
let bytes = [KIND_PATH, 0, 0, 0, 4, b'a'];
|
|
@@ -47,6 +47,7 @@ mod tests {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// @behavior WE-047
|
|
50
51
|
#[test]
|
|
51
52
|
fn round_trips_through_the_wire() {
|
|
52
53
|
let mut w = Writer::new();
|
|
@@ -60,6 +61,7 @@ mod tests {
|
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
// @behavior WE-048
|
|
63
65
|
#[test]
|
|
64
66
|
fn an_empty_backtrace_is_legal() {
|
|
65
67
|
let record = ErrorRecord {
|
|
@@ -77,6 +79,7 @@ mod tests {
|
|
|
77
79
|
);
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// @behavior WE-049
|
|
80
83
|
#[test]
|
|
81
84
|
fn golden_layout_is_name_then_message_then_backtrace() {
|
|
82
85
|
let record = ErrorRecord {
|
|
@@ -133,6 +133,7 @@ impl Fault {
|
|
|
133
133
|
mod tests {
|
|
134
134
|
use super::*;
|
|
135
135
|
|
|
136
|
+
// @behavior WE-038
|
|
136
137
|
#[test]
|
|
137
138
|
fn every_kind_round_trips_through_the_wire() {
|
|
138
139
|
for kind in [
|
|
@@ -155,6 +156,7 @@ mod tests {
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
// @behavior WE-039
|
|
158
160
|
#[test]
|
|
159
161
|
fn a_kind_the_reader_does_not_know_degrades_to_undefined() {
|
|
160
162
|
let encoded = vec![0xfe, 0, 0, 0, 1, b'x'];
|
|
@@ -167,6 +169,7 @@ mod tests {
|
|
|
167
169
|
);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
// @behavior WE-040
|
|
170
173
|
#[test]
|
|
171
174
|
fn a_field_the_reader_does_not_know_is_skipped() {
|
|
172
175
|
let mut w = Writer::new();
|
|
@@ -182,6 +185,7 @@ mod tests {
|
|
|
182
185
|
);
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
// @behavior WE-041
|
|
185
189
|
#[test]
|
|
186
190
|
fn golden_layout_is_kind_then_message() {
|
|
187
191
|
let mut w = Writer::new();
|
|
@@ -196,6 +200,7 @@ mod tests {
|
|
|
196
200
|
);
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
// @behavior WE-042 WE-043
|
|
199
204
|
#[test]
|
|
200
205
|
fn names_map_both_ways() {
|
|
201
206
|
for kind in [
|
|
@@ -103,6 +103,7 @@ impl Snippets {
|
|
|
103
103
|
mod tests {
|
|
104
104
|
use super::*;
|
|
105
105
|
|
|
106
|
+
// @behavior WE-050
|
|
106
107
|
#[test]
|
|
107
108
|
fn empty_bindings_round_trip() {
|
|
108
109
|
let encoded = Bindings::default().encode();
|
|
@@ -113,6 +114,7 @@ mod tests {
|
|
|
113
114
|
);
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// @behavior WE-051
|
|
116
118
|
#[test]
|
|
117
119
|
fn bindings_round_trip_every_path() {
|
|
118
120
|
let bindings = Bindings {
|
|
@@ -126,6 +128,7 @@ mod tests {
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
// @behavior WE-052
|
|
129
132
|
#[test]
|
|
130
133
|
fn snippets_round_trip_both_kinds_in_order() {
|
|
131
134
|
let snippets = Snippets {
|
|
@@ -147,6 +150,7 @@ mod tests {
|
|
|
147
150
|
);
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
// @behavior WE-053
|
|
150
154
|
#[test]
|
|
151
155
|
fn an_empty_snippet_table_round_trips() {
|
|
152
156
|
let encoded = Snippets::default().encode();
|
|
@@ -157,6 +161,7 @@ mod tests {
|
|
|
157
161
|
);
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
// @behavior WE-054
|
|
160
165
|
#[test]
|
|
161
166
|
fn golden_layout_pins_the_bindings_as_a_counted_list() {
|
|
162
167
|
let bindings = Bindings {
|
|
@@ -172,6 +177,7 @@ mod tests {
|
|
|
172
177
|
);
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
// @behavior WE-055
|
|
175
181
|
#[test]
|
|
176
182
|
fn golden_layout_pins_the_snippet_entry_shape() {
|
|
177
183
|
let snippets = Snippets {
|
|
@@ -198,6 +204,7 @@ mod tests {
|
|
|
198
204
|
);
|
|
199
205
|
}
|
|
200
206
|
|
|
207
|
+
// @behavior WE-056
|
|
201
208
|
#[test]
|
|
202
209
|
fn an_unknown_snippet_kind_is_refused() {
|
|
203
210
|
let bytes = {
|
|
@@ -211,6 +218,7 @@ mod tests {
|
|
|
211
218
|
);
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
// @behavior WE-057
|
|
214
222
|
#[test]
|
|
215
223
|
fn a_snippet_count_the_frame_cannot_satisfy_is_refused() {
|
|
216
224
|
let bytes = [0xff, 0xff, 0xff, 0xff];
|
|
@@ -220,6 +228,7 @@ mod tests {
|
|
|
220
228
|
);
|
|
221
229
|
}
|
|
222
230
|
|
|
231
|
+
// @behavior WE-058
|
|
223
232
|
#[test]
|
|
224
233
|
fn trailing_bytes_after_a_frame_are_refused() {
|
|
225
234
|
let mut encoded = Bindings::default().encode();
|
|
@@ -118,6 +118,7 @@ mod tests {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// @behavior WE-027
|
|
121
122
|
#[test]
|
|
122
123
|
fn an_ok_outcome_round_trips() {
|
|
123
124
|
let outcome = Outcome::Ok(vec![0x2a]);
|
|
@@ -129,6 +130,7 @@ mod tests {
|
|
|
129
130
|
);
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
// @behavior WE-028
|
|
132
134
|
#[test]
|
|
133
135
|
fn a_panic_round_trips() {
|
|
134
136
|
let outcome = Outcome::Panic(panic_sample());
|
|
@@ -140,6 +142,7 @@ mod tests {
|
|
|
140
142
|
);
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
// @behavior WE-029
|
|
143
146
|
#[test]
|
|
144
147
|
fn a_panic_carrying_available_names_round_trips() {
|
|
145
148
|
let outcome = Outcome::Panic(Panic {
|
|
@@ -154,6 +157,7 @@ mod tests {
|
|
|
154
157
|
);
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
// @behavior WE-030
|
|
157
161
|
#[test]
|
|
158
162
|
fn a_panic_offering_no_correction_decodes_as_an_empty_list() {
|
|
159
163
|
let encoded = Outcome::Panic(panic_sample()).encode();
|
|
@@ -166,6 +170,7 @@ mod tests {
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// @behavior WE-031
|
|
169
174
|
#[test]
|
|
170
175
|
fn attribution_reads_origin_alone() {
|
|
171
176
|
let service = Panic {
|
|
@@ -180,6 +185,7 @@ mod tests {
|
|
|
180
185
|
);
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
// @behavior WE-032
|
|
183
189
|
#[test]
|
|
184
190
|
fn bytes_past_the_available_list_are_refused() {
|
|
185
191
|
let mut encoded = Outcome::Panic(panic_sample()).encode();
|
|
@@ -190,6 +196,7 @@ mod tests {
|
|
|
190
196
|
);
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
// @behavior WE-033
|
|
193
200
|
#[test]
|
|
194
201
|
fn an_unrecognised_origin_attributes_to_the_sandbox() {
|
|
195
202
|
// Written by hand: the origin field is an open set on the wire, so
|
|
@@ -207,6 +214,7 @@ mod tests {
|
|
|
207
214
|
);
|
|
208
215
|
}
|
|
209
216
|
|
|
217
|
+
// @behavior WE-034
|
|
210
218
|
#[test]
|
|
211
219
|
fn golden_layout_pins_the_ok_tag() {
|
|
212
220
|
assert_eq!(
|
|
@@ -216,6 +224,7 @@ mod tests {
|
|
|
216
224
|
);
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
// @behavior WE-035
|
|
219
228
|
#[test]
|
|
220
229
|
fn golden_layout_pins_the_panic_field_order() {
|
|
221
230
|
let panic = Panic {
|
|
@@ -243,6 +252,7 @@ mod tests {
|
|
|
243
252
|
);
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
// @behavior WE-036
|
|
246
256
|
#[test]
|
|
247
257
|
fn a_zero_length_outcome_is_refused() {
|
|
248
258
|
assert!(
|
|
@@ -251,6 +261,7 @@ mod tests {
|
|
|
251
261
|
);
|
|
252
262
|
}
|
|
253
263
|
|
|
264
|
+
// @behavior WE-037
|
|
254
265
|
#[test]
|
|
255
266
|
fn an_unknown_outcome_tag_is_refused() {
|
|
256
267
|
assert!(
|