kobako 0.14.0-aarch64-linux → 0.15.0-aarch64-linux
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 -1
- data/CHANGELOG.md +51 -0
- data/README.md +104 -29
- data/ROADMAP.md +2 -3
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +7 -0
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
- data/crates/kobako-wasmtime/README.md +1 -1
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/catalog/handles.rb +1 -1
- data/lib/kobako/catalog/services.rb +123 -0
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/catalog.rb +2 -2
- data/lib/kobako/codec/decoder.rb +3 -3
- data/lib/kobako/codec/encoder.rb +4 -4
- data/lib/kobako/codec/ext_types.rb +169 -0
- data/lib/kobako/codec/state.rb +98 -0
- data/lib/kobako/codec/utils.rb +2 -2
- data/lib/kobako/codec.rb +23 -6
- data/lib/kobako/fault.rb +1 -1
- data/lib/kobako/handle.rb +1 -1
- data/lib/kobako/outcome.rb +13 -7
- data/lib/kobako/pool.rb +1 -1
- data/lib/kobako/sandbox.rb +20 -13
- data/lib/kobako/transport/dispatcher.rb +25 -18
- data/lib/kobako/transport/request.rb +14 -9
- data/lib/kobako/transport/response.rb +9 -2
- data/lib/kobako/transport/yield.rb +4 -1
- data/lib/kobako/transport/yielder.rb +14 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/services.rbs +25 -0
- data/sig/kobako/codec/ext_types.rbs +31 -0
- data/sig/kobako/codec/state.rbs +20 -0
- data/sig/kobako/codec.rbs +3 -0
- data/sig/kobako/sandbox.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/yielder.rbs +1 -1
- data/sig/kobako/transport.rbs +2 -2
- metadata +8 -8
- data/lib/kobako/catalog/namespaces.rb +0 -115
- data/lib/kobako/codec/factory.rb +0 -187
- data/lib/kobako/namespace.rb +0 -78
- data/sig/kobako/catalog/namespaces.rbs +0 -17
- data/sig/kobako/codec/factory.rbs +0 -34
- data/sig/kobako/namespace.rbs +0 -21
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "msgpack"
|
|
4
4
|
|
|
5
5
|
require_relative "error"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "ext_types"
|
|
7
7
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
@@ -13,8 +13,8 @@ module Kobako
|
|
|
13
13
|
# The codec backbone is the official +msgpack+ gem: integers, floats,
|
|
14
14
|
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
15
|
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
|
-
# Capability Handle, 0x02 Exception envelope) are registered
|
|
17
|
-
# the
|
|
16
|
+
# Capability Handle, 0x02 Exception envelope) are registered by
|
|
17
|
+
# ExtTypes on the process-wide factory.
|
|
18
18
|
#
|
|
19
19
|
# Public API is a single function — +.encode+. The codec is stateless;
|
|
20
20
|
# there is no buffer accumulator and no streaming write API. Callers
|
|
@@ -32,7 +32,7 @@ module Kobako
|
|
|
32
32
|
# +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
|
|
33
33
|
# propagating.
|
|
34
34
|
def self.encode(value)
|
|
35
|
-
|
|
35
|
+
FACTORY.dump(value)
|
|
36
36
|
rescue ::RangeError, ::NoMethodError => e
|
|
37
37
|
raise UnsupportedType, e.message
|
|
38
38
|
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msgpack"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
require_relative "state"
|
|
8
|
+
require_relative "../handle"
|
|
9
|
+
require_relative "../fault"
|
|
10
|
+
|
|
11
|
+
module Kobako
|
|
12
|
+
module Codec
|
|
13
|
+
# The kobako wire ext-type conversions
|
|
14
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types)
|
|
15
|
+
# as pure functions: per-operation decode state is threaded in as an
|
|
16
|
+
# argument, so the module itself holds nothing. #build_factory assembles
|
|
17
|
+
# the one +MessagePack::Factory+ these conversions are registered on.
|
|
18
|
+
module ExtTypes
|
|
19
|
+
# MessagePack ext type code reserved for Symbol
|
|
20
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
21
|
+
# → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
|
|
22
|
+
# Rust side.
|
|
23
|
+
EXT_SYMBOL = 0x00
|
|
24
|
+
# MessagePack ext type code reserved for Capability Handle
|
|
25
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
26
|
+
# → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
|
|
27
|
+
# Rust side.
|
|
28
|
+
EXT_HANDLE = 0x01
|
|
29
|
+
# MessagePack ext type code reserved for Exception envelope
|
|
30
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
31
|
+
# → ext 0x02). Module-private — mirrors +codec::EXT_ERRENV+ on the
|
|
32
|
+
# Rust side.
|
|
33
|
+
EXT_ERRENV = 0x02
|
|
34
|
+
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Assemble a +MessagePack::Factory+ with the three kobako ext types
|
|
39
|
+
# registered, frozen because registration is its only mutation and
|
|
40
|
+
# happens exactly once. The stateful conversions resolve their
|
|
41
|
+
# per-operation state at call time, so one registered factory serves
|
|
42
|
+
# every thread.
|
|
43
|
+
def build_factory
|
|
44
|
+
factory = MessagePack::Factory.new
|
|
45
|
+
register_symbol(factory)
|
|
46
|
+
register_handle(factory)
|
|
47
|
+
register_fault(factory)
|
|
48
|
+
factory.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Symbol-to-name packer for the ext-0x00 registration.
|
|
52
|
+
def pack_symbol(symbol)
|
|
53
|
+
symbol.name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
57
|
+
# InvalidEncoding on invalid bytes — SPEC forbids the
|
|
58
|
+
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
59
|
+
# would otherwise apply. The re-tag step lives here because the
|
|
60
|
+
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
61
|
+
# itself is shared with Decoder via Utils.assert_utf8!. The
|
|
62
|
+
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
63
|
+
# rather than wire-ext-code vocabulary.
|
|
64
|
+
def unpack_symbol(payload)
|
|
65
|
+
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
66
|
+
Utils.assert_utf8!(name, "Symbol payload")
|
|
67
|
+
name.to_sym
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Handle-id packer for the ext-0x01 registration: the fixext-4
|
|
71
|
+
# big-endian id frame.
|
|
72
|
+
def pack_handle(handle)
|
|
73
|
+
[handle.id].pack("N")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Peel off the fixext-4 frame, hand the bytes to the
|
|
77
|
+
# Host-Gem-internal +Kobako::Handle.restore+ factory, and
|
|
78
|
+
# translate the +ArgumentError+ raised by Handle's invariants
|
|
79
|
+
# into a wire-layer +InvalidType+ via Codec::Utils.with_boundary.
|
|
80
|
+
# The Value Object owns the id-range contract; this method only
|
|
81
|
+
# owns the frame shape. Records the Handle sighting on +state+ so a
|
|
82
|
+
# Handle-free decode can skip the downstream resolution walk.
|
|
83
|
+
def unpack_handle(payload, state)
|
|
84
|
+
state.record_handle!
|
|
85
|
+
bytes = payload.b
|
|
86
|
+
raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
87
|
+
|
|
88
|
+
id = bytes.unpack1("N") # : Integer
|
|
89
|
+
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Encode the inner ext-0x02 map via Encoder (not the raw factory) so
|
|
93
|
+
# the embedded payload flows through the same boundary as a top-level
|
|
94
|
+
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
95
|
+
# registered ext-type packers. A +details+ chain nested past the
|
|
96
|
+
# +state+ depth cap has no wire representation and surfaces as
|
|
97
|
+
# +UnsupportedType+. In a payload position (+state+ inside a
|
|
98
|
+
# forbid_faults bracket) the envelope has no wire representation at
|
|
99
|
+
# all, so the refusal routes the value into the position's
|
|
100
|
+
# non-representable handling — the Dispatcher's auto-wrap rescue,
|
|
101
|
+
# or a raise at the yield site.
|
|
102
|
+
def pack_fault(fault, state)
|
|
103
|
+
if state.faults_forbidden?
|
|
104
|
+
raise UnsupportedType, "Kobako::Fault has no wire representation in a payload position"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
state.within_ext_frame(UnsupportedType) do
|
|
108
|
+
Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
113
|
+
# inside Decoder.decode's block form, so the value-object's
|
|
114
|
+
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
115
|
+
# decoder boundary. Inner decode goes through Decoder (not the raw
|
|
116
|
+
# factory) so the embedded +str+ payloads flow through the same
|
|
117
|
+
# UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
118
|
+
# +details+ re-enters this method, so the +state+ ext-frame guard
|
|
119
|
+
# bounds the chain depth to keep it from exhausting the native stack.
|
|
120
|
+
# In a payload position (+state+ inside a forbid_faults bracket) the
|
|
121
|
+
# envelope is a wire violation outright — its sole legal position is
|
|
122
|
+
# the Response fault field.
|
|
123
|
+
def unpack_fault(payload, state)
|
|
124
|
+
if state.faults_forbidden?
|
|
125
|
+
raise InvalidType, "Fault envelope (ext 0x02) is not a legal value in a payload position"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
state.within_ext_frame(InvalidType) do
|
|
129
|
+
Decoder.decode(payload) do |map|
|
|
130
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def register_symbol(factory)
|
|
138
|
+
factory.register_type(
|
|
139
|
+
EXT_SYMBOL, Symbol,
|
|
140
|
+
packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
|
|
141
|
+
unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def register_handle(factory)
|
|
146
|
+
factory.register_type(
|
|
147
|
+
EXT_HANDLE, Kobako::Handle,
|
|
148
|
+
packer: ->(handle) { ExtTypes.pack_handle(handle) },
|
|
149
|
+
unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def register_fault(factory)
|
|
154
|
+
factory.register_type(
|
|
155
|
+
EXT_ERRENV, Kobako::Fault,
|
|
156
|
+
packer: ->(fault) { ExtTypes.pack_fault(fault, State.current) },
|
|
157
|
+
unpacker: ->(payload) { ExtTypes.unpack_fault(payload, State.current) }
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The process-wide registered factory: ext registration is paid once at
|
|
163
|
+
# load, and a registered +MessagePack::Factory+ only reads its type
|
|
164
|
+
# registry afterwards, so every thread shares this instance for byte
|
|
165
|
+
# work.
|
|
166
|
+
FACTORY = ExtTypes.build_factory
|
|
167
|
+
private_constant :FACTORY
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Codec
|
|
5
|
+
# Codec-internal, per-thread state of the operation in flight: the
|
|
6
|
+
# ext-envelope nesting depth and whether a Capability Handle crossed
|
|
7
|
+
# the current decode. Thread scoping is what makes plain instance
|
|
8
|
+
# variables sound — host codec calls run synchronously on their
|
|
9
|
+
# owning thread, and a nested decode (an ext 0x02 Fault re-entering
|
|
10
|
+
# through its +details+) reuses the same thread instance, so the
|
|
11
|
+
# depth counter accumulates across the re-entry instead of resetting.
|
|
12
|
+
class State
|
|
13
|
+
# An ext 0x02 (Fault) envelope nests through its +details+ field, and
|
|
14
|
+
# each level re-enters the codec with a fresh +MessagePack+ unpacker
|
|
15
|
+
# whose built-in stack guard resets — so ext-envelope depth is tracked
|
|
16
|
+
# here instead. The cap matches the wire's overall nesting bound and
|
|
17
|
+
# keeps a nested chain from exhausting the native stack: an over-deep
|
|
18
|
+
# chain fails as a clean wire error, never a stack-level trap.
|
|
19
|
+
MAX_EXT_DEPTH = 128
|
|
20
|
+
private_constant :MAX_EXT_DEPTH
|
|
21
|
+
|
|
22
|
+
# Thread-local slot holding the calling thread's State.
|
|
23
|
+
STATE_KEY = :__kobako_codec_state__
|
|
24
|
+
private_constant :STATE_KEY
|
|
25
|
+
|
|
26
|
+
# The calling thread's State, built on first use so the mutable
|
|
27
|
+
# state stays isolated to the thread that runs the codec call.
|
|
28
|
+
def self.current
|
|
29
|
+
Thread.current[STATE_KEY] ||= new
|
|
30
|
+
end
|
|
31
|
+
private_class_method :new
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@ext_depth = 0
|
|
35
|
+
@carried_handle = false
|
|
36
|
+
@faults_forbidden = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bracket a decode and return the block's result together with
|
|
40
|
+
# whether the decoded tree carried an ext 0x01 Capability Handle.
|
|
41
|
+
# ExtTypes#unpack_handle is the sole chokepoint every Handle passes
|
|
42
|
+
# through, so one decode pass records the whole tree and a caller
|
|
43
|
+
# can skip an all-identity Handle-resolution walk when none was
|
|
44
|
+
# present.
|
|
45
|
+
def track_handles
|
|
46
|
+
@carried_handle = false
|
|
47
|
+
result = yield
|
|
48
|
+
[result, @carried_handle]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Record that an ext 0x01 Capability Handle crossed the current
|
|
52
|
+
# decode; #track_handles reports it to the bracketing caller.
|
|
53
|
+
def record_handle!
|
|
54
|
+
@carried_handle = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Bracket a codec operation in a payload position, where an ext 0x02
|
|
58
|
+
# Fault envelope has no legal wire representation: the fault field of
|
|
59
|
+
# an error Response is its only home. The ext-type conversions
|
|
60
|
+
# consult #faults_forbidden? and refuse the envelope in both
|
|
61
|
+
# directions while the bracket is open. Save/restore keeps a nested
|
|
62
|
+
# legal operation on the same thread unaffected.
|
|
63
|
+
def forbid_faults
|
|
64
|
+
previous = @faults_forbidden
|
|
65
|
+
@faults_forbidden = true
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
@faults_forbidden = previous
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether the operation in flight sits inside a #forbid_faults
|
|
72
|
+
# bracket — i.e. in a payload position where ext 0x02 is a wire
|
|
73
|
+
# violation.
|
|
74
|
+
def faults_forbidden?
|
|
75
|
+
@faults_forbidden
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Track ext-envelope re-entry depth and refuse a chain past
|
|
79
|
+
# MAX_EXT_DEPTH, raising +over_limit+ so the failure lands in the
|
|
80
|
+
# caller's existing wire-error class. The next depth is checked before
|
|
81
|
+
# it is committed, so an over-deep rejection leaves the counter
|
|
82
|
+
# untouched, and the +ensure+ restores the entry value on the way out.
|
|
83
|
+
def within_ext_frame(over_limit)
|
|
84
|
+
depth = @ext_depth + 1
|
|
85
|
+
raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
|
|
86
|
+
|
|
87
|
+
@ext_depth = depth
|
|
88
|
+
begin
|
|
89
|
+
yield
|
|
90
|
+
ensure
|
|
91
|
+
@ext_depth = depth - 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private_constant :State
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Kobako
|
|
|
10
10
|
# - UTF-8 assertion at the codec boundary
|
|
11
11
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
12
12
|
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
13
|
-
# Decoder when walking +str+ family payloads and by
|
|
13
|
+
# Decoder when walking +str+ family payloads and by ExtTypes
|
|
14
14
|
# when validating the +ext 0x00+ Symbol payload.
|
|
15
15
|
# - +ArgumentError+ translation at the codec boundary
|
|
16
16
|
# (#with_boundary) so the public taxonomy stays
|
|
@@ -38,7 +38,7 @@ module Kobako
|
|
|
38
38
|
#
|
|
39
39
|
# Reach for this only where a value object is constructed outside a
|
|
40
40
|
# Decoder.decode block, whose rescue already performs the same
|
|
41
|
-
# mapping (worked example:
|
|
41
|
+
# mapping (worked example: ExtTypes#unpack_handle building
|
|
42
42
|
# +Handle.restore+ from a raw fixext payload). Do not use it for
|
|
43
43
|
# general-purpose validation outside the codec boundary —
|
|
44
44
|
# host-layer +ArgumentError+ values should propagate unchanged.
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require_relative "codec/error"
|
|
4
4
|
require_relative "codec/utils"
|
|
5
5
|
require_relative "codec/handle_walk"
|
|
6
|
-
require_relative "codec/
|
|
6
|
+
require_relative "codec/state"
|
|
7
|
+
require_relative "codec/ext_types"
|
|
7
8
|
require_relative "codec/encoder"
|
|
8
9
|
require_relative "codec/decoder"
|
|
9
10
|
|
|
@@ -18,14 +19,30 @@ module Kobako
|
|
|
18
19
|
# the kobako root so the codec can register them without depending
|
|
19
20
|
# upward on Transport.
|
|
20
21
|
#
|
|
21
|
-
# Backed by the official +msgpack+ gem
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# Backed by the official +msgpack+ gem: ExtTypes registers the three
|
|
23
|
+
# kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle,
|
|
24
|
+
# 0x02 Exception envelope) on one process-wide +MessagePack::Factory+,
|
|
25
|
+
# and Encoder / Decoder are thin wrappers over it. The Rust side
|
|
25
26
|
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
|
-
# the ext-code constants live as module-private values on
|
|
27
|
+
# the ext-code constants live as module-private values on ExtTypes
|
|
27
28
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
29
|
# +codec::EXT_ERRENV+ on that side.
|
|
29
30
|
module Codec
|
|
31
|
+
# Bracket a decode and return the block's result together with whether
|
|
32
|
+
# the decoded tree carried an ext 0x01 Capability Handle — the signal a
|
|
33
|
+
# dispatch path uses to skip an all-identity Handle-resolution walk.
|
|
34
|
+
# The tracking state is codec-internal; this is its only readout.
|
|
35
|
+
def self.track_handles(&block)
|
|
36
|
+
State.current.track_handles(&block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bracket a codec operation in a payload position: an ext 0x02 Fault
|
|
40
|
+
# envelope is only legal in the Response fault field, so the envelope
|
|
41
|
+
# layers open this bracket around every other encode / decode and the
|
|
42
|
+
# ext-type conversions refuse the envelope while it is open — a wire
|
|
43
|
+
# violation on decode, no wire representation on encode.
|
|
44
|
+
def self.forbid_faults(&block)
|
|
45
|
+
State.current.forbid_faults(&block)
|
|
46
|
+
end
|
|
30
47
|
end
|
|
31
48
|
end
|
data/lib/kobako/fault.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kobako
|
|
|
5
5
|
#
|
|
6
6
|
# Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
|
|
7
7
|
# +Fault+ is a MessagePack ext-type leaf registered by
|
|
8
|
-
# +Kobako::Codec::
|
|
8
|
+
# +Kobako::Codec::ExtTypes+ and rides nested inside other envelopes (a
|
|
9
9
|
# +Kobako::Transport::Response+ error payload, or another Fault's
|
|
10
10
|
# +details+). It lives at the kobako root rather than under +Transport+
|
|
11
11
|
# because the Codec layer must register it, and Codec must not depend
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Kobako
|
|
|
18
18
|
# constructor — is removed for the same reason: a legitimate Handle
|
|
19
19
|
# must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
|
|
20
20
|
# Handles through +.restore+, which exists at exactly two call
|
|
21
|
-
# sites: +Kobako::Codec::
|
|
21
|
+
# sites: +Kobako::Codec::ExtTypes#unpack_handle+ (wire decode) and
|
|
22
22
|
# +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
23
23
|
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
24
24
|
# of any public surface.
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -70,7 +70,9 @@ module Kobako
|
|
|
70
70
|
# "Symbol payload must be …" wording, but operators triaging a
|
|
71
71
|
# corrupted Sandbox runtime still need it.
|
|
72
72
|
def decode_value(body)
|
|
73
|
-
|
|
73
|
+
# The Result envelope is a payload position: an ext 0x02 Fault in it
|
|
74
|
+
# is a wire violation routed into the invalid-result rescue below.
|
|
75
|
+
Kobako::Codec.forbid_faults { Kobako::Codec::Decoder.decode(body) }
|
|
74
76
|
rescue Kobako::Codec::Error => e
|
|
75
77
|
raise build_transport_error(
|
|
76
78
|
"Sandbox produced an invalid result value",
|
|
@@ -99,13 +101,17 @@ module Kobako
|
|
|
99
101
|
# through the decoder boundary; the message itself is never user-
|
|
100
102
|
# facing — it lands in +details+ via the rescue chain above.
|
|
101
103
|
def build_panic_record(body)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
# The Panic envelope is a payload position: an ext 0x02 Fault in its
|
|
105
|
+
# +details+ is a wire violation routed into the invalid-panic rescue.
|
|
106
|
+
Kobako::Codec.forbid_faults do
|
|
107
|
+
Kobako::Codec::Decoder.decode(body) do |map|
|
|
108
|
+
raise Kobako::Codec::InvalidType, "panic body must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
Panic.new(
|
|
111
|
+
origin: map["origin"], klass: map["class"], message: map["message"],
|
|
112
|
+
backtrace: map["backtrace"] || [], details: map["details"]
|
|
113
|
+
)
|
|
114
|
+
end
|
|
109
115
|
end
|
|
110
116
|
end
|
|
111
117
|
|
data/lib/kobako/pool.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Kobako
|
|
|
24
24
|
# seconds (+nil+ waits indefinitely); every other keyword is
|
|
25
25
|
# forwarded verbatim to +Kobako::Sandbox.new+. The optional block
|
|
26
26
|
# runs exactly once per constructed Sandbox — it is the setup window
|
|
27
|
-
# for +#
|
|
27
|
+
# for +#bind+ / +#preload+ before that Sandbox's first checkout.
|
|
28
28
|
# No Sandbox is constructed here. Raises +ArgumentError+ for an
|
|
29
29
|
# invalid +slots+ / +checkout_timeout+.
|
|
30
30
|
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Kobako
|
|
|
17
17
|
#
|
|
18
18
|
# The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
|
|
19
19
|
# +Kobako::Catalog::Handles+, the per-instance
|
|
20
|
-
# +Kobako::Catalog::
|
|
20
|
+
# +Kobako::Catalog::Services+ (which receives the +Catalog::Handles+ by
|
|
21
21
|
# injection so guest→host dispatch and host→guest auto-wrap share one
|
|
22
22
|
# allocator), and the dispatch +Proc+ / +yield_to_guest+ lambda installed
|
|
23
23
|
# on the Runtime via +Runtime#on_dispatch=+. The underlying wasmtime Engine
|
|
@@ -100,21 +100,25 @@ module Kobako
|
|
|
100
100
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
101
101
|
@options = SandboxOptions.new(**)
|
|
102
102
|
@handler = Catalog::Handles.new
|
|
103
|
-
@services = Kobako::Catalog::
|
|
103
|
+
@services = Kobako::Catalog::Services.new(handler: @handler)
|
|
104
104
|
@snippets = Catalog::Snippets.new
|
|
105
105
|
@runtime = build_runtime!
|
|
106
106
|
install_dispatch_proc!
|
|
107
107
|
reset_invocation_state!
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# +
|
|
110
|
+
# Bind +object+ as the Service reachable at +path+ — a Symbol or
|
|
111
|
+
# String of one or more +::+-separated constant-form segments
|
|
112
|
+
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
113
|
+
# chaining.
|
|
113
114
|
#
|
|
114
|
-
# Raises +ArgumentError+ when
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
# Raises +ArgumentError+ when a segment is malformed, when +path+
|
|
116
|
+
# collides with an existing binding (a name is a bound Service or a
|
|
117
|
+
# grouping prefix, never both), or when called after the first
|
|
118
|
+
# invocation has sealed Service registration.
|
|
119
|
+
def bind(path, object)
|
|
120
|
+
@services.bind(path, object)
|
|
121
|
+
self
|
|
118
122
|
end
|
|
119
123
|
|
|
120
124
|
# Register a snippet on this Sandbox in one of two forms:
|
|
@@ -175,15 +179,15 @@ module Kobako
|
|
|
175
179
|
#
|
|
176
180
|
# Source delivery uses the WASI stdin three-frame protocol
|
|
177
181
|
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
178
|
-
# Frame 1 carries the msgpack-encoded preamble (
|
|
179
|
-
#
|
|
182
|
+
# Frame 1 carries the msgpack-encoded preamble (Service registry
|
|
183
|
+
# snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
180
184
|
# Frame 3 carries the snippet table registered via +#preload+.
|
|
181
185
|
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
182
186
|
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
183
187
|
# array, never an absent frame.
|
|
184
188
|
#
|
|
185
189
|
# The first invocation seals the Service registry and snippet table;
|
|
186
|
-
# subsequent +#
|
|
190
|
+
# subsequent +#bind+ / +#preload+ calls raise +ArgumentError+.
|
|
187
191
|
#
|
|
188
192
|
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
189
193
|
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
@@ -318,7 +322,10 @@ module Kobako
|
|
|
318
322
|
# token; restore it to the host object the guest referenced before
|
|
319
323
|
# handing the value to the Host App. @handler still holds this
|
|
320
324
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
321
|
-
|
|
325
|
+
# A Handle-free result resolves to itself, so the restoration walk is
|
|
326
|
+
# skipped when the decode carried none.
|
|
327
|
+
value, carried_handle = Codec.track_handles { Outcome.decode(return_bytes) }
|
|
328
|
+
carried_handle ? Codec::HandleWalk.deep_restore(value, @handler) : value
|
|
322
329
|
rescue Kobako::TrapError => e
|
|
323
330
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
324
331
|
ensure
|
|
@@ -13,7 +13,7 @@ module Kobako
|
|
|
13
13
|
module Transport
|
|
14
14
|
# Pure-function dispatcher for guest-initiated transport calls.
|
|
15
15
|
# Decodes a msgpack-encoded Request envelope, resolves the target
|
|
16
|
-
# object through the Catalog::
|
|
16
|
+
# object through the Catalog::Services (path lookup) or
|
|
17
17
|
# Catalog::Handles (Handle lookup), invokes the method, and returns
|
|
18
18
|
# a msgpack-encoded Response envelope.
|
|
19
19
|
#
|
|
@@ -24,7 +24,7 @@ module Kobako
|
|
|
24
24
|
#
|
|
25
25
|
# Entry point:
|
|
26
26
|
#
|
|
27
|
-
# Kobako::Transport::Dispatcher.dispatch(request_bytes,
|
|
27
|
+
# Kobako::Transport::Dispatcher.dispatch(request_bytes, services, handler, yield_to_guest)
|
|
28
28
|
# # => msgpack-encoded Response bytes (never raises)
|
|
29
29
|
module Dispatcher
|
|
30
30
|
# Throw tag for the Yielder's break unwind back to the
|
|
@@ -67,7 +67,7 @@ module Kobako
|
|
|
67
67
|
|
|
68
68
|
# Dispatch a single transport request and return the encoded
|
|
69
69
|
# Response bytes. Invoked from the +Runtime#on_dispatch+ Proc that
|
|
70
|
-
# +Kobako::Sandbox#initialize+ installs on the ext side; +
|
|
70
|
+
# +Kobako::Sandbox#initialize+ installs on the ext side; +services+,
|
|
71
71
|
# +handler+, and +yield_to_guest+ are captured in that Proc's
|
|
72
72
|
# closure so the Dispatcher stays stateless and the registry doesn't
|
|
73
73
|
# need to publish accessors for the Sandbox-owned +Catalog::Handles+
|
|
@@ -77,24 +77,31 @@ module Kobako
|
|
|
77
77
|
# returns a binary String — every failure path is reified as a
|
|
78
78
|
# Response.error envelope so the guest sees a transport error rather
|
|
79
79
|
# than a wasm trap.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
#
|
|
81
|
+
# The decode runs inside +Codec.track_handles+ so #resolve_call_args
|
|
82
|
+
# can skip the argument walk when no Capability Handle crossed the
|
|
83
|
+
# wire.
|
|
84
|
+
def dispatch(request_bytes, services, handler, yield_to_guest)
|
|
85
|
+
request, carried_handle = Kobako::Codec.track_handles { Kobako::Transport::Request.decode(request_bytes) }
|
|
86
|
+
target = resolve_target(request.target, services, handler)
|
|
87
|
+
args, kwargs = resolve_call_args(request, handler, carried_handle)
|
|
84
88
|
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given
|
|
85
|
-
|
|
86
|
-
encode_ok(value, handler)
|
|
89
|
+
encode_ok(catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }, handler)
|
|
87
90
|
rescue StandardError => e
|
|
88
91
|
encode_caught_error(e)
|
|
89
92
|
ensure
|
|
90
93
|
yielder&.invalidate!
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
# Resolve positional and keyword arguments off +request+ in one
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
96
|
+
# Resolve positional and keyword arguments off +request+ in one step.
|
|
97
|
+
# +carried_handle+ reports whether the decode carried any Capability
|
|
98
|
+
# Handle; when it did not, every argument resolves to itself, so the
|
|
99
|
+
# decoded values pass straight through and the walk is skipped entirely.
|
|
100
|
+
# Otherwise both go through #resolve_arg so Handles round-trip back to
|
|
101
|
+
# the host-side Ruby object before the call reaches +public_send+.
|
|
102
|
+
def resolve_call_args(request, handler, carried_handle)
|
|
103
|
+
return [request.args, request.kwargs] unless carried_handle
|
|
104
|
+
|
|
98
105
|
[request.args.map { |v| resolve_arg(v, handler) },
|
|
99
106
|
request.kwargs.transform_values { |v| resolve_arg(v, handler) }]
|
|
100
107
|
end
|
|
@@ -187,17 +194,17 @@ module Kobako
|
|
|
187
194
|
# Target type is already validated by +Transport::Request.decode+
|
|
188
195
|
# before this method is reached, so no else-branch is needed here —
|
|
189
196
|
# the wire layer is the system boundary that enforces the invariant.
|
|
190
|
-
def resolve_target(target,
|
|
197
|
+
def resolve_target(target, services, handler)
|
|
191
198
|
case target
|
|
192
199
|
when String
|
|
193
|
-
resolve_path(target,
|
|
200
|
+
resolve_path(target, services)
|
|
194
201
|
when Kobako::Handle
|
|
195
202
|
require_live_object!(target.id, handler)
|
|
196
203
|
end
|
|
197
204
|
end
|
|
198
205
|
|
|
199
|
-
def resolve_path(path,
|
|
200
|
-
|
|
206
|
+
def resolve_path(path, services)
|
|
207
|
+
services.lookup(path)
|
|
201
208
|
rescue KeyError => e
|
|
202
209
|
raise UndefinedTargetError, e.message
|
|
203
210
|
end
|
|
@@ -12,7 +12,7 @@ module Kobako
|
|
|
12
12
|
#
|
|
13
13
|
# 5-element msgpack array:
|
|
14
14
|
# +[target, method_name, args, kwargs, block_given]+. +target+ is
|
|
15
|
-
# either a +String+ (+"
|
|
15
|
+
# either a +String+ (+"MyService::KV"+, e.g. +"MyService::KV"+)
|
|
16
16
|
# or a Handle. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
|
|
17
17
|
# enforced at construction so the Value Object is the single source of
|
|
18
18
|
# truth. +block_given+ is a Boolean signalling whether the guest call
|
|
@@ -44,16 +44,21 @@ module Kobako
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Decode +bytes+ into a Request. Raises +Codec::InvalidType+ when the
|
|
47
|
-
# envelope is not the expected 5-element msgpack array,
|
|
48
|
-
#
|
|
47
|
+
# envelope is not the expected 5-element msgpack array, when any
|
|
48
|
+
# position carries an ext 0x02 Fault envelope (a Request is a payload
|
|
49
|
+
# position; the Response fault field is the envelope's only home), or
|
|
50
|
+
# when the Value Object's construction invariants reject the decoded
|
|
51
|
+
# fields.
|
|
49
52
|
def self.decode(bytes)
|
|
50
|
-
Codec
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
Codec.forbid_faults do
|
|
54
|
+
Codec::Decoder.decode(bytes) do |arr|
|
|
55
|
+
unless arr.is_a?(Array) && arr.length == 5
|
|
56
|
+
raise Codec::InvalidType, "Request envelope is malformed (expected a 5-element array)"
|
|
57
|
+
end
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
target, method_name, args, kwargs, block_given = arr
|
|
60
|
+
new(target: target, method_name: method_name, args: args, kwargs: kwargs, block_given: block_given)
|
|
61
|
+
end
|
|
57
62
|
end
|
|
58
63
|
end
|
|
59
64
|
|
|
@@ -53,9 +53,16 @@ module Kobako
|
|
|
53
53
|
def error? = status == STATUS_ERROR
|
|
54
54
|
|
|
55
55
|
# Encode this Response to msgpack bytes as the 2-element
|
|
56
|
-
# +[status, payload]+ array.
|
|
56
|
+
# +[status, payload]+ array. The ok variant's value is a payload
|
|
57
|
+
# position, so a +Kobako::Fault+ inside it has no wire
|
|
58
|
+
# representation and surfaces as +UnsupportedType+ — the
|
|
59
|
+
# Dispatcher's auto-wrap rescue turns it into a Capability Handle.
|
|
60
|
+
# The error variant carries the Fault envelope in its one legal
|
|
61
|
+
# position and encodes it plainly.
|
|
57
62
|
def encode
|
|
58
|
-
Codec::Encoder.encode([status, payload])
|
|
63
|
+
return Codec::Encoder.encode([status, payload]) if error?
|
|
64
|
+
|
|
65
|
+
Codec.forbid_faults { Codec::Encoder.encode([status, payload]) }
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
# Decode +bytes+ into a Response. Raises +Codec::InvalidType+ when the
|