kobako 0.1.2 → 0.2.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +1 -1
  3. data/README.md +95 -60
  4. data/data/kobako.wasm +0 -0
  5. data/ext/kobako/Cargo.toml +1 -1
  6. data/ext/kobako/src/wasm/cache.rs +39 -1
  7. data/ext/kobako/src/wasm/dispatch.rs +20 -20
  8. data/ext/kobako/src/wasm/host_state.rs +261 -34
  9. data/ext/kobako/src/wasm/instance.rs +467 -272
  10. data/ext/kobako/src/wasm.rs +50 -19
  11. data/lib/kobako/capture.rb +46 -0
  12. data/lib/kobako/codec/decoder.rb +66 -0
  13. data/lib/kobako/codec/encoder.rb +37 -0
  14. data/lib/kobako/codec/error.rb +33 -0
  15. data/lib/kobako/codec/factory.rb +155 -0
  16. data/lib/kobako/codec/utils.rb +55 -0
  17. data/lib/kobako/codec.rb +27 -0
  18. data/lib/kobako/errors.rb +24 -1
  19. data/lib/kobako/outcome/panic.rb +42 -0
  20. data/lib/kobako/outcome.rb +133 -0
  21. data/lib/kobako/rpc/dispatcher.rb +169 -0
  22. data/lib/kobako/rpc/envelope.rb +118 -0
  23. data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
  24. data/lib/kobako/{wire → rpc}/handle.rb +4 -2
  25. data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
  26. data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
  27. data/lib/kobako/rpc/server.rb +156 -0
  28. data/lib/kobako/rpc.rb +11 -0
  29. data/lib/kobako/sandbox.rb +149 -69
  30. data/lib/kobako/version.rb +1 -1
  31. data/lib/kobako/wasm.rb +6 -16
  32. data/lib/kobako.rb +2 -0
  33. data/sig/kobako/capture.rbs +13 -0
  34. data/sig/kobako/codec/decoder.rbs +11 -0
  35. data/sig/kobako/codec/encoder.rbs +7 -0
  36. data/sig/kobako/codec/error.rbs +18 -0
  37. data/sig/kobako/codec/factory.rbs +31 -0
  38. data/sig/kobako/codec/utils.rbs +9 -0
  39. data/sig/kobako/errors.rbs +52 -0
  40. data/sig/kobako/outcome/panic.rbs +34 -0
  41. data/sig/kobako/outcome.rbs +24 -0
  42. data/sig/kobako/rpc/dispatcher.rbs +33 -0
  43. data/sig/kobako/rpc/envelope.rbs +51 -0
  44. data/sig/kobako/rpc/fault.rbs +20 -0
  45. data/sig/kobako/rpc/handle.rbs +19 -0
  46. data/sig/kobako/rpc/handle_table.rbs +25 -0
  47. data/sig/kobako/rpc/namespace.rbs +24 -0
  48. data/sig/kobako/rpc/server.rbs +37 -0
  49. data/sig/kobako/rpc.rbs +4 -0
  50. data/sig/kobako/sandbox.rbs +53 -0
  51. data/sig/kobako/wasm.rbs +37 -0
  52. data/sig/kobako.rbs +0 -1
  53. metadata +37 -17
  54. data/lib/kobako/registry/dispatcher.rb +0 -168
  55. data/lib/kobako/registry.rb +0 -160
  56. data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
  57. data/lib/kobako/sandbox/output_buffer.rb +0 -79
  58. data/lib/kobako/wire/codec/decoder.rb +0 -87
  59. data/lib/kobako/wire/codec/encoder.rb +0 -41
  60. data/lib/kobako/wire/codec/error.rb +0 -35
  61. data/lib/kobako/wire/codec/factory.rb +0 -136
  62. data/lib/kobako/wire/codec.rb +0 -44
  63. data/lib/kobako/wire/envelope/payloads.rb +0 -145
  64. data/lib/kobako/wire/envelope.rb +0 -147
  65. data/lib/kobako/wire.rb +0 -40
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "outcome/panic"
4
+
5
+ module Kobako
6
+ # Host-facing boundary for the OUTCOME_BUFFER produced by
7
+ # +__kobako_run+. Takes raw outcome bytes — a one-byte tag followed by
8
+ # the msgpack-encoded body — and maps them to either the unwrapped
9
+ # mruby return value or a raised three-layer
10
+ # ({SPEC.md "Error Scenarios"}[link:../../SPEC.md]) exception.
11
+ #
12
+ # Self-contained: this module owns the wire framing (tag bytes,
13
+ # body decoding), and the +Panic+ wire record lives at
14
+ # +Kobako::Outcome::Panic+. The byte-level msgpack codec at
15
+ # +Kobako::Codec+ is invoked for the body itself; otherwise
16
+ # nothing in +RPC+ participates.
17
+ #
18
+ # * tag 0x01, decode OK → return decoded value
19
+ # * tag 0x01, decode fails → SandboxError (E-09)
20
+ # * tag 0x02, origin="service" → ServiceError (E-13)
21
+ # * tag 0x02, origin="sandbox"/missing → SandboxError (E-04..E-07)
22
+ # * tag 0x02, decode fails → SandboxError (E-08)
23
+ # * unknown tag → TrapError (E-03)
24
+ module Outcome
25
+ # First byte of the OUTCOME_BUFFER for the success branch — body is
26
+ # the bare msgpack encoding of the returned value
27
+ # ({SPEC.md Outcome Envelope}[link:../../SPEC.md]).
28
+ TYPE_VALUE = 0x01
29
+ # First byte of the OUTCOME_BUFFER for the failure branch — body is
30
+ # the msgpack Panic map.
31
+ TYPE_PANIC = 0x02
32
+
33
+ module_function
34
+
35
+ def decode(bytes)
36
+ tag, body = split_tag(bytes)
37
+ case tag
38
+ when TYPE_VALUE
39
+ decode_value(body)
40
+ when TYPE_PANIC
41
+ decode_panic(body)
42
+ else
43
+ raise build_trap_error(tag)
44
+ end
45
+ end
46
+
47
+ # TrapError for unknown or absent tag
48
+ # ({SPEC.md ABI Signatures}[link:../../SPEC.md]: len=0 and unknown-tag
49
+ # both walk the trap path).
50
+ def build_trap_error(tag)
51
+ return TrapError.new("guest exited without writing an outcome (len=0)") if tag.nil?
52
+
53
+ TrapError.new(format("unknown outcome tag 0x%<tag>02x", tag: tag))
54
+ end
55
+
56
+ def split_tag(bytes)
57
+ bytes = bytes.b
58
+ return [nil, "".b] if bytes.empty?
59
+
60
+ tag = bytes.getbyte(0) # : Integer
61
+ body = bytes.byteslice(1, bytes.bytesize - 1) # : String
62
+ [tag, body]
63
+ end
64
+
65
+ # Decode failure on the success tag is a SandboxError (E-09): the
66
+ # framing was fine, but the carried value is unrepresentable.
67
+ def decode_value(body)
68
+ Kobako::Codec::Decoder.decode(body)
69
+ rescue Kobako::Codec::Error => e
70
+ raise build_wire_violation_error("result envelope decode failed: #{e.message}")
71
+ end
72
+
73
+ # Decode failure on the panic tag is a SandboxError (E-08). Either
74
+ # path raises — on success the decoded Panic is mapped to its three-
75
+ # layer exception via +build_panic_error+ and raised; on wire-decode
76
+ # failure the rescue path raises the wire-violation +SandboxError+.
77
+ def decode_panic(body)
78
+ raise build_panic_error(parse_panic(body))
79
+ rescue Kobako::Codec::Error => e
80
+ raise build_wire_violation_error("panic envelope decode failed: #{e.message}")
81
+ end
82
+
83
+ # Build a +Panic+ value object from the msgpack-decoded body. Raises
84
+ # +Kobako::Codec::InvalidType+ when the body is not a map or
85
+ # when required keys are missing — both routed by +decode_panic+ to
86
+ # +build_wire_violation_error+.
87
+ def parse_panic(body)
88
+ map = Kobako::Codec::Decoder.decode(body)
89
+ raise Kobako::Codec::InvalidType, "Panic envelope must be a map, got #{map.class}" unless map.is_a?(Hash)
90
+
91
+ Kobako::Codec::Utils.wire_boundary do
92
+ Panic.new(
93
+ origin: map["origin"], klass: map["class"], message: map["message"],
94
+ backtrace: map["backtrace"] || [], details: map["details"]
95
+ )
96
+ end
97
+ end
98
+
99
+ # Map a decoded Panic record into the corresponding three-layer
100
+ # Ruby exception. +origin == "service"+ → ServiceError (with the
101
+ # +::Disconnected+ subclass selected when the panic carries the
102
+ # disconnected sentinel —
103
+ # {SPEC "Error Classes"}[link:../../SPEC.md]); everything else
104
+ # → SandboxError.
105
+ def build_panic_error(panic)
106
+ panic_target_class(panic).new(
107
+ panic.message,
108
+ origin: panic.origin,
109
+ klass: panic.klass,
110
+ backtrace_lines: panic.backtrace,
111
+ details: panic.details
112
+ )
113
+ end
114
+
115
+ # {SPEC "Error Classes"}[link:../../SPEC.md]: when
116
+ # +origin="service"+ and the panic +class+ field names
117
+ # +ServiceError::Disconnected+, surface that subclass so callers can
118
+ # rescue the disconnected path specifically (E-14).
119
+ def panic_target_class(panic)
120
+ return SandboxError unless panic.origin == Panic::ORIGIN_SERVICE
121
+
122
+ panic.klass == "Kobako::ServiceError::Disconnected" ? ServiceError::Disconnected : ServiceError
123
+ end
124
+
125
+ def build_wire_violation_error(message)
126
+ SandboxError.new(
127
+ message,
128
+ origin: Panic::ORIGIN_SANDBOX,
129
+ klass: "Kobako::RPC::WireError"
130
+ )
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module RPC
5
+ # Pure-function dispatcher for guest-initiated RPC calls. Decodes a
6
+ # msgpack-encoded Request envelope, resolves the target object through
7
+ # the Server (path lookup or HandleTable lookup), invokes the method,
8
+ # and returns a msgpack-encoded Response envelope.
9
+ #
10
+ # The module is stateless — all mutable state is threaded through the
11
+ # +server+ argument so Dispatcher has no instance variables and no side
12
+ # effects beyond mutating the HandleTable via +alloc+ when a non-wire-
13
+ # representable return value must be wrapped ({SPEC.md B-14}[link:../../../SPEC.md]).
14
+ #
15
+ # Entry point:
16
+ #
17
+ # Kobako::RPC::Dispatcher.dispatch(request_bytes, server)
18
+ # # => msgpack-encoded Response bytes (never raises)
19
+ module Dispatcher
20
+ module_function
21
+
22
+ # Internal sentinel raised when target resolution fails. Mapped to
23
+ # Response.error with type="undefined". Contained at the wire boundary —
24
+ # not part of the public Kobako error taxonomy
25
+ # ({SPEC.md E-xx}[link:../../../SPEC.md]).
26
+ class UndefinedTargetError < StandardError; end
27
+
28
+ # Internal sentinel raised when a Handle target resolves to the
29
+ # +:disconnected+ sentinel in the HandleTable (ABA protection,
30
+ # {SPEC.md E-14}[link:../../../SPEC.md]). Mapped to Response.error with
31
+ # type="disconnected". Contained at the wire boundary.
32
+ class DisconnectedTargetError < StandardError; end
33
+
34
+ # Dispatch a single RPC request and return the encoded response bytes.
35
+ # Called by +Kobako::RPC::Server#dispatch+ which is invoked from the
36
+ # Rust ext inside +__kobako_dispatch+. +request_bytes+ is the
37
+ # msgpack-encoded Request envelope. +server+ is the live Server for
38
+ # this run, used to resolve path-based targets via +#lookup+ and to
39
+ # access the +#handle_table+ for Handle-based targets and return-value
40
+ # wrapping. Always returns a binary String — never raises. Any failure
41
+ # during decode, lookup, or method invocation is reified as a
42
+ # Response.error envelope so the guest sees the failure as a normal RPC
43
+ # error rather than a wasm trap
44
+ # ({SPEC.md B-12}[link:../../../SPEC.md]).
45
+ def dispatch(request_bytes, server)
46
+ request = Kobako::RPC.decode_request(request_bytes)
47
+ handle_table = server.handle_table
48
+ target = resolve_target(request.target, server, handle_table)
49
+ args = request.args.map { |v| resolve_arg(v, handle_table) }
50
+ kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handle_table) }
51
+ value = invoke(target, request.method_name, args, kwargs)
52
+ encode_ok(value, server)
53
+ rescue StandardError => e
54
+ encode_caught_error(e)
55
+ end
56
+
57
+ # Map an error caught at the dispatch boundary to a +Response.error+
58
+ # envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
59
+ # rescue. Returns a msgpack-encoded Response envelope (binary). Four
60
+ # error buckets ({SPEC.md B-12}[link:../../../SPEC.md]):
61
+ # +Kobako::Codec::Error+ → type="runtime" (wire decode failed);
62
+ # +DisconnectedTargetError+ → type="disconnected" (E-14);
63
+ # +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
64
+ # type="argument" (B-12 arity mismatch); everything else →
65
+ # type="runtime".
66
+ def encode_caught_error(error)
67
+ case error
68
+ when Kobako::Codec::Error then encode_error("runtime", "wire decode failed: #{error.message}")
69
+ when DisconnectedTargetError then encode_error("disconnected", error.message)
70
+ when UndefinedTargetError then encode_error("undefined", error.message)
71
+ when ArgumentError then encode_error("argument", error.message)
72
+ else encode_error("runtime", "#{error.class}: #{error.message}")
73
+ end
74
+ end
75
+
76
+ # Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
77
+ # (the +Envelope::Request+ invariant pins it). The empty-kwargs
78
+ # branch omits the +**+ splat so Ruby 3.x's strict kwargs
79
+ # separation does not reject calls to no-kwarg methods when the
80
+ # wire carries the uniform empty-map shape.
81
+ def invoke(target, method, args, kwargs)
82
+ if kwargs.empty?
83
+ target.public_send(method.to_sym, *args)
84
+ else
85
+ target.public_send(method.to_sym, *args, **kwargs)
86
+ end
87
+ end
88
+
89
+ # {SPEC.md B-16}[link:../../../SPEC.md] — An RPC::Handle arriving as a positional or keyword
90
+ # argument identifies a host-side object previously allocated by a prior
91
+ # RPC's Handle wrap (B-14). Resolve it back to the Ruby object before
92
+ # the dispatch reaches +public_send+. A Handle whose entry is the
93
+ # +:disconnected+ sentinel (E-14) raises DisconnectedTargetError so
94
+ # the dispatcher emits a Response.error with type="disconnected".
95
+ def resolve_arg(value, handle_table)
96
+ case value
97
+ when Kobako::RPC::Handle
98
+ require_live_object!(value.id, handle_table)
99
+ else
100
+ value
101
+ end
102
+ end
103
+
104
+ # Resolve a Request target to the Ruby object the Server (or
105
+ # HandleTable) holds. String targets go through the Server;
106
+ # Handle targets (ext 0x01) go through the HandleTable.
107
+ #
108
+ # Target type is already validated by +RPC.decode_request+
109
+ # before this method is reached, so no else-branch is needed here —
110
+ # the wire layer is the system boundary that enforces the invariant.
111
+ def resolve_target(target, server, handle_table)
112
+ case target
113
+ when String
114
+ resolve_path(target, server)
115
+ when Kobako::RPC::Handle
116
+ resolve_handle(target, handle_table)
117
+ end
118
+ end
119
+
120
+ def resolve_path(path, server)
121
+ server.lookup(path)
122
+ rescue KeyError => e
123
+ raise UndefinedTargetError, e.message
124
+ end
125
+
126
+ def resolve_handle(handle, handle_table)
127
+ require_live_object!(handle.id, handle_table)
128
+ end
129
+
130
+ # Resolve +id+ through the HandleTable, distinguishing the
131
+ # +:disconnected+ sentinel (E-14) from an unknown id (E-13).
132
+ def require_live_object!(id, handle_table)
133
+ object = handle_table.fetch(id)
134
+ raise DisconnectedTargetError, "Handle id #{id} is disconnected" if object == :disconnected
135
+
136
+ object
137
+ rescue Kobako::HandleTableError => e
138
+ raise UndefinedTargetError, e.message
139
+ end
140
+
141
+ # Encode +value+ as a +Response.ok+ envelope. When the value is not
142
+ # wire-representable per {SPEC.md B-13}[link:../../../SPEC.md]'s type
143
+ # mapping, the +UnsupportedType+ rescue routes it through the
144
+ # HandleTable via {#wrap_as_handle} and re-encodes with the Capability
145
+ # Handle in place ({SPEC.md B-14}[link:../../../SPEC.md]). The happy
146
+ # path encodes exactly once.
147
+ def encode_ok(value, server)
148
+ response = Kobako::RPC::Response.ok(value)
149
+ Kobako::RPC.encode_response(response)
150
+ rescue Kobako::Codec::UnsupportedType
151
+ encode_ok(wrap_as_handle(value, server), server)
152
+ end
153
+
154
+ # Allocate +value+ in the Server's HandleTable and return a +Handle+
155
+ # that the wire codec can carry ({SPEC.md B-14}[link:../../../SPEC.md]).
156
+ # Used as the fallback path of {#encode_ok} when +value+ has no wire
157
+ # representation.
158
+ def wrap_as_handle(value, server)
159
+ Kobako::RPC::Handle.new(server.handle_table.alloc(value))
160
+ end
161
+
162
+ def encode_error(type, message)
163
+ fault = Kobako::RPC::Fault.new(type: type, message: message)
164
+ response = Kobako::RPC::Response.error(fault)
165
+ Kobako::RPC.encode_response(response)
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "handle"
4
+ require_relative "fault"
5
+ require_relative "../codec"
6
+
7
+ module Kobako
8
+ # See lib/kobako/rpc.rb for the umbrella module doc; this file owns the
9
+ # Request / Response value objects and their encode/decode helpers.
10
+ module RPC
11
+ # ---------------- Response status bytes (SPEC.md Response Shape) ---
12
+
13
+ # Response variant marker for the success branch.
14
+ STATUS_OK = 0
15
+ # Response variant marker for the fault branch.
16
+ STATUS_ERROR = 1
17
+
18
+ # Value object for a single guest-initiated RPC Request
19
+ # ({SPEC.md Wire Codec → Request}[link:../../../SPEC.md]).
20
+ #
21
+ # 4-element msgpack array: +[target, method, args, kwargs]+. +target+
22
+ # is either a +String+ (+"Namespace::Member"+) or a {Handle}. SPEC pins
23
+ # +kwargs+ map keys to ext 0x00 Symbol; enforced at construction so the
24
+ # Value Object is the single source of truth.
25
+ Request = Data.define(:target, :method_name, :args, :kwargs) do
26
+ # steep:ignore:start
27
+ def initialize(target:, method:, args: [], kwargs: {})
28
+ unless target.is_a?(String) || target.is_a?(Kobako::RPC::Handle)
29
+ raise ArgumentError, "Request target must be String or Kobako::RPC::Handle, got #{target.class}"
30
+ end
31
+ raise ArgumentError, "Request method must be String" unless method.is_a?(String)
32
+ raise ArgumentError, "Request args must be Array" unless args.is_a?(Array)
33
+
34
+ validate_kwargs!(kwargs)
35
+ super(target: target, method_name: method, args: args, kwargs: kwargs)
36
+ end
37
+
38
+ private
39
+
40
+ def validate_kwargs!(kwargs)
41
+ raise ArgumentError, "Request kwargs must be Hash" unless kwargs.is_a?(Hash)
42
+
43
+ kwargs.each_key do |k|
44
+ raise ArgumentError, "Request kwargs keys must be Symbol, got #{k.class}" unless k.is_a?(Symbol)
45
+ end
46
+ end
47
+ # steep:ignore:end
48
+ end
49
+
50
+ # Encode a {Request} to msgpack bytes. The Value Object's own
51
+ # invariants are the contract; this method does not re-check the shape.
52
+ def self.encode_request(request)
53
+ Codec::Encoder.encode([request.target, request.method_name, request.args, request.kwargs])
54
+ end
55
+
56
+ def self.decode_request(bytes)
57
+ arr = Codec::Decoder.decode(bytes)
58
+ unless arr.is_a?(Array) && arr.length == 4
59
+ raise Codec::InvalidType, "Request must be a 4-element array, got #{arr.inspect}"
60
+ end
61
+
62
+ target, method_name, args, kwargs = arr
63
+ Codec::Utils.wire_boundary do
64
+ Request.new(target: target, method: method_name, args: args, kwargs: kwargs)
65
+ end
66
+ end
67
+
68
+ # Value object for a single host-side RPC Response
69
+ # ({SPEC.md Wire Codec → Response}[link:../../../SPEC.md]).
70
+ #
71
+ # 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
72
+ # (success) or 1 (fault). For success the second element is the return
73
+ # value; for fault it is a {Fault} (ext 0x02 envelope).
74
+ Response = Data.define(:status, :payload) do
75
+ # steep:ignore:start
76
+ def self.ok(value)
77
+ new(status: STATUS_OK, payload: value)
78
+ end
79
+
80
+ def self.error(fault)
81
+ unless fault.is_a?(Kobako::RPC::Fault)
82
+ raise ArgumentError, "Response.error requires Kobako::RPC::Fault, got #{fault.class}"
83
+ end
84
+
85
+ new(status: STATUS_ERROR, payload: fault)
86
+ end
87
+
88
+ def initialize(status:, payload:)
89
+ unless [STATUS_OK, STATUS_ERROR].include?(status)
90
+ raise ArgumentError, "Response status must be 0 or 1, got #{status.inspect}"
91
+ end
92
+ if status == STATUS_ERROR && !payload.is_a?(Kobako::RPC::Fault)
93
+ raise ArgumentError, "Response status=1 payload must be Kobako::RPC::Fault"
94
+ end
95
+
96
+ super
97
+ end
98
+
99
+ def ok? = status == STATUS_OK
100
+ def error? = status == STATUS_ERROR
101
+ # steep:ignore:end
102
+ end
103
+
104
+ def self.encode_response(response)
105
+ Codec::Encoder.encode([response.status, response.payload])
106
+ end
107
+
108
+ def self.decode_response(bytes)
109
+ arr = Codec::Decoder.decode(bytes)
110
+ unless arr.is_a?(Array) && arr.length == 2
111
+ raise Codec::InvalidType, "Response must be a 2-element array, got #{arr.inspect}"
112
+ end
113
+
114
+ status, payload = arr
115
+ Codec::Utils.wire_boundary { Response.new(status: status, payload: payload) }
116
+ end
117
+ end
118
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- module Wire
4
+ module RPC
5
5
  # Wire-level value object for an ext-0x02 Exception envelope.
6
6
  #
7
7
  # SPEC pins the payload (Wire Codec → Ext Types → ext 0x02) to a
@@ -17,12 +17,13 @@ module Kobako
17
17
  # Built on +Data.define+ so equality, hash, and immutability are
18
18
  # inherited from the value-object machinery; only the field invariants
19
19
  # ride on top.
20
- Exception = Data.define(:type, :message, :details) do
20
+ Fault = Data.define(:type, :message, :details) do
21
21
  # +VALID_TYPES+ is attached to the Exception class below this block.
22
22
  # Reach it through +self.class::VALID_TYPES+ — Data.define's block
23
23
  # scope resolves bare constants against the enclosing +Wire+ module,
24
24
  # so a bare +VALID_TYPES+ would raise +NameError+. Same pattern as
25
- # +Wire::Handle+.
25
+ # +RPC::Handle+.
26
+ # steep:ignore:start
26
27
  def initialize(type:, message:, details: nil)
27
28
  valid_types = self.class::VALID_TYPES
28
29
  raise ArgumentError, "type must be String" unless type.is_a?(String)
@@ -31,8 +32,9 @@ module Kobako
31
32
 
32
33
  super
33
34
  end
35
+ # steep:ignore:end
34
36
  end
35
37
 
36
- Exception::VALID_TYPES = %w[runtime argument disconnected undefined].freeze
38
+ Fault::VALID_TYPES = %w[runtime argument disconnected undefined].freeze
37
39
  end
38
40
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- module Wire
4
+ module RPC
5
5
  # Wire-level value object for an ext-0x01 Capability Handle.
6
6
  #
7
7
  # SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
@@ -10,7 +10,7 @@ module Kobako
10
10
  #
11
11
  # This is intentionally a thin value object built on +Data.define+ so
12
12
  # equality, hash, and immutability are inherited. The runtime-facing
13
- # +Kobako::Handle+ class lives at a higher layer and may add behaviour
13
+ # +Kobako::RPC::Handle+ class lives at a higher layer and may add behaviour
14
14
  # (HandleTable bookkeeping, reset semantics). The codec only needs to
15
15
  # carry the opaque integer ID across the wire.
16
16
  Handle = Data.define(:id) do
@@ -20,6 +20,7 @@ module Kobako
20
20
  # +MIN_ID+ would raise +NameError+. Use +self.class::CONST+ to
21
21
  # reach the constants attached to the Handle class itself. Do not
22
22
  # "simplify" this back to bare +MIN_ID+/+MAX_ID+.
23
+ # steep:ignore:start
23
24
  def initialize(id:)
24
25
  min = self.class::MIN_ID
25
26
  max = self.class::MAX_ID
@@ -28,6 +29,7 @@ module Kobako
28
29
 
29
30
  super
30
31
  end
32
+ # steep:ignore:end
31
33
  end
32
34
 
33
35
  Handle::MIN_ID = 1
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../wire/handle"
3
+ require_relative "handle"
4
4
 
5
5
  module Kobako
6
- class Registry
6
+ module RPC
7
7
  # Host-side mapping from opaque integer Handle IDs to Ruby objects
8
- # (capability proxies). One table is owned per Kobako::Registry instance
9
- # (and therefore per Kobako::Sandbox instance). See
8
+ # (capability proxies). One table is owned per +Kobako::RPC::Server+
9
+ # instance (and therefore per +Kobako::Sandbox+ instance). See
10
10
  # {SPEC.md B-15}[link:../../../SPEC.md].
11
11
  #
12
12
  # Lifecycle invariants ({SPEC.md}[link:../../../SPEC.md]):
@@ -26,22 +26,22 @@ module Kobako
26
26
  class HandleTable
27
27
  # Build a fresh, empty HandleTable. +next_id+ is an internal seam that
28
28
  # sets the starting value of the monotonic counter (defaults to 1 per
29
- # B-15); tests pass a value near +Wire::Handle::MAX_ID+ to exercise
29
+ # B-15); tests pass a value near +RPC::Handle::MAX_ID+ to exercise
30
30
  # the cap-exhaustion path without 2³¹ allocations.
31
31
  def initialize(next_id: 1)
32
- @entries = {}
32
+ @entries = {} # : Hash[Integer, untyped]
33
33
  @next_id = next_id
34
34
  end
35
35
 
36
36
  # Bind +object+ in the table and return its newly-allocated Handle ID.
37
37
  # +object+ is any host-side Ruby object to bind. Returns a freshly-
38
- # allocated Handle ID in +[1, Wire::Handle::MAX_ID]+. Raises
38
+ # allocated Handle ID in +[1, RPC::Handle::MAX_ID]+. Raises
39
39
  # +Kobako::HandleTableExhausted+ if the next ID would exceed the cap.
40
- # The cap is anchored on +Wire::Handle+ — the wire codec and the
40
+ # The cap is anchored on +RPC::Handle+ — the wire codec and the
41
41
  # allocator share the same invariant ({SPEC.md B-21}[link:../../../SPEC.md]).
42
42
  def alloc(object)
43
43
  id = @next_id
44
- cap = Wire::Handle::MAX_ID
44
+ cap = RPC::Handle::MAX_ID
45
45
  raise HandleTableExhausted, "HandleTable exhausted: id #{id} exceeds MAX_ID #{cap}" if id > cap
46
46
 
47
47
  @entries[id] = object
@@ -1,23 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- class Registry
5
- # A named namespace of Service Members for one Sandbox ({SPEC.md B-07..B-11}[link:../../../SPEC.md]).
6
- class ServiceGroup
4
+ module RPC
5
+ # A named grouping of Members for one Sandbox
6
+ # ({SPEC.md B-07..B-11}[link:../../../SPEC.md]). Returned by
7
+ # +Sandbox#define+. Each instance owns a flat name→object table of
8
+ # Members; member binding is validated against {NAME_PATTERN}.
9
+ class Namespace
10
+ # Ruby constant-name pattern shared by Namespace and Member names
11
+ # ({SPEC.md B-07/B-08 Notes}[link:../../../SPEC.md]).
12
+ NAME_PATTERN = /\A[A-Z]\w*\z/
13
+
7
14
  attr_reader :name, :members
8
15
 
9
- # Build a new ServiceGroup. +name+ is an already-validated Group name
10
- # (must satisfy +NAME_PATTERN+; validation is the caller's responsibility).
16
+ # Build a new Namespace. +name+ is an already-validated Namespace
17
+ # name (must satisfy {NAME_PATTERN}; validation is the caller's
18
+ # responsibility).
11
19
  def initialize(name)
12
20
  @name = name
13
21
  @members = {}
14
22
  end
15
23
 
16
- # Bind +object+ under +member+ inside this group. +member+ is a
24
+ # Bind +object+ under +member+ inside this Namespace. +member+ is a
17
25
  # constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
18
26
  # object that responds to the methods guest code will invoke. Returns
19
27
  # +self+ for chaining. Raises +ArgumentError+ when +member+ does not
20
- # match the constant pattern, or a member of the same name is already
28
+ # match the constant pattern, or a Member of the same name is already
21
29
  # bound ({SPEC.md B-11}[link:../../../SPEC.md]).
22
30
  def bind(member, object)
23
31
  member_str = validate_member_name!(member)
@@ -32,12 +40,13 @@ module Kobako
32
40
  @members[member.to_s]
33
41
  end
34
42
 
35
- # Strict variant of {#[]}; raises +KeyError+ when no member is
43
+ # Strict variant of {#[]}; raises +KeyError+ when no Member is
36
44
  # registered under +member+.
37
45
  def fetch(member)
38
46
  member_str = member.to_s
39
47
  unless @members.key?(member_str)
40
- raise KeyError, "no member named #{member_str.inspect} in group #{@name.inspect}"
48
+ raise KeyError,
49
+ "no member named #{member_str.inspect} in namespace #{@name.inspect}"
41
50
  end
42
51
 
43
52
  @members[member_str]
@@ -53,9 +62,9 @@ module Kobako
53
62
 
54
63
  def validate_member_name!(member)
55
64
  member_str = member.to_s
56
- unless Registry::NAME_PATTERN.match?(member_str)
65
+ unless NAME_PATTERN.match?(member_str)
57
66
  raise ArgumentError,
58
- "MemberName must match #{Registry::NAME_PATTERN.inspect} (got #{member.inspect})"
67
+ "MemberName must match #{NAME_PATTERN.inspect} (got #{member.inspect})"
59
68
  end
60
69
 
61
70
  member_str