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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43f465417ab6549eb551ce4d45acc511245e5c69c8ed0da630352a7db1a97457
4
- data.tar.gz: e27fbee4cee43ddac783ba6240832c3f0131fc2a62c5ef4f5ef7399c0dfdaa1a
3
+ metadata.gz: 31ada84079df29e6403f69a5d3a5dcf6dd8780430a4063cbc3576399272904f8
4
+ data.tar.gz: 3f66558a59dde0642cc6fa8b4871c009e2aaf6efb9efb973e74a45e5158fa9c2
5
5
  SHA512:
6
- metadata.gz: fb388ad5d74a6c3f7a9fc4816e004200e427ec819ae24495612ac2376f8cdf6e6fa2bc28f5a5c53c3312daef3abd7895c33f4e87f9f04da8668ef0ccf1ca5fad
7
- data.tar.gz: 3d598ff006a0db04c6b78a572a46a7afa2cbe051e9853f9cf71ec8c71d339aaf78b24590dccbadd8af298bd137e118817293d87cd43e605fb86ce7063727c6cd
6
+ metadata.gz: e839baee0544f72426b9887a745a564bf8350370beca72f4dd3e230fb71b568682bb6e7794acad502e5e27c984cafe9fe7ffb14f0d5ab8ea6cbbc2e863a73439
7
+ data.tar.gz: c54343f8b35e7e340ff90155c23e185f1be3888226675aa0f5a1fb6ac6912a77f9bff5b69f8db8583c622eee5b3a39666dca6c3d780fd5f8cc977571c6a2cc35
@@ -1 +1 @@
1
- {".":"0.21.1","wasm/kobako-core":"0.13.1","wasm/kobako-mruby":"0.13.1","wasm/kobako-io":"0.13.1","wasm/kobako-json":"0.13.1","wasm/kobako-regexp":"0.13.1","wasm/kobako-baker":"0.13.1","crates/kobako-transport":"0.13.1","crates/kobako-codec":"0.13.1","crates/kobako-runtime":"0.13.1","crates/kobako-wasmtime":"0.13.1","crates/kobako":"0.13.1"}
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 ~28 µs host-side `Sandbox.new`. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a significant gain (~4-5% in the [serverless example](examples/serverless/README.md), and proportionally less once the script itself does real work). 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. 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) | ~28 µs |
503
- | Warm `#eval("nil")` on a reused Sandbox | ~71 µs |
504
- | Warm `#run(:Entrypoint, ...)` dispatch | ~97 µs |
505
- | Service call amortized inside one invocation | ~6.8 µs |
506
- | Snippet replay per invocation | ~7.6 µs each |
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 17k `#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.
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.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
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
 
@@ -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.13.1" # x-release-please-version
29
+ kobako-runtime = "0.14.0" # x-release-please-version
30
30
  ```
31
31
 
32
32
  ## License
@@ -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
 
@@ -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.13.1" # x-release-please-version
38
+ kobako-transport = "0.14.0" # x-release-please-version
39
39
  ```
40
40
 
41
41
  ## License
@@ -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
 
@@ -24,7 +24,7 @@ SDK consumes the same surface.
24
24
 
25
25
  ```toml
26
26
  [dependencies]
27
- kobako-wasmtime = "0.13.1" # x-release-please-version
27
+ kobako-wasmtime = "0.14.0" # x-release-please-version
28
28
  ```
29
29
 
30
30
  ## License
data/data/kobako.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
@@ -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
@@ -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
@@ -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 select a +SandboxError+ subclass.
21
- # A name absent here settles as plain +SandboxError+, so the guest
22
- # widens the taxonomy only by naming a class the host already defines.
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 == "service"+ selects ServiceError; a sandbox-origin failure
56
- # naming one of the guest-written subclass names selects that subclass
57
- # so callers can rescue that path specifically.
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
- return ServiceError if origin == ORIGIN_SERVICE
65
+ base = origin == ORIGIN_SERVICE ? ServiceError : SandboxError
66
+ selected = SUBCLASSES.fetch(klass, base)
67
+ return base unless selected <= base
60
68
 
61
- SUBCLASSES.fetch(klass, SandboxError)
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 = Kobako::Codec.track_handles { Payload::Arguments.decode(call.payload) }
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
- def caught_fault(error)
99
- case error
100
- when Kobako::Codec::Error then fault("runtime",
101
- "Sandbox received a malformed request: #{error.message}")
102
- when UndefinedTargetError then fault("undefined", error.message)
103
- when ArgumentError then fault("argument", error.message)
104
- else fault("runtime", "#{error.class}: #{error.message}")
105
- end
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 the guest's class and message at the Service's
24
- # yield site
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(Kobako::Codec::Encoder.encode(args))
56
- raise "#{klass}: #{body}" if arm == :error
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.21.1"
4
+ VERSION = "0.22.0"
5
5
  end
@@ -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
 
@@ -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?.caught_fault: (StandardError error) -> [String, String]
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.21.1
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-07-30 00:00:00.000000000 Z
11
+ date: 2026-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack