kobako 0.14.0-aarch64-linux → 0.15.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +51 -0
  4. data/README.md +104 -29
  5. data/ROADMAP.md +2 -3
  6. data/SECURITY.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +7 -0
  8. data/crates/kobako-runtime/README.md +1 -1
  9. data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
  10. data/crates/kobako-wasmtime/README.md +1 -1
  11. data/data/kobako.wasm +0 -0
  12. data/lib/kobako/3.3/kobako.so +0 -0
  13. data/lib/kobako/3.4/kobako.so +0 -0
  14. data/lib/kobako/4.0/kobako.so +0 -0
  15. data/lib/kobako/catalog/handles.rb +1 -1
  16. data/lib/kobako/catalog/services.rb +123 -0
  17. data/lib/kobako/catalog/snippets.rb +1 -1
  18. data/lib/kobako/catalog.rb +2 -2
  19. data/lib/kobako/codec/decoder.rb +3 -3
  20. data/lib/kobako/codec/encoder.rb +4 -4
  21. data/lib/kobako/codec/ext_types.rb +169 -0
  22. data/lib/kobako/codec/state.rb +98 -0
  23. data/lib/kobako/codec/utils.rb +2 -2
  24. data/lib/kobako/codec.rb +23 -6
  25. data/lib/kobako/fault.rb +1 -1
  26. data/lib/kobako/handle.rb +1 -1
  27. data/lib/kobako/outcome.rb +13 -7
  28. data/lib/kobako/pool.rb +1 -1
  29. data/lib/kobako/sandbox.rb +20 -13
  30. data/lib/kobako/transport/dispatcher.rb +25 -18
  31. data/lib/kobako/transport/request.rb +14 -9
  32. data/lib/kobako/transport/response.rb +9 -2
  33. data/lib/kobako/transport/yield.rb +4 -1
  34. data/lib/kobako/transport/yielder.rb +14 -6
  35. data/lib/kobako/version.rb +1 -1
  36. data/sig/kobako/catalog/services.rbs +25 -0
  37. data/sig/kobako/codec/ext_types.rbs +31 -0
  38. data/sig/kobako/codec/state.rbs +20 -0
  39. data/sig/kobako/codec.rbs +3 -0
  40. data/sig/kobako/sandbox.rbs +1 -1
  41. data/sig/kobako/transport/dispatcher.rbs +4 -4
  42. data/sig/kobako/transport/yielder.rbs +1 -1
  43. data/sig/kobako/transport.rbs +2 -2
  44. metadata +8 -8
  45. data/lib/kobako/catalog/namespaces.rb +0 -115
  46. data/lib/kobako/codec/factory.rb +0 -187
  47. data/lib/kobako/namespace.rb +0 -78
  48. data/sig/kobako/catalog/namespaces.rbs +0 -17
  49. data/sig/kobako/codec/factory.rbs +0 -34
  50. data/sig/kobako/namespace.rbs +0 -21
@@ -3,7 +3,7 @@
3
3
  require "msgpack"
4
4
 
5
5
  require_relative "error"
6
- require_relative "factory"
6
+ require_relative "ext_types"
7
7
 
8
8
  module Kobako
9
9
  module Codec
@@ -13,8 +13,8 @@ module Kobako
13
13
  # The codec backbone is the official +msgpack+ gem: integers, floats,
14
14
  # strings, arrays, and maps go through the gem's narrowest-encoding
15
15
  # logic; the three kobako-specific ext types (0x00 Symbol, 0x01
16
- # Capability Handle, 0x02 Exception envelope) are registered on
17
- # the cached Kobako::Codec::Factory singleton.
16
+ # Capability Handle, 0x02 Exception envelope) are registered by
17
+ # ExtTypes on the process-wide factory.
18
18
  #
19
19
  # Public API is a single function — +.encode+. The codec is stateless;
20
20
  # there is no buffer accumulator and no streaming write API. Callers
@@ -32,7 +32,7 @@ module Kobako
32
32
  # +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
33
33
  # propagating.
34
34
  def self.encode(value)
35
- Factory.dump(value)
35
+ FACTORY.dump(value)
36
36
  rescue ::RangeError, ::NoMethodError => e
37
37
  raise UnsupportedType, e.message
38
38
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "msgpack"
4
+
5
+ require_relative "error"
6
+ require_relative "utils"
7
+ require_relative "state"
8
+ require_relative "../handle"
9
+ require_relative "../fault"
10
+
11
+ module Kobako
12
+ module Codec
13
+ # The kobako wire ext-type conversions
14
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types)
15
+ # as pure functions: per-operation decode state is threaded in as an
16
+ # argument, so the module itself holds nothing. #build_factory assembles
17
+ # the one +MessagePack::Factory+ these conversions are registered on.
18
+ module ExtTypes
19
+ # MessagePack ext type code reserved for Symbol
20
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
21
+ # → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
22
+ # Rust side.
23
+ EXT_SYMBOL = 0x00
24
+ # MessagePack ext type code reserved for Capability Handle
25
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
26
+ # → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
27
+ # Rust side.
28
+ EXT_HANDLE = 0x01
29
+ # MessagePack ext type code reserved for Exception envelope
30
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
31
+ # → ext 0x02). Module-private — mirrors +codec::EXT_ERRENV+ on the
32
+ # Rust side.
33
+ EXT_ERRENV = 0x02
34
+ private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
35
+
36
+ module_function
37
+
38
+ # Assemble a +MessagePack::Factory+ with the three kobako ext types
39
+ # registered, frozen because registration is its only mutation and
40
+ # happens exactly once. The stateful conversions resolve their
41
+ # per-operation state at call time, so one registered factory serves
42
+ # every thread.
43
+ def build_factory
44
+ factory = MessagePack::Factory.new
45
+ register_symbol(factory)
46
+ register_handle(factory)
47
+ register_fault(factory)
48
+ factory.freeze
49
+ end
50
+
51
+ # Symbol-to-name packer for the ext-0x00 registration.
52
+ def pack_symbol(symbol)
53
+ symbol.name
54
+ end
55
+
56
+ # Validate the ext-0x00 payload as UTF-8 and intern. Raises
57
+ # InvalidEncoding on invalid bytes — SPEC forbids the
58
+ # binary-encoding fallback that msgpack-gem's default unpacker
59
+ # would otherwise apply. The re-tag step lives here because the
60
+ # msgpack ext-type unpacker hands us binary bytes; the assertion
61
+ # itself is shared with Decoder via Utils.assert_utf8!. The
62
+ # +"Symbol"+ label keeps the error message in Ruby vocabulary
63
+ # rather than wire-ext-code vocabulary.
64
+ def unpack_symbol(payload)
65
+ name = payload.b.force_encoding(Encoding::UTF_8)
66
+ Utils.assert_utf8!(name, "Symbol payload")
67
+ name.to_sym
68
+ end
69
+
70
+ # Handle-id packer for the ext-0x01 registration: the fixext-4
71
+ # big-endian id frame.
72
+ def pack_handle(handle)
73
+ [handle.id].pack("N")
74
+ end
75
+
76
+ # Peel off the fixext-4 frame, hand the bytes to the
77
+ # Host-Gem-internal +Kobako::Handle.restore+ factory, and
78
+ # translate the +ArgumentError+ raised by Handle's invariants
79
+ # into a wire-layer +InvalidType+ via Codec::Utils.with_boundary.
80
+ # The Value Object owns the id-range contract; this method only
81
+ # owns the frame shape. Records the Handle sighting on +state+ so a
82
+ # Handle-free decode can skip the downstream resolution walk.
83
+ def unpack_handle(payload, state)
84
+ state.record_handle!
85
+ bytes = payload.b
86
+ raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
87
+
88
+ id = bytes.unpack1("N") # : Integer
89
+ Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
90
+ end
91
+
92
+ # Encode the inner ext-0x02 map via Encoder (not the raw factory) so
93
+ # the embedded payload flows through the same boundary as a top-level
94
+ # encode — nested kobako values (Handle, nested Fault) reach the
95
+ # registered ext-type packers. A +details+ chain nested past the
96
+ # +state+ depth cap has no wire representation and surfaces as
97
+ # +UnsupportedType+. In a payload position (+state+ inside a
98
+ # forbid_faults bracket) the envelope has no wire representation at
99
+ # all, so the refusal routes the value into the position's
100
+ # non-representable handling — the Dispatcher's auto-wrap rescue,
101
+ # or a raise at the yield site.
102
+ def pack_fault(fault, state)
103
+ if state.faults_forbidden?
104
+ raise UnsupportedType, "Kobako::Fault has no wire representation in a payload position"
105
+ end
106
+
107
+ state.within_ext_frame(UnsupportedType) do
108
+ Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
109
+ end
110
+ end
111
+
112
+ # Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
113
+ # inside Decoder.decode's block form, so the value-object's
114
+ # +ArgumentError+ invariants surface as +InvalidType+ through the
115
+ # decoder boundary. Inner decode goes through Decoder (not the raw
116
+ # factory) so the embedded +str+ payloads flow through the same
117
+ # UTF-8 validation as a top-level decode. A nested ext 0x02 in
118
+ # +details+ re-enters this method, so the +state+ ext-frame guard
119
+ # bounds the chain depth to keep it from exhausting the native stack.
120
+ # In a payload position (+state+ inside a forbid_faults bracket) the
121
+ # envelope is a wire violation outright — its sole legal position is
122
+ # the Response fault field.
123
+ def unpack_fault(payload, state)
124
+ if state.faults_forbidden?
125
+ raise InvalidType, "Fault envelope (ext 0x02) is not a legal value in a payload position"
126
+ end
127
+
128
+ state.within_ext_frame(InvalidType) do
129
+ Decoder.decode(payload) do |map|
130
+ raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
131
+
132
+ Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
133
+ end
134
+ end
135
+ end
136
+
137
+ def register_symbol(factory)
138
+ factory.register_type(
139
+ EXT_SYMBOL, Symbol,
140
+ packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
141
+ unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
142
+ )
143
+ end
144
+
145
+ def register_handle(factory)
146
+ factory.register_type(
147
+ EXT_HANDLE, Kobako::Handle,
148
+ packer: ->(handle) { ExtTypes.pack_handle(handle) },
149
+ unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
150
+ )
151
+ end
152
+
153
+ def register_fault(factory)
154
+ factory.register_type(
155
+ EXT_ERRENV, Kobako::Fault,
156
+ packer: ->(fault) { ExtTypes.pack_fault(fault, State.current) },
157
+ unpacker: ->(payload) { ExtTypes.unpack_fault(payload, State.current) }
158
+ )
159
+ end
160
+ end
161
+
162
+ # The process-wide registered factory: ext registration is paid once at
163
+ # load, and a registered +MessagePack::Factory+ only reads its type
164
+ # registry afterwards, so every thread shares this instance for byte
165
+ # work.
166
+ FACTORY = ExtTypes.build_factory
167
+ private_constant :FACTORY
168
+ end
169
+ end
@@ -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