kobako 0.5.0-x86_64-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 +7 -0
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE +201 -0
- data/README.md +408 -0
- 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/capture.rb +43 -0
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/catalog/snippets.rb +149 -0
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +73 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +34 -0
- data/lib/kobako/codec/factory.rb +162 -0
- data/lib/kobako/codec/utils.rb +145 -0
- data/lib/kobako/codec.rb +31 -0
- data/lib/kobako/errors.rb +140 -0
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +60 -0
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome/panic.rb +42 -0
- data/lib/kobako/outcome.rb +166 -0
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +314 -0
- data/lib/kobako/sandbox_options.rb +70 -0
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +29 -0
- data/lib/kobako/snippet/source.rb +28 -0
- data/lib/kobako/snippet.rb +18 -0
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/transport/error.rb +24 -0
- 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/usage.rb +41 -0
- data/lib/kobako/version.rb +5 -0
- data/lib/kobako.rb +10 -0
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +11 -0
- data/sig/kobako/catalog/handles.rbs +19 -0
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/catalog/snippets.rbs +27 -0
- data/sig/kobako/catalog.rbs +4 -0
- data/sig/kobako/codec/decoder.rbs +12 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +19 -0
- data/sig/kobako/errors.rbs +55 -0
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +18 -0
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +55 -0
- data/sig/kobako/sandbox_options.rbs +32 -0
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/snippet/binary.rbs +12 -0
- data/sig/kobako/snippet/source.rbs +13 -0
- data/sig/kobako/snippet.rbs +4 -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
- data/sig/kobako/usage.rbs +11 -0
- data/sig/kobako.rbs +3 -0
- metadata +145 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
require "msgpack"
|
|
6
|
+
|
|
7
|
+
require_relative "error"
|
|
8
|
+
require_relative "utils"
|
|
9
|
+
require_relative "../handle"
|
|
10
|
+
require_relative "../fault"
|
|
11
|
+
|
|
12
|
+
module Kobako
|
|
13
|
+
module Codec
|
|
14
|
+
# Cached +MessagePack::Factory+ that owns the kobako wire ext-type
|
|
15
|
+
# registration ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
16
|
+
# § Ext Types).
|
|
17
|
+
#
|
|
18
|
+
# The factory is the single place in the host gem that touches the
|
|
19
|
+
# msgpack API — both {Encoder} and {Decoder} delegate through it, so
|
|
20
|
+
# the three kobako ext codes (0x00 Symbol, 0x01 Capability Handle,
|
|
21
|
+
# 0x02 Exception envelope) are configured exactly once at first use.
|
|
22
|
+
#
|
|
23
|
+
# Lifecycle is owned by +Singleton+ from the Ruby standard library:
|
|
24
|
+
# +Factory.instance+ is lazy, thread-safe, and process-wide. Class-level
|
|
25
|
+
# +Factory.dump+ / +Factory.load+ shortcuts are exposed via
|
|
26
|
+
# +SingleForwardable+ so callers do not have to spell the +.instance+
|
|
27
|
+
# hop at every call site; the instance-level +#dump+ / +#load+ are in
|
|
28
|
+
# turn delegated to the wrapped +MessagePack::Factory+ via +Forwardable+.
|
|
29
|
+
class Factory
|
|
30
|
+
include Singleton
|
|
31
|
+
extend Forwardable
|
|
32
|
+
extend SingleForwardable
|
|
33
|
+
|
|
34
|
+
# MessagePack ext type code reserved for Symbol
|
|
35
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
36
|
+
# → ext 0x00). Class-private — mirrors +codec::EXT_SYMBOL+ on the
|
|
37
|
+
# Rust side.
|
|
38
|
+
EXT_SYMBOL = 0x00
|
|
39
|
+
# MessagePack ext type code reserved for Capability Handle
|
|
40
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
41
|
+
# → ext 0x01). Class-private — mirrors +codec::EXT_HANDLE+ on the
|
|
42
|
+
# Rust side.
|
|
43
|
+
EXT_HANDLE = 0x01
|
|
44
|
+
# MessagePack ext type code reserved for Exception envelope
|
|
45
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
46
|
+
# → ext 0x02). Class-private — mirrors +codec::EXT_ERRENV+ on the
|
|
47
|
+
# Rust side.
|
|
48
|
+
EXT_ERRENV = 0x02
|
|
49
|
+
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
50
|
+
|
|
51
|
+
# Instance-level pass-through onto the wrapped +MessagePack::Factory+.
|
|
52
|
+
# Spelled +def_instance_delegators+ rather than +def_delegators+ because
|
|
53
|
+
# the class also extends +SingleForwardable+ (see the +extend+ block
|
|
54
|
+
# above), which defines its own +def_delegators+ that shadows
|
|
55
|
+
# +Forwardable+'s — the unambiguous forms keep both delegation tiers
|
|
56
|
+
# wired to the right scope.
|
|
57
|
+
def_instance_delegators :@factory, :dump, :load
|
|
58
|
+
|
|
59
|
+
# Class-level shortcuts so callers can write +Factory.dump(v)+ instead
|
|
60
|
+
# of +Factory.instance.dump(v)+; both resolve to the same singleton.
|
|
61
|
+
def_single_delegators :instance, :dump, :load
|
|
62
|
+
|
|
63
|
+
def initialize
|
|
64
|
+
@factory = MessagePack::Factory.new
|
|
65
|
+
register_symbol
|
|
66
|
+
register_handle
|
|
67
|
+
register_fault
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def register_symbol
|
|
73
|
+
@factory.register_type(
|
|
74
|
+
EXT_SYMBOL, Symbol,
|
|
75
|
+
packer: method(:pack_symbol),
|
|
76
|
+
unpacker: method(:unpack_symbol)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Symbol-to-name packer — extracted to a real method so Steep can
|
|
81
|
+
# resolve the proc shape without tripping on +lambda(&:name)+'s
|
|
82
|
+
# +Symbol#to_proc+ inference path.
|
|
83
|
+
def pack_symbol(symbol)
|
|
84
|
+
symbol.name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
88
|
+
# {InvalidEncoding} on invalid bytes — SPEC forbids the
|
|
89
|
+
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
90
|
+
# would otherwise apply. The re-tag step lives here because the
|
|
91
|
+
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
92
|
+
# itself is shared with {Decoder} via {Utils.assert_utf8!}. The
|
|
93
|
+
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
94
|
+
# rather than wire-ext-code vocabulary.
|
|
95
|
+
def unpack_symbol(payload)
|
|
96
|
+
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
97
|
+
Utils.assert_utf8!(name, "Symbol payload")
|
|
98
|
+
name.to_sym
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def register_handle
|
|
102
|
+
@factory.register_type(
|
|
103
|
+
EXT_HANDLE, Kobako::Handle,
|
|
104
|
+
packer: ->(handle) { [handle.id].pack("N") },
|
|
105
|
+
unpacker: ->(payload) { unpack_handle(payload) }
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def register_fault
|
|
110
|
+
@factory.register_type(
|
|
111
|
+
EXT_ERRENV, Kobako::Fault,
|
|
112
|
+
packer: ->(fault) { pack_fault(fault) },
|
|
113
|
+
unpacker: ->(payload) { unpack_fault(payload) }
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Peel off the fixext-4 frame, hand the bytes to the
|
|
118
|
+
# Host-Gem-internal +Kobako::Handle.restore+ factory, and
|
|
119
|
+
# translate the +ArgumentError+ raised by Handle's invariants
|
|
120
|
+
# into a wire-layer +InvalidType+ via {Codec::Utils.with_boundary}.
|
|
121
|
+
# The Value Object owns the id-range contract; this method only
|
|
122
|
+
# owns the frame shape.
|
|
123
|
+
def unpack_handle(payload)
|
|
124
|
+
bytes = payload.b
|
|
125
|
+
raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
126
|
+
|
|
127
|
+
id = bytes.unpack1("N") # : Integer
|
|
128
|
+
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
|
|
132
|
+
# the embedded payload flows through the same boundary as a top-level
|
|
133
|
+
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
134
|
+
# registered ext-type packers via the cached singleton.
|
|
135
|
+
def pack_fault(fault)
|
|
136
|
+
Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
140
|
+
# inside {Decoder.decode}'s block form, so the value-object's
|
|
141
|
+
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
142
|
+
# decoder boundary. Inner decode goes through {Decoder} (not
|
|
143
|
+
# +factory.load+) so the embedded +str+ payloads flow through the
|
|
144
|
+
# same UTF-8 validation as a top-level decode.
|
|
145
|
+
#
|
|
146
|
+
# This establishes a runtime cycle Factory → Decoder → Factory: the
|
|
147
|
+
# singleton instance feeds +Decoder.decode+, which re-enters this
|
|
148
|
+
# method when a nested ext 0x02 appears inside +details+. The recursion
|
|
149
|
+
# is bounded by msgpack nesting depth — identical to nested Array /
|
|
150
|
+
# Hash payloads — so no extra guard is needed. Do not switch back to
|
|
151
|
+
# +factory.load+ to "simplify": that path bypasses UTF-8 validation
|
|
152
|
+
# and re-opens the Decoder's special case for Fault (removed in M5).
|
|
153
|
+
def unpack_fault(payload)
|
|
154
|
+
Decoder.decode(payload) do |map|
|
|
155
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error"
|
|
4
|
+
require_relative "../handle"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
module Codec
|
|
8
|
+
# Codec helpers shared by the host-side encoders and decoders.
|
|
9
|
+
# Three concerns live here today:
|
|
10
|
+
#
|
|
11
|
+
# - UTF-8 assertion at the codec boundary
|
|
12
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
13
|
+
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
14
|
+
# {Decoder} when walking +str+ family payloads and by {Factory}
|
|
15
|
+
# when validating the +ext 0x00+ Symbol payload.
|
|
16
|
+
# - +ArgumentError+ translation at the codec boundary
|
|
17
|
+
# ({with_boundary}) so the public taxonomy stays
|
|
18
|
+
# {Kobako::Codec::Error}.
|
|
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+
|
|
23
|
+
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
|
|
24
|
+
#
|
|
25
|
+
# All helpers are pure — they only inspect inputs, never mutate
|
|
26
|
+
# them — except {deep_wrap}, whose only side effect is allocating
|
|
27
|
+
# new Handle ids into the supplied table.
|
|
28
|
+
module Utils
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# Raise {InvalidEncoding} unless +string+'s bytes are valid under
|
|
32
|
+
# its current encoding tag. +label+ is the caller-supplied prefix
|
|
33
|
+
# for the error message (e.g. +"str payload"+, +"Symbol payload"+).
|
|
34
|
+
def assert_utf8!(string, label)
|
|
35
|
+
return if string.valid_encoding?
|
|
36
|
+
|
|
37
|
+
raise InvalidEncoding, "#{label} is not valid UTF-8"
|
|
38
|
+
end
|
|
39
|
+
|
|
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.
|
|
45
|
+
#
|
|
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
|
|
55
|
+
yield
|
|
56
|
+
rescue ::ArgumentError => e
|
|
57
|
+
raise InvalidType, e.message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Inclusive Integer range the msgpack gem encodes without raising
|
|
61
|
+
# +RangeError+ at encode time — signed +int 64+ minimum through
|
|
62
|
+
# unsigned +uint 64+ maximum
|
|
63
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
64
|
+
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
65
|
+
# Anchored as a +Range+ so {primitive_type?} stays a single
|
|
66
|
+
# dispatch line. This is the codec's encode domain — not to
|
|
67
|
+
# be confused with the Handle id range, which lives on
|
|
68
|
+
# +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
|
|
69
|
+
# represents a different concept entirely.
|
|
70
|
+
MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
|
|
71
|
+
|
|
72
|
+
# Codec-type predicate
|
|
73
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
74
|
+
# Mapping). Returns +true+ when +value+ belongs to the closed
|
|
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 /
|
|
80
|
+
# unsigned-64 union are rejected so the predicate agrees with the
|
|
81
|
+
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
82
|
+
# already surfaces as {UnsupportedType}.
|
|
83
|
+
def representable?(value)
|
|
84
|
+
primitive_type?(value) || container_representable?(value)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
88
|
+
# leaf that fails {representable?} with a +Kobako::Handle+
|
|
89
|
+
# allocated from +handler+
|
|
90
|
+
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
|
|
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.
|
|
96
|
+
#
|
|
97
|
+
# +value+ may be any Ruby value; +handler+ must respond to
|
|
98
|
+
# +#alloc(object) -> Kobako::Handle+ (a host-side
|
|
99
|
+
# +Kobako::Catalog::Handles+). Returns a structurally equivalent value
|
|
100
|
+
# whose leaves are either representable or +Kobako::Handle+
|
|
101
|
+
# tokens.
|
|
102
|
+
#
|
|
103
|
+
# The block bodies spell +Utils.deep_wrap+ explicitly rather than
|
|
104
|
+
# the unqualified +deep_wrap+ because +module_function+ makes the
|
|
105
|
+
# instance copy of these helpers private; an implicit receiver
|
|
106
|
+
# inside a block would resolve against the enclosing +self+
|
|
107
|
+
# (still +Utils+ at definition time, but the qualified form keeps
|
|
108
|
+
# the dispatch readable when the recursive call sits inside a
|
|
109
|
+
# Proc captured from elsewhere).
|
|
110
|
+
def deep_wrap(value, handler)
|
|
111
|
+
case value
|
|
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) }
|
|
114
|
+
else
|
|
115
|
+
representable?(value) ? value : handler.alloc(value)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Predicate split out of {representable?} for cyclomatic
|
|
120
|
+
# budget — the closed-set non-container branch. Returns +true+ for
|
|
121
|
+
# the scalar leaves and an existing Handle. Not part of the
|
|
122
|
+
# public surface; reach for {representable?} instead.
|
|
123
|
+
def primitive_type?(value)
|
|
124
|
+
case value
|
|
125
|
+
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
126
|
+
when ::Integer then MSGPACK_INT_RANGE.cover?(value)
|
|
127
|
+
else false
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Predicate split out of {representable?} for cyclomatic
|
|
132
|
+
# budget — the container branch. Recurses into Array elements and
|
|
133
|
+
# Hash key+value pairs through the public {representable?}.
|
|
134
|
+
# Not part of the public surface; reach for {representable?}
|
|
135
|
+
# instead.
|
|
136
|
+
def container_representable?(value)
|
|
137
|
+
case value
|
|
138
|
+
when ::Array then value.all? { |element| Utils.representable?(element) }
|
|
139
|
+
when ::Hash then value.all? { |key, val| Utils.representable?(key) && Utils.representable?(val) }
|
|
140
|
+
else false
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/lib/kobako/codec.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "codec/error"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
# Host-side MessagePack codec for the kobako wire contract — the
|
|
7
|
+
# byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
|
|
8
|
+
# Two consumers sit on top:
|
|
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.
|
|
15
|
+
#
|
|
16
|
+
# Backed by the official +msgpack+ gem via {Factory}; {Encoder} and
|
|
17
|
+
# {Decoder} are thin wrappers that register the three kobako-specific
|
|
18
|
+
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
19
|
+
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
20
|
+
# mirrors this layer as the +codec+ module in the +kobako-wasm+ crate;
|
|
21
|
+
# the ext-code constants live as module-private values on {Factory}
|
|
22
|
+
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
23
|
+
# +codec::EXT_ERRENV+ on that side.
|
|
24
|
+
module Codec
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require_relative "codec/utils"
|
|
29
|
+
require_relative "codec/factory"
|
|
30
|
+
require_relative "codec/encoder"
|
|
31
|
+
require_relative "codec/decoder"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Top-level Kobako namespace.
|
|
4
|
+
module Kobako
|
|
5
|
+
# Error taxonomy (docs/behavior.md § Error Scenarios).
|
|
6
|
+
#
|
|
7
|
+
# Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
|
|
8
|
+
# exactly one of three invocation-outcome classes. Attribution is decided after the
|
|
9
|
+
# guest binary returns control to the host (docs/behavior.md
|
|
10
|
+
# "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
|
|
11
|
+
#
|
|
12
|
+
# Three invocation-outcome branches:
|
|
13
|
+
#
|
|
14
|
+
# * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
|
|
15
|
+
# wire-violation fallback signalling a corrupted
|
|
16
|
+
# guest runtime).
|
|
17
|
+
# * {SandboxError} — sandbox / wire layer (mruby script error,
|
|
18
|
+
# wire-decode failure on an otherwise valid tag,
|
|
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.
|
|
32
|
+
#
|
|
33
|
+
# Subclasses pinned by docs/behavior.md Error Classes:
|
|
34
|
+
#
|
|
35
|
+
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
36
|
+
# at +wasm_path+ (E-40).
|
|
37
|
+
# * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit (B-21).
|
|
38
|
+
|
|
39
|
+
# Base for all kobako-raised errors so callers that want to ignore the
|
|
40
|
+
# taxonomy can rescue a single class.
|
|
41
|
+
class Error < StandardError; end
|
|
42
|
+
|
|
43
|
+
# Wasm engine layer. Raised when the Wasm execution engine crashed
|
|
44
|
+
# (trap, OOM, unreachable) or when the wire layer detected a structural
|
|
45
|
+
# violation that signals a corrupted guest execution environment
|
|
46
|
+
# (zero-length OUTCOME_BUFFER, unknown outcome tag — SPEC E-02 / E-03).
|
|
47
|
+
#
|
|
48
|
+
# Two named subclasses cover the configured per-invocation caps from B-01:
|
|
49
|
+
#
|
|
50
|
+
# * {TimeoutError} — wall-clock +timeout+ exceeded (E-19).
|
|
51
|
+
# * {MemoryLimitError} — guest +memory.grow+ would exceed
|
|
52
|
+
# +memory_limit+ (E-20).
|
|
53
|
+
#
|
|
54
|
+
# Host Apps that only care about "guest is unrecoverable, discard the
|
|
55
|
+
# Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
|
|
56
|
+
# want to surface a specific reason to operators can rescue the subclass
|
|
57
|
+
# first.
|
|
58
|
+
class TrapError < Error; end
|
|
59
|
+
|
|
60
|
+
# Wall-clock timeout cap exhausted. {docs/behavior.md E-19}[link:../../docs/behavior.md]:
|
|
61
|
+
# the absolute deadline +entry_time + timeout+ passed and the next guest
|
|
62
|
+
# wasm safepoint trapped. The Sandbox is unrecoverable after this point;
|
|
63
|
+
# discard and recreate before another execution.
|
|
64
|
+
class TimeoutError < TrapError; end
|
|
65
|
+
|
|
66
|
+
# Linear-memory cap exhausted. {docs/behavior.md E-20}[link:../../docs/behavior.md]:
|
|
67
|
+
# a guest +memory.grow+ would have pushed linear memory past the
|
|
68
|
+
# configured +memory_limit+. The Sandbox is unrecoverable after this
|
|
69
|
+
# point; discard and recreate before another execution.
|
|
70
|
+
class MemoryLimitError < TrapError; end
|
|
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
|
+
|
|
93
|
+
# Sandbox / wire layer. Raised when the guest ran to completion but
|
|
94
|
+
# execution failed due to a mruby script error, a protocol fault, or a
|
|
95
|
+
# host-side wire decode failure on an otherwise valid outcome tag.
|
|
96
|
+
class SandboxError < Error
|
|
97
|
+
attr_reader :origin, :klass, :backtrace_lines, :details
|
|
98
|
+
|
|
99
|
+
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
|
|
100
|
+
super(message)
|
|
101
|
+
@origin = origin
|
|
102
|
+
@klass = klass
|
|
103
|
+
@backtrace_lines = backtrace_lines
|
|
104
|
+
@details = details
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Service layer. Raised when a Service capability call inside a mruby
|
|
109
|
+
# script reported an application-level failure that the script did not
|
|
110
|
+
# rescue.
|
|
111
|
+
class ServiceError < Error
|
|
112
|
+
attr_reader :origin, :klass, :backtrace_lines, :details
|
|
113
|
+
|
|
114
|
+
def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
|
|
115
|
+
super(message)
|
|
116
|
+
@origin = origin
|
|
117
|
+
@klass = klass
|
|
118
|
+
@backtrace_lines = backtrace_lines
|
|
119
|
+
@details = details
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
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
|
|
128
|
+
|
|
129
|
+
# docs/behavior.md Error Classes: BytecodeError is the SandboxError
|
|
130
|
+
# subclass raised when a `#preload(binary:)` snippet fails structural
|
|
131
|
+
# validation during the first invocation's snippet replay against a
|
|
132
|
+
# fresh `mrb_state` (E-37 RITE version mismatch, E-38 corrupt body).
|
|
133
|
+
# Bytecode that loads cleanly and then raises at top level is E-36
|
|
134
|
+
# and surfaces as plain `SandboxError` with the natural mruby class
|
|
135
|
+
# preserved. Inherits from SandboxError so a single
|
|
136
|
+
# `rescue Kobako::SandboxError` covers both source and bytecode
|
|
137
|
+
# snippet failures while callers wanting bytecode-specific handling
|
|
138
|
+
# can `rescue Kobako::BytecodeError` directly.
|
|
139
|
+
class BytecodeError < SandboxError; end
|
|
140
|
+
end
|
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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Wire-level value object for an ext-0x01 Capability Handle, used in both
|
|
5
|
+
# directions across the Sandbox boundary: as a Service method's return
|
|
6
|
+
# value (guest→host return path; {docs/behavior.md B-14}[link:../../docs/behavior.md])
|
|
7
|
+
# and as a +#run+ argument auto-wrapped by the host
|
|
8
|
+
# ({docs/behavior.md B-34}[link:../../docs/behavior.md]).
|
|
9
|
+
#
|
|
10
|
+
# SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
|
|
11
|
+
# payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
|
|
12
|
+
# § Ext Types → ext 0x01). ID 0 is reserved as the invalid sentinel;
|
|
13
|
+
# the maximum valid ID is 0x7fff_ffff (2^31 - 1).
|
|
14
|
+
#
|
|
15
|
+
# The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
|
|
16
|
+
# privatised so Host App code cannot fabricate a Handle from a bare
|
|
17
|
+
# integer; legitimate Handle instances enter Host App code only as
|
|
18
|
+
# fields on raised error objects. The Host Gem itself constructs
|
|
19
|
+
# Handles through {.restore}, which exists at exactly two call
|
|
20
|
+
# sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
|
|
21
|
+
# +Kobako::Codec::Utils.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
22
|
+
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
23
|
+
# of any public surface.
|
|
24
|
+
#
|
|
25
|
+
# The mruby counterpart +Kobako::Handle+ lives inside the Wasm guest
|
|
26
|
+
# under the same canonical name and shares neither code nor instances
|
|
27
|
+
# with this host-side class.
|
|
28
|
+
class Handle < Data.define(:id)
|
|
29
|
+
# Inclusive lower bound on the wire Handle ID. ID 0 is reserved as
|
|
30
|
+
# the invalid sentinel and is never allocated.
|
|
31
|
+
MIN_ID = 1
|
|
32
|
+
# Inclusive upper bound on the wire Handle ID. The cap matches the
|
|
33
|
+
# u32 signed-positive range so Handle IDs fit in a signed integer
|
|
34
|
+
# on either side of the wire without re-encoding.
|
|
35
|
+
MAX_ID = 0x7fff_ffff
|
|
36
|
+
|
|
37
|
+
def initialize(id:)
|
|
38
|
+
raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
|
|
39
|
+
raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
|
|
40
|
+
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :new
|
|
45
|
+
|
|
46
|
+
# Host Gem–internal factory. Allocates the Data instance through
|
|
47
|
+
# +Class#allocate+ and dispatches +#initialize+ explicitly so the
|
|
48
|
+
# invariant checks still run, while keeping the public +.new+
|
|
49
|
+
# privatised against Host App callers.
|
|
50
|
+
#
|
|
51
|
+
# Two collaborators call this: the codec when an ext 0x01 frame is
|
|
52
|
+
# decoded off the wire, and the allocator paths when a host-side
|
|
53
|
+
# Ruby object is registered into the Sandbox's +Catalog::Handles+. Both
|
|
54
|
+
# paths live inside +lib/kobako/+ and treat this method as a
|
|
55
|
+
# package-private constructor.
|
|
56
|
+
def self.restore(id)
|
|
57
|
+
allocate.tap { |handle| handle.send(:initialize, id: id) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
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
|