kobako 0.3.0 → 0.4.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 +85 -5
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/wasm/cache.rs +12 -4
- data/ext/kobako/src/wasm/dispatch.rs +15 -14
- data/ext/kobako/src/wasm/host_state.rs +111 -5
- data/ext/kobako/src/wasm/instance.rs +135 -33
- data/ext/kobako/src/wasm.rs +1 -0
- data/lib/kobako/codec/decoder.rb +0 -2
- data/lib/kobako/codec/factory.rb +13 -10
- data/lib/kobako/codec/utils.rb +105 -13
- data/lib/kobako/handle.rb +62 -0
- data/lib/kobako/handle_table.rb +119 -0
- data/lib/kobako/invocation.rb +56 -25
- data/lib/kobako/outcome.rb +42 -12
- data/lib/kobako/rpc/dispatcher.rb +22 -20
- data/lib/kobako/rpc/envelope.rb +7 -7
- data/lib/kobako/rpc/fault.rb +1 -1
- data/lib/kobako/rpc/server.rb +12 -24
- data/lib/kobako/rpc/wire_error.rb +23 -0
- data/lib/kobako/sandbox.rb +77 -24
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/codec/factory.rbs +1 -1
- data/sig/kobako/codec/utils.rbs +10 -0
- data/sig/kobako/handle.rbs +19 -0
- data/sig/kobako/handle_table.rbs +23 -0
- data/sig/kobako/invocation.rbs +3 -1
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/rpc/dispatcher.rbs +7 -7
- data/sig/kobako/rpc/envelope.rbs +3 -3
- data/sig/kobako/rpc/server.rbs +1 -7
- data/sig/kobako/rpc/wire_error.rbs +6 -0
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/usage.rbs +11 -0
- data/sig/kobako/wasm.rbs +2 -0
- metadata +9 -5
- data/lib/kobako/rpc/handle.rb +0 -39
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/handle_table.rbs +0 -25
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "handle"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
7
|
+
# The table is owned by +Kobako::Sandbox+ ({docs/behavior.md B-19}[link:../../docs/behavior.md])
|
|
8
|
+
# and injected into the per-Sandbox +Kobako::RPC::Server+ so guest→host
|
|
9
|
+
# RPC dispatch resolves Handle targets and arguments against the same
|
|
10
|
+
# table that host→guest wire encoding allocates into
|
|
11
|
+
# ({docs/behavior.md B-14, B-34}[link:../../docs/behavior.md]).
|
|
12
|
+
#
|
|
13
|
+
# Lifecycle invariants ({docs/behavior.md}[link:../../docs/behavior.md]):
|
|
14
|
+
#
|
|
15
|
+
# - {docs/behavior.md B-15}[link:../../docs/behavior.md] — Handle IDs are
|
|
16
|
+
# allocated by a monotonically increasing counter scoped to a single
|
|
17
|
+
# invocation. The first ID issued in an invocation is 1; ID 0 is reserved
|
|
18
|
+
# as the invalid sentinel and is never returned by +#alloc+.
|
|
19
|
+
#
|
|
20
|
+
# - {docs/behavior.md B-19}[link:../../docs/behavior.md] — At every
|
|
21
|
+
# invocation boundary (via +#reset!+), every Handle issued under the
|
|
22
|
+
# old state becomes invalid. Reset applies uniformly regardless of
|
|
23
|
+
# allocation source (B-14 Service return or B-34 host-injected
|
|
24
|
+
# argument).
|
|
25
|
+
#
|
|
26
|
+
# - {docs/behavior.md B-21}[link:../../docs/behavior.md] — The cap is
|
|
27
|
+
# +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap raises immediately
|
|
28
|
+
# — no silent truncation, no wrap, no ID reuse.
|
|
29
|
+
class HandleTable
|
|
30
|
+
# Build a fresh, empty HandleTable. +next_id+ is an internal seam that
|
|
31
|
+
# sets the starting value of the monotonic counter (defaults to 1 per
|
|
32
|
+
# B-15); tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
33
|
+
# the cap-exhaustion path without 2³¹ allocations.
|
|
34
|
+
def initialize(next_id: 1)
|
|
35
|
+
@entries = {} # : Hash[Integer, untyped]
|
|
36
|
+
@next_id = next_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bind +object+ in the table and return a +Kobako::Handle+ token
|
|
40
|
+
# for it. +object+ is any host-side Ruby object to bind. Returns a
|
|
41
|
+
# freshly-allocated +Kobako::Handle+ whose +#id+ falls in
|
|
42
|
+
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
43
|
+
# +Kobako::HandleTableExhausted+ if the next ID would exceed the
|
|
44
|
+
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
45
|
+
# and the allocator share the same invariant
|
|
46
|
+
# ({docs/behavior.md B-21}[link:../../docs/behavior.md]).
|
|
47
|
+
#
|
|
48
|
+
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
49
|
+
# allocator's output a domain entity; +Kobako::Handle.from_wire+
|
|
50
|
+
# is reserved for the codec's wire-decode path, where the id is
|
|
51
|
+
# the only thing the bytes carry.
|
|
52
|
+
def alloc(object)
|
|
53
|
+
id = @next_id
|
|
54
|
+
cap = Kobako::Handle::MAX_ID
|
|
55
|
+
if id > cap
|
|
56
|
+
raise HandleTableExhausted,
|
|
57
|
+
"Handle id space exhausted: allocation would assign id #{id}, exceeding the cap (#{cap})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@entries[id] = object
|
|
61
|
+
@next_id = id + 1
|
|
62
|
+
Kobako::Handle.from_wire(id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resolve a Handle ID to its bound object. +id+ is a Handle ID previously
|
|
66
|
+
# returned by +#alloc+. Returns the bound object. Raises
|
|
67
|
+
# +Kobako::HandleTableError+ if +id+ is not currently bound.
|
|
68
|
+
def fetch(id)
|
|
69
|
+
require_bound!(id)
|
|
70
|
+
@entries[id]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Remove and return the binding for +id+. +id+ is the Handle ID to
|
|
74
|
+
# release. Returns the previously-bound object. Raises
|
|
75
|
+
# +Kobako::HandleTableError+ if +id+ is not currently bound.
|
|
76
|
+
def release(id)
|
|
77
|
+
require_bound!(id)
|
|
78
|
+
@entries.delete(id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Clear all entries AND reset the counter to 1. Called at the per-invocation
|
|
82
|
+
# boundary by +Kobako::Sandbox+ — see
|
|
83
|
+
# {docs/behavior.md B-19}[link:../../docs/behavior.md]. Returns +self+.
|
|
84
|
+
def reset!
|
|
85
|
+
@entries.clear
|
|
86
|
+
@next_id = 1
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Mark the entry at +id+ as disconnected (ABA protection). +id+ is the
|
|
91
|
+
# Handle ID to poison; silently ignored if +id+ is not currently bound.
|
|
92
|
+
# Returns +self+ for chainability, matching the convention of +#reset!+.
|
|
93
|
+
def mark_disconnected(id)
|
|
94
|
+
@entries[id] = :disconnected if @entries.key?(id)
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns the number of currently-bound entries.
|
|
99
|
+
def size
|
|
100
|
+
@entries.size
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns +true+ when +id+ is currently bound, +false+ otherwise.
|
|
104
|
+
def include?(id)
|
|
105
|
+
@entries.key?(id)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Single source of truth for the "unknown Handle id" raise shared by
|
|
111
|
+
# {#fetch} and {#release}. Returns +nil+ on success; raises
|
|
112
|
+
# +Kobako::HandleTableError+ when +id+ is not currently bound.
|
|
113
|
+
def require_bound!(id)
|
|
114
|
+
return if @entries.key?(id)
|
|
115
|
+
|
|
116
|
+
raise HandleTableError, "unknown Handle id: #{id.inspect}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/kobako/invocation.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "handle"
|
|
4
4
|
require_relative "codec"
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
@@ -15,13 +15,21 @@ module Kobako
|
|
|
15
15
|
# anything that passes +Invocation.new+ is safe to encode and ship to
|
|
16
16
|
# the guest.
|
|
17
17
|
#
|
|
18
|
-
# Invocation sits at top level, not under +Kobako::RPC+: RPC in SPEC
|
|
19
|
-
# the guest→host capability channel (Server / Client / Request /
|
|
20
|
-
# Response
|
|
21
|
-
# entrypoint dispatch)
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
18
|
+
# Invocation sits at top level, not under +Kobako::RPC+: RPC in SPEC
|
|
19
|
+
# is the guest→host capability channel (Server / Client / Request /
|
|
20
|
+
# Response); Invocation is the opposite direction (host→guest
|
|
21
|
+
# entrypoint dispatch). +#encode+ takes the Sandbox's HandleTable
|
|
22
|
+
# and routes any non-wire-representable +args+ / +kwargs+ leaf
|
|
23
|
+
# through it as a +Kobako::Handle+
|
|
24
|
+
# ({docs/behavior.md B-34}[link:../../docs/behavior.md]) — the
|
|
25
|
+
# symmetric counterpart of the guest→host wrap path in
|
|
26
|
+
# +Kobako::RPC::Dispatcher#wrap_as_handle+ (B-14). A
|
|
27
|
+
# +Kobako::Handle+ that arrives **already constructed** in the
|
|
28
|
+
# caller's +args+ / +kwargs+ is rejected at construction (E-29):
|
|
29
|
+
# legitimate Handles only enter Host App code through error fields,
|
|
30
|
+
# so a Handle reaching the call site is by definition smuggled in.
|
|
31
|
+
# The +#encode+ output is the "Invocation envelope" that ships
|
|
32
|
+
# through the +__kobako_run+ command buffer.
|
|
25
33
|
#
|
|
26
34
|
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
27
35
|
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
@@ -46,15 +54,22 @@ module Kobako
|
|
|
46
54
|
# Encode this Invocation to the msgpack bytes the guest's
|
|
47
55
|
# +__kobako_run+ entry point consumes as its command-buffer payload
|
|
48
56
|
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]).
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# (
|
|
53
|
-
|
|
57
|
+
# Walks +args+ / +kwargs+ through {Codec::Utils.deep_wrap} so any
|
|
58
|
+
# non-wire-representable leaf is allocated into +handle_table+ and
|
|
59
|
+
# replaced with a +Kobako::Handle+
|
|
60
|
+
# ({docs/behavior.md B-34}[link:../../docs/behavior.md]); the
|
|
61
|
+
# +handle_table+ argument is the Sandbox's table, sharing the same
|
|
62
|
+
# allocator the guest→host return path (B-14) uses.
|
|
63
|
+
#
|
|
64
|
+
# Layout: msgpack map with string keys +"entrypoint"+ (Symbol via
|
|
65
|
+
# ext 0x00), +"args"+ (Array), +"kwargs"+ (Map with Symbol keys);
|
|
66
|
+
# any wrapped leaf rides as ext 0x01 in its original position
|
|
67
|
+
# (docs/wire-codec.md § ext 0x01 position rules).
|
|
68
|
+
def encode(handle_table)
|
|
54
69
|
Codec::Encoder.encode(
|
|
55
70
|
"entrypoint" => entrypoint,
|
|
56
|
-
"args" => args,
|
|
57
|
-
"kwargs" => kwargs
|
|
71
|
+
"args" => Codec::Utils.deep_wrap(args, handle_table),
|
|
72
|
+
"kwargs" => Codec::Utils.deep_wrap(kwargs, handle_table)
|
|
58
73
|
)
|
|
59
74
|
end
|
|
60
75
|
|
|
@@ -80,22 +95,25 @@ module Kobako
|
|
|
80
95
|
target_str.to_sym
|
|
81
96
|
end
|
|
82
97
|
|
|
83
|
-
# E-29: +args+ must not contain a +Kobako::
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
98
|
+
# E-29: +args+ must not contain a +Kobako::Handle+. The Handle
|
|
99
|
+
# allocator lives inside the Host Gem; legitimate paths surface
|
|
100
|
+
# Handle objects only through raised error fields, so a Handle
|
|
101
|
+
# reaching +args+ is a forged or smuggled token. Non-wire-
|
|
102
|
+
# representable arguments that are not Handles are handled by
|
|
103
|
+
# auto-wrap inside +#encode+ (B-34) — the reject path is reserved
|
|
104
|
+
# for Handle objects specifically.
|
|
88
105
|
def validate_args!(args)
|
|
89
106
|
raise ArgumentError, "Invocation args must be Array" unless args.is_a?(Array)
|
|
90
|
-
raise ArgumentError, "
|
|
107
|
+
raise ArgumentError, forged_handle_message("args") if args.any?(Kobako::Handle)
|
|
91
108
|
|
|
92
109
|
args
|
|
93
110
|
end
|
|
94
111
|
|
|
95
|
-
# E-30
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
112
|
+
# E-30 covers the non-Symbol kwargs-key case; E-29 also rejects a
|
|
113
|
+
# +Kobako::Handle+ arriving as a kwargs value (same forged-token
|
|
114
|
+
# principle as the +args+ branch). Both checks live here so the
|
|
115
|
+
# Host App sees the host-side error message before any encode /
|
|
116
|
+
# decode boundary.
|
|
99
117
|
def validate_kwargs!(kwargs)
|
|
100
118
|
raise ArgumentError, "Invocation kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
101
119
|
|
|
@@ -104,9 +122,22 @@ module Kobako
|
|
|
104
122
|
raise ArgumentError,
|
|
105
123
|
"Invocation kwargs keys must be Symbols (got #{bad_keys.inspect})"
|
|
106
124
|
end
|
|
125
|
+
raise ArgumentError, forged_handle_message("kwargs values") if kwargs.each_value.any?(Kobako::Handle)
|
|
107
126
|
|
|
108
127
|
kwargs
|
|
109
128
|
end
|
|
129
|
+
|
|
130
|
+
# Single source of truth for the E-29 reject message so the args
|
|
131
|
+
# and kwargs branches stay phrased identically. Message stays in
|
|
132
|
+
# caller vocabulary: it names the affected slot and the reason
|
|
133
|
+
# without leaking SPEC anchor identifiers (B-xx / E-xx live in
|
|
134
|
+
# source comments, not user-visible errors) or self-referential
|
|
135
|
+
# architecture terms — the error is raised BY kobako, so saying
|
|
136
|
+
# "allocated by the Host Gem" reads as third-person about self.
|
|
137
|
+
def forged_handle_message(slot)
|
|
138
|
+
"Invocation #{slot} must not contain a Kobako::Handle — " \
|
|
139
|
+
"Kobako::Handle instances are internal wire tokens, not caller-constructible"
|
|
140
|
+
end
|
|
110
141
|
# steep:ignore:end
|
|
111
142
|
end
|
|
112
143
|
end
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "outcome/panic"
|
|
4
|
+
require_relative "rpc/wire_error"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
# Host-facing boundary for the OUTCOME_BUFFER produced by
|
|
@@ -46,11 +47,19 @@ module Kobako
|
|
|
46
47
|
|
|
47
48
|
# TrapError for unknown or absent tag
|
|
48
49
|
# ({docs/wire-codec.md ABI Signatures}[link:../../docs/wire-codec.md]:
|
|
49
|
-
#
|
|
50
|
+
# zero-length output and unrecognised first byte both walk the trap
|
|
51
|
+
# path). The user-facing message stays in caller vocabulary — the
|
|
52
|
+
# raw tag byte (or absence) belongs in +details+ for operators, not
|
|
53
|
+
# in the message a caller sees.
|
|
50
54
|
def build_trap_error(tag)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if tag.nil?
|
|
56
|
+
TrapError.new("Sandbox exited without producing a result")
|
|
57
|
+
else
|
|
58
|
+
TrapError.new(
|
|
59
|
+
"Sandbox produced an unrecognised result; the runtime is corrupted, " \
|
|
60
|
+
"discard this Sandbox before another invocation"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
54
63
|
end
|
|
55
64
|
|
|
56
65
|
def split_tag(bytes)
|
|
@@ -63,11 +72,18 @@ module Kobako
|
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
# Decode failure on the success tag is a SandboxError (E-09): the
|
|
66
|
-
# framing was fine, but the carried value is unrepresentable.
|
|
75
|
+
# framing was fine, but the carried value is unrepresentable. The
|
|
76
|
+
# specific codec fault is stashed in +details[:wire_error]+ rather
|
|
77
|
+
# than spliced into the message — callers cannot act on the inner
|
|
78
|
+
# "Symbol payload must be …" wording, but operators triaging a
|
|
79
|
+
# corrupted Sandbox runtime still need it.
|
|
67
80
|
def decode_value(body)
|
|
68
81
|
Kobako::Codec::Decoder.decode(body)
|
|
69
82
|
rescue Kobako::Codec::Error => e
|
|
70
|
-
raise build_wire_violation_error(
|
|
83
|
+
raise build_wire_violation_error(
|
|
84
|
+
"Sandbox produced an invalid result value",
|
|
85
|
+
wire_error: e.message
|
|
86
|
+
)
|
|
71
87
|
end
|
|
72
88
|
|
|
73
89
|
# Decode failure on the panic tag is a SandboxError (E-08). Either
|
|
@@ -77,16 +93,21 @@ module Kobako
|
|
|
77
93
|
def decode_panic(body)
|
|
78
94
|
raise build_panic_error(parse_panic(body))
|
|
79
95
|
rescue Kobako::Codec::Error => e
|
|
80
|
-
raise build_wire_violation_error(
|
|
96
|
+
raise build_wire_violation_error(
|
|
97
|
+
"Sandbox produced an invalid panic record",
|
|
98
|
+
wire_error: e.message
|
|
99
|
+
)
|
|
81
100
|
end
|
|
82
101
|
|
|
83
102
|
# Build a +Panic+ value object from the msgpack-decoded body. Raises
|
|
84
103
|
# +Kobako::Codec::InvalidType+ when the body is not a map or
|
|
85
104
|
# when required keys are missing — both routed by +decode_panic+ to
|
|
86
|
-
# +build_wire_violation_error+.
|
|
105
|
+
# +build_wire_violation_error+. The +InvalidType+ message itself is
|
|
106
|
+
# never user-facing; it lands in +details[:wire_error]+ via the
|
|
107
|
+
# rescue chain above.
|
|
87
108
|
def parse_panic(body)
|
|
88
109
|
map = Kobako::Codec::Decoder.decode(body)
|
|
89
|
-
raise Kobako::Codec::InvalidType, "
|
|
110
|
+
raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
90
111
|
|
|
91
112
|
Kobako::Codec::Utils.wire_boundary do
|
|
92
113
|
Panic.new(
|
|
@@ -129,11 +150,20 @@ module Kobako
|
|
|
129
150
|
end
|
|
130
151
|
end
|
|
131
152
|
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
# Lift the wire-violation fallback to the real
|
|
154
|
+
# +Kobako::RPC::WireError+ class so callers can +rescue+ it
|
|
155
|
+
# specifically instead of pattern-matching on +error.klass+. The
|
|
156
|
+
# +klass+ field is still populated so existing operator-side
|
|
157
|
+
# tooling that greps on the string continues to work.
|
|
158
|
+
# +wire_error+ carries the inner codec / framing message for
|
|
159
|
+
# operator diagnosis without polluting the user-facing
|
|
160
|
+
# +#message+.
|
|
161
|
+
def build_wire_violation_error(message, wire_error: nil)
|
|
162
|
+
Kobako::RPC::WireError.new(
|
|
134
163
|
message,
|
|
135
164
|
origin: Panic::ORIGIN_SANDBOX,
|
|
136
|
-
klass: "Kobako::RPC::WireError"
|
|
165
|
+
klass: "Kobako::RPC::WireError",
|
|
166
|
+
details: wire_error.nil? ? nil : { wire_error: wire_error }
|
|
137
167
|
)
|
|
138
168
|
end
|
|
139
169
|
end
|
|
@@ -34,22 +34,23 @@ module Kobako
|
|
|
34
34
|
# Dispatch a single RPC request and return the encoded response bytes.
|
|
35
35
|
# Called by +Kobako::RPC::Server#dispatch+ which is invoked from the
|
|
36
36
|
# Rust ext inside +__kobako_dispatch+. +request_bytes+ is the
|
|
37
|
-
# msgpack-encoded Request envelope. +server+
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
37
|
+
# msgpack-encoded Request envelope. +server+ resolves path-based
|
|
38
|
+
# Member targets via +#lookup+. +handle_table+ is the Sandbox's
|
|
39
|
+
# HandleTable, injected separately so Dispatcher does not depend
|
|
40
|
+
# on Server publishing a Handle accessor — Handle is a
|
|
41
|
+
# Sandbox-level domain entity (B-19) and the dispatcher is its
|
|
42
|
+
# only consumer here. Always returns a binary String — never
|
|
43
|
+
# raises. Any failure during decode, lookup, or method invocation
|
|
44
|
+
# is reified as a Response.error envelope so the guest sees the
|
|
45
|
+
# failure as a normal RPC error rather than a wasm trap
|
|
44
46
|
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
45
|
-
def dispatch(request_bytes, server)
|
|
47
|
+
def dispatch(request_bytes, server, handle_table)
|
|
46
48
|
request = Kobako::RPC.decode_request(request_bytes)
|
|
47
|
-
handle_table = server.handle_table
|
|
48
49
|
target = resolve_target(request.target, server, handle_table)
|
|
49
50
|
args = request.args.map { |v| resolve_arg(v, handle_table) }
|
|
50
51
|
kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handle_table) }
|
|
51
52
|
value = invoke(target, request.method_name, args, kwargs)
|
|
52
|
-
encode_ok(value,
|
|
53
|
+
encode_ok(value, handle_table)
|
|
53
54
|
rescue StandardError => e
|
|
54
55
|
encode_caught_error(e)
|
|
55
56
|
end
|
|
@@ -58,14 +59,15 @@ module Kobako
|
|
|
58
59
|
# envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
|
|
59
60
|
# rescue. Returns a msgpack-encoded Response envelope (binary). Four
|
|
60
61
|
# error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
|
|
61
|
-
# +Kobako::Codec::Error+ → type="runtime" (
|
|
62
|
+
# +Kobako::Codec::Error+ → type="runtime" (malformed RPC request);
|
|
62
63
|
# +DisconnectedTargetError+ → type="disconnected" (E-14);
|
|
63
64
|
# +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
|
|
64
65
|
# type="argument" (B-12 arity mismatch); everything else →
|
|
65
66
|
# type="runtime".
|
|
66
67
|
def encode_caught_error(error)
|
|
67
68
|
case error
|
|
68
|
-
when Kobako::Codec::Error then encode_error("runtime",
|
|
69
|
+
when Kobako::Codec::Error then encode_error("runtime",
|
|
70
|
+
"Sandbox received a malformed RPC request: #{error.message}")
|
|
69
71
|
when DisconnectedTargetError then encode_error("disconnected", error.message)
|
|
70
72
|
when UndefinedTargetError then encode_error("undefined", error.message)
|
|
71
73
|
when ArgumentError then encode_error("argument", error.message)
|
|
@@ -86,7 +88,7 @@ module Kobako
|
|
|
86
88
|
end
|
|
87
89
|
end
|
|
88
90
|
|
|
89
|
-
# {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An
|
|
91
|
+
# {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An Kobako::Handle arriving as a positional or keyword
|
|
90
92
|
# argument identifies a host-side object previously allocated by a prior
|
|
91
93
|
# RPC's Handle wrap (B-14). Resolve it back to the Ruby object before
|
|
92
94
|
# the dispatch reaches +public_send+. A Handle whose entry is the
|
|
@@ -94,7 +96,7 @@ module Kobako
|
|
|
94
96
|
# the dispatcher emits a Response.error with type="disconnected".
|
|
95
97
|
def resolve_arg(value, handle_table)
|
|
96
98
|
case value
|
|
97
|
-
when Kobako::
|
|
99
|
+
when Kobako::Handle
|
|
98
100
|
require_live_object!(value.id, handle_table)
|
|
99
101
|
else
|
|
100
102
|
value
|
|
@@ -112,7 +114,7 @@ module Kobako
|
|
|
112
114
|
case target
|
|
113
115
|
when String
|
|
114
116
|
resolve_path(target, server)
|
|
115
|
-
when Kobako::
|
|
117
|
+
when Kobako::Handle
|
|
116
118
|
resolve_handle(target, handle_table)
|
|
117
119
|
end
|
|
118
120
|
end
|
|
@@ -144,19 +146,19 @@ module Kobako
|
|
|
144
146
|
# HandleTable via {#wrap_as_handle} and re-encodes with the Capability
|
|
145
147
|
# Handle in place ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
|
|
146
148
|
# path encodes exactly once.
|
|
147
|
-
def encode_ok(value,
|
|
149
|
+
def encode_ok(value, handle_table)
|
|
148
150
|
response = Kobako::RPC::Response.ok(value)
|
|
149
151
|
Kobako::RPC.encode_response(response)
|
|
150
152
|
rescue Kobako::Codec::UnsupportedType
|
|
151
|
-
encode_ok(wrap_as_handle(value,
|
|
153
|
+
encode_ok(wrap_as_handle(value, handle_table), handle_table)
|
|
152
154
|
end
|
|
153
155
|
|
|
154
|
-
# Allocate +value+ in the
|
|
156
|
+
# Allocate +value+ in the Sandbox's HandleTable and return a +Handle+
|
|
155
157
|
# that the wire codec can carry ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
156
158
|
# Used as the fallback path of {#encode_ok} when +value+ has no wire
|
|
157
159
|
# representation.
|
|
158
|
-
def wrap_as_handle(value,
|
|
159
|
-
|
|
160
|
+
def wrap_as_handle(value, handle_table)
|
|
161
|
+
handle_table.alloc(value)
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
def encode_error(type, message)
|
data/lib/kobako/rpc/envelope.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "handle"
|
|
3
|
+
require_relative "../handle"
|
|
4
4
|
require_relative "fault"
|
|
5
5
|
require_relative "../codec"
|
|
6
6
|
|
|
@@ -25,8 +25,8 @@ module Kobako
|
|
|
25
25
|
Request = Data.define(:target, :method_name, :args, :kwargs) do
|
|
26
26
|
# steep:ignore:start
|
|
27
27
|
def initialize(target:, method:, args: [], kwargs: {})
|
|
28
|
-
unless target.is_a?(String) || target.is_a?(Kobako::
|
|
29
|
-
raise ArgumentError, "Request target must be String or Kobako::
|
|
28
|
+
unless target.is_a?(String) || target.is_a?(Kobako::Handle)
|
|
29
|
+
raise ArgumentError, "Request target must be String or Kobako::Handle, got #{target.class}"
|
|
30
30
|
end
|
|
31
31
|
raise ArgumentError, "Request method must be String" unless method.is_a?(String)
|
|
32
32
|
raise ArgumentError, "Request args must be Array" unless args.is_a?(Array)
|
|
@@ -56,7 +56,7 @@ module Kobako
|
|
|
56
56
|
def self.decode_request(bytes)
|
|
57
57
|
arr = Codec::Decoder.decode(bytes)
|
|
58
58
|
unless arr.is_a?(Array) && arr.length == 4
|
|
59
|
-
raise Codec::InvalidType, "Request
|
|
59
|
+
raise Codec::InvalidType, "Request envelope is malformed (expected a 4-element array)"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
target, method_name, args, kwargs = arr
|
|
@@ -87,10 +87,10 @@ module Kobako
|
|
|
87
87
|
|
|
88
88
|
def initialize(status:, payload:)
|
|
89
89
|
unless [STATUS_OK, STATUS_ERROR].include?(status)
|
|
90
|
-
raise ArgumentError, "Response status must be 0 or 1, got #{status.inspect}"
|
|
90
|
+
raise ArgumentError, "Response status must be 0 (ok) or 1 (error), got #{status.inspect}"
|
|
91
91
|
end
|
|
92
92
|
if status == STATUS_ERROR && !payload.is_a?(Kobako::RPC::Fault)
|
|
93
|
-
raise ArgumentError, "Response status
|
|
93
|
+
raise ArgumentError, "Response with error status must carry a Kobako::RPC::Fault payload"
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
super
|
|
@@ -108,7 +108,7 @@ module Kobako
|
|
|
108
108
|
def self.decode_response(bytes)
|
|
109
109
|
arr = Codec::Decoder.decode(bytes)
|
|
110
110
|
unless arr.is_a?(Array) && arr.length == 2
|
|
111
|
-
raise Codec::InvalidType, "Response
|
|
111
|
+
raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
status, payload = arr
|
data/lib/kobako/rpc/fault.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Kobako
|
|
|
23
23
|
# Reach it through +self.class::VALID_TYPES+ — Data.define's block
|
|
24
24
|
# scope resolves bare constants against the enclosing +Wire+ module,
|
|
25
25
|
# so a bare +VALID_TYPES+ would raise +NameError+. Same pattern as
|
|
26
|
-
# +
|
|
26
|
+
# +Kobako::Handle+.
|
|
27
27
|
# steep:ignore:start
|
|
28
28
|
def initialize(type:, message:, details: nil)
|
|
29
29
|
valid_types = self.class::VALID_TYPES
|
data/lib/kobako/rpc/server.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "msgpack"
|
|
|
4
4
|
require_relative "../errors"
|
|
5
5
|
require_relative "envelope"
|
|
6
6
|
require_relative "namespace"
|
|
7
|
-
require_relative "handle_table"
|
|
7
|
+
require_relative "../handle_table"
|
|
8
8
|
require_relative "dispatcher"
|
|
9
9
|
|
|
10
10
|
module Kobako
|
|
@@ -24,10 +24,11 @@ module Kobako
|
|
|
24
24
|
#
|
|
25
25
|
# Namespaces live at +Kobako::RPC::Namespace+
|
|
26
26
|
# (lib/kobako/rpc/namespace.rb). The opaque Handle allocator lives at
|
|
27
|
-
# +Kobako::
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# (
|
|
27
|
+
# +Kobako::HandleTable+ (lib/kobako/handle_table.rb) and is owned by
|
|
28
|
+
# the Sandbox — the Server only holds an injected reference so RPC
|
|
29
|
+
# dispatch resolves against the same table the wire layer allocates
|
|
30
|
+
# into (docs/behavior.md B-19). Dispatch helpers live at
|
|
31
|
+
# +Kobako::RPC::Dispatcher+ (lib/kobako/rpc/dispatcher.rb).
|
|
31
32
|
class Server
|
|
32
33
|
# Build a fresh Server. +handle_table+ is an internal seam that
|
|
33
34
|
# injects a pre-configured +HandleTable+; tests pass one whose +next_id+
|
|
@@ -76,11 +77,6 @@ module Kobako
|
|
|
76
77
|
!namespace.nil? && !member_name.nil? && !namespace[member_name].nil?
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
# Returns all declared +Kobako::RPC::Namespace+ instances as an +Array+.
|
|
80
|
-
def namespaces
|
|
81
|
-
@namespaces.values
|
|
82
|
-
end
|
|
83
|
-
|
|
84
80
|
# Returns the number of declared namespaces as an +Integer+.
|
|
85
81
|
def size
|
|
86
82
|
@namespaces.size
|
|
@@ -122,27 +118,19 @@ module Kobako
|
|
|
122
118
|
@sealed
|
|
123
119
|
end
|
|
124
120
|
|
|
125
|
-
# Reset the HandleTable for a new invocation boundary. Called by
|
|
126
|
-
# +Sandbox+ before each invocation
|
|
127
|
-
# ({docs/behavior.md B-19}[link:../../../docs/behavior.md]).
|
|
128
|
-
def reset_handles!
|
|
129
|
-
@handle_table.reset!
|
|
130
|
-
end
|
|
131
|
-
|
|
132
121
|
# Dispatch a single RPC request and return the encoded response bytes
|
|
133
122
|
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]). +request_bytes+ is a
|
|
134
123
|
# msgpack-encoded Request envelope. Called by the Rust ext from inside
|
|
135
124
|
# +__kobako_dispatch+. Always returns a binary +String+ — never raises.
|
|
136
|
-
#
|
|
137
|
-
# +
|
|
138
|
-
#
|
|
125
|
+
# Forwards both the Server (for namespace lookup) and the injected
|
|
126
|
+
# +HandleTable+ (for Handle resolution / return-value wrapping) to
|
|
127
|
+
# +Dispatcher.dispatch+. The Server holds the HandleTable as an
|
|
128
|
+
# injected reference, not an owned resource — the Sandbox owns it
|
|
129
|
+
# (B-19) — so the table is not exposed via accessors.
|
|
139
130
|
def dispatch(request_bytes)
|
|
140
|
-
Dispatcher.dispatch(request_bytes, self)
|
|
131
|
+
Dispatcher.dispatch(request_bytes, self, @handle_table)
|
|
141
132
|
end
|
|
142
133
|
|
|
143
|
-
# Expose the +HandleTable+ for tests and wire-layer Handle wrapping.
|
|
144
|
-
attr_reader :handle_table
|
|
145
|
-
|
|
146
134
|
private
|
|
147
135
|
|
|
148
136
|
# Split +target+ on the +::+ separator and resolve the namespace half.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
module RPC
|
|
7
|
+
# +Kobako::SandboxError+ subclass raised when the host detects a
|
|
8
|
+
# structural violation of the wire contract while decoding bytes
|
|
9
|
+
# produced by the guest (a malformed Outcome envelope, a result body
|
|
10
|
+
# that fails msgpack decode, a Panic envelope missing required
|
|
11
|
+
# fields). Distinct from a Wasm trap (engine signalled the guest
|
|
12
|
+
# runtime is unrecoverable) and from a normal sandbox-layer failure
|
|
13
|
+
# (the script raised but the protocol was respected): a +WireError+
|
|
14
|
+
# always indicates the guest runtime is corrupted — the only safe
|
|
15
|
+
# recovery is to discard the Sandbox and start a new invocation.
|
|
16
|
+
#
|
|
17
|
+
# Inherits from +Kobako::SandboxError+ so a single
|
|
18
|
+
# +rescue Kobako::SandboxError+ still catches it; callers that want
|
|
19
|
+
# to distinguish wire-violation paths from script failures can
|
|
20
|
+
# +rescue Kobako::RPC::WireError+ directly.
|
|
21
|
+
class WireError < Kobako::SandboxError; end
|
|
22
|
+
end
|
|
23
|
+
end
|