kobako 0.21.1-aarch64-linux → 0.22.0-aarch64-linux
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 +21 -0
- data/README.md +9 -9
- data/crates/kobako-runtime/CHANGELOG.md +7 -0
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-transport/CHANGELOG.md +13 -0
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
- data/crates/kobako-wasmtime/README.md +1 -1
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/codec/encoder.rb +14 -0
- data/lib/kobako/codec/error.rb +2 -1
- data/lib/kobako/errors.rb +34 -1
- data/lib/kobako/outcome.rb +17 -9
- data/lib/kobako/transport/dispatcher.rb +63 -10
- data/lib/kobako/transport/yielder.rb +40 -4
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/errors.rbs +13 -0
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +8 -1
- data/sig/kobako/transport/yielder.rbs +7 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31ada84079df29e6403f69a5d3a5dcf6dd8780430a4063cbc3576399272904f8
|
|
4
|
+
data.tar.gz: 3f66558a59dde0642cc6fa8b4871c009e2aaf6efb9efb973e74a45e5158fa9c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e839baee0544f72426b9887a745a564bf8350370beca72f4dd3e230fb71b568682bb6e7794acad502e5e27c984cafe9fe7ffb14f0d5ab8ea6cbbc2e863a73439
|
|
7
|
+
data.tar.gz: c54343f8b35e7e340ff90155c23e185f1be3888226675aa0f5a1fb6ac6912a77f9bff5b69f8db8583c622eee5b3a39666dca6c3d780fd5f8cc977571c6a2cc35
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.22.0","wasm/kobako-core":"0.14.0","wasm/kobako-mruby":"0.14.0","wasm/kobako-io":"0.14.0","wasm/kobako-json":"0.14.0","wasm/kobako-regexp":"0.14.0","wasm/kobako-baker":"0.14.0","crates/kobako-transport":"0.14.0","crates/kobako-codec":"0.14.0","crates/kobako-runtime":"0.14.0","crates/kobako-wasmtime":"0.14.0","crates/kobako":"0.14.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.0](https://github.com/elct9620/kobako/compare/v0.21.1...v0.22.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **spec:** give the ubiquitous language a place N-6 can be checked from ([ab44dba](https://github.com/elct9620/kobako/commit/ab44dba0b08e558a5b96b95b3c4d1c70bf6bc7ad))
|
|
9
|
+
* **transport:** give a failed Service call a class that says why ([b39f176](https://github.com/elct9620/kobako/commit/b39f1761e9922b7ac761571393f9a6f4478ae522))
|
|
10
|
+
* **transport:** let a guest block's exception continue as itself ([c8b1de7](https://github.com/elct9620/kobako/commit/c8b1de7154336fab8fa20c0d7a5b3baaad11d551))
|
|
11
|
+
* **transport:** separate a failed exchange from a failed Service ([fb92515](https://github.com/elct9620/kobako/commit/fb92515fc76ffebbb8491b525d53b1725cf97dd0))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **codec:** keep a value the packer cannot walk inside the codec taxonomy ([ac99d58](https://github.com/elct9620/kobako/commit/ac99d58f0c9a274b230d645d7c37aba88516eab2))
|
|
17
|
+
* **regexp:** stop raising where MRI answers no match ([d229974](https://github.com/elct9620/kobako/commit/d2299740a2a0206c88e7c397cdb72dc730c4c1cf))
|
|
18
|
+
* **transport:** answer a held block failure only to the block that raised ([24dfd6e](https://github.com/elct9620/kobako/commit/24dfd6ec999e0ba1587b9c69da6d2b29563fc875))
|
|
19
|
+
* **transport:** report a yield the host cannot write as the Service's ([19459f8](https://github.com/elct9620/kobako/commit/19459f87555190043127290441bf6b95bcad93d6))
|
|
20
|
+
* **transport:** report an answer the host cannot write as the Service's ([bff4cdc](https://github.com/elct9620/kobako/commit/bff4cdccc076859d7da43f5dedb9a283ffed94ee))
|
|
21
|
+
* **transport:** spend a block's failure when its Service yields again ([3042a72](https://github.com/elct9620/kobako/commit/3042a72b9e3bb0d6a1d5cb5734d7eaee6a669e85))
|
|
22
|
+
* **wire:** let a Fault reader survive what it predates ([eeabfb7](https://github.com/elct9620/kobako/commit/eeabfb7aa4de3d08852ca1b41517b924d72a7312))
|
|
23
|
+
|
|
3
24
|
## [0.21.1](https://github.com/elct9620/kobako/compare/v0.21.0...v0.21.1) (2026-07-30)
|
|
4
25
|
|
|
5
26
|
|
data/README.md
CHANGED
|
@@ -321,7 +321,7 @@ For workloads that must be isolated from each other (one Sandbox per tenant, per
|
|
|
321
321
|
|
|
322
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.
|
|
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
|
|
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)).
|
|
325
325
|
|
|
326
326
|
```ruby
|
|
327
327
|
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
@@ -498,17 +498,17 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
498
498
|
| Phase | Cost |
|
|
499
499
|
|--------------------------------------------------------------|-----------------------|
|
|
500
500
|
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
501
|
-
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time
|
|
502
|
-
| Subsequent `Sandbox.new` (caches warm) | ~
|
|
503
|
-
| Warm `#eval
|
|
504
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~
|
|
505
|
-
| Service call amortized inside one invocation | ~
|
|
506
|
-
| Snippet replay per invocation | ~7.
|
|
501
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~1.3 ms one-time |
|
|
502
|
+
| Subsequent `Sandbox.new` (caches warm) | ~3 µs |
|
|
503
|
+
| Warm `#eval` with one Service call on a reused Sandbox | ~71 µs |
|
|
504
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~81 µs |
|
|
505
|
+
| Service call amortized inside one invocation | ~5.7 µs |
|
|
506
|
+
| Snippet replay per invocation | ~7.5 µs each |
|
|
507
507
|
| Per additional idle Sandbox (RSS) | ~1 KB |
|
|
508
508
|
|
|
509
|
-
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. Under the default `gvl: :hold`, wasm work is GVL-serialized: aggregate throughput stays around
|
|
509
|
+
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. Under the default `gvl: :hold`, wasm work is GVL-serialized: aggregate throughput stays around 16k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. Opting a Sandbox into `gvl: :release` lifts that ceiling for compute-bound scripts (see [Concurrency](#concurrency)). A +10% regression on any SPEC-mandated benchmark blocks release.
|
|
510
510
|
|
|
511
|
-
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.
|
|
511
|
+
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.5× `match?` (~1.1 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
|
|
512
512
|
|
|
513
513
|
```bash
|
|
514
514
|
bundle exec rake bench # every gated regression benchmark (~5-8 min)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.13.1...kobako-runtime-v0.14.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
3
10
|
## [0.13.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.13.0...kobako-runtime-v0.13.1) (2026-07-30)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.13.1...kobako-transport-v0.14.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **transport:** let a guest block's exception continue as itself ([c8b1de7](https://github.com/elct9620/kobako/commit/c8b1de7154336fab8fa20c0d7a5b3baaad11d551))
|
|
9
|
+
* **transport:** separate a failed exchange from a failed Service ([fb92515](https://github.com/elct9620/kobako/commit/fb92515fc76ffebbb8491b525d53b1725cf97dd0))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **wire:** let a Fault reader survive what it predates ([eeabfb7](https://github.com/elct9620/kobako/commit/eeabfb7aa4de3d08852ca1b41517b924d72a7312))
|
|
15
|
+
|
|
3
16
|
## [0.13.1](https://github.com/elct9620/kobako/compare/kobako-transport-v0.13.0...kobako-transport-v0.13.1) (2026-07-30)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.13.1...kobako-wasmtime-v0.14.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
3
10
|
## [0.13.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.13.0...kobako-wasmtime-v0.13.1) (2026-07-30)
|
|
4
11
|
|
|
5
12
|
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/lib/kobako/3.3/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/3.4/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/4.0/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -30,10 +30,24 @@ module Kobako
|
|
|
30
30
|
# rescue below maps the two violations the guard does not reach onto the
|
|
31
31
|
# same error: an integer outside i64..u64 (+RangeError+) and any
|
|
32
32
|
# packer-internal +NoMethodError+.
|
|
33
|
+
#
|
|
34
|
+
# A value that nests without bound — a reference cycle necessarily
|
|
35
|
+
# does — exhausts the packer's own recursion instead, which Ruby
|
|
36
|
+
# reports outside +StandardError+. Mapping it keeps an unwritable
|
|
37
|
+
# value a wire violation the dispatch boundary can answer, rather than
|
|
38
|
+
# one that escapes every caller's rescue and traps the invocation.
|
|
39
|
+
#
|
|
40
|
+
# The refusal is spent once per thread: a thread that has absorbed one
|
|
41
|
+
# such overflow aborts on the next instead of raising, and a Hash cycle
|
|
42
|
+
# never reaches Ruby at all — the packer walks a Hash through C frames
|
|
43
|
+
# that carry no stack guard. Bounding the walk before the packer is
|
|
44
|
+
# handed the value is what would make the refusal repeatable.
|
|
33
45
|
def self.encode(value)
|
|
34
46
|
FACTORY.dump(value)
|
|
35
47
|
rescue ::RangeError, ::NoMethodError => e
|
|
36
48
|
raise UnsupportedTypeError, e.message
|
|
49
|
+
rescue ::SystemStackError
|
|
50
|
+
raise InvalidTypeError, "value nests deeper than this host can write (a reference cycle necessarily does)"
|
|
37
51
|
end
|
|
38
52
|
end
|
|
39
53
|
end
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Kobako
|
|
|
18
18
|
class TruncatedInputError < Error; end
|
|
19
19
|
|
|
20
20
|
# The type byte at the current position is not in the 11-entry kobako
|
|
21
|
-
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag)
|
|
21
|
+
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag) —
|
|
22
|
+
# or, on encode, a value nesting past what the packer can walk.
|
|
22
23
|
class InvalidTypeError < Error; end
|
|
23
24
|
|
|
24
25
|
# A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -116,12 +116,45 @@ module Kobako
|
|
|
116
116
|
|
|
117
117
|
# Service layer. Raised when a Service capability call inside a mruby
|
|
118
118
|
# script reported an application-level failure that the script did not
|
|
119
|
-
# rescue.
|
|
119
|
+
# rescue. The base class covers a Service that ran and raised; the two
|
|
120
|
+
# subclasses below cover the calls that never reached one, so a Host App
|
|
121
|
+
# routes them apart with +rescue+ instead of by reading the message.
|
|
120
122
|
class ServiceError < Error
|
|
121
123
|
include Diagnosable
|
|
122
124
|
include CarriesExecution
|
|
123
125
|
end
|
|
124
126
|
|
|
127
|
+
# The ServiceError subclass raised when the call reached no Service
|
|
128
|
+
# method: the bound path holds nothing, the Capability Handle is not
|
|
129
|
+
# live in this invocation, or the method is absent or outside the guest
|
|
130
|
+
# surface. The causes stay indistinguishable — an opaque target must
|
|
131
|
+
# disclose nothing about which methods it defines — so what a Host App
|
|
132
|
+
# learns is that this call will not succeed by being retried.
|
|
133
|
+
class NoServiceError < ServiceError; end
|
|
134
|
+
|
|
135
|
+
# The ServiceError subclass raised when the call reached the Service
|
|
136
|
+
# method but its arguments did not fit — an unknown keyword, or an
|
|
137
|
+
# arity mismatch.
|
|
138
|
+
class ServiceArgumentError < ServiceError; end
|
|
139
|
+
|
|
140
|
+
# Raised at a Service method's +yield+ site when the guest block it
|
|
141
|
+
# yielded to raised. A Service rescues it the way it would rescue a
|
|
142
|
+
# block's exception without a Sandbox between the two frames; leaving it
|
|
143
|
+
# unrescued returns it to the guest, which re-raises the exception it
|
|
144
|
+
# raised in the first place. It therefore never reaches the Host App as
|
|
145
|
+
# an invocation outcome and carries no Execution. +klass+ names the
|
|
146
|
+
# guest-side class, which has no host counterpart to rebuild.
|
|
147
|
+
class BlockError < Error
|
|
148
|
+
include Diagnosable
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Raised at a Service method's +yield+ site when a yield argument has no
|
|
152
|
+
# wire representation, so the round-trip never left the host and the
|
|
153
|
+
# block never ran. As BlockError, a Service rescues it at that site and
|
|
154
|
+
# may go on, so it never reaches the Host App as an invocation outcome;
|
|
155
|
+
# unrescued, the dispatch answers as that Service having failed.
|
|
156
|
+
class YieldValueError < Error; end
|
|
157
|
+
|
|
125
158
|
# HandleExhaustedError is the canonical SandboxError subclass for the
|
|
126
159
|
# id-cap-hit path. Raised when the per-invocation Handle ID counter in
|
|
127
160
|
# Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -17,12 +17,16 @@ module Kobako
|
|
|
17
17
|
ORIGIN_SANDBOX = "sandbox"
|
|
18
18
|
ORIGIN_SERVICE = "service"
|
|
19
19
|
|
|
20
|
-
# The guest-written class names that
|
|
21
|
-
# A name absent here settles as
|
|
22
|
-
# widens the taxonomy only by naming a class the
|
|
20
|
+
# The guest-written class names that narrow the class a Panic settles
|
|
21
|
+
# as. A name absent here settles as the base class its origin already
|
|
22
|
+
# chose, so the guest widens the taxonomy only by naming a class the
|
|
23
|
+
# host already defines.
|
|
23
24
|
SUBCLASSES = {
|
|
24
25
|
"Kobako::BytecodeError" => BytecodeError,
|
|
25
|
-
"Kobako::UndefinedEntrypointError" => UndefinedEntrypointError
|
|
26
|
+
"Kobako::UndefinedEntrypointError" => UndefinedEntrypointError,
|
|
27
|
+
"Kobako::Transport::Error" => Kobako::Transport::Error,
|
|
28
|
+
"Kobako::NoServiceError" => NoServiceError,
|
|
29
|
+
"Kobako::ServiceArgumentError" => ServiceArgumentError
|
|
26
30
|
}.freeze
|
|
27
31
|
|
|
28
32
|
module_function
|
|
@@ -52,13 +56,17 @@ module Kobako
|
|
|
52
56
|
UndefinedEntrypointError.new(message, name: entrypoint, available: available.map(&:to_sym), **attribution)
|
|
53
57
|
end
|
|
54
58
|
|
|
55
|
-
# +origin
|
|
56
|
-
#
|
|
57
|
-
#
|
|
59
|
+
# +origin+ picks the branch, and the guest-written class name may
|
|
60
|
+
# narrow within it so callers can rescue one path specifically. A name
|
|
61
|
+
# naming a class outside the branch its origin chose is ignored rather
|
|
62
|
+
# than honoured: what a guest calls its exception must not move the
|
|
63
|
+
# failure to a layer the attribution did not put it in.
|
|
58
64
|
def error_class(origin, klass)
|
|
59
|
-
|
|
65
|
+
base = origin == ORIGIN_SERVICE ? ServiceError : SandboxError
|
|
66
|
+
selected = SUBCLASSES.fetch(klass, base)
|
|
67
|
+
return base unless selected <= base
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
selected
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
# An arm the host cannot settle: the guest wrote nothing, or wrote
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../codec"
|
|
4
|
+
require_relative "../errors"
|
|
4
5
|
require_relative "../payload"
|
|
5
6
|
require_relative "call"
|
|
6
7
|
require_relative "reflection"
|
|
@@ -36,6 +37,29 @@ module Kobako
|
|
|
36
37
|
# not part of the public Kobako error taxonomy.
|
|
37
38
|
class UndefinedTargetError < StandardError; end
|
|
38
39
|
|
|
40
|
+
# The codec fault of a request that never became a call, restated in
|
|
41
|
+
# the vocabulary the guest can act on while staying inside the codec
|
|
42
|
+
# taxonomy the fault boundary sorts on.
|
|
43
|
+
class UnreadableRequestError < Kobako::Codec::Error; end
|
|
44
|
+
|
|
45
|
+
# The category kobako's own refusals answer under, keyed by the class
|
|
46
|
+
# each is raised as and ordered most specific first. A class absent
|
|
47
|
+
# here is the Service's own exception, which answers under +runtime+
|
|
48
|
+
# wearing the +<class>: <message>+ shape that says so.
|
|
49
|
+
#
|
|
50
|
+
# +Kobako::Codec::Error+ is the floor rather than a path of its own:
|
|
51
|
+
# a codec fault reaching the boundary unnamed is the exchange failing,
|
|
52
|
+
# and must not be dressed as something a Service raised.
|
|
53
|
+
OWN_FAULTS = {
|
|
54
|
+
HandleExhaustedError => "internal",
|
|
55
|
+
UndefinedTargetError => "undefined",
|
|
56
|
+
ArgumentError => "argument",
|
|
57
|
+
YieldValueError => "runtime",
|
|
58
|
+
Kobako::Codec::Error => "internal",
|
|
59
|
+
Kobako::SandboxError => "runtime"
|
|
60
|
+
}.freeze
|
|
61
|
+
private_constant :OWN_FAULTS
|
|
62
|
+
|
|
39
63
|
# Answer a single routed Call with +[ok, bytes]+, which the native
|
|
40
64
|
# side puts on the Reply's ok or fault arm. Invoked from the
|
|
41
65
|
# per-invocation dispatch Proc that
|
|
@@ -61,7 +85,7 @@ module Kobako
|
|
|
61
85
|
# stays uncaught and traps the invocation rather than being masked as a
|
|
62
86
|
# rescuable fault.
|
|
63
87
|
rescue StandardError => e
|
|
64
|
-
[false, *caught_fault(e)] # : [bool, String, String?]
|
|
88
|
+
[false, *caught_fault(e, yielder)] # : [bool, String, String?]
|
|
65
89
|
ensure
|
|
66
90
|
yielder&.invalidate!
|
|
67
91
|
end
|
|
@@ -70,12 +94,23 @@ module Kobako
|
|
|
70
94
|
# the +catch+ frame a guest +break+ unwinds to. Split from #dispatch
|
|
71
95
|
# so the reply-shaping and the failure boundary stay one glance wide.
|
|
72
96
|
def run(call, resolver, handler, yielder)
|
|
73
|
-
arguments, carried_handle =
|
|
97
|
+
arguments, carried_handle = decode_arguments(call.payload)
|
|
74
98
|
receiver = resolve_target(call.target, resolver, handler)
|
|
75
99
|
args, kwargs = resolve_call_args(arguments, handler, carried_handle)
|
|
76
100
|
catch(BREAK_THROW) { invoke(receiver, call.method_name, args, kwargs, yielder) }
|
|
77
101
|
end
|
|
78
102
|
|
|
103
|
+
# Decode the Call's payload into its arguments, reporting whether any
|
|
104
|
+
# Capability Handle crossed. A codec fault here is a request that
|
|
105
|
+
# never became a call, restated so it cannot read as an unwritable
|
|
106
|
+
# reply — the same restatement #encode_ok makes in the other
|
|
107
|
+
# direction.
|
|
108
|
+
def decode_arguments(payload)
|
|
109
|
+
Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
|
|
110
|
+
rescue Kobako::Codec::Error => e
|
|
111
|
+
raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
|
|
79
114
|
# Resolve positional and keyword arguments off the decoded payload in
|
|
80
115
|
# one step. +carried_handle+ reports whether the decode carried any
|
|
81
116
|
# Capability Handle; when it did not, every argument resolves to
|
|
@@ -95,14 +130,24 @@ module Kobako
|
|
|
95
130
|
# is the +StandardError+ caught by #dispatch's rescue; the category
|
|
96
131
|
# tells the guest which kind of failure it was so it can raise the
|
|
97
132
|
# matching proxy-side error.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
#
|
|
134
|
+
# The class prefix marks a Service's own exception and nothing else:
|
|
135
|
+
# it is the +<class>: <message>+ shape a Host App is told to keep
|
|
136
|
+
# secrets out of, so wearing it says the Service raised. kobako's own
|
|
137
|
+
# refusals answer under their own wording instead of borrowing that
|
|
138
|
+
# shape.
|
|
139
|
+
#
|
|
140
|
+
# The guest's own block failing is not the Service's to report at
|
|
141
|
+
# all, so the Yielder that raised it is asked first — it recognises
|
|
142
|
+
# its own by identity and words the failure the guest's way.
|
|
143
|
+
def caught_fault(error, yielder)
|
|
144
|
+
block_failure = yielder&.fault_text(error)
|
|
145
|
+
return fault("block", block_failure) if block_failure
|
|
146
|
+
|
|
147
|
+
own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) }
|
|
148
|
+
return fault(own.last, error.message) if own
|
|
149
|
+
|
|
150
|
+
fault("runtime", "#{error.class}: #{error.message}")
|
|
106
151
|
end
|
|
107
152
|
|
|
108
153
|
# Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
|
|
@@ -182,10 +227,18 @@ module Kobako
|
|
|
182
227
|
# +UnsupportedTypeError+; the rescue routes it through the
|
|
183
228
|
# Catalog::Handles via #wrap_as_handle and re-encodes with the
|
|
184
229
|
# Capability Handle in place. The happy path encodes exactly once.
|
|
230
|
+
#
|
|
231
|
+
# Any other codec fault is the answer failing to encode rather than the
|
|
232
|
+
# request failing to decode, and a Service is the only side that can
|
|
233
|
+
# change what it returns — so it is named here, where the direction is
|
|
234
|
+
# known, instead of falling to the boundary's codec floor and reporting
|
|
235
|
+
# as an exchange that produced no Service outcome.
|
|
185
236
|
def encode_ok(value, handler)
|
|
186
237
|
Kobako::Codec::Encoder.encode(value)
|
|
187
238
|
rescue Kobako::Codec::UnsupportedTypeError
|
|
188
239
|
encode_ok(wrap_as_handle(value, handler), handler)
|
|
240
|
+
rescue Kobako::Codec::Error => e
|
|
241
|
+
raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
|
|
189
242
|
end
|
|
190
243
|
|
|
191
244
|
# Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../codec"
|
|
4
|
+
require_relative "../errors"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
@@ -20,8 +21,8 @@ module Kobako
|
|
|
20
21
|
# * ok — return the decoded value to +yield+'s caller
|
|
21
22
|
# * break — +throw break_tag, value+ so the Dispatcher's +catch+
|
|
22
23
|
# frame unwinds the Service method
|
|
23
|
-
# * error — raise
|
|
24
|
-
#
|
|
24
|
+
# * error — raise a BlockError at the Service's yield site,
|
|
25
|
+
# carrying the guest's class on +#klass+
|
|
25
26
|
#
|
|
26
27
|
# The Dispatcher calls #invalidate! from its +ensure+ block once
|
|
27
28
|
# dispatch completes; any later call to a stashed Yielder then raises
|
|
@@ -40,6 +41,23 @@ module Kobako
|
|
|
40
41
|
@break_tag = break_tag
|
|
41
42
|
@handler = handler
|
|
42
43
|
@active = true
|
|
44
|
+
@raised = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The text the guest's own block failure crosses back as, or +nil+
|
|
48
|
+
# when +error+ is not the BlockError this Yielder raised. Identity
|
|
49
|
+
# rather than class: a Service that rescued the block's failure and
|
|
50
|
+
# raised its own has reported something else, and the guest must
|
|
51
|
+
# hear about that instead.
|
|
52
|
+
#
|
|
53
|
+
# The class travels with the message because not every block failure
|
|
54
|
+
# is an exception the guest holds — a block value the guest refused
|
|
55
|
+
# has a class to raise under and no object to continue.
|
|
56
|
+
def fault_text(error)
|
|
57
|
+
raised = @raised
|
|
58
|
+
return if raised.nil? || !error.equal?(raised)
|
|
59
|
+
|
|
60
|
+
"#{raised.klass}: #{raised.message}"
|
|
43
61
|
end
|
|
44
62
|
|
|
45
63
|
# Re-enter the guest with +args+ and reify the Yield Reply into
|
|
@@ -52,8 +70,8 @@ module Kobako
|
|
|
52
70
|
def yield(*args)
|
|
53
71
|
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
54
72
|
|
|
55
|
-
arm, body, klass = @yield_to_guest.call(
|
|
56
|
-
raise
|
|
73
|
+
arm, body, klass = @yield_to_guest.call(encode_args(args))
|
|
74
|
+
raise remember(BlockError.new(body, klass: klass)) if arm == :error
|
|
57
75
|
|
|
58
76
|
value, carried_handle = decode_body(body)
|
|
59
77
|
throw @break_tag, value if arm == :break
|
|
@@ -76,6 +94,24 @@ module Kobako
|
|
|
76
94
|
|
|
77
95
|
private
|
|
78
96
|
|
|
97
|
+
# Encode what the Service is yielding. A value outside the wire type
|
|
98
|
+
# set fails here, before the guest is re-entered, and is restated so
|
|
99
|
+
# the Service reads a refusal of its own argument rather than a codec
|
|
100
|
+
# class it never named.
|
|
101
|
+
def encode_args(args)
|
|
102
|
+
Kobako::Codec::Encoder.encode(args)
|
|
103
|
+
rescue Kobako::Codec::Error => e
|
|
104
|
+
raise YieldValueError, "Service yielded a value the block cannot receive: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Hold onto the BlockError being raised so #fault_text can recognise
|
|
108
|
+
# it if it comes back unrescued, and return it so the raise site reads
|
|
109
|
+
# as one statement. Only the newest is kept: a Service that rescued
|
|
110
|
+
# an earlier one and yielded again has already handled it.
|
|
111
|
+
def remember(error)
|
|
112
|
+
@raised = error
|
|
113
|
+
end
|
|
114
|
+
|
|
79
115
|
# Decode a value-carrying arm's payload, answering the value and
|
|
80
116
|
# whether the decode carried a Capability Handle. The tracking
|
|
81
117
|
# bracket opens only around this decode: the guest re-entry may run
|
data/lib/kobako/version.rb
CHANGED
data/sig/kobako/errors.rbs
CHANGED
|
@@ -47,6 +47,19 @@ module Kobako
|
|
|
47
47
|
include CarriesExecution
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
class NoServiceError < ServiceError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class ServiceArgumentError < ServiceError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class BlockError < Error
|
|
57
|
+
include Diagnosable
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class YieldValueError < Error
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
class HandleExhaustedError < SandboxError
|
|
51
64
|
end
|
|
52
65
|
|
data/sig/kobako/outcome.rbs
CHANGED
|
@@ -2,7 +2,7 @@ module Kobako
|
|
|
2
2
|
module Outcome
|
|
3
3
|
ORIGIN_SANDBOX: String
|
|
4
4
|
ORIGIN_SERVICE: String
|
|
5
|
-
SUBCLASSES: Hash[String, singleton(SandboxError)]
|
|
5
|
+
SUBCLASSES: Hash[String, (singleton(SandboxError) | singleton(ServiceError))]
|
|
6
6
|
|
|
7
7
|
type panic_fields = [String, String, String, Array[String], Array[String]]
|
|
8
8
|
|
|
@@ -4,8 +4,13 @@ module Kobako
|
|
|
4
4
|
class UndefinedTargetError < StandardError
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
class UnreadableRequestError < Kobako::Codec::Error
|
|
8
|
+
end
|
|
9
|
+
|
|
7
10
|
BREAK_THROW: Symbol
|
|
8
11
|
|
|
12
|
+
OWN_FAULTS: Hash[singleton(StandardError), String]
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
|
|
@@ -15,7 +20,9 @@ module Kobako
|
|
|
15
20
|
|
|
16
21
|
def self?.resolve_call_args: (Kobako::Payload::Arguments arguments, Kobako::Codec::_HandleTable handler, bool carried_handle) -> [Array[untyped], Hash[Symbol, untyped]]
|
|
17
22
|
|
|
18
|
-
def self?.
|
|
23
|
+
def self?.decode_arguments: (String payload) -> [Kobako::Payload::Arguments, bool]
|
|
24
|
+
|
|
25
|
+
def self?.caught_fault: (StandardError error, Kobako::Transport::Yielder? yielder) -> [String, String]
|
|
19
26
|
|
|
20
27
|
def self?.invoke: (untyped target, String method, Array[untyped] args, Hash[Symbol, untyped] kwargs, ?Kobako::Transport::Yielder? yielder) -> untyped
|
|
21
28
|
|
|
@@ -5,9 +5,12 @@ module Kobako
|
|
|
5
5
|
@break_tag: Symbol
|
|
6
6
|
@handler: Kobako::Codec::_HandleTable
|
|
7
7
|
@active: bool
|
|
8
|
+
@raised: Kobako::BlockError?
|
|
8
9
|
|
|
9
10
|
def initialize: (Kobako::Transport::_GuestYielder yield_to_guest, Symbol break_tag, Kobako::Codec::_HandleTable handler) -> void
|
|
10
11
|
|
|
12
|
+
def fault_text: (Exception error) -> String?
|
|
13
|
+
|
|
11
14
|
def yield: (*untyped args) -> untyped
|
|
12
15
|
|
|
13
16
|
def to_proc: () -> Proc
|
|
@@ -16,6 +19,10 @@ module Kobako
|
|
|
16
19
|
|
|
17
20
|
private
|
|
18
21
|
|
|
22
|
+
def encode_args: (Array[untyped] args) -> String
|
|
23
|
+
|
|
24
|
+
def remember: (Kobako::BlockError error) -> Kobako::BlockError
|
|
25
|
+
|
|
19
26
|
def decode_body: (String body) -> [untyped, bool]
|
|
20
27
|
|
|
21
28
|
def restore: (untyped value, bool carried_handle) -> untyped
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kobako
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Aotokitsuruya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: msgpack
|