kobako 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -1
- data/README.md +205 -59
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/wasm/cache.rs +15 -7
- data/ext/kobako/src/wasm/dispatch.rs +88 -36
- data/ext/kobako/src/wasm/host_state.rs +298 -55
- data/ext/kobako/src/wasm/instance.rs +477 -160
- data/ext/kobako/src/wasm.rs +20 -5
- data/lib/kobako/capture.rb +12 -10
- data/lib/kobako/codec/decoder.rb +3 -4
- data/lib/kobako/codec/encoder.rb +1 -1
- data/lib/kobako/codec/error.rb +3 -2
- data/lib/kobako/codec/factory.rb +24 -17
- data/lib/kobako/codec/utils.rb +105 -12
- data/lib/kobako/codec.rb +2 -1
- data/lib/kobako/errors.rb +22 -10
- data/lib/kobako/handle.rb +62 -0
- data/lib/kobako/handle_table.rb +119 -0
- data/lib/kobako/invocation.rb +143 -0
- data/lib/kobako/outcome/panic.rb +2 -2
- data/lib/kobako/outcome.rb +61 -24
- data/lib/kobako/rpc/dispatcher.rb +30 -28
- data/lib/kobako/rpc/envelope.rb +10 -10
- data/lib/kobako/rpc/fault.rb +4 -3
- data/lib/kobako/rpc/namespace.rb +3 -3
- data/lib/kobako/rpc/server.rb +23 -33
- data/lib/kobako/rpc/wire_error.rb +23 -0
- data/lib/kobako/sandbox.rb +211 -136
- data/lib/kobako/sandbox_options.rb +73 -0
- data/lib/kobako/snippet/binary.rb +30 -0
- data/lib/kobako/snippet/source.rb +28 -0
- data/lib/kobako/snippet/table.rb +174 -0
- data/lib/kobako/snippet.rb +20 -0
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/codec/factory.rbs +1 -1
- data/sig/kobako/codec/utils.rbs +10 -0
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/handle.rbs +19 -0
- data/sig/kobako/handle_table.rbs +23 -0
- data/sig/kobako/invocation.rbs +25 -0
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/rpc/dispatcher.rbs +7 -7
- data/sig/kobako/rpc/envelope.rbs +3 -3
- data/sig/kobako/rpc/server.rbs +1 -7
- data/sig/kobako/rpc/wire_error.rbs +6 -0
- data/sig/kobako/sandbox.rbs +22 -17
- data/sig/kobako/sandbox_options.rbs +32 -0
- data/sig/kobako/snippet/binary.rbs +12 -0
- data/sig/kobako/snippet/source.rbs +13 -0
- data/sig/kobako/snippet/table.rbs +36 -0
- data/sig/kobako/snippet.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- data/sig/kobako/wasm.rbs +5 -1
- metadata +21 -5
- data/lib/kobako/rpc/handle.rb +0 -38
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/handle_table.rbs +0 -25
data/ext/kobako/src/wasm.rs
CHANGED
|
@@ -32,10 +32,23 @@ mod host_state;
|
|
|
32
32
|
mod instance;
|
|
33
33
|
|
|
34
34
|
use magnus::value::Lazy;
|
|
35
|
-
use magnus::{
|
|
35
|
+
use magnus::{
|
|
36
|
+
function, method, prelude::*, Error as MagnusError, ExceptionClass, RModule, RString, Ruby,
|
|
37
|
+
};
|
|
36
38
|
|
|
37
39
|
use instance::Instance;
|
|
38
40
|
|
|
41
|
+
/// Copy the bytes of +s+ into a fresh +Vec<u8>+. Single safe entry to
|
|
42
|
+
/// what would otherwise be an inline +unsafe { rstring.as_slice() }
|
|
43
|
+
/// .to_vec()+ duplicated at every host-↔-guest boundary. The borrow
|
|
44
|
+
/// does not outlive this call, so no Ruby allocation can move the
|
|
45
|
+
/// underlying RString between the borrow and the copy — the safety
|
|
46
|
+
/// invariant the inline form relied on is established once here.
|
|
47
|
+
pub(crate) fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
48
|
+
// SAFETY: see item doc.
|
|
49
|
+
unsafe { s.as_slice() }.to_vec()
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
// ---------------------------------------------------------------------------
|
|
40
53
|
// Error classes (lazy-resolved from Ruby once Kobako::Wasm is defined).
|
|
41
54
|
// ---------------------------------------------------------------------------
|
|
@@ -69,14 +82,14 @@ pub(crate) fn wasm_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
/// Construct a `Kobako::Wasm::TimeoutError` magnus error. Surfaces the
|
|
72
|
-
///
|
|
85
|
+
/// docs/behavior.md E-19 wall-clock cap path so the Sandbox layer can rewrap it
|
|
73
86
|
/// as `Kobako::TimeoutError`.
|
|
74
87
|
pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
75
88
|
MagnusError::new(ruby.get_inner(&WASM_TIMEOUT_ERROR), msg.into())
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
/// Construct a `Kobako::Wasm::MemoryLimitError` magnus error. Surfaces
|
|
79
|
-
/// the
|
|
92
|
+
/// the docs/behavior.md E-20 linear-memory cap path so the Sandbox layer can
|
|
80
93
|
/// rewrap it as `Kobako::MemoryLimitError`.
|
|
81
94
|
pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
82
95
|
MagnusError::new(ruby.get_inner(&WASM_MEMORY_LIMIT_ERROR), msg.into())
|
|
@@ -92,7 +105,7 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
92
105
|
// Error hierarchy. ModuleNotBuiltError is the headline error for the
|
|
93
106
|
// common pre-build state where `data/kobako.wasm` has not yet been
|
|
94
107
|
// produced (e.g. fresh clone before `rake compile`). TimeoutError and
|
|
95
|
-
// MemoryLimitError carry the
|
|
108
|
+
// MemoryLimitError carry the docs/behavior.md B-01 per-run cap paths up to the
|
|
96
109
|
// Sandbox layer.
|
|
97
110
|
let base_err = wasm.define_error("Error", ruby.exception_standard_error())?;
|
|
98
111
|
wasm.define_error("ModuleNotBuiltError", base_err)?;
|
|
@@ -102,10 +115,12 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
102
115
|
let instance = wasm.define_class("Instance", ruby.class_object())?;
|
|
103
116
|
instance.define_singleton_method("from_path", function!(Instance::from_path, 5))?;
|
|
104
117
|
instance.define_method("server=", method!(Instance::set_server, 1))?;
|
|
105
|
-
instance.define_method("
|
|
118
|
+
instance.define_method("eval", method!(Instance::eval, 3))?;
|
|
119
|
+
instance.define_method("run", method!(Instance::run, 3))?;
|
|
106
120
|
instance.define_method("stdout", method!(Instance::stdout, 0))?;
|
|
107
121
|
instance.define_method("stderr", method!(Instance::stderr, 0))?;
|
|
108
122
|
instance.define_method("outcome!", method!(Instance::outcome, 0))?;
|
|
123
|
+
instance.define_method("usage", method!(Instance::usage, 0))?;
|
|
109
124
|
|
|
110
125
|
Ok(())
|
|
111
126
|
}
|
data/lib/kobako/capture.rb
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
# Host-side captured prefix of guest stdout / stderr produced during a
|
|
5
|
-
# single +Kobako::Sandbox
|
|
6
|
-
# pipe sets when the guest wrote past the configured per-channel
|
|
7
|
-
# ({
|
|
5
|
+
# single +Kobako::Sandbox+ invocation, paired with the truncation flag
|
|
6
|
+
# the WASI pipe sets when the guest wrote past the configured per-channel
|
|
7
|
+
# cap ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
|
|
8
8
|
#
|
|
9
9
|
# Immutable value object: the captured bytes and the truncation flag
|
|
10
10
|
# always travel together and the instance is frozen on construction.
|
|
11
11
|
# Construct via +Capture.from_ext+ for ext-provided binary bytes (handles
|
|
12
|
-
# UTF-8 / ASCII-8BIT fallback) or reach +Capture::EMPTY+ for the pre-
|
|
13
|
-
# sentinel that +Sandbox+ uses before any
|
|
12
|
+
# UTF-8 / ASCII-8BIT fallback) or reach +Capture::EMPTY+ for the pre-
|
|
13
|
+
# invocation sentinel that +Sandbox+ uses before any invocation has
|
|
14
|
+
# executed.
|
|
14
15
|
class Capture
|
|
15
16
|
attr_reader :bytes
|
|
16
17
|
|
|
@@ -24,8 +25,8 @@ module Kobako
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# Returns +true+ iff the underlying capture channel exceeded its
|
|
27
|
-
# configured cap during the originating +Sandbox
|
|
28
|
-
# ({
|
|
28
|
+
# configured cap during the originating +Sandbox+ invocation
|
|
29
|
+
# ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
|
|
29
30
|
def truncated? = @truncated
|
|
30
31
|
|
|
31
32
|
# Construct a Capture from ext-provided binary bytes. Coerces +bytes+
|
|
@@ -38,9 +39,10 @@ module Kobako
|
|
|
38
39
|
new(bytes: copy, truncated: truncated)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
# Pre-
|
|
42
|
-
# bytes and +truncated? == false+; reused by every fresh
|
|
43
|
-
# by +Sandbox
|
|
42
|
+
# Pre-invocation sentinel ({docs/behavior.md B-05}[link:../../docs/behavior.md]).
|
|
43
|
+
# Empty UTF-8 bytes and +truncated? == false+; reused by every fresh
|
|
44
|
+
# +Sandbox+ and by +Sandbox+ between invocations to denote "no capture
|
|
45
|
+
# yet".
|
|
44
46
|
EMPTY = new(bytes: "", truncated: false)
|
|
45
47
|
end
|
|
46
48
|
end
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -9,7 +9,7 @@ require_relative "utils"
|
|
|
9
9
|
module Kobako
|
|
10
10
|
module Codec
|
|
11
11
|
# Module-level entry point for the host side of the kobako wire
|
|
12
|
-
# (
|
|
12
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
13
13
|
#
|
|
14
14
|
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
15
15
|
# ({Truncated}, {InvalidType}, {InvalidEncoding}, {UnsupportedType}) so
|
|
@@ -38,11 +38,10 @@ module Kobako
|
|
|
38
38
|
# buffer edge.
|
|
39
39
|
rescue ::MessagePack::UnpackError, ::EOFError => e
|
|
40
40
|
raise Truncated, e.message
|
|
41
|
-
rescue ::EncodingError => e
|
|
42
|
-
raise InvalidEncoding, e.message
|
|
43
41
|
end
|
|
44
42
|
|
|
45
|
-
# SPEC pins +str+ family payloads to UTF-8
|
|
43
|
+
# SPEC pins +str+ family payloads to UTF-8
|
|
44
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § str/bin
|
|
46
45
|
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
47
46
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
48
47
|
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -8,7 +8,7 @@ require_relative "factory"
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
10
10
|
# Module-level entry point for the host side of the kobako wire
|
|
11
|
-
# (
|
|
11
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
12
12
|
#
|
|
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
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -4,8 +4,9 @@ module Kobako
|
|
|
4
4
|
module Codec
|
|
5
5
|
# Base class for all wire-codec faults raised by the pure-Ruby host codec.
|
|
6
6
|
#
|
|
7
|
-
# The wire codec implements the binary contract pinned in
|
|
8
|
-
#
|
|
7
|
+
# The wire codec implements the binary contract pinned in
|
|
8
|
+
# {docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping.
|
|
9
|
+
# Every wire violation surfaces as a
|
|
9
10
|
# subclass of {Error} so callers can pattern-match on the specific
|
|
10
11
|
# fault while still rescuing all codec faults via this base class.
|
|
11
12
|
#
|
data/lib/kobako/codec/factory.rb
CHANGED
|
@@ -6,13 +6,14 @@ require "msgpack"
|
|
|
6
6
|
|
|
7
7
|
require_relative "error"
|
|
8
8
|
require_relative "utils"
|
|
9
|
-
require_relative "../
|
|
9
|
+
require_relative "../handle"
|
|
10
10
|
require_relative "../rpc/fault"
|
|
11
11
|
|
|
12
12
|
module Kobako
|
|
13
13
|
module Codec
|
|
14
14
|
# Cached +MessagePack::Factory+ that owns the kobako wire ext-type
|
|
15
|
-
# registration (
|
|
15
|
+
# registration ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
16
|
+
# § Ext Types).
|
|
16
17
|
#
|
|
17
18
|
# The factory is the single place in the host gem that touches the
|
|
18
19
|
# msgpack API — both {Encoder} and {Decoder} delegate through it, so
|
|
@@ -31,16 +32,19 @@ module Kobako
|
|
|
31
32
|
extend SingleForwardable
|
|
32
33
|
|
|
33
34
|
# MessagePack ext type code reserved for Symbol
|
|
34
|
-
# (
|
|
35
|
-
# mirrors +codec::EXT_SYMBOL+ on the
|
|
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.
|
|
36
38
|
EXT_SYMBOL = 0x00
|
|
37
39
|
# MessagePack ext type code reserved for Capability Handle
|
|
38
|
-
# (
|
|
39
|
-
# mirrors +codec::EXT_HANDLE+ on the
|
|
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.
|
|
40
43
|
EXT_HANDLE = 0x01
|
|
41
44
|
# MessagePack ext type code reserved for Exception envelope
|
|
42
|
-
# (
|
|
43
|
-
# mirrors +codec::EXT_ERRENV+ on the
|
|
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.
|
|
44
48
|
EXT_ERRENV = 0x02
|
|
45
49
|
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
46
50
|
|
|
@@ -85,16 +89,18 @@ module Kobako
|
|
|
85
89
|
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
86
90
|
# would otherwise apply. The re-tag step lives here because the
|
|
87
91
|
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
88
|
-
# itself is shared with {Decoder} via {Utils.assert_utf8!}.
|
|
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.
|
|
89
95
|
def unpack_symbol(payload)
|
|
90
96
|
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
91
|
-
Utils.assert_utf8!(name, "
|
|
97
|
+
Utils.assert_utf8!(name, "Symbol payload")
|
|
92
98
|
name.to_sym
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
def register_handle
|
|
96
102
|
@factory.register_type(
|
|
97
|
-
EXT_HANDLE,
|
|
103
|
+
EXT_HANDLE, Kobako::Handle,
|
|
98
104
|
packer: ->(handle) { [handle.id].pack("N") },
|
|
99
105
|
unpacker: ->(payload) { unpack_handle(payload) }
|
|
100
106
|
)
|
|
@@ -108,17 +114,18 @@ module Kobako
|
|
|
108
114
|
)
|
|
109
115
|
end
|
|
110
116
|
|
|
111
|
-
# Peel off the fixext-4 frame, hand the bytes to
|
|
112
|
-
#
|
|
113
|
-
#
|
|
117
|
+
# Peel off the fixext-4 frame, hand the bytes to the
|
|
118
|
+
# Host-Gem-internal +Kobako::Handle.from_wire+ factory, and
|
|
119
|
+
# translate the +ArgumentError+ raised by Handle's invariants
|
|
120
|
+
# into a wire-layer +InvalidType+ via {Codec::Utils.wire_boundary}.
|
|
114
121
|
# The Value Object owns the id-range contract; this method only
|
|
115
122
|
# owns the frame shape.
|
|
116
123
|
def unpack_handle(payload)
|
|
117
124
|
bytes = payload.b
|
|
118
|
-
raise InvalidType, "
|
|
125
|
+
raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
119
126
|
|
|
120
127
|
id = bytes.unpack1("N") # : Integer
|
|
121
|
-
Codec::Utils.wire_boundary {
|
|
128
|
+
Codec::Utils.wire_boundary { Kobako::Handle.from_wire(id) }
|
|
122
129
|
end
|
|
123
130
|
|
|
124
131
|
# Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
|
|
@@ -144,7 +151,7 @@ module Kobako
|
|
|
144
151
|
# and re-opens the Decoder's special case for Fault (removed in M5).
|
|
145
152
|
def unpack_fault(payload)
|
|
146
153
|
map = Decoder.decode(payload)
|
|
147
|
-
raise InvalidType, "
|
|
154
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
148
155
|
|
|
149
156
|
Codec::Utils.wire_boundary do
|
|
150
157
|
RPC::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "error"
|
|
4
|
+
require_relative "../handle"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
module Codec
|
|
7
8
|
# Wire-codec helpers shared by the host-side encoders and decoders.
|
|
8
|
-
#
|
|
9
|
-
# (SPEC.md → Wire Codec → str/bin Encoding Rules and Ext Types →
|
|
10
|
-
# ext 0x00). Two call sites lean on this:
|
|
9
|
+
# Three concerns live here today:
|
|
11
10
|
#
|
|
12
|
-
# -
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# - UTF-8 assertion at the wire 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
|
+
# - Wire-boundary +ArgumentError+ translation
|
|
17
|
+
# ({wire_boundary}) so the public taxonomy stays
|
|
18
|
+
# {Kobako::Codec::Error}.
|
|
19
|
+
# - Wire-representability predicate ({wire_representable?}) and
|
|
20
|
+
# the symmetric host→guest +#run+ argument walk
|
|
21
|
+
# ({deep_wrap}) used by +Kobako::Invocation#encode+ to route
|
|
22
|
+
# non-wire-representable leaves through the Sandbox's
|
|
23
|
+
# +Kobako::HandleTable+
|
|
24
|
+
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
|
|
16
25
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# against +string+'s current encoding tag.
|
|
26
|
+
# All helpers are pure — they only inspect inputs, never mutate
|
|
27
|
+
# them — except {deep_wrap}, whose only side effect is allocating
|
|
28
|
+
# new Handle ids into the supplied table.
|
|
21
29
|
module Utils
|
|
22
30
|
module_function
|
|
23
31
|
|
|
24
32
|
# Raise {InvalidEncoding} unless +string+'s bytes are valid under
|
|
25
33
|
# its current encoding tag. +label+ is the caller-supplied prefix
|
|
26
|
-
# for the error message (e.g. +"str payload"+, +"
|
|
34
|
+
# for the error message (e.g. +"str payload"+, +"Symbol payload"+).
|
|
27
35
|
def assert_utf8!(string, label)
|
|
28
36
|
return if string.valid_encoding?
|
|
29
37
|
|
|
@@ -50,6 +58,91 @@ module Kobako
|
|
|
50
58
|
rescue ::ArgumentError => e
|
|
51
59
|
raise InvalidType, e.message
|
|
52
60
|
end
|
|
61
|
+
|
|
62
|
+
# Inclusive Integer range the msgpack gem encodes without raising
|
|
63
|
+
# +RangeError+ at encode time — signed +int 64+ minimum through
|
|
64
|
+
# unsigned +uint 64+ maximum
|
|
65
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
66
|
+
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
67
|
+
# Anchored as a +Range+ so {primitive_wire_type?} stays a single
|
|
68
|
+
# dispatch line. This is the codec's wire-encode domain — not to
|
|
69
|
+
# be confused with the Handle id range, which lives on
|
|
70
|
+
# +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
|
|
71
|
+
# represents a different concept entirely.
|
|
72
|
+
MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
|
|
73
|
+
|
|
74
|
+
# Wire-type predicate
|
|
75
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
76
|
+
# Mapping). Returns +true+ when +value+ belongs to the closed
|
|
77
|
+
# 12-entry wire set — +nil+, +TrueClass+, +FalseClass+, +Integer+
|
|
78
|
+
# (in the +i64..u64+ value domain), +Float+, +String+, +Symbol+,
|
|
79
|
+
# +Kobako::Handle+, +Array+ whose every element is itself
|
|
80
|
+
# wire-representable, or +Hash+ whose every key and value are
|
|
81
|
+
# wire-representable. Integers outside the codec's signed-64 /
|
|
82
|
+
# unsigned-64 union are rejected so the predicate agrees with the
|
|
83
|
+
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
84
|
+
# already surfaces as {UnsupportedType}.
|
|
85
|
+
def wire_representable?(value)
|
|
86
|
+
primitive_wire_type?(value) || container_wire_representable?(value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
90
|
+
# leaf that fails {wire_representable?} with a +Kobako::Handle+
|
|
91
|
+
# allocated from +handle_table+
|
|
92
|
+
# ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
|
|
93
|
+
# walk only descends through wire-representable container shapes
|
|
94
|
+
# (Array, Hash) one structural level at a time; a non-
|
|
95
|
+
# wire-representable leaf is wrapped as-is without inspecting its
|
|
96
|
+
# internal structure. An existing +Kobako::Handle+ is wire-
|
|
97
|
+
# representable and passes through unchanged — auto-wrap never
|
|
98
|
+
# re-wraps a Handle.
|
|
99
|
+
#
|
|
100
|
+
# +value+ may be any Ruby value; +handle_table+ must respond to
|
|
101
|
+
# +#alloc(object) -> Kobako::Handle+ (a host-side
|
|
102
|
+
# +Kobako::HandleTable+). Returns a structurally equivalent value
|
|
103
|
+
# whose leaves are either wire-representable or +Kobako::Handle+
|
|
104
|
+
# tokens.
|
|
105
|
+
#
|
|
106
|
+
# The block bodies spell +Utils.deep_wrap+ explicitly rather than
|
|
107
|
+
# the unqualified +deep_wrap+ because +module_function+ makes the
|
|
108
|
+
# instance copy of these helpers private; an implicit receiver
|
|
109
|
+
# inside a block would resolve against the enclosing +self+
|
|
110
|
+
# (still +Utils+ at definition time, but the qualified form keeps
|
|
111
|
+
# the dispatch readable when the recursive call sits inside a
|
|
112
|
+
# Proc captured from elsewhere).
|
|
113
|
+
def deep_wrap(value, handle_table)
|
|
114
|
+
case value
|
|
115
|
+
when ::Array then value.map { |element| Utils.deep_wrap(element, handle_table) }
|
|
116
|
+
when ::Hash then value.transform_values { |val| Utils.deep_wrap(val, handle_table) }
|
|
117
|
+
else
|
|
118
|
+
wire_representable?(value) ? value : handle_table.alloc(value)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Predicate split out of {wire_representable?} for cyclomatic
|
|
123
|
+
# budget — the closed-set non-container branch. Returns +true+ for
|
|
124
|
+
# the wire scalar leaves and an existing Handle. Not part of the
|
|
125
|
+
# public surface; reach for {wire_representable?} instead.
|
|
126
|
+
def primitive_wire_type?(value)
|
|
127
|
+
case value
|
|
128
|
+
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
129
|
+
when ::Integer then MSGPACK_INT_RANGE.cover?(value)
|
|
130
|
+
else false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Predicate split out of {wire_representable?} for cyclomatic
|
|
135
|
+
# budget — the container branch. Recurses into Array elements and
|
|
136
|
+
# Hash key+value pairs through the public {wire_representable?}.
|
|
137
|
+
# Not part of the public surface; reach for {wire_representable?}
|
|
138
|
+
# instead.
|
|
139
|
+
def container_wire_representable?(value)
|
|
140
|
+
case value
|
|
141
|
+
when ::Array then value.all? { |element| Utils.wire_representable?(element) }
|
|
142
|
+
when ::Hash then value.all? { |key, val| Utils.wire_representable?(key) && Utils.wire_representable?(val) }
|
|
143
|
+
else false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
53
146
|
end
|
|
54
147
|
end
|
|
55
148
|
end
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -4,7 +4,8 @@ require_relative "codec/error"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
# Host-side MessagePack codec for the kobako wire contract — the
|
|
7
|
-
# byte-level layer (
|
|
7
|
+
# byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
|
|
8
|
+
# Two consumers sit on top:
|
|
8
9
|
# +Kobako::RPC+ pins the RPC framing (Request / Response)
|
|
9
10
|
# and +Kobako::Outcome+ owns the per-+#run+ outcome envelope (Result
|
|
10
11
|
# body / Panic map).
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# Top-level Kobako namespace.
|
|
4
4
|
module Kobako
|
|
5
|
-
# Three-class error taxonomy (
|
|
5
|
+
# Three-class error taxonomy (docs/behavior.md § Error Scenarios).
|
|
6
6
|
#
|
|
7
|
-
# Every `Kobako::Sandbox
|
|
7
|
+
# Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
|
|
8
8
|
# exactly one of these three classes. Attribution is decided after the
|
|
9
|
-
# guest binary returns control to the host (
|
|
10
|
-
# then "Step 2 — Outcome envelope tag").
|
|
9
|
+
# guest binary returns control to the host (docs/behavior.md
|
|
10
|
+
# "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
|
|
11
11
|
#
|
|
12
12
|
# Three top-level branches:
|
|
13
13
|
#
|
|
@@ -20,7 +20,7 @@ module Kobako
|
|
|
20
20
|
# * {ServiceError} — service / capability layer (a Service RPC that
|
|
21
21
|
# failed and was not rescued inside the script).
|
|
22
22
|
#
|
|
23
|
-
# Subclasses pinned by
|
|
23
|
+
# Subclasses pinned by docs/behavior.md Error Classes:
|
|
24
24
|
#
|
|
25
25
|
# * {HandleTableExhausted} < {SandboxError} — id cap hit (B-21).
|
|
26
26
|
# * {ServiceError::Disconnected} < {ServiceError} — `:disconnected`
|
|
@@ -35,7 +35,7 @@ module Kobako
|
|
|
35
35
|
# violation that signals a corrupted guest execution environment
|
|
36
36
|
# (zero-length OUTCOME_BUFFER, unknown outcome tag — SPEC E-02 / E-03).
|
|
37
37
|
#
|
|
38
|
-
# Two named subclasses cover the configured per-
|
|
38
|
+
# Two named subclasses cover the configured per-invocation caps from B-01:
|
|
39
39
|
#
|
|
40
40
|
# * {TimeoutError} — wall-clock +timeout+ exceeded (E-19).
|
|
41
41
|
# * {MemoryLimitError} — guest +memory.grow+ would exceed
|
|
@@ -47,13 +47,13 @@ module Kobako
|
|
|
47
47
|
# first.
|
|
48
48
|
class TrapError < Error; end
|
|
49
49
|
|
|
50
|
-
# Wall-clock timeout cap exhausted. {
|
|
50
|
+
# Wall-clock timeout cap exhausted. {docs/behavior.md E-19}[link:../../docs/behavior.md]:
|
|
51
51
|
# the absolute deadline +entry_time + timeout+ passed and the next guest
|
|
52
52
|
# wasm safepoint trapped. The Sandbox is unrecoverable after this point;
|
|
53
53
|
# discard and recreate before another execution.
|
|
54
54
|
class TimeoutError < TrapError; end
|
|
55
55
|
|
|
56
|
-
# Linear-memory cap exhausted. {
|
|
56
|
+
# Linear-memory cap exhausted. {docs/behavior.md E-20}[link:../../docs/behavior.md]:
|
|
57
57
|
# a guest +memory.grow+ would have pushed linear memory past the
|
|
58
58
|
# configured +memory_limit+. The Sandbox is unrecoverable after this
|
|
59
59
|
# point; discard and recreate before another execution.
|
|
@@ -88,7 +88,7 @@ module Kobako
|
|
|
88
88
|
@details = details
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
#
|
|
91
|
+
# docs/behavior.md Error Classes: ServiceError::Disconnected is raised
|
|
92
92
|
# when the RPC target Handle resolves to the `:disconnected` sentinel
|
|
93
93
|
# in the HandleTable (ABA protection rule — id exists but entry was
|
|
94
94
|
# invalidated). E-14.
|
|
@@ -103,9 +103,21 @@ module Kobako
|
|
|
103
103
|
# directly), it surfaces as a SandboxError.
|
|
104
104
|
class HandleTableError < SandboxError; end
|
|
105
105
|
|
|
106
|
-
#
|
|
106
|
+
# docs/behavior.md Error Classes: HandleTableExhausted is the canonical
|
|
107
107
|
# SandboxError subclass for the id-cap-hit path (B-21). Inherits from
|
|
108
108
|
# HandleTableError so a single `rescue Kobako::HandleTableError` covers
|
|
109
109
|
# both lookup-failure and cap-exhaustion paths.
|
|
110
110
|
class HandleTableExhausted < HandleTableError; end
|
|
111
|
+
|
|
112
|
+
# docs/behavior.md Error Classes: BytecodeError is the SandboxError
|
|
113
|
+
# subclass raised when a `#preload(binary:)` snippet fails structural
|
|
114
|
+
# validation during the first invocation's snippet replay against a
|
|
115
|
+
# fresh `mrb_state` (E-37 RITE version mismatch, E-38 corrupt body).
|
|
116
|
+
# Bytecode that loads cleanly and then raises at top level is E-36
|
|
117
|
+
# and surfaces as plain `SandboxError` with the natural mruby class
|
|
118
|
+
# preserved. Inherits from SandboxError so a single
|
|
119
|
+
# `rescue Kobako::SandboxError` covers both source and bytecode
|
|
120
|
+
# snippet failures while callers wanting bytecode-specific handling
|
|
121
|
+
# can `rescue Kobako::BytecodeError` directly.
|
|
122
|
+
class BytecodeError < SandboxError; end
|
|
111
123
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
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 {.from_wire}, which exists at exactly two call
|
|
20
|
+
# sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
|
|
21
|
+
# +Kobako::Codec::Utils.deep_wrap+ / +Kobako::RPC::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
|
+
# steep:ignore:start
|
|
38
|
+
def initialize(id:)
|
|
39
|
+
raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
|
|
40
|
+
raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
|
|
41
|
+
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
# steep:ignore:end
|
|
45
|
+
|
|
46
|
+
private_class_method :new
|
|
47
|
+
|
|
48
|
+
# Host Gem–internal factory. Allocates the Data instance through
|
|
49
|
+
# +Class#allocate+ and dispatches +#initialize+ explicitly so the
|
|
50
|
+
# invariant checks still run, while keeping the public +.new+
|
|
51
|
+
# privatised against Host App callers.
|
|
52
|
+
#
|
|
53
|
+
# Two collaborators call this: the codec when an ext 0x01 frame is
|
|
54
|
+
# decoded off the wire, and the allocator paths when a host-side
|
|
55
|
+
# Ruby object is registered into the Sandbox's +HandleTable+. Both
|
|
56
|
+
# paths live inside +lib/kobako/+ and treat this method as a
|
|
57
|
+
# package-private constructor.
|
|
58
|
+
def self.from_wire(id)
|
|
59
|
+
allocate.tap { |handle| handle.send(:initialize, id: id) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|