kobako 0.14.0 → 0.16.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +67 -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 +14 -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 +14 -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/extensions.rb +114 -0
  25. data/lib/kobako/catalog/handles.rb +1 -1
  26. data/lib/kobako/catalog/services.rb +135 -0
  27. data/lib/kobako/catalog/snippets.rb +1 -1
  28. data/lib/kobako/catalog.rb +7 -5
  29. data/lib/kobako/codec/decoder.rb +3 -3
  30. data/lib/kobako/codec/encoder.rb +4 -4
  31. data/lib/kobako/codec/ext_types.rb +169 -0
  32. data/lib/kobako/codec/state.rb +98 -0
  33. data/lib/kobako/codec/utils.rb +2 -2
  34. data/lib/kobako/codec.rb +23 -6
  35. data/lib/kobako/extension.rb +47 -0
  36. data/lib/kobako/fault.rb +1 -1
  37. data/lib/kobako/handle.rb +1 -1
  38. data/lib/kobako/outcome.rb +13 -7
  39. data/lib/kobako/pool.rb +1 -1
  40. data/lib/kobako/sandbox.rb +47 -27
  41. data/lib/kobako/transport/dispatcher.rb +25 -18
  42. data/lib/kobako/transport/request.rb +14 -9
  43. data/lib/kobako/transport/response.rb +9 -2
  44. data/lib/kobako/transport/yield.rb +4 -1
  45. data/lib/kobako/transport/yielder.rb +14 -6
  46. data/lib/kobako/version.rb +1 -1
  47. data/lib/kobako.rb +1 -0
  48. data/sig/kobako/catalog/extensions.rbs +25 -0
  49. data/sig/kobako/catalog/services.rbs +27 -0
  50. data/sig/kobako/codec/ext_types.rbs +31 -0
  51. data/sig/kobako/codec/state.rbs +20 -0
  52. data/sig/kobako/codec.rbs +3 -0
  53. data/sig/kobako/extension.rbs +20 -0
  54. data/sig/kobako/sandbox.rbs +3 -1
  55. data/sig/kobako/transport/dispatcher.rbs +4 -4
  56. data/sig/kobako/transport/yielder.rbs +1 -1
  57. data/sig/kobako/transport.rbs +2 -2
  58. metadata +11 -7
  59. data/lib/kobako/catalog/namespaces.rb +0 -115
  60. data/lib/kobako/codec/factory.rb +0 -187
  61. data/lib/kobako/namespace.rb +0 -78
  62. data/sig/kobako/catalog/namespaces.rbs +0 -17
  63. data/sig/kobako/codec/factory.rbs +0 -34
  64. data/sig/kobako/namespace.rbs +0 -21
@@ -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.16.0"
5
5
  end
data/lib/kobako.rb CHANGED
@@ -10,6 +10,7 @@ rescue LoadError
10
10
  end
11
11
 
12
12
  require_relative "kobako/errors"
13
+ require_relative "kobako/extension"
13
14
  require_relative "kobako/transport"
14
15
  require_relative "kobako/catalog"
15
16
  require_relative "kobako/runtime"
@@ -0,0 +1,25 @@
1
+ module Kobako
2
+ module Catalog
3
+ class Extensions
4
+ def initialize: () -> void
5
+
6
+ def install: (untyped extension, snippets: Kobako::Catalog::Snippets, services: Kobako::Catalog::Services) -> self
7
+
8
+ def seal!: () -> self
9
+
10
+ def refresh_backends!: (Kobako::Catalog::Services services) -> self
11
+
12
+ private
13
+
14
+ def refresh_backend: (untyped extension, Kobako::Catalog::Services services, Hash[untyped, untyped] resolved) -> void
15
+
16
+ def validate!: (untyped extension) -> void
17
+
18
+ def assert_dependencies!: () -> void
19
+
20
+ def callable?: (untyped provider) -> bool
21
+
22
+ def initial_object: (untyped provider) -> untyped
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
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 refresh: (Symbol | String path, untyped object) -> self
13
+
14
+ def encode: () -> String
15
+
16
+ def seal!: () -> self
17
+
18
+ def sealed?: () -> bool
19
+
20
+ private
21
+
22
+ def validate_path!: (Symbol | String path) -> String
23
+
24
+ def collision?: (String path) -> bool
25
+ end
26
+ end
27
+ 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
@@ -0,0 +1,20 @@
1
+ module Kobako
2
+ class Extension < Data
3
+ attr_reader name: Symbol
4
+ attr_reader source: String
5
+ attr_reader backend: Kobako::Extension::Backend?
6
+ attr_reader depends_on: Array[Symbol]
7
+
8
+ def self.new: (name: Symbol, source: String, ?backend: Kobako::Extension::Backend?, ?depends_on: Array[Symbol]) -> Kobako::Extension
9
+
10
+ def initialize: (name: Symbol, source: String, ?backend: Kobako::Extension::Backend?, ?depends_on: Array[Symbol]) -> void
11
+
12
+ class Backend < Data
13
+ attr_reader path: String
14
+ attr_reader provider: untyped
15
+
16
+ def self.new: (path: String, provider: untyped) -> Kobako::Extension::Backend
17
+ | (String path, untyped provider) -> Kobako::Extension::Backend
18
+ end
19
+ end
20
+ end
@@ -29,7 +29,9 @@ 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
+
34
+ def install: (*untyped extensions) -> Kobako::Sandbox
33
35
 
34
36
  def preload: (code: String, name: Symbol | String) -> Kobako::Sandbox
35
37
  | (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
@@ -10,10 +10,10 @@ module Kobako
10
10
  end
11
11
 
12
12
  # The Service registry a dispatch resolves constant-path targets
13
- # against. The Sandbox's Kobako::Catalog::Namespaces is the
13
+ # against. The Sandbox's Kobako::Catalog::Services is the
14
14
  # production conformer; modelled structurally so the Transport
15
15
  # layer needs no upward dependency on Catalog.
16
- interface _NamespaceRegistry
16
+ interface _ServiceRegistry
17
17
  def lookup: (String target) -> untyped
18
18
  end
19
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kobako
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aotokitsuruya
@@ -91,20 +91,22 @@ files:
91
91
  - lib/kobako.rb
92
92
  - lib/kobako/capture.rb
93
93
  - lib/kobako/catalog.rb
94
+ - lib/kobako/catalog/extensions.rb
94
95
  - lib/kobako/catalog/handles.rb
95
- - lib/kobako/catalog/namespaces.rb
96
+ - lib/kobako/catalog/services.rb
96
97
  - lib/kobako/catalog/snippets.rb
97
98
  - lib/kobako/codec.rb
98
99
  - lib/kobako/codec/decoder.rb
99
100
  - lib/kobako/codec/encoder.rb
100
101
  - lib/kobako/codec/error.rb
101
- - lib/kobako/codec/factory.rb
102
+ - lib/kobako/codec/ext_types.rb
102
103
  - lib/kobako/codec/handle_walk.rb
104
+ - lib/kobako/codec/state.rb
103
105
  - lib/kobako/codec/utils.rb
104
106
  - lib/kobako/errors.rb
107
+ - lib/kobako/extension.rb
105
108
  - lib/kobako/fault.rb
106
109
  - lib/kobako/handle.rb
107
- - lib/kobako/namespace.rb
108
110
  - lib/kobako/outcome.rb
109
111
  - lib/kobako/outcome/panic.rb
110
112
  - lib/kobako/pool.rb
@@ -129,20 +131,22 @@ files:
129
131
  - sig/kobako.rbs
130
132
  - sig/kobako/capture.rbs
131
133
  - sig/kobako/catalog.rbs
134
+ - sig/kobako/catalog/extensions.rbs
132
135
  - sig/kobako/catalog/handles.rbs
133
- - sig/kobako/catalog/namespaces.rbs
136
+ - sig/kobako/catalog/services.rbs
134
137
  - sig/kobako/catalog/snippets.rbs
135
138
  - sig/kobako/codec.rbs
136
139
  - sig/kobako/codec/decoder.rbs
137
140
  - sig/kobako/codec/encoder.rbs
138
141
  - sig/kobako/codec/error.rbs
139
- - sig/kobako/codec/factory.rbs
142
+ - sig/kobako/codec/ext_types.rbs
140
143
  - sig/kobako/codec/handle_walk.rbs
144
+ - sig/kobako/codec/state.rbs
141
145
  - sig/kobako/codec/utils.rbs
142
146
  - sig/kobako/errors.rbs
147
+ - sig/kobako/extension.rbs
143
148
  - sig/kobako/fault.rbs
144
149
  - sig/kobako/handle.rbs
145
- - sig/kobako/namespace.rbs
146
150
  - sig/kobako/outcome.rbs
147
151
  - sig/kobako/outcome/panic.rbs
148
152
  - sig/kobako/pool.rbs
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "handles"
4
- require_relative "../codec"
5
- require_relative "../errors"
6
- require_relative "../namespace"
7
-
8
- module Kobako
9
- module Catalog
10
- # Kobako::Catalog::Namespaces — per-Sandbox registry of
11
- # +Kobako::Namespace+ entities. Holds the Namespace / Member bindings
12
- # and the preamble emitted on Frame 1.
13
- #
14
- # Public API:
15
- #
16
- # namespaces = Kobako::Catalog::Namespaces.new
17
- # namespace = namespaces.define(:MyService) # => Kobako::Namespace
18
- # namespace.bind(:KV, kv_object) # => namespace (chainable)
19
- # namespaces.encode # => msgpack bytes for Frame 1
20
- # namespaces.lookup("MyService::KV") # => kv_object
21
- #
22
- # Namespaces live at +Kobako::Namespace+. Per-dispatch routing is
23
- # +Kobako::Transport::Dispatcher+'s responsibility — the Dispatcher
24
- # receives this registry and the +Catalog::Handles+ as arguments from
25
- # the +Runtime#on_dispatch+ Proc that +Kobako::Sandbox#initialize+
26
- # installs. The registry holds an injected +Catalog::Handles+ reference so
27
- # dispatch target resolution and host→guest auto-wrap share the same
28
- # Sandbox-owned allocator.
29
- class Namespaces
30
- # Build a fresh registry. +handler+ is an internal seam that injects
31
- # a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
32
- # is pinned near +MAX_ID+ to exercise the cap-exhaustion path
33
- # without 2³¹ allocations. Production callers leave it at the default.
34
- def initialize(handler: Catalog::Handles.new)
35
- @namespaces = {} # : Hash[String, Kobako::Namespace]
36
- @handler = handler
37
- @sealed = false
38
- @encoded = nil # : String?
39
- end
40
-
41
- # Declare or retrieve the Namespace named +name+ (idempotent).
42
- # +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
43
- # +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
44
- # name, creating it if it does not exist. Raises +ArgumentError+ when
45
- # +name+ is malformed, or when called after the owning Sandbox has been
46
- # sealed by its first invocation.
47
- def define(name)
48
- raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
49
-
50
- name_str = name.to_s
51
- unless Namespace::NAME_PATTERN.match?(name_str)
52
- raise ArgumentError,
53
- "Namespace name must match #{Namespace::NAME_PATTERN.inspect} (got #{name.inspect})"
54
- end
55
-
56
- @namespaces[name_str] ||= Namespace.new(name_str)
57
- end
58
-
59
- # Resolve a +target+ path of the form +"<Namespace>::<Member>"+
60
- # (e.g. +"MyService::KV"+) to the bound Host object. +target+ is a
61
- # two-level path using the +::+ separator. Returns the bound Host
62
- # object. Raises +KeyError+ when the namespace or the member is not
63
- # bound.
64
- def lookup(target)
65
- namespace_name, member_name = target.to_s.split("::", 2)
66
- namespace = @namespaces[namespace_name]
67
- raise KeyError, "no namespace named #{namespace_name.inspect}" if namespace.nil?
68
- raise KeyError, "no member in target #{target.inspect}" unless member_name
69
-
70
- namespace.fetch(member_name)
71
- end
72
-
73
- # Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
74
- # Routes through Kobako::Codec::Encoder like every other host-side
75
- # wire encode so there is a single codec path; the preamble carries
76
- # only Strings and Arrays, so none of the kobako ext types actually
77
- # fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
78
- # Returns a binary +String+ of msgpack bytes.
79
- #
80
- # Once sealed, the bytes are computed once and reused for every
81
- # subsequent invocation: sealing freezes Service registration at the
82
- # first invocation, so the preamble is exactly the bindings that
83
- # existed at that moment — a bind reaching a +Kobako::Namespace+
84
- # after the seal raises +ArgumentError+ and never alters Frame 1.
85
- # The memo is gated on the seal rather than dropped per mutation (the
86
- # +Catalog::Snippets#encode+ approach) because a +Member+ bind lands
87
- # on a child +Kobako::Namespace+, invisible to this collection; only
88
- # the seal guarantees nothing further can change.
89
- def encode
90
- return @encoded if @encoded
91
-
92
- bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
93
- @encoded = bytes if @sealed
94
- bytes
95
- end
96
-
97
- # Mark the registry as sealed and propagate the seal to every
98
- # declared +Kobako::Namespace+. Called by +Sandbox+ on the first
99
- # invocation. After sealing, both #define and +Namespace#bind+
100
- # raise ArgumentError. Idempotent.
101
- def seal!
102
- return self if @sealed
103
-
104
- @sealed = true
105
- @namespaces.each_value(&:seal!)
106
- self
107
- end
108
-
109
- # Returns +true+ when #seal! has been called, +false+ otherwise.
110
- def sealed?
111
- @sealed
112
- end
113
- end
114
- end
115
- end