kobako 0.2.1 → 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 +205 -59
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/wasm/cache.rs +15 -7
- data/ext/kobako/src/wasm/dispatch.rs +88 -36
- data/ext/kobako/src/wasm/host_state.rs +298 -55
- data/ext/kobako/src/wasm/instance.rs +477 -160
- data/ext/kobako/src/wasm.rs +20 -5
- data/lib/kobako/capture.rb +12 -10
- data/lib/kobako/codec/decoder.rb +3 -4
- data/lib/kobako/codec/encoder.rb +1 -1
- data/lib/kobako/codec/error.rb +3 -2
- data/lib/kobako/codec/factory.rb +24 -17
- data/lib/kobako/codec/utils.rb +105 -12
- data/lib/kobako/codec.rb +2 -1
- data/lib/kobako/errors.rb +22 -10
- data/lib/kobako/handle.rb +62 -0
- data/lib/kobako/handle_table.rb +119 -0
- data/lib/kobako/invocation.rb +143 -0
- data/lib/kobako/outcome/panic.rb +2 -2
- data/lib/kobako/outcome.rb +61 -24
- data/lib/kobako/rpc/dispatcher.rb +30 -28
- data/lib/kobako/rpc/envelope.rb +10 -10
- data/lib/kobako/rpc/fault.rb +4 -3
- data/lib/kobako/rpc/namespace.rb +3 -3
- data/lib/kobako/rpc/server.rb +23 -33
- data/lib/kobako/rpc/wire_error.rb +23 -0
- data/lib/kobako/sandbox.rb +211 -136
- data/lib/kobako/sandbox_options.rb +73 -0
- data/lib/kobako/snippet/binary.rb +30 -0
- data/lib/kobako/snippet/source.rb +28 -0
- data/lib/kobako/snippet/table.rb +174 -0
- data/lib/kobako/snippet.rb +20 -0
- 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/errors.rbs +3 -0
- data/sig/kobako/handle.rbs +19 -0
- data/sig/kobako/handle_table.rbs +23 -0
- data/sig/kobako/invocation.rbs +25 -0
- 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 +22 -17
- data/sig/kobako/sandbox_options.rbs +32 -0
- data/sig/kobako/snippet/binary.rbs +12 -0
- data/sig/kobako/snippet/source.rbs +13 -0
- data/sig/kobako/snippet/table.rbs +36 -0
- data/sig/kobako/snippet.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- data/sig/kobako/wasm.rbs +5 -1
- metadata +21 -5
- data/lib/kobako/rpc/handle.rb +0 -38
- 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
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "handle"
|
|
4
|
+
require_relative "codec"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# Host-side value object for a single +Sandbox#run+ invocation
|
|
8
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md];
|
|
9
|
+
# {docs/behavior.md B-31}[link:../../docs/behavior.md]).
|
|
10
|
+
#
|
|
11
|
+
# An Invocation captures the host-layer concept of "a single +#run+
|
|
12
|
+
# call": the entrypoint constant name plus its positional and keyword
|
|
13
|
+
# arguments. Host pre-flight (E-24 / E-25 / E-29 / E-30) is enforced at
|
|
14
|
+
# construction so the Value Object is the single source of truth —
|
|
15
|
+
# anything that passes +Invocation.new+ is safe to encode and ship to
|
|
16
|
+
# the guest.
|
|
17
|
+
#
|
|
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.
|
|
33
|
+
#
|
|
34
|
+
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
35
|
+
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
36
|
+
class Invocation < Data.define(:entrypoint, :args, :kwargs)
|
|
37
|
+
# Ruby constant-name pattern enforced on the +entrypoint+ Symbol
|
|
38
|
+
# ({docs/behavior.md E-25}[link:../../docs/behavior.md]). Parallel to
|
|
39
|
+
# +Kobako::Snippet::Table::NAME_PATTERN+; the two constants name the
|
|
40
|
+
# same regex but cover distinct surfaces (snippet identity vs.
|
|
41
|
+
# entrypoint resolution) so a future divergence stays local.
|
|
42
|
+
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
43
|
+
|
|
44
|
+
# steep:ignore:start
|
|
45
|
+
def initialize(entrypoint:, args: [], kwargs: {})
|
|
46
|
+
super(
|
|
47
|
+
entrypoint: normalize_entrypoint(entrypoint),
|
|
48
|
+
args: validate_args!(args),
|
|
49
|
+
kwargs: validate_kwargs!(kwargs)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
# steep:ignore:end
|
|
53
|
+
|
|
54
|
+
# Encode this Invocation to the msgpack bytes the guest's
|
|
55
|
+
# +__kobako_run+ entry point consumes as its command-buffer payload
|
|
56
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]).
|
|
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)
|
|
69
|
+
Codec::Encoder.encode(
|
|
70
|
+
"entrypoint" => entrypoint,
|
|
71
|
+
"args" => Codec::Utils.deep_wrap(args, handle_table),
|
|
72
|
+
"kwargs" => Codec::Utils.deep_wrap(kwargs, handle_table)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# steep:ignore:start
|
|
79
|
+
# E-24: target must be a Symbol or String (TypeError, not
|
|
80
|
+
# ArgumentError — the wrong-type case is a Host App programming
|
|
81
|
+
# error before the invocation reaches the guest). E-25: after
|
|
82
|
+
# +.to_s+ the value must match NAME_PATTERN (ArgumentError),
|
|
83
|
+
# rejecting +::+-segmented names and any non-constant form.
|
|
84
|
+
def normalize_entrypoint(target)
|
|
85
|
+
unless target.is_a?(Symbol) || target.is_a?(String)
|
|
86
|
+
raise TypeError, "Invocation entrypoint must be a Symbol or String, got #{target.class}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
target_str = target.to_s
|
|
90
|
+
unless NAME_PATTERN.match?(target_str)
|
|
91
|
+
raise ArgumentError,
|
|
92
|
+
"Invocation entrypoint must match #{NAME_PATTERN.inspect} (got #{target.inspect})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
target_str.to_sym
|
|
96
|
+
end
|
|
97
|
+
|
|
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.
|
|
105
|
+
def validate_args!(args)
|
|
106
|
+
raise ArgumentError, "Invocation args must be Array" unless args.is_a?(Array)
|
|
107
|
+
raise ArgumentError, forged_handle_message("args") if args.any?(Kobako::Handle)
|
|
108
|
+
|
|
109
|
+
args
|
|
110
|
+
end
|
|
111
|
+
|
|
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.
|
|
117
|
+
def validate_kwargs!(kwargs)
|
|
118
|
+
raise ArgumentError, "Invocation kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
bad_keys = kwargs.each_key.grep_v(Symbol)
|
|
121
|
+
unless bad_keys.empty?
|
|
122
|
+
raise ArgumentError,
|
|
123
|
+
"Invocation kwargs keys must be Symbols (got #{bad_keys.inspect})"
|
|
124
|
+
end
|
|
125
|
+
raise ArgumentError, forged_handle_message("kwargs values") if kwargs.each_value.any?(Kobako::Handle)
|
|
126
|
+
|
|
127
|
+
kwargs
|
|
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
|
|
141
|
+
# steep:ignore:end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/kobako/outcome/panic.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Outcome
|
|
5
|
-
#
|
|
6
|
-
# Envelope}[link:../../../
|
|
5
|
+
# Wire-contract Outcome Envelope → Panic envelope ({docs/wire-contract.md
|
|
6
|
+
# Outcome Envelope}[link:../../../docs/wire-contract.md]). Wire-shaped failure record
|
|
7
7
|
# carried in the OUTCOME_BUFFER when the guest run terminates with
|
|
8
8
|
# an uncaught top-level exception.
|
|
9
9
|
#
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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
|
|
7
|
-
# +
|
|
8
|
+
# +__kobako_eval+. Takes raw outcome bytes — a one-byte tag followed by
|
|
8
9
|
# the msgpack-encoded body — and maps them to either the unwrapped
|
|
9
10
|
# mruby return value or a raised three-layer
|
|
10
|
-
# ({
|
|
11
|
+
# ({docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]) exception.
|
|
11
12
|
#
|
|
12
13
|
# Self-contained: this module owns the wire framing (tag bytes,
|
|
13
14
|
# body decoding), and the +Panic+ wire record lives at
|
|
@@ -24,7 +25,7 @@ module Kobako
|
|
|
24
25
|
module Outcome
|
|
25
26
|
# First byte of the OUTCOME_BUFFER for the success branch — body is
|
|
26
27
|
# the bare msgpack encoding of the returned value
|
|
27
|
-
# ({
|
|
28
|
+
# ({docs/wire-contract.md Outcome Envelope}[link:../../docs/wire-contract.md]).
|
|
28
29
|
TYPE_VALUE = 0x01
|
|
29
30
|
# First byte of the OUTCOME_BUFFER for the failure branch — body is
|
|
30
31
|
# the msgpack Panic map.
|
|
@@ -45,12 +46,20 @@ module Kobako
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# TrapError for unknown or absent tag
|
|
48
|
-
# ({
|
|
49
|
-
# both walk the trap
|
|
49
|
+
# ({docs/wire-codec.md ABI Signatures}[link:../../docs/wire-codec.md]:
|
|
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(
|
|
@@ -100,7 +121,7 @@ module Kobako
|
|
|
100
121
|
# Ruby exception. +origin == "service"+ → ServiceError (with the
|
|
101
122
|
# +::Disconnected+ subclass selected when the panic carries the
|
|
102
123
|
# disconnected sentinel —
|
|
103
|
-
# {
|
|
124
|
+
# {docs/behavior.md Error Classes}[link:../../docs/behavior.md]); everything else
|
|
104
125
|
# → SandboxError.
|
|
105
126
|
def build_panic_error(panic)
|
|
106
127
|
panic_target_class(panic).new(
|
|
@@ -112,21 +133,37 @@ module Kobako
|
|
|
112
133
|
)
|
|
113
134
|
end
|
|
114
135
|
|
|
115
|
-
# {
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
136
|
+
# {docs/behavior.md Error Classes}[link:../../docs/behavior.md]: map
|
|
137
|
+
# the panic +class+ field to the matching Ruby exception subclass so
|
|
138
|
+
# callers can rescue specific failure paths. +origin="service"+ plus
|
|
139
|
+
# +class="Kobako::ServiceError::Disconnected"+ selects the
|
|
140
|
+
# +Disconnected+ subclass (E-14); +origin="sandbox"+ plus
|
|
141
|
+
# +class="Kobako::BytecodeError"+ selects the +BytecodeError+
|
|
142
|
+
# subclass (E-37 / E-38). Everything else falls back to the base
|
|
143
|
+
# class for the origin.
|
|
119
144
|
def panic_target_class(panic)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
case panic.origin
|
|
146
|
+
when Panic::ORIGIN_SERVICE
|
|
147
|
+
panic.klass == "Kobako::ServiceError::Disconnected" ? ServiceError::Disconnected : ServiceError
|
|
148
|
+
else
|
|
149
|
+
panic.klass == "Kobako::BytecodeError" ? BytecodeError : SandboxError
|
|
150
|
+
end
|
|
123
151
|
end
|
|
124
152
|
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
127
163
|
message,
|
|
128
164
|
origin: Panic::ORIGIN_SANDBOX,
|
|
129
|
-
klass: "Kobako::RPC::WireError"
|
|
165
|
+
klass: "Kobako::RPC::WireError",
|
|
166
|
+
details: wire_error.nil? ? nil : { wire_error: wire_error }
|
|
130
167
|
)
|
|
131
168
|
end
|
|
132
169
|
end
|
|
@@ -10,7 +10,7 @@ module Kobako
|
|
|
10
10
|
# The module is stateless — all mutable state is threaded through the
|
|
11
11
|
# +server+ argument so Dispatcher has no instance variables and no side
|
|
12
12
|
# effects beyond mutating the HandleTable via +alloc+ when a non-wire-
|
|
13
|
-
# representable return value must be wrapped ({
|
|
13
|
+
# representable return value must be wrapped ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
14
14
|
#
|
|
15
15
|
# Entry point:
|
|
16
16
|
#
|
|
@@ -22,34 +22,35 @@ module Kobako
|
|
|
22
22
|
# Internal sentinel raised when target resolution fails. Mapped to
|
|
23
23
|
# Response.error with type="undefined". Contained at the wire boundary —
|
|
24
24
|
# not part of the public Kobako error taxonomy
|
|
25
|
-
# ({
|
|
25
|
+
# ({docs/behavior.md E-xx}[link:../../../docs/behavior.md]).
|
|
26
26
|
class UndefinedTargetError < StandardError; end
|
|
27
27
|
|
|
28
28
|
# Internal sentinel raised when a Handle target resolves to the
|
|
29
29
|
# +:disconnected+ sentinel in the HandleTable (ABA protection,
|
|
30
|
-
# {
|
|
30
|
+
# {docs/behavior.md E-14}[link:../../../docs/behavior.md]). Mapped to Response.error with
|
|
31
31
|
# type="disconnected". Contained at the wire boundary.
|
|
32
32
|
class DisconnectedTargetError < StandardError; end
|
|
33
33
|
|
|
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
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
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
|
|
46
|
+
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
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
|
|
@@ -57,15 +58,16 @@ module Kobako
|
|
|
57
58
|
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
58
59
|
# envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
|
|
59
60
|
# rescue. Returns a msgpack-encoded Response envelope (binary). Four
|
|
60
|
-
# error buckets ({
|
|
61
|
-
# +Kobako::Codec::Error+ → type="runtime" (
|
|
61
|
+
# error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
|
|
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
|
-
# {
|
|
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
|
|
@@ -139,24 +141,24 @@ module Kobako
|
|
|
139
141
|
end
|
|
140
142
|
|
|
141
143
|
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
142
|
-
# wire-representable per {
|
|
144
|
+
# wire-representable per {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
|
|
143
145
|
# mapping, the +UnsupportedType+ rescue routes it through the
|
|
144
146
|
# HandleTable via {#wrap_as_handle} and re-encodes with the Capability
|
|
145
|
-
# Handle in place ({
|
|
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
|
|
155
|
-
# that the wire codec can carry ({
|
|
156
|
+
# Allocate +value+ in the Sandbox's HandleTable and return a +Handle+
|
|
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
|
|
|
@@ -8,7 +8,7 @@ module Kobako
|
|
|
8
8
|
# See lib/kobako/rpc.rb for the umbrella module doc; this file owns the
|
|
9
9
|
# Request / Response value objects and their encode/decode helpers.
|
|
10
10
|
module RPC
|
|
11
|
-
# ---------------- Response status bytes (
|
|
11
|
+
# ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
|
|
12
12
|
|
|
13
13
|
# Response variant marker for the success branch.
|
|
14
14
|
STATUS_OK = 0
|
|
@@ -16,7 +16,7 @@ module Kobako
|
|
|
16
16
|
STATUS_ERROR = 1
|
|
17
17
|
|
|
18
18
|
# Value object for a single guest-initiated RPC Request
|
|
19
|
-
# ({
|
|
19
|
+
# ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
|
|
20
20
|
#
|
|
21
21
|
# 4-element msgpack array: +[target, method, args, kwargs]+. +target+
|
|
22
22
|
# is either a +String+ (+"Namespace::Member"+) or a {Handle}. SPEC pins
|
|
@@ -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
|
|
@@ -66,7 +66,7 @@ module Kobako
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# Value object for a single host-side RPC Response
|
|
69
|
-
# ({
|
|
69
|
+
# ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
|
|
70
70
|
#
|
|
71
71
|
# 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
|
|
72
72
|
# (success) or 1 (fault). For success the second element is the return
|
|
@@ -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
|
@@ -4,8 +4,9 @@ module Kobako
|
|
|
4
4
|
module RPC
|
|
5
5
|
# Wire-level value object for an ext-0x02 Exception envelope.
|
|
6
6
|
#
|
|
7
|
-
# SPEC pins the payload
|
|
8
|
-
#
|
|
7
|
+
# SPEC pins the payload
|
|
8
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
9
|
+
# → ext 0x02) to a msgpack map with exactly three keys:
|
|
9
10
|
# * "type" — one of "runtime", "argument", "disconnected", "undefined"
|
|
10
11
|
# * "message" — human-readable string
|
|
11
12
|
# * "details" — any wire-legal value, or nil when absent
|
|
@@ -22,7 +23,7 @@ module Kobako
|
|
|
22
23
|
# Reach it through +self.class::VALID_TYPES+ — Data.define's block
|
|
23
24
|
# scope resolves bare constants against the enclosing +Wire+ module,
|
|
24
25
|
# so a bare +VALID_TYPES+ would raise +NameError+. Same pattern as
|
|
25
|
-
# +
|
|
26
|
+
# +Kobako::Handle+.
|
|
26
27
|
# steep:ignore:start
|
|
27
28
|
def initialize(type:, message:, details: nil)
|
|
28
29
|
valid_types = self.class::VALID_TYPES
|