kobako 0.14.0 → 0.16.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 -1
- data/CHANGELOG.md +67 -0
- data/Cargo.lock +3 -3
- data/README.md +104 -29
- data/ROADMAP.md +2 -3
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/snapshot.rs +7 -3
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/capture.rs +17 -13
- data/crates/kobako-wasmtime/src/driver.rs +6 -7
- data/crates/kobako-wasmtime/src/frames.rs +2 -9
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
- data/crates/kobako-wasmtime/src/trap.rs +30 -57
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/errors.rs +9 -39
- data/ext/kobako/src/runtime.rs +5 -20
- data/lib/kobako/catalog/extensions.rb +114 -0
- data/lib/kobako/catalog/handles.rb +1 -1
- data/lib/kobako/catalog/services.rb +135 -0
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/catalog.rb +7 -5
- 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/extension.rb +47 -0
- 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 +47 -27
- 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/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +25 -0
- data/sig/kobako/catalog/services.rbs +27 -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/extension.rbs +20 -0
- data/sig/kobako/sandbox.rbs +3 -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 +11 -7
- 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
|
@@ -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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Kobako::Extension — a guest idiom paired with an optional host backend,
|
|
5
|
+
# installed on a Sandbox via +Sandbox#install+. It composes the existing
|
|
6
|
+
# +#preload+ (the guest +source+) and +#bind+ (the +backend+) verbs into
|
|
7
|
+
# one setup unit, so guest code sees a native-style constant whose pure
|
|
8
|
+
# methods run in-guest and whose privileged methods dispatch to the
|
|
9
|
+
# backend.
|
|
10
|
+
#
|
|
11
|
+
# The four readers form the contract +#install+ duck-types on:
|
|
12
|
+
#
|
|
13
|
+
# * +name+ — a Symbol matching +/\A[A-Z]\w*\z/+, the preloaded snippet's
|
|
14
|
+
# canonical backtrace name and the +depends_on+ match key. Independent
|
|
15
|
+
# of any bound path.
|
|
16
|
+
# * +source+ — the mruby idiom as a String; always present, since an
|
|
17
|
+
# Extension always carries a guest idiom. A host object with no idiom
|
|
18
|
+
# is bound with +#bind+ directly.
|
|
19
|
+
# * +backend+ — an +Extension::Backend+ or +nil+ for a pure-guest
|
|
20
|
+
# Extension.
|
|
21
|
+
# * +depends_on+ — Symbol names of Extensions that must also be
|
|
22
|
+
# installed, checked for presence at the first invocation.
|
|
23
|
+
#
|
|
24
|
+
# +Kobako::Extension+ is the bundled value type; any object exposing the
|
|
25
|
+
# four readers is equally valid, so a Host App or gem may supply its own.
|
|
26
|
+
class Extension < Data.define(:name, :source, :backend, :depends_on)
|
|
27
|
+
# Kobako::Extension::Backend — the host attachment of an Extension,
|
|
28
|
+
# pairing +path+ (the constant path the backend binds at, single-segment
|
|
29
|
+
# +"File"+ or nested +"MyApp::Store"+, spelling the guest constant the
|
|
30
|
+
# idiom routes to) with +provider+ (the source of the bound object).
|
|
31
|
+
#
|
|
32
|
+
# A +provider+ that is not itself callable is the bound object, resolved
|
|
33
|
+
# once for the Sandbox's life; a callable provider is invoked once per
|
|
34
|
+
# invocation to yield that invocation's object, so a fresh object backs
|
|
35
|
+
# the path every invocation. Callability is the sole discriminator — a
|
|
36
|
+
# fixed backend that is itself callable is supplied through a
|
|
37
|
+
# non-callable wrapper.
|
|
38
|
+
class Backend < Data.define(:path, :provider)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# +backend+ and +depends_on+ default to absent so the common
|
|
42
|
+
# pure-idiom and single-backend shapes stay terse.
|
|
43
|
+
def initialize(name:, source:, backend: nil, depends_on: [])
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
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
|
|
@@ -48,29 +48,21 @@ module Kobako
|
|
|
48
48
|
# so use +#stdout_truncated?+ to observe overflow. Populated on every
|
|
49
49
|
# outcome — including a rescued +TrapError+, after which it holds the
|
|
50
50
|
# bytes written before the trap fired — mirroring +#usage+.
|
|
51
|
-
def stdout
|
|
52
|
-
@stdout_capture.bytes
|
|
53
|
-
end
|
|
51
|
+
def stdout = @stdout_capture.bytes
|
|
54
52
|
|
|
55
53
|
# Returns the bytes the guest wrote to stderr during the most recent
|
|
56
54
|
# invocation as a UTF-8 String, clipped at +stderr_limit+. Empty before
|
|
57
55
|
# any invocation. Mirror of +#stdout+.
|
|
58
|
-
def stderr
|
|
59
|
-
@stderr_capture.bytes
|
|
60
|
-
end
|
|
56
|
+
def stderr = @stderr_capture.bytes
|
|
61
57
|
|
|
62
58
|
# Returns +true+ iff stdout capture during the most recent invocation
|
|
63
59
|
# exceeded +stdout_limit+. Resets to +false+ at the start of the next
|
|
64
60
|
# invocation.
|
|
65
|
-
def stdout_truncated?
|
|
66
|
-
@stdout_capture.truncated?
|
|
67
|
-
end
|
|
61
|
+
def stdout_truncated? = @stdout_capture.truncated?
|
|
68
62
|
|
|
69
63
|
# Returns +true+ iff stderr capture during the most recent invocation
|
|
70
64
|
# exceeded +stderr_limit+. Mirror of +#stdout_truncated?+.
|
|
71
|
-
def stderr_truncated?
|
|
72
|
-
@stderr_capture.truncated?
|
|
73
|
-
end
|
|
65
|
+
def stderr_truncated? = @stderr_capture.truncated?
|
|
74
66
|
|
|
75
67
|
# Returns the +Kobako::Usage+ value object for the most recent
|
|
76
68
|
# invocation. Carries +wall_time+ (Float seconds the guest export call spent
|
|
@@ -100,21 +92,42 @@ module Kobako
|
|
|
100
92
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
101
93
|
@options = SandboxOptions.new(**)
|
|
102
94
|
@handler = Catalog::Handles.new
|
|
103
|
-
@services = Kobako::Catalog::
|
|
95
|
+
@services = Kobako::Catalog::Services.new(handler: @handler)
|
|
104
96
|
@snippets = Catalog::Snippets.new
|
|
97
|
+
@extensions = Catalog::Extensions.new
|
|
105
98
|
@runtime = build_runtime!
|
|
106
99
|
install_dispatch_proc!
|
|
107
100
|
reset_invocation_state!
|
|
108
101
|
end
|
|
109
102
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# +
|
|
103
|
+
# Bind +object+ as the Service reachable at +path+ — a Symbol or
|
|
104
|
+
# String of one or more +::+-separated constant-form segments
|
|
105
|
+
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
106
|
+
# chaining.
|
|
107
|
+
#
|
|
108
|
+
# Raises +ArgumentError+ when a segment is malformed, when +path+
|
|
109
|
+
# collides with an existing binding (a name is a bound Service or a
|
|
110
|
+
# grouping prefix, never both), or when called after the first
|
|
111
|
+
# invocation has sealed Service registration.
|
|
112
|
+
def bind(path, object)
|
|
113
|
+
@services.bind(path, object)
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Install one or more Extensions — each a guest idiom (+source+) paired
|
|
118
|
+
# with an optional host +backend+, composed onto the Sandbox through
|
|
119
|
+
# +#preload+ and +#bind+. An Extension is any object exposing
|
|
120
|
+
# +name+ / +source+ / +backend+ / +depends_on+; +Kobako::Extension+ is
|
|
121
|
+
# the bundled value type. Returns +self+.
|
|
113
122
|
#
|
|
114
|
-
# Raises +ArgumentError+
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
# Raises +ArgumentError+ for a malformed Extension, a call after the
|
|
124
|
+
# first invocation seals registration, or — at that first invocation —
|
|
125
|
+
# an unmet +depends_on+.
|
|
126
|
+
def install(*extensions)
|
|
127
|
+
raise ArgumentError, "cannot install after first Sandbox invocation" if @services.sealed?
|
|
128
|
+
|
|
129
|
+
extensions.each { |extension| @extensions.install(extension, snippets: @snippets, services: @services) }
|
|
130
|
+
self
|
|
118
131
|
end
|
|
119
132
|
|
|
120
133
|
# Register a snippet on this Sandbox in one of two forms:
|
|
@@ -175,15 +188,15 @@ module Kobako
|
|
|
175
188
|
#
|
|
176
189
|
# Source delivery uses the WASI stdin three-frame protocol
|
|
177
190
|
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
178
|
-
# Frame 1 carries the msgpack-encoded preamble (
|
|
179
|
-
#
|
|
191
|
+
# Frame 1 carries the msgpack-encoded preamble (Service registry
|
|
192
|
+
# snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
180
193
|
# Frame 3 carries the snippet table registered via +#preload+.
|
|
181
194
|
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
182
195
|
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
183
196
|
# array, never an absent frame.
|
|
184
197
|
#
|
|
185
198
|
# The first invocation seals the Service registry and snippet table;
|
|
186
|
-
# subsequent +#
|
|
199
|
+
# subsequent +#bind+ / +#preload+ calls raise +ArgumentError+.
|
|
187
200
|
#
|
|
188
201
|
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
189
202
|
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
@@ -237,14 +250,18 @@ module Kobako
|
|
|
237
250
|
end
|
|
238
251
|
end
|
|
239
252
|
|
|
240
|
-
# Per-invocation prologue. Seals the Service / snippet
|
|
241
|
-
# first call (idempotent
|
|
253
|
+
# Per-invocation prologue. Seals the Service / snippet / Extension
|
|
254
|
+
# registries on first call (idempotent — asserting Extension
|
|
255
|
+
# dependencies then), refreshes each callable Extension backend to this
|
|
256
|
+
# invocation's fresh object, and zeros the per-invocation capability
|
|
242
257
|
# state — capture buffers, truncation predicates, and the
|
|
243
258
|
# +Catalog::Handles+ counter — before the guest runs. The
|
|
244
259
|
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
245
260
|
# beyond this class — it is not part of the Host App's surface.
|
|
246
261
|
def begin_invocation!
|
|
247
262
|
@services.seal!
|
|
263
|
+
@extensions.seal!
|
|
264
|
+
@extensions.refresh_backends!(@services)
|
|
248
265
|
@handler.reset!
|
|
249
266
|
reset_invocation_state!
|
|
250
267
|
end
|
|
@@ -318,7 +335,10 @@ module Kobako
|
|
|
318
335
|
# token; restore it to the host object the guest referenced before
|
|
319
336
|
# handing the value to the Host App. @handler still holds this
|
|
320
337
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
321
|
-
|
|
338
|
+
# A Handle-free result resolves to itself, so the restoration walk is
|
|
339
|
+
# skipped when the decode carried none.
|
|
340
|
+
value, carried_handle = Codec.track_handles { Outcome.decode(return_bytes) }
|
|
341
|
+
carried_handle ? Codec::HandleWalk.deep_restore(value, @handler) : value
|
|
322
342
|
rescue Kobako::TrapError => e
|
|
323
343
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
324
344
|
ensure
|