kobako 0.4.0 → 0.5.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/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +0 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -5
- data/lib/kobako/codec/factory.rb +12 -12
- data/lib/kobako/codec/utils.rb +56 -59
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +4 -6
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +31 -35
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +83 -72
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -4
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/catalog/handles.rbs +19 -0
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +2 -2
- data/sig/kobako/codec/utils.rbs +5 -5
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +2 -3
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +5 -8
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- metadata +48 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -162
- data/ext/kobako/src/wasm/instance.rs +0 -873
- data/ext/kobako/src/wasm.rs +0 -126
- data/lib/kobako/handle_table.rb +0 -119
- data/lib/kobako/invocation.rb +0 -143
- data/lib/kobako/rpc/dispatcher.rb +0 -171
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -146
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/handle_table.rbs +0 -23
- data/sig/kobako/invocation.rbs +0 -25
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -31
- data/sig/kobako/rpc/wire_error.rbs +0 -6
- data/sig/kobako/wasm.rbs +0 -41
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -5,22 +5,21 @@ require_relative "../handle"
|
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
7
|
module Codec
|
|
8
|
-
#
|
|
8
|
+
# Codec helpers shared by the host-side encoders and decoders.
|
|
9
9
|
# Three concerns live here today:
|
|
10
10
|
#
|
|
11
|
-
# - UTF-8 assertion at the
|
|
11
|
+
# - UTF-8 assertion at the codec boundary
|
|
12
12
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
13
13
|
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
14
14
|
# {Decoder} when walking +str+ family payloads and by {Factory}
|
|
15
15
|
# when validating the +ext 0x00+ Symbol payload.
|
|
16
|
-
# -
|
|
17
|
-
# ({
|
|
16
|
+
# - +ArgumentError+ translation at the codec boundary
|
|
17
|
+
# ({with_boundary}) so the public taxonomy stays
|
|
18
18
|
# {Kobako::Codec::Error}.
|
|
19
|
-
# -
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# +Kobako::HandleTable+
|
|
19
|
+
# - Representability predicate ({representable?}) and the symmetric
|
|
20
|
+
# host→guest +#run+ argument walk ({deep_wrap}) used by
|
|
21
|
+
# +Kobako::Transport::Run#encode+ to route non-representable leaves
|
|
22
|
+
# through the Sandbox's +Kobako::Catalog::Handles+
|
|
24
23
|
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
|
|
25
24
|
#
|
|
26
25
|
# All helpers are pure — they only inspect inputs, never mutate
|
|
@@ -38,22 +37,21 @@ module Kobako
|
|
|
38
37
|
raise InvalidEncoding, "#{label} is not valid UTF-8"
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
# Run +block+ at the
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# {Kobako::Codec::Error} and never leaks +ArgumentError+ from the
|
|
47
|
-
# Ruby standard library.
|
|
40
|
+
# Run +block+ at the codec boundary: a value object raises
|
|
41
|
+
# +ArgumentError+ when an invariant is violated at construction, and
|
|
42
|
+
# this helper surfaces that as {InvalidType} so the public taxonomy
|
|
43
|
+
# stays {Kobako::Codec::Error} and never leaks +ArgumentError+ from
|
|
44
|
+
# the Ruby standard library.
|
|
48
45
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
46
|
+
# Most construction sites no longer reach for this directly: a value
|
|
47
|
+
# object built inside a {Decoder.decode} block has its
|
|
48
|
+
# +ArgumentError+ mapped to {InvalidType} by the decoder's own
|
|
49
|
+
# rescue. The lone remaining caller is {Factory#unpack_handle}, which
|
|
50
|
+
# builds +Handle.restore+ from a raw 4-byte fixext payload without a
|
|
51
|
+
# {Decoder.decode} call. Do not use it for general-purpose validation
|
|
52
|
+
# outside the codec boundary — host-layer +ArgumentError+ values
|
|
53
|
+
# should propagate unchanged.
|
|
54
|
+
def with_boundary
|
|
57
55
|
yield
|
|
58
56
|
rescue ::ArgumentError => e
|
|
59
57
|
raise InvalidType, e.message
|
|
@@ -64,43 +62,42 @@ module Kobako
|
|
|
64
62
|
# unsigned +uint 64+ maximum
|
|
65
63
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
66
64
|
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
67
|
-
# Anchored as a +Range+ so {
|
|
68
|
-
# dispatch line. This is the codec's
|
|
65
|
+
# Anchored as a +Range+ so {primitive_type?} stays a single
|
|
66
|
+
# dispatch line. This is the codec's encode domain — not to
|
|
69
67
|
# be confused with the Handle id range, which lives on
|
|
70
68
|
# +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
|
|
71
69
|
# represents a different concept entirely.
|
|
72
70
|
MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
|
|
73
71
|
|
|
74
|
-
#
|
|
72
|
+
# Codec-type predicate
|
|
75
73
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
76
74
|
# Mapping). Returns +true+ when +value+ belongs to the closed
|
|
77
|
-
# 12-entry
|
|
78
|
-
# (in the +i64..u64+ value domain), +Float+, +String+,
|
|
79
|
-
# +Kobako::Handle+, +Array+ whose every element is itself
|
|
80
|
-
#
|
|
81
|
-
#
|
|
75
|
+
# 12-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
|
|
76
|
+
# +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
|
|
77
|
+
# +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
|
|
78
|
+
# representable, or +Hash+ whose every key and value are
|
|
79
|
+
# representable. Integers outside the codec's signed-64 /
|
|
82
80
|
# unsigned-64 union are rejected so the predicate agrees with the
|
|
83
81
|
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
84
82
|
# already surfaces as {UnsupportedType}.
|
|
85
|
-
def
|
|
86
|
-
|
|
83
|
+
def representable?(value)
|
|
84
|
+
primitive_type?(value) || container_representable?(value)
|
|
87
85
|
end
|
|
88
86
|
|
|
89
87
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
90
|
-
# leaf that fails {
|
|
91
|
-
# allocated from +
|
|
88
|
+
# leaf that fails {representable?} with a +Kobako::Handle+
|
|
89
|
+
# allocated from +handler+
|
|
92
90
|
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
|
|
93
|
-
# walk only descends through
|
|
94
|
-
# (Array, Hash) one structural level at a time; a non-
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
# re-wraps a Handle.
|
|
91
|
+
# walk only descends through representable container shapes
|
|
92
|
+
# (Array, Hash) one structural level at a time; a non-representable
|
|
93
|
+
# leaf is wrapped as-is without inspecting its internal structure.
|
|
94
|
+
# An existing +Kobako::Handle+ is representable and passes through
|
|
95
|
+
# unchanged — auto-wrap never re-wraps a Handle.
|
|
99
96
|
#
|
|
100
|
-
# +value+ may be any Ruby value; +
|
|
97
|
+
# +value+ may be any Ruby value; +handler+ must respond to
|
|
101
98
|
# +#alloc(object) -> Kobako::Handle+ (a host-side
|
|
102
|
-
# +Kobako::
|
|
103
|
-
# whose leaves are either
|
|
99
|
+
# +Kobako::Catalog::Handles+). Returns a structurally equivalent value
|
|
100
|
+
# whose leaves are either representable or +Kobako::Handle+
|
|
104
101
|
# tokens.
|
|
105
102
|
#
|
|
106
103
|
# The block bodies spell +Utils.deep_wrap+ explicitly rather than
|
|
@@ -110,20 +107,20 @@ module Kobako
|
|
|
110
107
|
# (still +Utils+ at definition time, but the qualified form keeps
|
|
111
108
|
# the dispatch readable when the recursive call sits inside a
|
|
112
109
|
# Proc captured from elsewhere).
|
|
113
|
-
def deep_wrap(value,
|
|
110
|
+
def deep_wrap(value, handler)
|
|
114
111
|
case value
|
|
115
|
-
when ::Array then value.map { |element| Utils.deep_wrap(element,
|
|
116
|
-
when ::Hash then value.transform_values { |val| Utils.deep_wrap(val,
|
|
112
|
+
when ::Array then value.map { |element| Utils.deep_wrap(element, handler) }
|
|
113
|
+
when ::Hash then value.transform_values { |val| Utils.deep_wrap(val, handler) }
|
|
117
114
|
else
|
|
118
|
-
|
|
115
|
+
representable?(value) ? value : handler.alloc(value)
|
|
119
116
|
end
|
|
120
117
|
end
|
|
121
118
|
|
|
122
|
-
# Predicate split out of {
|
|
119
|
+
# Predicate split out of {representable?} for cyclomatic
|
|
123
120
|
# budget — the closed-set non-container branch. Returns +true+ for
|
|
124
|
-
# the
|
|
125
|
-
# public surface; reach for {
|
|
126
|
-
def
|
|
121
|
+
# the scalar leaves and an existing Handle. Not part of the
|
|
122
|
+
# public surface; reach for {representable?} instead.
|
|
123
|
+
def primitive_type?(value)
|
|
127
124
|
case value
|
|
128
125
|
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
129
126
|
when ::Integer then MSGPACK_INT_RANGE.cover?(value)
|
|
@@ -131,15 +128,15 @@ module Kobako
|
|
|
131
128
|
end
|
|
132
129
|
end
|
|
133
130
|
|
|
134
|
-
# Predicate split out of {
|
|
131
|
+
# Predicate split out of {representable?} for cyclomatic
|
|
135
132
|
# budget — the container branch. Recurses into Array elements and
|
|
136
|
-
# Hash key+value pairs through the public {
|
|
137
|
-
# Not part of the public surface; reach for {
|
|
133
|
+
# Hash key+value pairs through the public {representable?}.
|
|
134
|
+
# Not part of the public surface; reach for {representable?}
|
|
138
135
|
# instead.
|
|
139
|
-
def
|
|
136
|
+
def container_representable?(value)
|
|
140
137
|
case value
|
|
141
|
-
when ::Array then value.all? { |element| Utils.
|
|
142
|
-
when ::Hash then value.all? { |key, val| Utils.
|
|
138
|
+
when ::Array then value.all? { |element| Utils.representable?(element) }
|
|
139
|
+
when ::Hash then value.all? { |key, val| Utils.representable?(key) && Utils.representable?(val) }
|
|
143
140
|
else false
|
|
144
141
|
end
|
|
145
142
|
end
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -6,9 +6,12 @@ module Kobako
|
|
|
6
6
|
# Host-side MessagePack codec for the kobako wire contract — the
|
|
7
7
|
# byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
|
|
8
8
|
# Two consumers sit on top:
|
|
9
|
-
# +Kobako::
|
|
10
|
-
# and +Kobako::Outcome+ owns the per-+#run+ outcome
|
|
11
|
-
# body / Panic map).
|
|
9
|
+
# +Kobako::Transport+ pins the host↔guest framing (Request / Response /
|
|
10
|
+
# Run / Yield) and +Kobako::Outcome+ owns the per-+#run+ outcome
|
|
11
|
+
# envelope (Result body / Panic map). The ext-type leaves this layer
|
|
12
|
+
# carries — +Kobako::Handle+ (0x01) and +Kobako::Fault+ (0x02) — live at
|
|
13
|
+
# the kobako root so the codec can register them without depending
|
|
14
|
+
# upward on Transport.
|
|
12
15
|
#
|
|
13
16
|
# Backed by the official +msgpack+ gem via {Factory}; {Encoder} and
|
|
14
17
|
# {Decoder} are thin wrappers that register the three kobako-specific
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -2,29 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
# Top-level Kobako namespace.
|
|
4
4
|
module Kobako
|
|
5
|
-
#
|
|
5
|
+
# Error taxonomy (docs/behavior.md § Error Scenarios).
|
|
6
6
|
#
|
|
7
7
|
# Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
|
|
8
|
-
# exactly one of
|
|
8
|
+
# exactly one of three invocation-outcome classes. Attribution is decided after the
|
|
9
9
|
# guest binary returns control to the host (docs/behavior.md
|
|
10
10
|
# "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
|
|
11
11
|
#
|
|
12
|
-
# Three
|
|
12
|
+
# Three invocation-outcome branches:
|
|
13
13
|
#
|
|
14
14
|
# * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
|
|
15
15
|
# wire-violation fallback signalling a corrupted
|
|
16
16
|
# guest runtime).
|
|
17
17
|
# * {SandboxError} — sandbox / wire layer (mruby script error,
|
|
18
18
|
# wire-decode failure on an otherwise valid tag,
|
|
19
|
-
#
|
|
20
|
-
# * {ServiceError} — service / capability layer (a Service
|
|
21
|
-
# failed and was not rescued inside the
|
|
19
|
+
# Catalog::Handles exhaustion, output buffer overrun).
|
|
20
|
+
# * {ServiceError} — service / capability layer (a Service capability
|
|
21
|
+
# call that failed and was not rescued inside the
|
|
22
|
+
# script).
|
|
23
|
+
#
|
|
24
|
+
# A fourth branch sits outside the invocation taxonomy:
|
|
25
|
+
#
|
|
26
|
+
# * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
|
|
27
|
+
# when the wasm runtime cannot be built from the
|
|
28
|
+
# configured +wasm_path+ before any invocation runs
|
|
29
|
+
# (docs/behavior.md E-40 / E-41). Not an invocation
|
|
30
|
+
# outcome, so it never passes through the two-step
|
|
31
|
+
# attribution decision.
|
|
22
32
|
#
|
|
23
33
|
# Subclasses pinned by docs/behavior.md Error Classes:
|
|
24
34
|
#
|
|
25
|
-
# * {
|
|
26
|
-
#
|
|
27
|
-
#
|
|
35
|
+
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
36
|
+
# at +wasm_path+ (E-40).
|
|
37
|
+
# * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit (B-21).
|
|
28
38
|
|
|
29
39
|
# Base for all kobako-raised errors so callers that want to ignore the
|
|
30
40
|
# taxonomy can rescue a single class.
|
|
@@ -59,6 +69,27 @@ module Kobako
|
|
|
59
69
|
# point; discard and recreate before another execution.
|
|
60
70
|
class MemoryLimitError < TrapError; end
|
|
61
71
|
|
|
72
|
+
# Construction-layer error raised by +Kobako::Sandbox.new+ /
|
|
73
|
+
# +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
|
|
74
|
+
# from the configured +wasm_path+ before any invocation runs —
|
|
75
|
+
# an unreadable artifact, bytes that are not a valid Wasm module, or
|
|
76
|
+
# engine / linker / instantiation setup failure
|
|
77
|
+
# ({docs/behavior.md E-41}[link:../../docs/behavior.md]). Construction
|
|
78
|
+
# is not an invocation, so +SetupError+ sits beside the invocation
|
|
79
|
+
# taxonomy under +Kobako::Error+ rather than under +TrapError+: no
|
|
80
|
+
# Sandbox is produced, so the +TrapError+ "discard and recreate"
|
|
81
|
+
# recovery contract does not apply.
|
|
82
|
+
class SetupError < Error; end
|
|
83
|
+
|
|
84
|
+
# The named +SetupError+ subclass for the common, actionable case:
|
|
85
|
+
# the Guest Binary artifact is absent at +wasm_path+ — the pre-build
|
|
86
|
+
# state on a fresh clone before +bundle exec rake compile+
|
|
87
|
+
# ({docs/behavior.md E-40}[link:../../docs/behavior.md]). Host Apps
|
|
88
|
+
# that only need "the Sandbox could not be set up" rescue +SetupError+;
|
|
89
|
+
# those wanting to special-case the unbuilt-artifact state rescue
|
|
90
|
+
# +ModuleNotBuiltError+ first.
|
|
91
|
+
class ModuleNotBuiltError < SetupError; end
|
|
92
|
+
|
|
62
93
|
# Sandbox / wire layer. Raised when the guest ran to completion but
|
|
63
94
|
# execution failed due to a mruby script error, a protocol fault, or a
|
|
64
95
|
# host-side wire decode failure on an otherwise valid outcome tag.
|
|
@@ -87,27 +118,13 @@ module Kobako
|
|
|
87
118
|
@backtrace_lines = backtrace_lines
|
|
88
119
|
@details = details
|
|
89
120
|
end
|
|
90
|
-
|
|
91
|
-
# docs/behavior.md Error Classes: ServiceError::Disconnected is raised
|
|
92
|
-
# when the RPC target Handle resolves to the `:disconnected` sentinel
|
|
93
|
-
# in the HandleTable (ABA protection rule — id exists but entry was
|
|
94
|
-
# invalidated). E-14.
|
|
95
|
-
class Disconnected < ServiceError; end
|
|
96
121
|
end
|
|
97
122
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
# directly), it surfaces as a SandboxError.
|
|
104
|
-
class HandleTableError < SandboxError; end
|
|
105
|
-
|
|
106
|
-
# docs/behavior.md Error Classes: HandleTableExhausted is the canonical
|
|
107
|
-
# SandboxError subclass for the id-cap-hit path (B-21). Inherits from
|
|
108
|
-
# HandleTableError so a single `rescue Kobako::HandleTableError` covers
|
|
109
|
-
# both lookup-failure and cap-exhaustion paths.
|
|
110
|
-
class HandleTableExhausted < HandleTableError; end
|
|
123
|
+
# docs/behavior.md Error Classes: HandlerExhaustedError is the canonical
|
|
124
|
+
# SandboxError subclass for the id-cap-hit path (B-21). Raised when the
|
|
125
|
+
# per-invocation Handle ID counter in Catalog::Handles reaches
|
|
126
|
+
# +0x7fff_ffff+ (2³¹ − 1) and further allocation would exceed the cap.
|
|
127
|
+
class HandlerExhaustedError < SandboxError; end
|
|
111
128
|
|
|
112
129
|
# docs/behavior.md Error Classes: BytecodeError is the SandboxError
|
|
113
130
|
# subclass raised when a `#preload(binary:)` snippet fails structural
|
data/lib/kobako/fault.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Wire-level value object for an ext-0x02 Exception envelope.
|
|
5
|
+
#
|
|
6
|
+
# Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
|
|
7
|
+
# +Fault+ is a MessagePack ext-type leaf registered by
|
|
8
|
+
# +Kobako::Codec::Factory+ and rides nested inside other envelopes (a
|
|
9
|
+
# +Kobako::Transport::Response+ error payload, or another Fault's
|
|
10
|
+
# +details+). It lives at the kobako root rather than under +Transport+
|
|
11
|
+
# because the Codec layer must register it, and Codec must not depend
|
|
12
|
+
# upward on Transport.
|
|
13
|
+
#
|
|
14
|
+
# SPEC pins the payload
|
|
15
|
+
# ({docs/wire-codec.md}[link:../../docs/wire-codec.md] § Ext Types
|
|
16
|
+
# → ext 0x02) to a msgpack map with exactly three keys:
|
|
17
|
+
# * "type" — one of "runtime", "argument", "undefined"
|
|
18
|
+
# * "message" — human-readable string
|
|
19
|
+
# * "details" — any wire-legal value, or nil when absent
|
|
20
|
+
#
|
|
21
|
+
# This object holds the *encoded* form. Reifying the corresponding Ruby
|
|
22
|
+
# exception class (RuntimeError, ArgumentError, Kobako::ServiceError, ...)
|
|
23
|
+
# is the responsibility of the dispatch layer, not the codec.
|
|
24
|
+
#
|
|
25
|
+
# Built on the +class X < Data.define(...)+ subclass form so the
|
|
26
|
+
# class body is fully Steep-visible; ruby/rbs upstream documents
|
|
27
|
+
# this as the Steep-friendly shape and the +Style/DataInheritance+
|
|
28
|
+
# cop is disabled on that basis (see +.rubocop.yml+).
|
|
29
|
+
class Fault < Data.define(:type, :message, :details)
|
|
30
|
+
VALID_TYPES = %w[runtime argument undefined].freeze
|
|
31
|
+
|
|
32
|
+
def initialize(type:, message:, details: nil)
|
|
33
|
+
raise ArgumentError, "type must be String" unless type.is_a?(String)
|
|
34
|
+
raise ArgumentError, "message must be String" unless message.is_a?(String)
|
|
35
|
+
raise ArgumentError, "type=#{type.inspect} not one of #{VALID_TYPES.inspect}" unless VALID_TYPES.include?(type)
|
|
36
|
+
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -16,9 +16,9 @@ module Kobako
|
|
|
16
16
|
# privatised so Host App code cannot fabricate a Handle from a bare
|
|
17
17
|
# integer; legitimate Handle instances enter Host App code only as
|
|
18
18
|
# fields on raised error objects. The Host Gem itself constructs
|
|
19
|
-
# Handles through {.
|
|
19
|
+
# Handles through {.restore}, which exists at exactly two call
|
|
20
20
|
# sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
|
|
21
|
-
# +Kobako::Codec::Utils.deep_wrap+ / +Kobako::
|
|
21
|
+
# +Kobako::Codec::Utils.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
22
22
|
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
23
23
|
# of any public surface.
|
|
24
24
|
#
|
|
@@ -34,14 +34,12 @@ module Kobako
|
|
|
34
34
|
# on either side of the wire without re-encoding.
|
|
35
35
|
MAX_ID = 0x7fff_ffff
|
|
36
36
|
|
|
37
|
-
# steep:ignore:start
|
|
38
37
|
def initialize(id:)
|
|
39
38
|
raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
|
|
40
39
|
raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
|
|
41
40
|
|
|
42
41
|
super
|
|
43
42
|
end
|
|
44
|
-
# steep:ignore:end
|
|
45
43
|
|
|
46
44
|
private_class_method :new
|
|
47
45
|
|
|
@@ -52,10 +50,10 @@ module Kobako
|
|
|
52
50
|
#
|
|
53
51
|
# Two collaborators call this: the codec when an ext 0x01 frame is
|
|
54
52
|
# decoded off the wire, and the allocator paths when a host-side
|
|
55
|
-
# Ruby object is registered into the Sandbox's +
|
|
53
|
+
# Ruby object is registered into the Sandbox's +Catalog::Handles+. Both
|
|
56
54
|
# paths live inside +lib/kobako/+ and treat this method as a
|
|
57
55
|
# package-private constructor.
|
|
58
|
-
def self.
|
|
56
|
+
def self.restore(id)
|
|
59
57
|
allocate.tap { |handle| handle.send(:initialize, id: id) }
|
|
60
58
|
end
|
|
61
59
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# A named grouping of Members for one Sandbox
|
|
5
|
+
# ({docs/behavior.md B-07..B-11}[link:../../docs/behavior.md]).
|
|
6
|
+
# Returned by +Sandbox#define+. Each instance owns a flat name→object
|
|
7
|
+
# table of Members; member binding is validated against {NAME_PATTERN}.
|
|
8
|
+
class Namespace
|
|
9
|
+
# Ruby constant-name pattern shared by Namespace and Member names
|
|
10
|
+
# ({docs/behavior.md B-07/B-08 Notes}[link:../../docs/behavior.md]).
|
|
11
|
+
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
12
|
+
|
|
13
|
+
attr_reader :name
|
|
14
|
+
|
|
15
|
+
# Build a new Namespace. +name+ is an already-validated Namespace
|
|
16
|
+
# name (must satisfy {NAME_PATTERN}; validation is the caller's
|
|
17
|
+
# responsibility).
|
|
18
|
+
def initialize(name)
|
|
19
|
+
@name = name
|
|
20
|
+
@members = {} # : Hash[String, untyped]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Bind +object+ under +member+ inside this Namespace. +member+ is a
|
|
24
|
+
# constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
|
|
25
|
+
# object that responds to the methods guest code will invoke. Returns
|
|
26
|
+
# +self+ for chaining. Raises +ArgumentError+ when +member+ does not
|
|
27
|
+
# match the constant pattern, or a Member of the same name is already
|
|
28
|
+
# bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]).
|
|
29
|
+
def bind(member, object)
|
|
30
|
+
member_str = validate_member_name!(member)
|
|
31
|
+
raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
|
|
32
|
+
|
|
33
|
+
@members[member_str] = object
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Member lookup; raises +KeyError+ when no Member is registered
|
|
38
|
+
# under +member+.
|
|
39
|
+
def fetch(member)
|
|
40
|
+
member_str = member.to_s
|
|
41
|
+
unless @members.key?(member_str)
|
|
42
|
+
raise KeyError,
|
|
43
|
+
"no member named #{member_str.inspect} in namespace #{@name.inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@members[member_str]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Structured description for the guest preamble (Frame 1). Returns a
|
|
50
|
+
# two-element array +[name, member_keys]+ suitable for msgpack encoding.
|
|
51
|
+
def to_preamble
|
|
52
|
+
[@name, @members.keys]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def validate_member_name!(member)
|
|
58
|
+
member_str = member.to_s
|
|
59
|
+
unless NAME_PATTERN.match?(member_str)
|
|
60
|
+
raise ArgumentError,
|
|
61
|
+
"MemberName must match #{NAME_PATTERN.inspect} (got #{member.inspect})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
member_str
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "outcome/panic"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "transport/error"
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
7
|
# Host-facing boundary for the OUTCOME_BUFFER produced by
|
|
@@ -14,7 +14,7 @@ module Kobako
|
|
|
14
14
|
# body decoding), and the +Panic+ wire record lives at
|
|
15
15
|
# +Kobako::Outcome::Panic+. The byte-level msgpack codec at
|
|
16
16
|
# +Kobako::Codec+ is invoked for the body itself; otherwise
|
|
17
|
-
# nothing in +
|
|
17
|
+
# nothing in +Transport+ participates.
|
|
18
18
|
#
|
|
19
19
|
# * tag 0x01, decode OK → return decoded value
|
|
20
20
|
# * tag 0x01, decode fails → SandboxError (E-09)
|
|
@@ -73,16 +73,16 @@ module Kobako
|
|
|
73
73
|
|
|
74
74
|
# Decode failure on the success tag is a SandboxError (E-09): the
|
|
75
75
|
# framing was fine, but the carried value is unrepresentable. The
|
|
76
|
-
# specific codec fault is stashed in +details
|
|
76
|
+
# specific codec fault is stashed in +details+ rather
|
|
77
77
|
# than spliced into the message — callers cannot act on the inner
|
|
78
78
|
# "Symbol payload must be …" wording, but operators triaging a
|
|
79
79
|
# corrupted Sandbox runtime still need it.
|
|
80
80
|
def decode_value(body)
|
|
81
81
|
Kobako::Codec::Decoder.decode(body)
|
|
82
82
|
rescue Kobako::Codec::Error => e
|
|
83
|
-
raise
|
|
83
|
+
raise build_transport_error(
|
|
84
84
|
"Sandbox produced an invalid result value",
|
|
85
|
-
|
|
85
|
+
detail: e.message
|
|
86
86
|
)
|
|
87
87
|
end
|
|
88
88
|
|
|
@@ -91,25 +91,25 @@ module Kobako
|
|
|
91
91
|
# layer exception via +build_panic_error+ and raised; on wire-decode
|
|
92
92
|
# failure the rescue path raises the wire-violation +SandboxError+.
|
|
93
93
|
def decode_panic(body)
|
|
94
|
-
raise build_panic_error(
|
|
94
|
+
raise build_panic_error(build_panic_record(body))
|
|
95
95
|
rescue Kobako::Codec::Error => e
|
|
96
|
-
raise
|
|
96
|
+
raise build_transport_error(
|
|
97
97
|
"Sandbox produced an invalid panic record",
|
|
98
|
-
|
|
98
|
+
detail: e.message
|
|
99
99
|
)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
# Build a +Panic+ value object from the msgpack-decoded body. Raises
|
|
103
|
-
# +Kobako::Codec::InvalidType+ when the body is not a map or
|
|
104
|
-
#
|
|
105
|
-
# +
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
# +Kobako::Codec::InvalidType+ when the body is not a map or when
|
|
104
|
+
# required keys are missing — both routed by +decode_panic+ to
|
|
105
|
+
# +build_transport_error+. The decode runs in block form so
|
|
106
|
+
# +Panic.new+'s +ArgumentError+ invariants surface as +InvalidType+
|
|
107
|
+
# through the decoder boundary; the message itself is never user-
|
|
108
|
+
# facing — it lands in +details+ via the rescue chain above.
|
|
109
|
+
def build_panic_record(body)
|
|
110
|
+
Kobako::Codec::Decoder.decode(body) do |map|
|
|
111
|
+
raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
111
112
|
|
|
112
|
-
Kobako::Codec::Utils.wire_boundary do
|
|
113
113
|
Panic.new(
|
|
114
114
|
origin: map["origin"], klass: map["class"], message: map["message"],
|
|
115
115
|
backtrace: map["backtrace"] || [], details: map["details"]
|
|
@@ -118,11 +118,8 @@ module Kobako
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Map a decoded Panic record into the corresponding three-layer
|
|
121
|
-
# Ruby exception. +origin == "service"+ → ServiceError
|
|
122
|
-
#
|
|
123
|
-
# disconnected sentinel —
|
|
124
|
-
# {docs/behavior.md Error Classes}[link:../../docs/behavior.md]); everything else
|
|
125
|
-
# → SandboxError.
|
|
121
|
+
# Ruby exception. +origin == "service"+ → ServiceError; everything
|
|
122
|
+
# else → SandboxError.
|
|
126
123
|
def build_panic_error(panic)
|
|
127
124
|
panic_target_class(panic).new(
|
|
128
125
|
panic.message,
|
|
@@ -133,37 +130,36 @@ module Kobako
|
|
|
133
130
|
)
|
|
134
131
|
end
|
|
135
132
|
|
|
136
|
-
# {docs/behavior.md Error
|
|
133
|
+
# {docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]: map
|
|
137
134
|
# the panic +class+ field to the matching Ruby exception subclass so
|
|
138
|
-
# callers can rescue specific failure paths. +origin="service"+
|
|
139
|
-
# +
|
|
140
|
-
# +Disconnected+ subclass (E-14); +origin="sandbox"+ plus
|
|
135
|
+
# callers can rescue specific failure paths. +origin="service"+ →
|
|
136
|
+
# +ServiceError+; +origin="sandbox"+ plus
|
|
141
137
|
# +class="Kobako::BytecodeError"+ selects the +BytecodeError+
|
|
142
138
|
# subclass (E-37 / E-38). Everything else falls back to the base
|
|
143
139
|
# class for the origin.
|
|
144
140
|
def panic_target_class(panic)
|
|
145
141
|
case panic.origin
|
|
146
142
|
when Panic::ORIGIN_SERVICE
|
|
147
|
-
|
|
143
|
+
ServiceError
|
|
148
144
|
else
|
|
149
145
|
panic.klass == "Kobako::BytecodeError" ? BytecodeError : SandboxError
|
|
150
146
|
end
|
|
151
147
|
end
|
|
152
148
|
|
|
153
149
|
# Lift the wire-violation fallback to the real
|
|
154
|
-
# +Kobako::
|
|
150
|
+
# +Kobako::Transport::Error+ class so callers can +rescue+ it
|
|
155
151
|
# specifically instead of pattern-matching on +error.klass+. The
|
|
156
152
|
# +klass+ field is still populated so existing operator-side
|
|
157
153
|
# tooling that greps on the string continues to work.
|
|
158
|
-
# +
|
|
159
|
-
# operator diagnosis without polluting
|
|
160
|
-
# +#message+.
|
|
161
|
-
def
|
|
162
|
-
Kobako::
|
|
154
|
+
# +detail+ carries the inner codec / framing message, stashed
|
|
155
|
+
# directly into +details+ for operator diagnosis without polluting
|
|
156
|
+
# the user-facing +#message+.
|
|
157
|
+
def build_transport_error(message, detail: nil)
|
|
158
|
+
Kobako::Transport::Error.new(
|
|
163
159
|
message,
|
|
164
160
|
origin: Panic::ORIGIN_SANDBOX,
|
|
165
|
-
klass: "Kobako::
|
|
166
|
-
details:
|
|
161
|
+
klass: "Kobako::Transport::Error",
|
|
162
|
+
details: detail
|
|
167
163
|
)
|
|
168
164
|
end
|
|
169
165
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Host-side wasmtime runtime, surfaced as a Ruby class by the native ext
|
|
5
|
+
# (see ext/kobako/src/runtime.rs). The +Kobako::Runtime+ class wraps the
|
|
6
|
+
# wasmtime engine + compiled module + Store; it is the only Ruby-visible
|
|
7
|
+
# wasmtime type and the foundational binding layer for +Kobako::Sandbox+.
|
|
8
|
+
#
|
|
9
|
+
# This file reopens the magnus-defined class only to add the pure-Ruby
|
|
10
|
+
# +.default_path+ helper. Every other method (+#from_path+ singleton,
|
|
11
|
+
# +#eval+ / +#run+, capture and usage readers) is registered from Rust
|
|
12
|
+
# at ext load time.
|
|
13
|
+
class Runtime
|
|
14
|
+
# Absolute path to the gem-bundled +data/kobako.wasm+ artifact. Computed
|
|
15
|
+
# from this file's location so it works for both +bundle exec+ (running
|
|
16
|
+
# from the repo) and an installed gem (running from the gem dir).
|
|
17
|
+
#
|
|
18
|
+
# Returns a String regardless of whether the file currently exists —
|
|
19
|
+
# call sites that need the file to be present should pass this through
|
|
20
|
+
# +Kobako::Runtime.from_path+, which raises
|
|
21
|
+
# +Kobako::ModuleNotBuiltError+ with a clear remediation message.
|
|
22
|
+
# Raises +Kobako::SetupError+ if +__dir__+ is unavailable so that
|
|
23
|
+
# +rescue Kobako::SetupError+ around +Kobako::Sandbox.new+ catches every
|
|
24
|
+
# construction-layer failure uniformly, including this path-resolution one.
|
|
25
|
+
def self.default_path
|
|
26
|
+
dir = __dir__ or raise Kobako::SetupError, "Kobako::Runtime.default_path requires __dir__"
|
|
27
|
+
File.expand_path("../../data/kobako.wasm", dir)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|