kobako 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +51 -0
  4. data/Cargo.lock +3 -3
  5. data/README.md +104 -29
  6. data/ROADMAP.md +2 -3
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +7 -0
  9. data/crates/kobako-runtime/Cargo.toml +1 -1
  10. data/crates/kobako-runtime/README.md +1 -1
  11. data/crates/kobako-runtime/src/snapshot.rs +7 -3
  12. data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
  13. data/crates/kobako-wasmtime/Cargo.toml +2 -2
  14. data/crates/kobako-wasmtime/README.md +1 -1
  15. data/crates/kobako-wasmtime/src/capture.rs +17 -13
  16. data/crates/kobako-wasmtime/src/driver.rs +6 -7
  17. data/crates/kobako-wasmtime/src/frames.rs +2 -9
  18. data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
  19. data/crates/kobako-wasmtime/src/trap.rs +30 -57
  20. data/data/kobako.wasm +0 -0
  21. data/ext/kobako/Cargo.toml +1 -1
  22. data/ext/kobako/src/runtime/errors.rs +9 -39
  23. data/ext/kobako/src/runtime.rs +5 -20
  24. data/lib/kobako/catalog/handles.rb +1 -1
  25. data/lib/kobako/catalog/services.rb +123 -0
  26. data/lib/kobako/catalog/snippets.rb +1 -1
  27. data/lib/kobako/catalog.rb +2 -2
  28. data/lib/kobako/codec/decoder.rb +3 -3
  29. data/lib/kobako/codec/encoder.rb +4 -4
  30. data/lib/kobako/codec/ext_types.rb +169 -0
  31. data/lib/kobako/codec/state.rb +98 -0
  32. data/lib/kobako/codec/utils.rb +2 -2
  33. data/lib/kobako/codec.rb +23 -6
  34. data/lib/kobako/fault.rb +1 -1
  35. data/lib/kobako/handle.rb +1 -1
  36. data/lib/kobako/outcome.rb +13 -7
  37. data/lib/kobako/pool.rb +1 -1
  38. data/lib/kobako/sandbox.rb +20 -13
  39. data/lib/kobako/transport/dispatcher.rb +25 -18
  40. data/lib/kobako/transport/request.rb +14 -9
  41. data/lib/kobako/transport/response.rb +9 -2
  42. data/lib/kobako/transport/yield.rb +4 -1
  43. data/lib/kobako/transport/yielder.rb +14 -6
  44. data/lib/kobako/version.rb +1 -1
  45. data/sig/kobako/catalog/services.rbs +25 -0
  46. data/sig/kobako/codec/ext_types.rbs +31 -0
  47. data/sig/kobako/codec/state.rbs +20 -0
  48. data/sig/kobako/codec.rbs +3 -0
  49. data/sig/kobako/sandbox.rbs +1 -1
  50. data/sig/kobako/transport/dispatcher.rbs +4 -4
  51. data/sig/kobako/transport/yielder.rbs +1 -1
  52. data/sig/kobako/transport.rbs +2 -2
  53. metadata +7 -7
  54. data/lib/kobako/catalog/namespaces.rb +0 -115
  55. data/lib/kobako/codec/factory.rb +0 -187
  56. data/lib/kobako/namespace.rb +0 -78
  57. data/sig/kobako/catalog/namespaces.rbs +0 -17
  58. data/sig/kobako/codec/factory.rbs +0 -34
  59. data/sig/kobako/namespace.rbs +0 -21
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Codec
5
+ # Codec-internal, per-thread state of the operation in flight: the
6
+ # ext-envelope nesting depth and whether a Capability Handle crossed
7
+ # the current decode. Thread scoping is what makes plain instance
8
+ # variables sound — host codec calls run synchronously on their
9
+ # owning thread, and a nested decode (an ext 0x02 Fault re-entering
10
+ # through its +details+) reuses the same thread instance, so the
11
+ # depth counter accumulates across the re-entry instead of resetting.
12
+ class State
13
+ # An ext 0x02 (Fault) envelope nests through its +details+ field, and
14
+ # each level re-enters the codec with a fresh +MessagePack+ unpacker
15
+ # whose built-in stack guard resets — so ext-envelope depth is tracked
16
+ # here instead. The cap matches the wire's overall nesting bound and
17
+ # keeps a nested chain from exhausting the native stack: an over-deep
18
+ # chain fails as a clean wire error, never a stack-level trap.
19
+ MAX_EXT_DEPTH = 128
20
+ private_constant :MAX_EXT_DEPTH
21
+
22
+ # Thread-local slot holding the calling thread's State.
23
+ STATE_KEY = :__kobako_codec_state__
24
+ private_constant :STATE_KEY
25
+
26
+ # The calling thread's State, built on first use so the mutable
27
+ # state stays isolated to the thread that runs the codec call.
28
+ def self.current
29
+ Thread.current[STATE_KEY] ||= new
30
+ end
31
+ private_class_method :new
32
+
33
+ def initialize
34
+ @ext_depth = 0
35
+ @carried_handle = false
36
+ @faults_forbidden = false
37
+ end
38
+
39
+ # Bracket a decode and return the block's result together with
40
+ # whether the decoded tree carried an ext 0x01 Capability Handle.
41
+ # ExtTypes#unpack_handle is the sole chokepoint every Handle passes
42
+ # through, so one decode pass records the whole tree and a caller
43
+ # can skip an all-identity Handle-resolution walk when none was
44
+ # present.
45
+ def track_handles
46
+ @carried_handle = false
47
+ result = yield
48
+ [result, @carried_handle]
49
+ end
50
+
51
+ # Record that an ext 0x01 Capability Handle crossed the current
52
+ # decode; #track_handles reports it to the bracketing caller.
53
+ def record_handle!
54
+ @carried_handle = true
55
+ end
56
+
57
+ # Bracket a codec operation in a payload position, where an ext 0x02
58
+ # Fault envelope has no legal wire representation: the fault field of
59
+ # an error Response is its only home. The ext-type conversions
60
+ # consult #faults_forbidden? and refuse the envelope in both
61
+ # directions while the bracket is open. Save/restore keeps a nested
62
+ # legal operation on the same thread unaffected.
63
+ def forbid_faults
64
+ previous = @faults_forbidden
65
+ @faults_forbidden = true
66
+ yield
67
+ ensure
68
+ @faults_forbidden = previous
69
+ end
70
+
71
+ # Whether the operation in flight sits inside a #forbid_faults
72
+ # bracket — i.e. in a payload position where ext 0x02 is a wire
73
+ # violation.
74
+ def faults_forbidden?
75
+ @faults_forbidden
76
+ end
77
+
78
+ # Track ext-envelope re-entry depth and refuse a chain past
79
+ # MAX_EXT_DEPTH, raising +over_limit+ so the failure lands in the
80
+ # caller's existing wire-error class. The next depth is checked before
81
+ # it is committed, so an over-deep rejection leaves the counter
82
+ # untouched, and the +ensure+ restores the entry value on the way out.
83
+ def within_ext_frame(over_limit)
84
+ depth = @ext_depth + 1
85
+ raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
86
+
87
+ @ext_depth = depth
88
+ begin
89
+ yield
90
+ ensure
91
+ @ext_depth = depth - 1
92
+ end
93
+ end
94
+ end
95
+
96
+ private_constant :State
97
+ end
98
+ end
@@ -10,7 +10,7 @@ module Kobako
10
10
  # - UTF-8 assertion at the codec boundary
11
11
  # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
12
12
  # § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
13
- # Decoder when walking +str+ family payloads and by Factory
13
+ # Decoder when walking +str+ family payloads and by ExtTypes
14
14
  # when validating the +ext 0x00+ Symbol payload.
15
15
  # - +ArgumentError+ translation at the codec boundary
16
16
  # (#with_boundary) so the public taxonomy stays
@@ -38,7 +38,7 @@ module Kobako
38
38
  #
39
39
  # Reach for this only where a value object is constructed outside a
40
40
  # Decoder.decode block, whose rescue already performs the same
41
- # mapping (worked example: Factory#unpack_handle building
41
+ # mapping (worked example: ExtTypes#unpack_handle building
42
42
  # +Handle.restore+ from a raw fixext payload). Do not use it for
43
43
  # general-purpose validation outside the codec boundary —
44
44
  # host-layer +ArgumentError+ values should propagate unchanged.
data/lib/kobako/codec.rb CHANGED
@@ -3,7 +3,8 @@
3
3
  require_relative "codec/error"
4
4
  require_relative "codec/utils"
5
5
  require_relative "codec/handle_walk"
6
- require_relative "codec/factory"
6
+ require_relative "codec/state"
7
+ require_relative "codec/ext_types"
7
8
  require_relative "codec/encoder"
8
9
  require_relative "codec/decoder"
9
10
 
@@ -18,14 +19,30 @@ module Kobako
18
19
  # the kobako root so the codec can register them without depending
19
20
  # upward on Transport.
20
21
  #
21
- # Backed by the official +msgpack+ gem via Factory; Encoder and
22
- # Decoder are thin wrappers that register the three kobako-specific
23
- # ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
24
- # envelope) on a single +MessagePack::Factory+ instance. The Rust side
22
+ # Backed by the official +msgpack+ gem: ExtTypes registers the three
23
+ # kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle,
24
+ # 0x02 Exception envelope) on one process-wide +MessagePack::Factory+,
25
+ # and Encoder / Decoder are thin wrappers over it. The Rust side
25
26
  # mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
26
- # the ext-code constants live as module-private values on Factory
27
+ # the ext-code constants live as module-private values on ExtTypes
27
28
  # alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
28
29
  # +codec::EXT_ERRENV+ on that side.
29
30
  module Codec
31
+ # Bracket a decode and return the block's result together with whether
32
+ # the decoded tree carried an ext 0x01 Capability Handle — the signal a
33
+ # dispatch path uses to skip an all-identity Handle-resolution walk.
34
+ # The tracking state is codec-internal; this is its only readout.
35
+ def self.track_handles(&block)
36
+ State.current.track_handles(&block)
37
+ end
38
+
39
+ # Bracket a codec operation in a payload position: an ext 0x02 Fault
40
+ # envelope is only legal in the Response fault field, so the envelope
41
+ # layers open this bracket around every other encode / decode and the
42
+ # ext-type conversions refuse the envelope while it is open — a wire
43
+ # violation on decode, no wire representation on encode.
44
+ def self.forbid_faults(&block)
45
+ State.current.forbid_faults(&block)
46
+ end
30
47
  end
31
48
  end
data/lib/kobako/fault.rb CHANGED
@@ -5,7 +5,7 @@ module Kobako
5
5
  #
6
6
  # Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
7
7
  # +Fault+ is a MessagePack ext-type leaf registered by
8
- # +Kobako::Codec::Factory+ and rides nested inside other envelopes (a
8
+ # +Kobako::Codec::ExtTypes+ and rides nested inside other envelopes (a
9
9
  # +Kobako::Transport::Response+ error payload, or another Fault's
10
10
  # +details+). It lives at the kobako root rather than under +Transport+
11
11
  # because the Codec layer must register it, and Codec must not depend
data/lib/kobako/handle.rb CHANGED
@@ -18,7 +18,7 @@ module Kobako
18
18
  # constructor — is removed for the same reason: a legitimate Handle
19
19
  # must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
20
20
  # Handles through +.restore+, which exists at exactly two call
21
- # sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
21
+ # sites: +Kobako::Codec::ExtTypes#unpack_handle+ (wire decode) and
22
22
  # +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
23
23
  # (allocator paths). Both live inside +lib/kobako/+ and are not part
24
24
  # of any public surface.
@@ -70,7 +70,9 @@ module Kobako
70
70
  # "Symbol payload must be …" wording, but operators triaging a
71
71
  # corrupted Sandbox runtime still need it.
72
72
  def decode_value(body)
73
- Kobako::Codec::Decoder.decode(body)
73
+ # The Result envelope is a payload position: an ext 0x02 Fault in it
74
+ # is a wire violation routed into the invalid-result rescue below.
75
+ Kobako::Codec.forbid_faults { Kobako::Codec::Decoder.decode(body) }
74
76
  rescue Kobako::Codec::Error => e
75
77
  raise build_transport_error(
76
78
  "Sandbox produced an invalid result value",
@@ -99,13 +101,17 @@ module Kobako
99
101
  # through the decoder boundary; the message itself is never user-
100
102
  # facing — it lands in +details+ via the rescue chain above.
101
103
  def build_panic_record(body)
102
- Kobako::Codec::Decoder.decode(body) do |map|
103
- raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
104
+ # The Panic envelope is a payload position: an ext 0x02 Fault in its
105
+ # +details+ is a wire violation routed into the invalid-panic rescue.
106
+ Kobako::Codec.forbid_faults do
107
+ Kobako::Codec::Decoder.decode(body) do |map|
108
+ raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
104
109
 
105
- Panic.new(
106
- origin: map["origin"], klass: map["class"], message: map["message"],
107
- backtrace: map["backtrace"] || [], details: map["details"]
108
- )
110
+ Panic.new(
111
+ origin: map["origin"], klass: map["class"], message: map["message"],
112
+ backtrace: map["backtrace"] || [], details: map["details"]
113
+ )
114
+ end
109
115
  end
110
116
  end
111
117
 
data/lib/kobako/pool.rb CHANGED
@@ -24,7 +24,7 @@ module Kobako
24
24
  # seconds (+nil+ waits indefinitely); every other keyword is
25
25
  # forwarded verbatim to +Kobako::Sandbox.new+. The optional block
26
26
  # runs exactly once per constructed Sandbox — it is the setup window
27
- # for +#define+ / +#preload+ before that Sandbox's first checkout.
27
+ # for +#bind+ / +#preload+ before that Sandbox's first checkout.
28
28
  # No Sandbox is constructed here. Raises +ArgumentError+ for an
29
29
  # invalid +slots+ / +checkout_timeout+.
30
30
  def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
@@ -17,7 +17,7 @@ module Kobako
17
17
  #
18
18
  # The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
19
19
  # +Kobako::Catalog::Handles+, the per-instance
20
- # +Kobako::Catalog::Namespaces+ (which receives the +Catalog::Handles+ by
20
+ # +Kobako::Catalog::Services+ (which receives the +Catalog::Handles+ by
21
21
  # injection so guest→host dispatch and host→guest auto-wrap share one
22
22
  # allocator), and the dispatch +Proc+ / +yield_to_guest+ lambda installed
23
23
  # on the Runtime via +Runtime#on_dispatch=+. The underlying wasmtime Engine
@@ -100,21 +100,25 @@ module Kobako
100
100
  @wasm_path = wasm_path || Kobako::Runtime.default_path
101
101
  @options = SandboxOptions.new(**)
102
102
  @handler = Catalog::Handles.new
103
- @services = Kobako::Catalog::Namespaces.new(handler: @handler)
103
+ @services = Kobako::Catalog::Services.new(handler: @handler)
104
104
  @snippets = Catalog::Snippets.new
105
105
  @runtime = build_runtime!
106
106
  install_dispatch_proc!
107
107
  reset_invocation_state!
108
108
  end
109
109
 
110
- # Declare or retrieve the Namespace named +name+ on this Sandbox. +name+
111
- # must be a Symbol or String in constant form. Returns the
112
- # +Kobako::Namespace+.
110
+ # Bind +object+ as the Service reachable at +path+ a Symbol or
111
+ # String of one or more +::+-separated constant-form segments
112
+ # (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
113
+ # chaining.
113
114
  #
114
- # Raises +ArgumentError+ when called after the first invocation, or
115
- # when +name+ does not match the constant-name pattern.
116
- def define(name)
117
- @services.define(name)
115
+ # Raises +ArgumentError+ when a segment is malformed, when +path+
116
+ # collides with an existing binding (a name is a bound Service or a
117
+ # grouping prefix, never both), or when called after the first
118
+ # invocation has sealed Service registration.
119
+ def bind(path, object)
120
+ @services.bind(path, object)
121
+ self
118
122
  end
119
123
 
120
124
  # Register a snippet on this Sandbox in one of two forms:
@@ -175,15 +179,15 @@ module Kobako
175
179
  #
176
180
  # Source delivery uses the WASI stdin three-frame protocol
177
181
  # ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
178
- # Frame 1 carries the msgpack-encoded preamble (Namespace / Member
179
- # registry snapshot), Frame 2 carries the user source UTF-8 bytes, and
182
+ # Frame 1 carries the msgpack-encoded preamble (Service registry
183
+ # snapshot), Frame 2 carries the user source UTF-8 bytes, and
180
184
  # Frame 3 carries the snippet table registered via +#preload+.
181
185
  # Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
182
186
  # mandatory-presence — an empty snippet table sends an empty msgpack
183
187
  # array, never an absent frame.
184
188
  #
185
189
  # The first invocation seals the Service registry and snippet table;
186
- # subsequent +#define+ / +#preload+ calls raise +ArgumentError+.
190
+ # subsequent +#bind+ / +#preload+ calls raise +ArgumentError+.
187
191
  #
188
192
  # Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
189
193
  # +Kobako::SandboxError+ when the guest ran to completion but failed
@@ -318,7 +322,10 @@ module Kobako
318
322
  # token; restore it to the host object the guest referenced before
319
323
  # handing the value to the Host App. @handler still holds this
320
324
  # invocation's table — reset only happens at the next #begin_invocation!.
321
- Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
325
+ # A Handle-free result resolves to itself, so the restoration walk is
326
+ # skipped when the decode carried none.
327
+ value, carried_handle = Codec.track_handles { Outcome.decode(return_bytes) }
328
+ carried_handle ? Codec::HandleWalk.deep_restore(value, @handler) : value
322
329
  rescue Kobako::TrapError => e
323
330
  raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
324
331
  ensure
@@ -13,7 +13,7 @@ module Kobako
13
13
  module Transport
14
14
  # Pure-function dispatcher for guest-initiated transport calls.
15
15
  # Decodes a msgpack-encoded Request envelope, resolves the target
16
- # object through the Catalog::Namespaces (path lookup) or
16
+ # object through the Catalog::Services (path lookup) or
17
17
  # Catalog::Handles (Handle lookup), invokes the method, and returns
18
18
  # a msgpack-encoded Response envelope.
19
19
  #
@@ -24,7 +24,7 @@ module Kobako
24
24
  #
25
25
  # Entry point:
26
26
  #
27
- # Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
27
+ # Kobako::Transport::Dispatcher.dispatch(request_bytes, services, handler, yield_to_guest)
28
28
  # # => msgpack-encoded Response bytes (never raises)
29
29
  module Dispatcher
30
30
  # Throw tag for the Yielder's break unwind back to the
@@ -67,7 +67,7 @@ module Kobako
67
67
 
68
68
  # Dispatch a single transport request and return the encoded
69
69
  # Response bytes. Invoked from the +Runtime#on_dispatch+ Proc that
70
- # +Kobako::Sandbox#initialize+ installs on the ext side; +namespaces+,
70
+ # +Kobako::Sandbox#initialize+ installs on the ext side; +services+,
71
71
  # +handler+, and +yield_to_guest+ are captured in that Proc's
72
72
  # closure so the Dispatcher stays stateless and the registry doesn't
73
73
  # need to publish accessors for the Sandbox-owned +Catalog::Handles+
@@ -77,24 +77,31 @@ module Kobako
77
77
  # returns a binary String — every failure path is reified as a
78
78
  # Response.error envelope so the guest sees a transport error rather
79
79
  # than a wasm trap.
80
- def dispatch(request_bytes, namespaces, handler, yield_to_guest)
81
- request = Kobako::Transport::Request.decode(request_bytes)
82
- target = resolve_target(request.target, namespaces, handler)
83
- args, kwargs = resolve_call_args(request, handler)
80
+ #
81
+ # The decode runs inside +Codec.track_handles+ so #resolve_call_args
82
+ # can skip the argument walk when no Capability Handle crossed the
83
+ # wire.
84
+ def dispatch(request_bytes, services, handler, yield_to_guest)
85
+ request, carried_handle = Kobako::Codec.track_handles { Kobako::Transport::Request.decode(request_bytes) }
86
+ target = resolve_target(request.target, services, handler)
87
+ args, kwargs = resolve_call_args(request, handler, carried_handle)
84
88
  yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given
85
- value = catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }
86
- encode_ok(value, handler)
89
+ encode_ok(catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }, handler)
87
90
  rescue StandardError => e
88
91
  encode_caught_error(e)
89
92
  ensure
90
93
  yielder&.invalidate!
91
94
  end
92
95
 
93
- # Resolve positional and keyword arguments off +request+ in one
94
- # step. Both pass through #resolve_arg so Capability Handles
95
- # round-trip back to the host-side Ruby object before the call
96
- # reaches +public_send+.
97
- def resolve_call_args(request, handler)
96
+ # Resolve positional and keyword arguments off +request+ in one step.
97
+ # +carried_handle+ reports whether the decode carried any Capability
98
+ # Handle; when it did not, every argument resolves to itself, so the
99
+ # decoded values pass straight through and the walk is skipped entirely.
100
+ # Otherwise both go through #resolve_arg so Handles round-trip back to
101
+ # the host-side Ruby object before the call reaches +public_send+.
102
+ def resolve_call_args(request, handler, carried_handle)
103
+ return [request.args, request.kwargs] unless carried_handle
104
+
98
105
  [request.args.map { |v| resolve_arg(v, handler) },
99
106
  request.kwargs.transform_values { |v| resolve_arg(v, handler) }]
100
107
  end
@@ -187,17 +194,17 @@ module Kobako
187
194
  # Target type is already validated by +Transport::Request.decode+
188
195
  # before this method is reached, so no else-branch is needed here —
189
196
  # the wire layer is the system boundary that enforces the invariant.
190
- def resolve_target(target, namespaces, handler)
197
+ def resolve_target(target, services, handler)
191
198
  case target
192
199
  when String
193
- resolve_path(target, namespaces)
200
+ resolve_path(target, services)
194
201
  when Kobako::Handle
195
202
  require_live_object!(target.id, handler)
196
203
  end
197
204
  end
198
205
 
199
- def resolve_path(path, namespaces)
200
- namespaces.lookup(path)
206
+ def resolve_path(path, services)
207
+ services.lookup(path)
201
208
  rescue KeyError => e
202
209
  raise UndefinedTargetError, e.message
203
210
  end
@@ -12,7 +12,7 @@ module Kobako
12
12
  #
13
13
  # 5-element msgpack array:
14
14
  # +[target, method_name, args, kwargs, block_given]+. +target+ is
15
- # either a +String+ (+"<Namespace>::<Member>"+, e.g. +"MyService::KV"+)
15
+ # either a +String+ (+"MyService::KV"+, e.g. +"MyService::KV"+)
16
16
  # or a Handle. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
17
17
  # enforced at construction so the Value Object is the single source of
18
18
  # truth. +block_given+ is a Boolean signalling whether the guest call
@@ -44,16 +44,21 @@ module Kobako
44
44
  end
45
45
 
46
46
  # Decode +bytes+ into a Request. Raises +Codec::InvalidType+ when the
47
- # envelope is not the expected 5-element msgpack array, or when the
48
- # Value Object's construction invariants reject the decoded fields.
47
+ # envelope is not the expected 5-element msgpack array, when any
48
+ # position carries an ext 0x02 Fault envelope (a Request is a payload
49
+ # position; the Response fault field is the envelope's only home), or
50
+ # when the Value Object's construction invariants reject the decoded
51
+ # fields.
49
52
  def self.decode(bytes)
50
- Codec::Decoder.decode(bytes) do |arr|
51
- unless arr.is_a?(Array) && arr.length == 5
52
- raise Codec::InvalidType, "Request envelope is malformed (expected a 5-element array)"
53
- end
53
+ Codec.forbid_faults do
54
+ Codec::Decoder.decode(bytes) do |arr|
55
+ unless arr.is_a?(Array) && arr.length == 5
56
+ raise Codec::InvalidType, "Request envelope is malformed (expected a 5-element array)"
57
+ end
54
58
 
55
- target, method_name, args, kwargs, block_given = arr
56
- new(target: target, method_name: method_name, args: args, kwargs: kwargs, block_given: block_given)
59
+ target, method_name, args, kwargs, block_given = arr
60
+ new(target: target, method_name: method_name, args: args, kwargs: kwargs, block_given: block_given)
61
+ end
57
62
  end
58
63
  end
59
64
 
@@ -53,9 +53,16 @@ module Kobako
53
53
  def error? = status == STATUS_ERROR
54
54
 
55
55
  # Encode this Response to msgpack bytes as the 2-element
56
- # +[status, payload]+ array.
56
+ # +[status, payload]+ array. The ok variant's value is a payload
57
+ # position, so a +Kobako::Fault+ inside it has no wire
58
+ # representation and surfaces as +UnsupportedType+ — the
59
+ # Dispatcher's auto-wrap rescue turns it into a Capability Handle.
60
+ # The error variant carries the Fault envelope in its one legal
61
+ # position and encodes it plainly.
57
62
  def encode
58
- Codec::Encoder.encode([status, payload])
63
+ return Codec::Encoder.encode([status, payload]) if error?
64
+
65
+ Codec.forbid_faults { Codec::Encoder.encode([status, payload]) }
59
66
  end
60
67
 
61
68
  # Decode +bytes+ into a Response. Raises +Codec::InvalidType+ when the
@@ -70,7 +70,10 @@ module Kobako
70
70
  body = bytes.byteslice(1, bytes.bytesize - 1) # : String
71
71
 
72
72
  reject_dead_tag!(tag)
73
- new(tag: tag, value: Codec::Decoder.decode(body))
73
+ # A YieldResponse is a payload position: an ext 0x02 Fault in its
74
+ # value is a wire violation (the Response fault field is the
75
+ # envelope's only home).
76
+ new(tag: tag, value: Codec.forbid_faults { Codec::Decoder.decode(body) })
74
77
  end
75
78
 
76
79
  def self.reject_dead_tag!(tag)
@@ -52,8 +52,14 @@ module Kobako
52
52
  def yield(*args)
53
53
  raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
54
54
 
55
- response = Kobako::Transport::Yield.decode(@yield_to_guest.call(Kobako::Codec::Encoder.encode(args)))
56
- return restore(response.value) if response.ok?
55
+ # Yield arguments are a payload position: a +Kobako::Fault+ among
56
+ # them has no wire representation, so the encode refuses it at
57
+ # this call site. The tracking bracket below opens only around the
58
+ # decode: the guest re-entry may run nested dispatches whose own
59
+ # brackets would otherwise pollute the signal.
60
+ bytes = @yield_to_guest.call(Kobako::Codec.forbid_faults { Kobako::Codec::Encoder.encode(args) })
61
+ response, carried_handle = Kobako::Codec.track_handles { Kobako::Transport::Yield.decode(bytes) }
62
+ return restore(response.value, carried_handle) if response.ok?
57
63
 
58
64
  throw @break_tag, response.value if response.break?
59
65
 
@@ -78,10 +84,12 @@ module Kobako
78
84
  # Restore any Capability Handle in a block's ok value to its host
79
85
  # object via the injected +Catalog::Handles+. Only the
80
86
  # ok path calls this — host code consumes the ok value, whereas a
81
- # break value returns to the guest and stays a Handle. Walks nested
82
- # Array / Hash one level at a time; a plain value passes through
83
- # unchanged.
84
- def restore(value)
87
+ # break value returns to the guest and stays a Handle. A response
88
+ # whose decode carried no Handle resolves to itself, so the walk is
89
+ # skipped entirely.
90
+ def restore(value, carried_handle)
91
+ return value unless carried_handle
92
+
85
93
  Kobako::Codec::HandleWalk.deep_restore(value, @handler)
86
94
  end
87
95
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.0"
5
5
  end
@@ -0,0 +1,25 @@
1
+ module Kobako
2
+ module Catalog
3
+ class Services
4
+ NAME_PATTERN: Regexp
5
+
6
+ def initialize: (?handler: Kobako::Catalog::Handles) -> void
7
+
8
+ def bind: (Symbol | String path, untyped object) -> self
9
+
10
+ def lookup: (Symbol | String target) -> untyped
11
+
12
+ def encode: () -> String
13
+
14
+ def seal!: () -> self
15
+
16
+ def sealed?: () -> bool
17
+
18
+ private
19
+
20
+ def validate_path!: (Symbol | String path) -> String
21
+
22
+ def collision?: (String path) -> bool
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ module Kobako
2
+ module Codec
3
+ module ExtTypes
4
+ EXT_SYMBOL: Integer
5
+ EXT_HANDLE: Integer
6
+ EXT_ERRENV: Integer
7
+
8
+ def self?.build_factory: () -> MessagePack::Factory
9
+
10
+ def self?.pack_symbol: (Symbol symbol) -> String
11
+
12
+ def self?.unpack_symbol: (String payload) -> Symbol
13
+
14
+ def self?.pack_handle: (Kobako::Handle handle) -> String
15
+
16
+ def self?.unpack_handle: (String payload, State state) -> Kobako::Handle
17
+
18
+ def self?.pack_fault: (Kobako::Fault fault, State state) -> String
19
+
20
+ def self?.unpack_fault: (String payload, State state) -> Kobako::Fault
21
+
22
+ def self?.register_symbol: (MessagePack::Factory factory) -> void
23
+
24
+ def self?.register_handle: (MessagePack::Factory factory) -> void
25
+
26
+ def self?.register_fault: (MessagePack::Factory factory) -> void
27
+ end
28
+
29
+ FACTORY: MessagePack::Factory
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ module Kobako
2
+ module Codec
3
+ class State
4
+ MAX_EXT_DEPTH: Integer
5
+ STATE_KEY: Symbol
6
+
7
+ def self.current: () -> State
8
+
9
+ def track_handles: [T] { () -> T } -> [T, bool]
10
+ def record_handle!: () -> void
11
+ def forbid_faults: [T] { () -> T } -> T
12
+ def faults_forbidden?: () -> bool
13
+ def within_ext_frame: [T] (singleton(Kobako::Codec::Error) over_limit) { () -> T } -> T
14
+
15
+ private
16
+
17
+ def initialize: () -> void
18
+ end
19
+ end
20
+ end
data/sig/kobako/codec.rbs CHANGED
@@ -1,5 +1,8 @@
1
1
  module Kobako
2
2
  module Codec
3
+ def self.track_handles: [T] { () -> T } -> [T, bool]
4
+ def self.forbid_faults: [T] { () -> T } -> T
5
+
3
6
  # The Handle allocator/resolver a wire walk works against: alloc
4
7
  # registers a non-wire-representable object and returns its Handle,
5
8
  # fetch resolves a wire Handle id back to the live object. The
@@ -29,7 +29,7 @@ module Kobako
29
29
 
30
30
  attr_reader usage: Kobako::Usage
31
31
 
32
- def define: (Symbol | String name) -> Kobako::Namespace
32
+ def bind: (Symbol | String path, untyped object) -> Kobako::Sandbox
33
33
 
34
34
  def preload: (code: String, name: Symbol | String) -> Kobako::Sandbox
35
35
  | (binary: String) -> Kobako::Sandbox
@@ -12,9 +12,9 @@ module Kobako
12
12
 
13
13
  CALLABLE_ALLOW: Array[Symbol]
14
14
 
15
- def self?.dispatch: (String request_bytes, Kobako::Transport::_NamespaceRegistry namespaces, Kobako::Codec::_HandleTable handler, Kobako::Transport::_GuestYielder yield_to_guest) -> String
15
+ def self?.dispatch: (String request_bytes, Kobako::Transport::_ServiceRegistry services, Kobako::Codec::_HandleTable handler, Kobako::Transport::_GuestYielder yield_to_guest) -> String
16
16
 
17
- def self?.resolve_call_args: (Kobako::Transport::Request request, Kobako::Codec::_HandleTable handler) -> [Array[untyped], Hash[Symbol, untyped]]
17
+ def self?.resolve_call_args: (Kobako::Transport::Request request, Kobako::Codec::_HandleTable handler, bool carried_handle) -> [Array[untyped], Hash[Symbol, untyped]]
18
18
 
19
19
  def self?.encode_caught_error: (StandardError error) -> String
20
20
 
@@ -26,9 +26,9 @@ module Kobako
26
26
 
27
27
  def self?.resolve_arg: (untyped value, Kobako::Codec::_HandleTable handler) -> untyped
28
28
 
29
- def self?.resolve_target: (String | Kobako::Handle target, Kobako::Transport::_NamespaceRegistry namespaces, Kobako::Codec::_HandleTable handler) -> untyped
29
+ def self?.resolve_target: (String | Kobako::Handle target, Kobako::Transport::_ServiceRegistry services, Kobako::Codec::_HandleTable handler) -> untyped
30
30
 
31
- def self?.resolve_path: (String path, Kobako::Transport::_NamespaceRegistry namespaces) -> untyped
31
+ def self?.resolve_path: (String path, Kobako::Transport::_ServiceRegistry services) -> untyped
32
32
 
33
33
  def self?.require_live_object!: (Integer id, Kobako::Codec::_HandleTable handler) -> untyped
34
34
 
@@ -16,7 +16,7 @@ module Kobako
16
16
 
17
17
  private
18
18
 
19
- def restore: (untyped value) -> untyped
19
+ def restore: (untyped value, bool carried_handle) -> untyped
20
20
 
21
21
  def yield_failure: (untyped payload, default: String) -> RuntimeError
22
22
  end