kobako 0.16.0 → 0.18.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 +47 -0
- data/Cargo.lock +3 -3
- data/README.md +29 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/frames.rs +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/lib/kobako/catalog/extensions.rb +12 -4
- data/lib/kobako/codec/encoder.rb +8 -9
- data/lib/kobako/codec/ext_types.rb +35 -5
- data/lib/kobako/codec/handle_walk.rb +53 -9
- data/lib/kobako/codec.rb +6 -0
- data/lib/kobako/extension.rb +3 -1
- data/lib/kobako/transport/dispatcher.rb +5 -0
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -0
- data/sig/kobako/codec/ext_types.rbs +3 -0
- data/sig/kobako/codec/handle_walk.rbs +7 -3
- data/sig/kobako/codec.rbs +12 -0
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/transport/request.rbs +5 -5
- data/sig/kobako/transport/response.rbs +1 -1
- data/sig/kobako/transport/run.rbs +2 -2
- data/sig/kobako/transport/yield.rbs +1 -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: dcd0d1cd3ff81352a53d9ff9d716eda2f0a57194de86d178164be2ccadd15bee
|
|
4
|
+
data.tar.gz: 109c6b3d75f3a01e4b27463a3d9a7f6b9648fbef233d44fd026eb15336d58032
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e9216cb7655f158b2d73b15d15f17ae334f69ca472f6e1c197f00a6630c2a7f2f54d211939bffe4f9f6ddba717f4688fea6be206fac6c0b2616a0f07cc9e470
|
|
7
|
+
data.tar.gz: d619788324d2d0dca679ab7c6510e5dae1819a378a8e24dc4cbaae111d3c66316077a9c510a07763a7bc0cfbdfa7375f8d3acf65e24cdd0b116b1a957927705e
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.18.0","wasm/kobako-core":"0.10.2","wasm/kobako-mruby":"0.10.2","wasm/kobako-io":"0.10.2","wasm/kobako-json":"0.10.2","wasm/kobako-regexp":"0.10.2","wasm/kobako-baker":"0.10.2","crates/kobako-codec":"0.10.2","crates/kobako-runtime":"0.10.2","crates/kobako-wasmtime":"0.10.2","crates/kobako":"0.10.2"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.0](https://github.com/elct9620/kobako/compare/v0.17.0...v0.18.0) (2026-07-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **bench:** add bench:all whole-round sweep ([6163cdf](https://github.com/elct9620/kobako/commit/6163cdffc6aa701a0306af40cc18121787b07e26))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **guest:** partition dispatch args by Ruby 3 call semantics (B-58) ([59a15a2](https://github.com/elct9620/kobako/commit/59a15a2d524b1ab902bc5e4a2763ffccb485e399))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Performance Improvements
|
|
17
|
+
|
|
18
|
+
* **bench:** re-bless the anchor onto the codec-decomposition round ([74dca53](https://github.com/elct9620/kobako/commit/74dca5374a587242f8b1e00768dd33c330ff298c))
|
|
19
|
+
|
|
20
|
+
## [0.17.0](https://github.com/elct9620/kobako/compare/v0.16.0...v0.17.0) (2026-07-17)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **examples:** add vfs, an overlay filesystem that protects the disk ([1c83d44](https://github.com/elct9620/kobako/commit/1c83d44de971bf76d3db84cc3021b23bfc363842))
|
|
26
|
+
* **tasks:** add a rake gate aggregate over every gate:* check ([c02e069](https://github.com/elct9620/kobako/commit/c02e06996134f124025538ba5c23f923606bc3d2))
|
|
27
|
+
* **tasks:** add KobakoReport, the shared static-analysis output template ([7854a61](https://github.com/elct9620/kobako/commit/7854a61d30b823534bcad869a4e4970beab80340))
|
|
28
|
+
* **tasks:** disclose the Ruby-only scope of the coverage report ([51cb291](https://github.com/elct9620/kobako/commit/51cb291bbc45671240a63e00a1461958694e4ee1))
|
|
29
|
+
* **tasks:** disclose the tail below the hotspots top-N cut ([d7584ee](https://github.com/elct9620/kobako/commit/d7584ee610e347dcef9cb2515680c97c911320e5))
|
|
30
|
+
* **tasks:** gate the pub-surface acknowledgement ledger for staleness ([5f97c77](https://github.com/elct9620/kobako/commit/5f97c773f300363e387ff8020523f141cdaaf88f))
|
|
31
|
+
* **tasks:** gate the RBS collection lock against Gemfile.lock drift ([8201ce0](https://github.com/elct9620/kobako/commit/8201ce03848d9710239c586321a6ffb75de1950d))
|
|
32
|
+
* **tasks:** measure Rust line coverage with cargo llvm-cov ([1f6bd9f](https://github.com/elct9620/kobako/commit/1f6bd9f2876f3ef6d8e60880c40198eb70327bf4))
|
|
33
|
+
* **tasks:** open the stats reports with a self-describing banner ([914d0cf](https://github.com/elct9620/kobako/commit/914d0cf74e338a0a7405cc6f981135defac534f1))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Bug Fixes
|
|
37
|
+
|
|
38
|
+
* **catalog:** match Extension dependencies by Symbol form ([57f17b8](https://github.com/elct9620/kobako/commit/57f17b872e506f3935ebd4ca1f9d81c4bc34455f))
|
|
39
|
+
* **codec:** cap the host wrap-walk nesting depth (E-54) ([be67f37](https://github.com/elct9620/kobako/commit/be67f37108f24699354950f4e72293169da66917))
|
|
40
|
+
* **codec:** keep the guard id out of the wire-symmetry inventory ([e51998b](https://github.com/elct9620/kobako/commit/e51998bc7d54715aecb58f3b7817c337cd882c8b))
|
|
41
|
+
* **codec:** reject a non-representable #run argument Hash key cleanly ([dbabace](https://github.com/elct9620/kobako/commit/dbabaceecb6f32f37a26839b8162c0e94fe881bb))
|
|
42
|
+
* **codec:** reject non-wire values via a factory guard, not to_msgpack ([8e5c6b8](https://github.com/elct9620/kobako/commit/8e5c6b8cb67faa2eb7b82d6e63e3f8464b1126a0))
|
|
43
|
+
* **codec:** tighten map-decode pre-allocation to the true pair bound ([7094f91](https://github.com/elct9620/kobako/commit/7094f91898dae4c91c4e6863b09d25a6f34d096e))
|
|
44
|
+
* **examples:** contain overlay writes and prove the vfs traversal guard ([2fe7965](https://github.com/elct9620/kobako/commit/2fe7965b8f588b160a907771a14e51f80cd8d3a7))
|
|
45
|
+
* **examples:** contain vfs reads against symlink escape ([43317c8](https://github.com/elct9620/kobako/commit/43317c82c4a4171c08fdbf0c9fe7014cc249b926))
|
|
46
|
+
* **guest:** reject non-representable dispatch args instead of to_s (E-55) ([6b9ab56](https://github.com/elct9620/kobako/commit/6b9ab562753407cf6168a5baad34140afd75a86a))
|
|
47
|
+
* **tasks:** land the gate move for parity and wire:symmetry ([a0dddbc](https://github.com/elct9620/kobako/commit/a0dddbca9638418dbf3cd08e29806e05c5198cd3))
|
|
48
|
+
* **tasks:** resync the pub-surface ledger to the renamed install cluster ([c6a6c33](https://github.com/elct9620/kobako/commit/c6a6c33f5802f4536a4ebcc2df36acc130059d89))
|
|
49
|
+
|
|
3
50
|
## [0.16.0](https://github.com/elct9620/kobako/compare/v0.15.0...v0.16.0) (2026-07-12)
|
|
4
51
|
|
|
5
52
|
|
data/Cargo.lock
CHANGED
|
@@ -929,7 +929,7 @@ dependencies = [
|
|
|
929
929
|
|
|
930
930
|
[[package]]
|
|
931
931
|
name = "kobako"
|
|
932
|
-
version = "0.
|
|
932
|
+
version = "0.18.0"
|
|
933
933
|
dependencies = [
|
|
934
934
|
"kobako-runtime",
|
|
935
935
|
"kobako-wasmtime",
|
|
@@ -938,11 +938,11 @@ dependencies = [
|
|
|
938
938
|
|
|
939
939
|
[[package]]
|
|
940
940
|
name = "kobako-runtime"
|
|
941
|
-
version = "0.10.
|
|
941
|
+
version = "0.10.2"
|
|
942
942
|
|
|
943
943
|
[[package]]
|
|
944
944
|
name = "kobako-wasmtime"
|
|
945
|
-
version = "0.10.
|
|
945
|
+
version = "0.10.2"
|
|
946
946
|
dependencies = [
|
|
947
947
|
"kobako-runtime",
|
|
948
948
|
"libc",
|
data/README.md
CHANGED
|
@@ -382,6 +382,34 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
382
382
|
|
|
383
383
|
Use the source form for snippets authored in your repo; use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Both replay through the same per-invocation path, so no snippet content failure surfaces at `#preload` — force the first replay with a no-op invocation (e.g. `sandbox.eval("nil")`) when you want validation before real traffic.
|
|
384
384
|
|
|
385
|
+
### Extensions
|
|
386
|
+
|
|
387
|
+
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).
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
FILE = <<~'MRUBY'
|
|
391
|
+
class File < Kobako::Member
|
|
392
|
+
def self.basename(path) = path.split("/").last || "" # pure: runs in-guest
|
|
393
|
+
# read / write are undefined here, so they dispatch to the backend
|
|
394
|
+
end
|
|
395
|
+
MRUBY
|
|
396
|
+
|
|
397
|
+
sandbox.install(
|
|
398
|
+
Kobako::Extension.new(
|
|
399
|
+
name: :File,
|
|
400
|
+
source: FILE,
|
|
401
|
+
backend: Kobako::Extension::Backend.new(
|
|
402
|
+
path: "File",
|
|
403
|
+
provider: -> { OverlayFileSystem.new(root) } # callable → fresh per invocation
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
sandbox.eval('File.read("sample.txt")') # dispatches to the backend's #read
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The `backend.provider` sets the bound object's lifetime: a fixed object is shared across every invocation, while a callable is invoked once per invocation to yield a fresh object — the form a writable backend needs, so its state cannot leak across calls. kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked `File` that reads through to disk while protecting it from guest writes.
|
|
412
|
+
|
|
385
413
|
## Security
|
|
386
414
|
|
|
387
415
|
kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
|
|
@@ -427,7 +455,7 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
427
455
|
|
|
428
456
|
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~33 MB total. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 17k `#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.
|
|
429
457
|
|
|
430
|
-
Regexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#
|
|
458
|
+
Regexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#11` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
|
|
431
459
|
|
|
432
460
|
```bash
|
|
433
461
|
bundle exec rake bench # six gated regression benchmarks (~5-8 min)
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.2](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.10.1...kobako-runtime-v0.10.2) (2026-07-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
10
|
+
## [0.10.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.10.0...kobako-runtime-v0.10.1) (2026-07-17)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
16
|
+
|
|
3
17
|
## [0.10.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.9.0...kobako-runtime-v0.10.0) (2026-07-12)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.2](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.10.1...kobako-wasmtime-v0.10.2) (2026-07-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
10
|
+
## [0.10.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.10.0...kobako-wasmtime-v0.10.1) (2026-07-17)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
16
|
+
|
|
3
17
|
## [0.10.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.9.0...kobako-wasmtime-v0.10.0) (2026-07-12)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
[package]
|
|
17
17
|
name = "kobako-wasmtime"
|
|
18
|
-
version = "0.10.
|
|
18
|
+
version = "0.10.2"
|
|
19
19
|
edition = "2021"
|
|
20
20
|
description = "wasmtime implementation of the kobako host runtime contract."
|
|
21
21
|
license = "Apache-2.0"
|
|
@@ -28,7 +28,7 @@ categories = ["wasm", "virtualization"]
|
|
|
28
28
|
# The engine-neutral contract this crate implements. The version pin
|
|
29
29
|
# rides the linked release group; the path keeps in-tree builds (and
|
|
30
30
|
# the Ruby gem, which ships both crates) resolving locally.
|
|
31
|
-
kobako-runtime = { version = "0.10.
|
|
31
|
+
kobako-runtime = { version = "0.10.2", path = "../kobako-runtime" }
|
|
32
32
|
# wasmtime — host-side embedder for kobako.wasm. We disable default-features
|
|
33
33
|
# and opt back in only what kobako needs: a Cranelift-backed runtime that can
|
|
34
34
|
# compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
|
|
@@ -44,7 +44,7 @@ pub(crate) fn write_envelope(
|
|
|
44
44
|
let memory = require_memory(exports)?;
|
|
45
45
|
|
|
46
46
|
let ptr = alloc
|
|
47
|
-
.call(store.as_context_mut(),
|
|
47
|
+
.call(store.as_context_mut(), len_i32 as u32)
|
|
48
48
|
.map_err(|e| Trap::Other(format!("failed to allocate input buffer: {e}")))?;
|
|
49
49
|
if ptr == 0 {
|
|
50
50
|
return Err(SetupError::Intact(
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/ext/kobako/Cargo.toml
CHANGED
|
@@ -23,8 +23,11 @@ module Kobako
|
|
|
23
23
|
# preloaded snippet, and — when it carries a +backend+ — reserve the
|
|
24
24
|
# backend path in +services+. A callable provider reserves the path
|
|
25
25
|
# with a placeholder the per-invocation refresh replaces; a fixed
|
|
26
|
-
# provider binds its object directly. Raises +ArgumentError+
|
|
27
|
-
#
|
|
26
|
+
# provider binds its object directly. Raises +ArgumentError+ for the two
|
|
27
|
+
# shapes +#validate!+ owns — +source+ not a String, or a present
|
|
28
|
+
# +backend+ that omits +path+ / +provider+. The Extension readers are
|
|
29
|
+
# duck-typed, so an object missing them surfaces the underlying
|
|
30
|
+
# +NoMethodError+, and a malformed +name+ or +backend.path+ surfaces
|
|
28
31
|
# through the +snippets+ / +services+ registration it routes to.
|
|
29
32
|
def install(extension, snippets:, services:)
|
|
30
33
|
validate!(extension)
|
|
@@ -92,10 +95,10 @@ module Kobako
|
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
def assert_dependencies!
|
|
95
|
-
names = @entries.map(
|
|
98
|
+
names = @entries.map { |extension| symbolize(extension.name) }
|
|
96
99
|
@entries.each do |extension|
|
|
97
100
|
(extension.depends_on || []).each do |dependency|
|
|
98
|
-
next if names.include?(dependency)
|
|
101
|
+
next if names.include?(symbolize(dependency))
|
|
99
102
|
|
|
100
103
|
raise ArgumentError,
|
|
101
104
|
"Extension #{extension.name.inspect} depends on #{dependency.inspect}, which is not installed"
|
|
@@ -103,6 +106,11 @@ module Kobako
|
|
|
103
106
|
end
|
|
104
107
|
end
|
|
105
108
|
|
|
109
|
+
# Match names and dependencies by Symbol so the Symbol-or-String forms
|
|
110
|
+
# of a constant token are interchangeable; a value that is neither
|
|
111
|
+
# falls through unchanged to the not-installed path.
|
|
112
|
+
def symbolize(name) = name.is_a?(String) ? name.to_sym : name
|
|
113
|
+
|
|
106
114
|
def callable?(provider) = provider.respond_to?(:call)
|
|
107
115
|
|
|
108
116
|
# A callable provider reserves its path with a placeholder that the
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -22,15 +22,14 @@ module Kobako
|
|
|
22
22
|
# themselves.
|
|
23
23
|
module Encoder
|
|
24
24
|
# Encode +value+ to wire bytes (binary-encoded String).
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# +
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# +NoMethodError
|
|
33
|
-
# propagating.
|
|
25
|
+
# SPEC's 12-entry type mapping is a closed set: a value outside it is
|
|
26
|
+
# rejected as +UnsupportedType+ by the factory's +BasicObject+ guard
|
|
27
|
+
# (ExtTypes#register_unrepresentable), which raises before the msgpack
|
|
28
|
+
# gem can route the value through +to_msgpack+ — so a permissive
|
|
29
|
+
# +method_missing+ object cannot answer that probe and mis-encode. The
|
|
30
|
+
# rescue below maps the two violations the guard does not reach onto the
|
|
31
|
+
# same error: an integer outside i64..u64 (+RangeError+) and any
|
|
32
|
+
# packer-internal +NoMethodError+.
|
|
34
33
|
def self.encode(value)
|
|
35
34
|
FACTORY.dump(value)
|
|
36
35
|
rescue ::RangeError, ::NoMethodError => e
|
|
@@ -33,18 +33,28 @@ module Kobako
|
|
|
33
33
|
EXT_ERRENV = 0x02
|
|
34
34
|
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
35
35
|
|
|
36
|
+
# Inert ext id the unrepresentable-value guard registers under. It is
|
|
37
|
+
# never emitted (the guard's packer always raises) and never decoded
|
|
38
|
+
# (no unpacker is registered, so the id stays an UnknownExtTypeError on
|
|
39
|
+
# the wire), so it is not a wire ext type: deliberately not named
|
|
40
|
+
# +EXT_*+ like the three real ext codes, since it has no Rust-side mirror
|
|
41
|
+
# and must stay outside the wire-symmetry inventory.
|
|
42
|
+
UNREPRESENTABLE_GUARD_ID = 0x7F
|
|
43
|
+
private_constant :UNREPRESENTABLE_GUARD_ID
|
|
44
|
+
|
|
36
45
|
module_function
|
|
37
46
|
|
|
38
|
-
# Assemble a +MessagePack::Factory+ with the three kobako ext types
|
|
39
|
-
# registered, frozen because
|
|
40
|
-
# happens exactly once. The
|
|
41
|
-
# per-operation state at call time,
|
|
42
|
-
# every thread.
|
|
47
|
+
# Assemble a +MessagePack::Factory+ with the three kobako ext types plus
|
|
48
|
+
# the unrepresentable-value guard registered, frozen because
|
|
49
|
+
# registration is its only mutation and happens exactly once. The
|
|
50
|
+
# stateful conversions resolve their per-operation state at call time,
|
|
51
|
+
# so one registered factory serves every thread.
|
|
43
52
|
def build_factory
|
|
44
53
|
factory = MessagePack::Factory.new
|
|
45
54
|
register_symbol(factory)
|
|
46
55
|
register_handle(factory)
|
|
47
56
|
register_fault(factory)
|
|
57
|
+
register_unrepresentable(factory)
|
|
48
58
|
factory.freeze
|
|
49
59
|
end
|
|
50
60
|
|
|
@@ -157,6 +167,26 @@ module Kobako
|
|
|
157
167
|
unpacker: ->(payload) { ExtTypes.unpack_fault(payload, State.current) }
|
|
158
168
|
)
|
|
159
169
|
end
|
|
170
|
+
|
|
171
|
+
# A catch-all packer that rejects any value with no wire representation
|
|
172
|
+
# as +UnsupportedType+. Registered on +BasicObject+ so it also covers
|
|
173
|
+
# BasicObject-based proxies; the narrower Symbol / Handle / Fault
|
|
174
|
+
# registrations still win by most-specific match, and native types never
|
|
175
|
+
# reach it. Packer-only: the guard never writes bytes, so its id is inert
|
|
176
|
+
# and the decode surface stays fail-closed.
|
|
177
|
+
#
|
|
178
|
+
# This makes the host's non-wire detection a positive allowlist — a value
|
|
179
|
+
# outside the type set is rejected here rather than routed to +to_msgpack+
|
|
180
|
+
# — matching the guest's classname allowlist and the Rust codec's closed
|
|
181
|
+
# +Value+ enum. Without it, a value with a permissive +method_missing+
|
|
182
|
+
# answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
|
|
183
|
+
# of crossing as a Capability Handle.
|
|
184
|
+
def register_unrepresentable(factory)
|
|
185
|
+
factory.register_type(
|
|
186
|
+
UNREPRESENTABLE_GUARD_ID, BasicObject,
|
|
187
|
+
packer: ->(_value) { raise UnsupportedType, "value has no wire representation" }
|
|
188
|
+
)
|
|
189
|
+
end
|
|
160
190
|
end
|
|
161
191
|
|
|
162
192
|
# The process-wide registered factory: ext registration is paid once at
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../handle"
|
|
4
|
+
require_relative "../errors"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
module Codec
|
|
@@ -42,8 +43,16 @@ module Kobako
|
|
|
42
43
|
# unsigned-64 union are rejected so the predicate agrees with the
|
|
43
44
|
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
44
45
|
# already surfaces as UnsupportedType.
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
#
|
|
47
|
+
# The optional +depth+ bounds the recursive descent at the wire's
|
|
48
|
+
# structural nesting cap so a self-referential container — reachable
|
|
49
|
+
# as a cyclic Hash key on the +#run+ argument path — reads as
|
|
50
|
+
# non-representable rather than looping, letting #deep_wrap_hash
|
|
51
|
+
# refuse it instead of overflowing this walk.
|
|
52
|
+
def representable?(value, depth = 0)
|
|
53
|
+
return false if depth > MAX_NESTING_DEPTH
|
|
54
|
+
|
|
55
|
+
primitive_type?(value) || container_representable?(value, depth)
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
@@ -53,7 +62,11 @@ module Kobako
|
|
|
53
62
|
# (Array, Hash) one structural level at a time; a non-representable
|
|
54
63
|
# leaf is wrapped as-is without inspecting its internal structure.
|
|
55
64
|
# An existing +Kobako::Handle+ is representable and passes through
|
|
56
|
-
# unchanged — auto-wrap never re-wraps a Handle.
|
|
65
|
+
# unchanged — auto-wrap never re-wraps a Handle. Only Hash *values*
|
|
66
|
+
# are wrapped: a Hash *key* must already be wire-representable, since a
|
|
67
|
+
# key is not auto-wrapped the way a value is — a non-representable key
|
|
68
|
+
# raises +Kobako::SandboxError+ rather than crossing as an opaque token
|
|
69
|
+
# the guest→host restore walk could not round-trip.
|
|
57
70
|
#
|
|
58
71
|
# +value+ may be any Ruby value; +handler+ must respond to
|
|
59
72
|
# +#alloc(object) -> Kobako::Handle+ (a host-side
|
|
@@ -64,15 +77,46 @@ module Kobako
|
|
|
64
77
|
# Recursive calls spell the +HandleWalk.+ receiver so the dispatch
|
|
65
78
|
# stays valid even when a block is captured and run under a
|
|
66
79
|
# different +self+ (+module_function+ privatizes the instance copies).
|
|
67
|
-
def deep_wrap(value, handler)
|
|
80
|
+
def deep_wrap(value, handler, depth = 0)
|
|
81
|
+
guard_nesting!(depth)
|
|
68
82
|
case value
|
|
69
|
-
when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler) }
|
|
70
|
-
when ::Hash then
|
|
83
|
+
when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler, depth + 1) }
|
|
84
|
+
when ::Hash then HandleWalk.deep_wrap_hash(value, handler, depth)
|
|
71
85
|
else
|
|
72
86
|
representable?(value) ? value : handler.alloc(value)
|
|
73
87
|
end
|
|
74
88
|
end
|
|
75
89
|
|
|
90
|
+
# Refuse a wrap walk that has descended past the wire's structural
|
|
91
|
+
# nesting bound — a reference cycle necessarily trips it — so a +#run+
|
|
92
|
+
# argument fails as a clean +Kobako::SandboxError+ rather than an
|
|
93
|
+
# unbounded host recursion.
|
|
94
|
+
def guard_nesting!(depth)
|
|
95
|
+
return unless depth > MAX_NESTING_DEPTH
|
|
96
|
+
|
|
97
|
+
raise Kobako::SandboxError,
|
|
98
|
+
"a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels and " \
|
|
99
|
+
"cannot cross the sandbox boundary (possible reference cycle)"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Wrap a Hash on the host→guest argument path: each value's
|
|
103
|
+
# non-representable leaves become Handles, while each key must be
|
|
104
|
+
# wire-representable as-is. A stateful object may cross the boundary as
|
|
105
|
+
# a Hash value but not as a key — the one deliberate asymmetry with the
|
|
106
|
+
# guest→host restore walk, which resolves Handle keys the guest built.
|
|
107
|
+
def deep_wrap_hash(hash, handler, depth)
|
|
108
|
+
wrapped = {} # : Hash[untyped, untyped]
|
|
109
|
+
hash.each do |key, val|
|
|
110
|
+
unless HandleWalk.representable?(key, depth + 1)
|
|
111
|
+
raise Kobako::SandboxError,
|
|
112
|
+
"a Hash passed to #run has a key that cannot cross the sandbox boundary " \
|
|
113
|
+
"(#{key.class}); only wire-representable values may be Hash keys"
|
|
114
|
+
end
|
|
115
|
+
wrapped[key] = HandleWalk.deep_wrap(val, handler, depth + 1)
|
|
116
|
+
end
|
|
117
|
+
wrapped
|
|
118
|
+
end
|
|
119
|
+
|
|
76
120
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
77
121
|
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
78
122
|
# it to. The symmetric inverse of #deep_wrap: that walk allocates objects
|
|
@@ -118,10 +162,10 @@ module Kobako
|
|
|
118
162
|
# elements and Hash key+value pairs through the public
|
|
119
163
|
# #representable?. Not part of the public surface; reach for
|
|
120
164
|
# #representable? instead.
|
|
121
|
-
def container_representable?(value)
|
|
165
|
+
def container_representable?(value, depth)
|
|
122
166
|
case value
|
|
123
|
-
when ::Array then value.all? { |element| HandleWalk.representable?(element) }
|
|
124
|
-
when ::Hash then value.all? { |
|
|
167
|
+
when ::Array then value.all? { |element| HandleWalk.representable?(element, depth + 1) }
|
|
168
|
+
when ::Hash then value.all? { |pair| pair.all? { |member| HandleWalk.representable?(member, depth + 1) } }
|
|
125
169
|
else false
|
|
126
170
|
end
|
|
127
171
|
end
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -28,6 +28,12 @@ module Kobako
|
|
|
28
28
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
29
29
|
# +codec::EXT_ERRENV+ on that side.
|
|
30
30
|
module Codec
|
|
31
|
+
# The maximum structural nesting depth the wire represents (the
|
|
32
|
+
# MessagePack ecosystem's bound), shared with the guest +kobako_codec+
|
|
33
|
+
# so both sides cap identically. The host→guest wrap walk refuses a
|
|
34
|
+
# +#run+ argument nesting past it — a reference cycle necessarily does.
|
|
35
|
+
MAX_NESTING_DEPTH = 128
|
|
36
|
+
|
|
31
37
|
# Bracket a decode and return the block's result together with whether
|
|
32
38
|
# the decoded tree carried an ext 0x01 Capability Handle — the signal a
|
|
33
39
|
# dispatch path uses to skip an all-identity Handle-resolution walk.
|
data/lib/kobako/extension.rb
CHANGED
|
@@ -34,7 +34,9 @@ module Kobako
|
|
|
34
34
|
# invocation to yield that invocation's object, so a fresh object backs
|
|
35
35
|
# the path every invocation. Callability is the sole discriminator — a
|
|
36
36
|
# fixed backend that is itself callable is supplied through a
|
|
37
|
-
# non-callable wrapper.
|
|
37
|
+
# non-callable wrapper. A callable provider that raises propagates its
|
|
38
|
+
# exception to the invocation caller and leaves the guest unrun; the
|
|
39
|
+
# next invocation resolves it afresh.
|
|
38
40
|
class Backend < Data.define(:path, :provider)
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -87,6 +87,11 @@ module Kobako
|
|
|
87
87
|
args, kwargs = resolve_call_args(request, handler, carried_handle)
|
|
88
88
|
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given
|
|
89
89
|
encode_ok(catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }, handler)
|
|
90
|
+
# StandardError is the boundary by intent: a Service method's
|
|
91
|
+
# application fault folds into a guest-rescuable Response.error, while a
|
|
92
|
+
# host-process failure (NoMemoryError, SignalException, a bare Exception)
|
|
93
|
+
# stays uncaught and traps the invocation rather than being masked as a
|
|
94
|
+
# rescuable fault.
|
|
90
95
|
rescue StandardError => e
|
|
91
96
|
encode_caught_error(e)
|
|
92
97
|
ensure
|
data/lib/kobako/version.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Kobako
|
|
|
4
4
|
EXT_SYMBOL: Integer
|
|
5
5
|
EXT_HANDLE: Integer
|
|
6
6
|
EXT_ERRENV: Integer
|
|
7
|
+
UNREPRESENTABLE_GUARD_ID: Integer
|
|
7
8
|
|
|
8
9
|
def self?.build_factory: () -> MessagePack::Factory
|
|
9
10
|
|
|
@@ -24,6 +25,8 @@ module Kobako
|
|
|
24
25
|
def self?.register_handle: (MessagePack::Factory factory) -> void
|
|
25
26
|
|
|
26
27
|
def self?.register_fault: (MessagePack::Factory factory) -> void
|
|
28
|
+
|
|
29
|
+
def self?.register_unrepresentable: (MessagePack::Factory factory) -> void
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
FACTORY: MessagePack::Factory
|
|
@@ -3,15 +3,19 @@ module Kobako
|
|
|
3
3
|
module HandleWalk
|
|
4
4
|
MSGPACK_INT_RANGE: Range[Integer]
|
|
5
5
|
|
|
6
|
-
def self?.representable?: (untyped value) -> bool
|
|
6
|
+
def self?.representable?: (untyped value, ?Integer depth) -> bool
|
|
7
7
|
|
|
8
|
-
def self?.deep_wrap: (untyped value, Kobako::Codec::_HandleTable handler) -> untyped
|
|
8
|
+
def self?.deep_wrap: (untyped value, Kobako::Codec::_HandleTable handler, ?Integer depth) -> untyped
|
|
9
|
+
|
|
10
|
+
def self?.guard_nesting!: (Integer depth) -> void
|
|
11
|
+
|
|
12
|
+
def self?.deep_wrap_hash: (Hash[untyped, untyped] hash, Kobako::Codec::_HandleTable handler, Integer depth) -> Hash[untyped, untyped]
|
|
9
13
|
|
|
10
14
|
def self?.deep_restore: (untyped value, Kobako::Codec::_HandleTable handler) -> untyped
|
|
11
15
|
|
|
12
16
|
def self?.primitive_type?: (untyped value) -> bool
|
|
13
17
|
|
|
14
|
-
def self?.container_representable?: (untyped value) -> bool
|
|
18
|
+
def self?.container_representable?: (untyped value, Integer depth) -> bool
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
end
|
data/sig/kobako/codec.rbs
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
module Kobako
|
|
2
2
|
module Codec
|
|
3
|
+
MAX_NESTING_DEPTH: Integer
|
|
4
|
+
|
|
5
|
+
# A value in the closed wire type set (docs/wire-codec.md § Type
|
|
6
|
+
# Mapping): the scalar leaves, a Capability Handle, and Array / Hash
|
|
7
|
+
# containers of the same. The by-value payloads that cross the wire —
|
|
8
|
+
# dispatch arguments and success values — inhabit this type; a Fault
|
|
9
|
+
# (ext 0x02) is an envelope-position value, not a wire_value.
|
|
10
|
+
type wire_value = nil | bool | Integer | Float | String | Symbol
|
|
11
|
+
| Kobako::Handle
|
|
12
|
+
| Array[wire_value]
|
|
13
|
+
| Hash[wire_value, wire_value]
|
|
14
|
+
|
|
3
15
|
def self.track_handles: [T] { () -> T } -> [T, bool]
|
|
4
16
|
def self.forbid_faults: [T] { () -> T } -> T
|
|
5
17
|
|
data/sig/kobako/outcome.rbs
CHANGED
|
@@ -3,19 +3,19 @@ module Kobako
|
|
|
3
3
|
class Request < Data
|
|
4
4
|
attr_reader target: String | Kobako::Handle
|
|
5
5
|
attr_reader method_name: String
|
|
6
|
-
attr_reader args: Array[
|
|
7
|
-
attr_reader kwargs: Hash[Symbol,
|
|
6
|
+
attr_reader args: Array[Kobako::Codec::wire_value]
|
|
7
|
+
attr_reader kwargs: Hash[Symbol, Kobako::Codec::wire_value]
|
|
8
8
|
attr_reader block_given: bool
|
|
9
9
|
|
|
10
|
-
def self.new: (target: String | Kobako::Handle, method_name: String, ?args: Array[
|
|
10
|
+
def self.new: (target: String | Kobako::Handle, method_name: String, ?args: Array[Kobako::Codec::wire_value], ?kwargs: Hash[Symbol, Kobako::Codec::wire_value], ?block_given: bool) -> Request
|
|
11
11
|
|
|
12
12
|
def self.decode: (String bytes) -> Request
|
|
13
13
|
|
|
14
|
-
def initialize: (target: String | Kobako::Handle, method_name: String, ?args: Array[
|
|
14
|
+
def initialize: (target: String | Kobako::Handle, method_name: String, ?args: Array[Kobako::Codec::wire_value], ?kwargs: Hash[Symbol, Kobako::Codec::wire_value], ?block_given: bool) -> void
|
|
15
15
|
|
|
16
16
|
def encode: () -> String
|
|
17
17
|
|
|
18
|
-
def with: (?target: String | Kobako::Handle, ?method_name: String, ?args: Array[
|
|
18
|
+
def with: (?target: String | Kobako::Handle, ?method_name: String, ?args: Array[Kobako::Codec::wire_value], ?kwargs: Hash[Symbol, Kobako::Codec::wire_value], ?block_given: bool) -> Request
|
|
19
19
|
|
|
20
20
|
def ==: (untyped other) -> bool
|
|
21
21
|
|
|
@@ -4,8 +4,8 @@ module Kobako
|
|
|
4
4
|
NAME_PATTERN: Regexp
|
|
5
5
|
|
|
6
6
|
attr_reader entrypoint: Symbol
|
|
7
|
-
attr_reader args: Array[
|
|
8
|
-
attr_reader kwargs: Hash[Symbol,
|
|
7
|
+
attr_reader args: Array[Kobako::Codec::wire_value]
|
|
8
|
+
attr_reader kwargs: Hash[Symbol, Kobako::Codec::wire_value]
|
|
9
9
|
|
|
10
10
|
def self.new: (entrypoint: Symbol | String, ?args: Array[untyped], ?kwargs: Hash[untyped, untyped]) -> Run
|
|
11
11
|
|