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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +89 -0
  4. data/Cargo.lock +156 -110
  5. data/README.md +19 -18
  6. data/ROADMAP.md +12 -12
  7. data/crates/kobako-runtime/CHANGELOG.md +14 -0
  8. data/crates/kobako-runtime/Cargo.toml +2 -2
  9. data/crates/kobako-runtime/README.md +1 -1
  10. data/crates/kobako-runtime/src/profile.rs +2 -2
  11. data/crates/kobako-transport/CHANGELOG.md +14 -0
  12. data/crates/kobako-transport/Cargo.toml +1 -1
  13. data/crates/kobako-transport/README.md +1 -1
  14. data/crates/kobako-transport/src/abi.rs +2 -0
  15. data/crates/kobako-transport/src/envelope/bytes.rs +6 -0
  16. data/crates/kobako-transport/src/envelope/call.rs +9 -0
  17. data/crates/kobako-transport/src/envelope/error_record.rs +3 -0
  18. data/crates/kobako-transport/src/envelope/fault.rs +5 -0
  19. data/crates/kobako-transport/src/envelope/invocation_frames.rs +9 -0
  20. data/crates/kobako-transport/src/envelope/outcome.rs +11 -0
  21. data/crates/kobako-transport/src/envelope/reply.rs +9 -0
  22. data/crates/kobako-transport/src/envelope/run.rs +3 -0
  23. data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
  24. data/crates/kobako-wasmtime/Cargo.toml +5 -5
  25. data/crates/kobako-wasmtime/README.md +1 -1
  26. data/crates/kobako-wasmtime/src/ambient.rs +2 -0
  27. data/crates/kobako-wasmtime/src/capture.rs +8 -0
  28. data/crates/kobako-wasmtime/src/frames.rs +3 -1
  29. data/crates/kobako-wasmtime/src/guest_mem.rs +8 -0
  30. data/crates/kobako-wasmtime/src/invocation.rs +10 -0
  31. data/crates/kobako-wasmtime/src/trap.rs +6 -0
  32. data/data/kobako.wasm +0 -0
  33. data/ext/kobako/Cargo.toml +1 -1
  34. data/lib/kobako/capture.rb +8 -22
  35. data/lib/kobako/catalog/handles.rb +33 -9
  36. data/lib/kobako/catalog/services.rb +9 -7
  37. data/lib/kobako/codec/encoder.rb +4 -13
  38. data/lib/kobako/codec/handle_walk.rb +1 -1
  39. data/lib/kobako/codec/nesting.rb +43 -0
  40. data/lib/kobako/codec.rb +4 -2
  41. data/lib/kobako/context.rb +8 -8
  42. data/lib/kobako/handle.rb +10 -40
  43. data/lib/kobako/sandbox.rb +5 -0
  44. data/lib/kobako/transport/dispatcher.rb +29 -26
  45. data/lib/kobako/transport/exposure.rb +100 -0
  46. data/lib/kobako/transport/reflection.rb +19 -20
  47. data/lib/kobako/transport/run.rb +5 -2
  48. data/lib/kobako/transport/yielder.rb +6 -4
  49. data/lib/kobako/transport.rb +4 -2
  50. data/lib/kobako/usage.rb +11 -22
  51. data/lib/kobako/version.rb +1 -1
  52. data/release-please-config.json +1 -1
  53. data/sig/kobako/catalog/handles.rbs +2 -0
  54. data/sig/kobako/catalog/services.rbs +1 -1
  55. data/sig/kobako/codec/nesting.rbs +9 -0
  56. data/sig/kobako/context.rbs +1 -1
  57. data/sig/kobako/transport/dispatcher.rbs +7 -7
  58. data/sig/kobako/transport/exposure.rbs +43 -0
  59. data/sig/kobako/transport/reflection.rbs +1 -3
  60. data/sig/kobako/transport.rbs +11 -1
  61. 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 its public methods through the Transport wire. See [`docs/behavior/registration.md`](docs/behavior/registration.md) B-08..B-12.
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/registration.md`](docs/behavior/registration.md) B-62..B-63).
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/lifecycle.md`](docs/behavior/lifecycle.md) B-04). Each invocation captures its own; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
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/lifecycle.md`](docs/behavior/lifecycle.md) B-35).
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) B-22). 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.
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 (B-64).
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/runtime.md`](docs/behavior/runtime.md) B-46..B-48). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
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. B-49 bakes the shared boot state into the artifact 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)).
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) B-23..B-30.
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 (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/dispatch.md`](docs/behavior/dispatch.md) B-13..B-21, B-34, B-37).
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"> (B-37 restoration)
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 (B-15, B-17). 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:
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 (B-19).
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/invocation.md`](docs/behavior/invocation.md) B-31..B-33).
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) B-55..B-57).
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 *every* public method of the object. So bind a purpose-built object scoped to the
470
- task, not a capable one whose other methods leak more than you intend.
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 becomes an allow-list.
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/lifecycle.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | B-22 (per-Thread isolation) is exercised only indirectly through the pool contention tests; no test cites it |
13
- | ✅ [F-02 Service binding](docs/behavior/registration.md) | [lib/kobako/catalog/services.rb](lib/kobako/catalog/services.rb) | — |
14
- | ✅ [F-04 Synchronous mruby source execution (`#eval`)](docs/behavior/lifecycle.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
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/errors.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) | E-26 (guest-entry envelope decode failure) has no exercising test — not reachable through the public API |
18
- | ✅ [F-08 Guest output capture](docs/behavior/lifecycle.md) | [lib/kobako/capture.rb](lib/kobako/capture.rb) | — |
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 (`rake anchors`, double-bake byte-identity, gemspec whitelist), not `test/` |
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/invocation.md) | [lib/kobako/catalog/snippets.rb](lib/kobako/catalog/snippets.rb) | — |
24
- | ✅ [F-14 Synchronous entrypoint dispatch (`#run`)](docs/behavior/invocation.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
25
- | ✅ [F-15 Warm Sandbox pool checkout (`Kobako::Pool`)](docs/behavior/runtime.md) | [lib/kobako/pool.rb](lib/kobako/pool.rb) | — |
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.14.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.14.0", path = "../kobako-transport" }
30
+ kobako-transport = { version = "0.16.0", path = "../kobako-transport" }
@@ -26,7 +26,7 @@ map the neutral types onto their own language surface.
26
26
 
27
27
  ```toml
28
28
  [dependencies]
29
- kobako-runtime = "0.14.0" # x-release-please-version
29
+ kobako-runtime = "0.16.0" # x-release-please-version
30
30
  ```
31
31
 
32
32
  ## License
@@ -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. The governing contract
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
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  [package]
14
14
  name = "kobako-transport"
15
- version = "0.14.0"
15
+ version = "0.16.0"
16
16
  edition = "2021"
17
17
  description = "The kobako core envelope — the fixed host↔guest wire frame every assembly shares."
18
18
  license = "Apache-2.0"
@@ -35,7 +35,7 @@ the golden vectors in this crate are derived from that document.
35
35
 
36
36
  ```toml
37
37
  [dependencies]
38
- kobako-transport = "0.14.0" # x-release-please-version
38
+ kobako-transport = "0.16.0" # x-release-please-version
39
39
  ```
40
40
 
41
41
  ## License
@@ -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!(