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.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -1
- data/README.md +95 -60
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/wasm/cache.rs +39 -1
- data/ext/kobako/src/wasm/dispatch.rs +20 -20
- data/ext/kobako/src/wasm/host_state.rs +261 -34
- data/ext/kobako/src/wasm/instance.rs +467 -272
- data/ext/kobako/src/wasm.rs +50 -19
- data/lib/kobako/capture.rb +46 -0
- data/lib/kobako/codec/decoder.rb +66 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +33 -0
- data/lib/kobako/codec/factory.rb +155 -0
- data/lib/kobako/codec/utils.rb +55 -0
- data/lib/kobako/codec.rb +27 -0
- data/lib/kobako/errors.rb +24 -1
- data/lib/kobako/outcome/panic.rb +42 -0
- data/lib/kobako/outcome.rb +133 -0
- data/lib/kobako/rpc/dispatcher.rb +169 -0
- data/lib/kobako/rpc/envelope.rb +118 -0
- data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
- data/lib/kobako/{wire → rpc}/handle.rb +4 -2
- data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
- data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
- data/lib/kobako/rpc/server.rb +156 -0
- data/lib/kobako/rpc.rb +11 -0
- data/lib/kobako/sandbox.rb +149 -69
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako/wasm.rb +6 -16
- data/lib/kobako.rb +2 -0
- data/sig/kobako/capture.rbs +13 -0
- data/sig/kobako/codec/decoder.rbs +11 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +9 -0
- data/sig/kobako/errors.rbs +52 -0
- data/sig/kobako/outcome/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/rpc/dispatcher.rbs +33 -0
- data/sig/kobako/rpc/envelope.rbs +51 -0
- data/sig/kobako/rpc/fault.rbs +20 -0
- data/sig/kobako/rpc/handle.rbs +19 -0
- data/sig/kobako/rpc/handle_table.rbs +25 -0
- data/sig/kobako/rpc/namespace.rbs +24 -0
- data/sig/kobako/rpc/server.rbs +37 -0
- data/sig/kobako/rpc.rbs +4 -0
- data/sig/kobako/sandbox.rbs +53 -0
- data/sig/kobako/wasm.rbs +37 -0
- data/sig/kobako.rbs +0 -1
- metadata +37 -17
- data/lib/kobako/registry/dispatcher.rb +0 -168
- data/lib/kobako/registry.rb +0 -160
- data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
- data/lib/kobako/sandbox/output_buffer.rb +0 -79
- data/lib/kobako/wire/codec/decoder.rb +0 -87
- data/lib/kobako/wire/codec/encoder.rb +0 -41
- data/lib/kobako/wire/codec/error.rb +0 -35
- data/lib/kobako/wire/codec/factory.rb +0 -136
- data/lib/kobako/wire/codec.rb +0 -44
- data/lib/kobako/wire/envelope/payloads.rb +0 -145
- data/lib/kobako/wire/envelope.rb +0 -147
- 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
|
|
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
|
-
|
|
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
|
-
# +
|
|
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
|
-
|
|
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
|
|
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 "
|
|
3
|
+
require_relative "handle"
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
|
-
|
|
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::
|
|
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 +
|
|
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,
|
|
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 +
|
|
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 =
|
|
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
|
-
|
|
5
|
-
# A named
|
|
6
|
-
|
|
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
|
|
10
|
-
# (must satisfy
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
65
|
+
unless NAME_PATTERN.match?(member_str)
|
|
57
66
|
raise ArgumentError,
|
|
58
|
-
"MemberName must match #{
|
|
67
|
+
"MemberName must match #{NAME_PATTERN.inspect} (got #{member.inspect})"
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
member_str
|