Module: Kobako::Transport::Dispatcher
- Defined in:
- lib/kobako/transport/dispatcher.rb,
sig/kobako/transport/dispatcher.rbs
Overview
Pure-function dispatcher for guest-initiated transport calls. Decodes a msgpack-encoded Request envelope, resolves the target object through the Catalog::Namespaces (path lookup) or Catalog::Handles (Handle lookup), invokes the method, and returns a msgpack-encoded Response envelope.
The module is stateless — all mutable state is threaded through
arguments so Dispatcher has no instance variables and no side
effects beyond mutating the Catalog::Handles via alloc when a
non-wire-representable return value must be wrapped.
Entry point:
Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
# => msgpack-encoded Response bytes (never raises)
Defined Under Namespace
Classes: UndefinedTargetError
Constant Summary collapse
- BREAK_THROW =
Throw tag for the Yielder's break unwind back to the dispatcher's
catchframe.private_constantis a convention boundary — not a defence. :__kobako_break__- META_OWNERS =
Modules whose instance methods are ambient Ruby reflection / metaprogramming surface (+send+,
public_send,instance_eval,method,tap,instance_variable_get, ...) rather than Service behaviour. A guest-supplied method name resolving to one of these is rejected: only methods the bound object itself exposes as Service behaviour are reachable, andpublic_send(:send, ...)would otherwise let a guest pivot throughsendinto the privateKernel#eval/#systemsurface (host RCE). [BasicObject, Kernel, Object, Module, Class].freeze
- GADGET_OWNERS =
Callable gadget types whose own public methods are reflection surface (+Proc#binding+ reaches
Binding#eval,Method#receiver/#unbindhand back the underlying object) rather than Service behaviour. Only CALLABLE_ALLOW is reachable on a target of these types; a bound lambda stays invocable, its reflective surface does not. [Proc, Method, UnboundMethod, Binding].freeze
- CALLABLE_ALLOW =
The sole methods reachable on a GADGET_OWNERS target: invoking it (+call+ /
[]/yield) and the harmlessarity/lambda?describers that aid guest-side debugging. %i[call [] yield arity lambda?].freeze
Class Method Summary collapse
-
.dispatch(request_bytes, namespaces, handler, yield_to_guest) ⇒ Object
Dispatch a single transport request and return the encoded Response bytes.
-
.encode_caught_error(error) ⇒ Object
Map an error caught at the dispatch boundary to a
Response.errorenvelope (binary msgpack). - .encode_error(type, message) ⇒ Object
-
.encode_ok(value, handler) ⇒ Object
Encode
valueas aResponse.okenvelope. -
.invoke(target, method, args, kwargs, yielder = nil) ⇒ Object
Dispatch
methodontarget. -
.reject_meta_method!(target, name) ⇒ Object
Guard the
public_sendbelow against ambient reflection methods. -
.reject_unexposed!(target, name) ⇒ Object
Consult the target's opt-in narrowing predicate.
-
.require_live_object!(id, handler) ⇒ Object
Resolve
idthrough the Catalog::Handles. -
.resolve_arg(value, handler) ⇒ Object
Resolve every Kobako::Handle in an argument — bare or nested in an Array / Hash — back to its host object before the dispatch reaches
public_send, symmetric with the guest→host return path. -
.resolve_call_args(request, handler) ⇒ Object
Resolve positional and keyword arguments off
requestin one step. - .resolve_path(path, namespaces) ⇒ Object
-
.resolve_target(target, namespaces, handler) ⇒ Object
Resolve a Request target to the Ruby object the registry (or Catalog::Handles) holds.
-
.wrap_as_handle(value, handler) ⇒ Object
Allocate
valuein the Sandbox's Catalog::Handles and return aHandlethat the wire codec can carry.
Instance Method Summary collapse
- #self?.dispatch ⇒ String
- #self?.encode_caught_error ⇒ String
- #self?.encode_error ⇒ String
- #self?.encode_ok ⇒ String
- #self?.invoke ⇒ Object
- #self?.reject_meta_method! ⇒ void
- #self?.reject_unexposed! ⇒ void
- #self?.require_live_object! ⇒ Object
- #self?.resolve_arg ⇒ Object
- #self?.resolve_call_args ⇒ [Array[untyped], Hash[Symbol, untyped]]
- #self?.resolve_path ⇒ Object
- #self?.resolve_target ⇒ Object
- #self?.wrap_as_handle ⇒ Kobako::Handle
Class Method Details
.dispatch(request_bytes, namespaces, handler, yield_to_guest) ⇒ Object
Dispatch a single transport request and return the encoded
Response bytes. Invoked from the Runtime#on_dispatch Proc that
Kobako::Sandbox#initialize installs on the ext side; namespaces,
handler, and yield_to_guest are captured in that Proc's
closure so the Dispatcher stays stateless and the registry doesn't
need to publish accessors for the Sandbox-owned Catalog::Handles
or Runtime. yield_to_guest is a String → String callable
(the ext's per-dispatch Kobako::Runtime::GuestYielder) used only
when the Request carries block_given: true. Always
returns a binary String — every failure path is reified as a
Response.error envelope so the guest sees a transport error rather
than a wasm trap.
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/kobako/transport/dispatcher.rb', line 80 def dispatch(request_bytes, namespaces, handler, yield_to_guest) request = Kobako::Transport::Request.decode(request_bytes) target = resolve_target(request.target, namespaces, handler) args, kwargs = resolve_call_args(request, handler) yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given value = catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) } encode_ok(value, handler) rescue StandardError => e encode_caught_error(e) ensure yielder&.invalidate! end |
.encode_caught_error(error) ⇒ Object
Map an error caught at the dispatch boundary to a Response.error
envelope (binary msgpack). error is the StandardError caught by
#dispatch's rescue; the type field tells the guest which kind
of failure it was so it can raise the matching proxy-side error.
106 107 108 109 110 111 112 113 114 |
# File 'lib/kobako/transport/dispatcher.rb', line 106 def encode_caught_error(error) case error when Kobako::Codec::Error then encode_error("runtime", "Sandbox received a malformed request: #{error.}") when UndefinedTargetError then encode_error("undefined", error.) when ArgumentError then encode_error("argument", error.) else encode_error("runtime", "#{error.class}: #{error.}") end end |
.encode_error(type, message) ⇒ Object
232 233 234 235 236 |
# File 'lib/kobako/transport/dispatcher.rb', line 232 def encode_error(type, ) fault = Kobako::Fault.new(type: type, message: ) response = Kobako::Transport::Response.error(fault) response.encode end |
.encode_ok(value, handler) ⇒ Object
Encode value as a Response.ok envelope. When the value is not
wire-representable per the codec's type mapping, the
UnsupportedType rescue routes it through the
Catalog::Handles via #wrap_as_handle and re-encodes with the Capability
Handle in place. The happy path encodes exactly once.
218 219 220 221 222 223 |
# File 'lib/kobako/transport/dispatcher.rb', line 218 def encode_ok(value, handler) response = Kobako::Transport::Response.ok(value) response.encode rescue Kobako::Codec::UnsupportedType encode_ok(wrap_as_handle(value, handler), handler) end |
.invoke(target, method, args, kwargs, yielder = nil) ⇒ Object
Dispatch method on target. kwargs is already Symbol-keyed
(the Request invariant pins it). The empty-kwargs branch omits
the ** splat so Ruby 3.x's strict kwargs separation does not
reject calls to no-kwarg methods when the wire carries the
uniform empty-map shape.
yielder is the host-side Yielder materialised when the guest
call site supplied a block; its Yielder#to_proc
rides the &block slot. &nil is a no-op block argument in Ruby,
so the same call site handles both cases without an explicit
conditional.
127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/kobako/transport/dispatcher.rb', line 127 def invoke(target, method, args, kwargs, yielder = nil) name = method.to_sym (target, name) reject_unexposed!(target, name) block = yielder&.to_proc if kwargs.empty? target.public_send(name, *args, &block) else target.public_send(name, *args, **kwargs, &block) end end |
.reject_meta_method!(target, name) ⇒ Object
Guard the public_send below against ambient reflection methods.
A public method whose owner is a META_OWNERS or GADGET_OWNERS module is
rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
stays invocable). A name with no concrete public method is allowed
only when the target opts into it via respond_to? (dynamic
method_missing Services), since the dangerous methods are all
concretely defined and therefore never reach that branch.
146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/kobako/transport/dispatcher.rb', line 146 def (target, name) owner = target.public_method(name).owner gadget = GADGET_OWNERS.include?(owner) return unless META_OWNERS.include?(owner) || gadget return if gadget && CALLABLE_ALLOW.include?(name) raise UndefinedTargetError, "method #{name.inspect} is not a Service method" rescue NameError return if target.respond_to?(name) raise UndefinedTargetError, "no public method #{name.inspect} on target" end |
.reject_unexposed!(target, name) ⇒ Object
Consult the target's opt-in narrowing predicate. A bound object
may define a private respond_to_guest?(name) to restrict which of its
methods the guest reaches; a falsy answer rejects the dispatch.
The predicate composes beneath #reject_meta_method! — it only narrows,
never re-opening the reflection surface the floor rejects — and is
consulted with the private surface included so the guest's public_send
dispatch can never reach respond_to_guest? itself.
166 167 168 169 170 171 |
# File 'lib/kobako/transport/dispatcher.rb', line 166 def reject_unexposed!(target, name) return unless target.respond_to?(:respond_to_guest?, true) return if target.__send__(:respond_to_guest?, name) raise UndefinedTargetError, "method #{name.inspect} is not exposed to the guest" end |
.require_live_object!(id, handler) ⇒ Object
Resolve id through the Catalog::Handles. An unknown id
surfaces as UndefinedTargetError.
207 208 209 210 211 |
# File 'lib/kobako/transport/dispatcher.rb', line 207 def require_live_object!(id, handler) handler.fetch(id) rescue Kobako::SandboxError => e raise UndefinedTargetError, e. end |
.resolve_arg(value, handler) ⇒ Object
Resolve every Kobako::Handle in an argument — bare or nested in an
Array / Hash — back to its host object before the dispatch reaches
public_send, symmetric with the guest→host return path. A Handle id
with no live entry surfaces as an unrecognized target.
177 178 179 180 181 |
# File 'lib/kobako/transport/dispatcher.rb', line 177 def resolve_arg(value, handler) Kobako::Codec::HandleWalk.deep_restore(value, handler) rescue Kobako::SandboxError => e raise UndefinedTargetError, e. end |
.resolve_call_args(request, handler) ⇒ Object
Resolve positional and keyword arguments off request in one
step. Both pass through #resolve_arg so Capability Handles
round-trip back to the host-side Ruby object before the call
reaches public_send.
97 98 99 100 |
# File 'lib/kobako/transport/dispatcher.rb', line 97 def resolve_call_args(request, handler) [request.args.map { |v| resolve_arg(v, handler) }, request.kwargs.transform_values { |v| resolve_arg(v, handler) }] end |
.resolve_path(path, namespaces) ⇒ Object
199 200 201 202 203 |
# File 'lib/kobako/transport/dispatcher.rb', line 199 def resolve_path(path, namespaces) namespaces.lookup(path) rescue KeyError => e raise UndefinedTargetError, e. end |
.resolve_target(target, namespaces, handler) ⇒ Object
Resolve a Request target to the Ruby object the registry (or Catalog::Handles) holds. String targets go through the registry; Handle targets (ext 0x01) go through the Catalog::Handles.
Target type is already validated by Transport::Request.decode
before this method is reached, so no else-branch is needed here —
the wire layer is the system boundary that enforces the invariant.
190 191 192 193 194 195 196 197 |
# File 'lib/kobako/transport/dispatcher.rb', line 190 def resolve_target(target, namespaces, handler) case target when String resolve_path(target, namespaces) when Kobako::Handle require_live_object!(target.id, handler) end end |
.wrap_as_handle(value, handler) ⇒ Object
Allocate value in the Sandbox's Catalog::Handles and return a Handle
that the wire codec can carry. Used as the fallback path of
#encode_ok when value has no wire representation.
228 229 230 |
# File 'lib/kobako/transport/dispatcher.rb', line 228 def wrap_as_handle(value, handler) handler.alloc(value) end |
Instance Method Details
#self?.dispatch ⇒ String
15 |
# File 'sig/kobako/transport/dispatcher.rbs', line 15
def self?.dispatch: (String request_bytes, Kobako::Catalog::Namespaces namespaces, Kobako::Catalog::Handles handler, Kobako::Transport::_GuestYielder yield_to_guest) -> String
|
#self?.encode_caught_error ⇒ String
19 |
# File 'sig/kobako/transport/dispatcher.rbs', line 19
def self?.encode_caught_error: (StandardError error) -> String
|
#self?.encode_error ⇒ String
39 |
# File 'sig/kobako/transport/dispatcher.rbs', line 39
def self?.encode_error: (String type, String message) -> String
|
#self?.encode_ok ⇒ String
35 |
# File 'sig/kobako/transport/dispatcher.rbs', line 35
def self?.encode_ok: (untyped value, Kobako::Catalog::Handles handler) -> String
|
#self?.invoke ⇒ Object
21 |
# File 'sig/kobako/transport/dispatcher.rbs', line 21
def self?.invoke: (untyped target, String method, Array[untyped] args, Hash[Symbol, untyped] kwargs, ?Kobako::Transport::Yielder? yielder) -> untyped
|
#self?.reject_meta_method! ⇒ void
This method returns an undefined value.
23 |
# File 'sig/kobako/transport/dispatcher.rbs', line 23
def self?.reject_meta_method!: (untyped target, Symbol name) -> void
|
#self?.reject_unexposed! ⇒ void
This method returns an undefined value.
25 |
# File 'sig/kobako/transport/dispatcher.rbs', line 25
def self?.reject_unexposed!: (untyped target, Symbol name) -> void
|
#self?.require_live_object! ⇒ Object
33 |
# File 'sig/kobako/transport/dispatcher.rbs', line 33
def self?.require_live_object!: (Integer id, Kobako::Catalog::Handles handler) -> untyped
|
#self?.resolve_arg ⇒ Object
27 |
# File 'sig/kobako/transport/dispatcher.rbs', line 27
def self?.resolve_arg: (untyped value, Kobako::Catalog::Handles handler) -> untyped
|
#self?.resolve_call_args ⇒ [Array[untyped], Hash[Symbol, untyped]]
17 |
# File 'sig/kobako/transport/dispatcher.rbs', line 17
def self?.resolve_call_args: (Kobako::Transport::Request request, Kobako::Catalog::Handles handler) -> [Array[untyped], Hash[Symbol, untyped]]
|
#self?.resolve_path ⇒ Object
31 |
# File 'sig/kobako/transport/dispatcher.rbs', line 31
def self?.resolve_path: (String path, Kobako::Catalog::Namespaces namespaces) -> untyped
|
#self?.resolve_target ⇒ Object
29 |
# File 'sig/kobako/transport/dispatcher.rbs', line 29
def self?.resolve_target: (String | Kobako::Handle target, Kobako::Catalog::Namespaces namespaces, Kobako::Catalog::Handles handler) -> untyped
|
#self?.wrap_as_handle ⇒ Kobako::Handle
37 |
# File 'sig/kobako/transport/dispatcher.rbs', line 37
def self?.wrap_as_handle: (untyped value, Kobako::Catalog::Handles handler) -> Kobako::Handle
|