kobako 0.2.1 → 0.3.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 +123 -57
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/wasm/cache.rs +3 -3
- data/ext/kobako/src/wasm/dispatch.rs +87 -36
- data/ext/kobako/src/wasm/host_state.rs +189 -52
- data/ext/kobako/src/wasm/instance.rs +367 -152
- data/ext/kobako/src/wasm.rs +19 -5
- data/lib/kobako/capture.rb +12 -10
- data/lib/kobako/codec/decoder.rb +3 -2
- data/lib/kobako/codec/encoder.rb +1 -1
- data/lib/kobako/codec/error.rb +3 -2
- data/lib/kobako/codec/factory.rb +11 -7
- data/lib/kobako/codec/utils.rb +3 -2
- data/lib/kobako/codec.rb +2 -1
- data/lib/kobako/errors.rb +22 -10
- data/lib/kobako/invocation.rb +112 -0
- data/lib/kobako/outcome/panic.rb +2 -2
- data/lib/kobako/outcome.rb +20 -13
- data/lib/kobako/rpc/dispatcher.rb +9 -9
- data/lib/kobako/rpc/envelope.rb +3 -3
- data/lib/kobako/rpc/fault.rb +3 -2
- data/lib/kobako/rpc/handle.rb +3 -2
- data/lib/kobako/rpc/handle_table.rb +7 -7
- data/lib/kobako/rpc/namespace.rb +3 -3
- data/lib/kobako/rpc/server.rb +14 -12
- data/lib/kobako/sandbox.rb +147 -125
- 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/version.rb +1 -1
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/invocation.rbs +23 -0
- data/sig/kobako/sandbox.rbs +17 -18
- 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/wasm.rbs +3 -1
- metadata +13 -1
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,7 +115,8 @@ 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))?;
|
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
|
|
@@ -42,7 +42,8 @@ module Kobako
|
|
|
42
42
|
raise InvalidEncoding, e.message
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# SPEC pins +str+ family payloads to UTF-8
|
|
45
|
+
# SPEC pins +str+ family payloads to UTF-8
|
|
46
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § str/bin
|
|
46
47
|
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
47
48
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
48
49
|
# 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
|
@@ -12,7 +12,8 @@ require_relative "../rpc/fault"
|
|
|
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
|
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -6,8 +6,9 @@ module Kobako
|
|
|
6
6
|
module Codec
|
|
7
7
|
# Wire-codec helpers shared by the host-side encoders and decoders.
|
|
8
8
|
# The single concern today is UTF-8 assertion at the wire boundary
|
|
9
|
-
# (
|
|
10
|
-
# ext 0x00). Two call sites lean on
|
|
9
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § str/bin
|
|
10
|
+
# Encoding Rules and § Ext Types → ext 0x00). Two call sites lean on
|
|
11
|
+
# this:
|
|
11
12
|
#
|
|
12
13
|
# - {Decoder} validates +str+ family payloads as it walks the
|
|
13
14
|
# decoded value tree.
|
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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rpc/handle"
|
|
4
|
+
require_relative "codec"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# Host-side value object for a single +Sandbox#run+ invocation
|
|
8
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md];
|
|
9
|
+
# {docs/behavior.md B-31}[link:../../docs/behavior.md]).
|
|
10
|
+
#
|
|
11
|
+
# An Invocation captures the host-layer concept of "a single +#run+
|
|
12
|
+
# call": the entrypoint constant name plus its positional and keyword
|
|
13
|
+
# arguments. Host pre-flight (E-24 / E-25 / E-29 / E-30) is enforced at
|
|
14
|
+
# construction so the Value Object is the single source of truth —
|
|
15
|
+
# anything that passes +Invocation.new+ is safe to encode and ship to
|
|
16
|
+
# the guest.
|
|
17
|
+
#
|
|
18
|
+
# Invocation sits at top level, not under +Kobako::RPC+: RPC in SPEC is
|
|
19
|
+
# the guest→host capability channel (Server / Client / Request /
|
|
20
|
+
# Response / Handle); Invocation is the opposite direction (host→guest
|
|
21
|
+
# entrypoint dispatch) and structurally rejects Handles (E-29), so it
|
|
22
|
+
# has no relationship with the HandleTable. The +#encode+ output is the
|
|
23
|
+
# "Invocation envelope" that ships through the +__kobako_run+ command
|
|
24
|
+
# buffer.
|
|
25
|
+
#
|
|
26
|
+
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
27
|
+
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
28
|
+
class Invocation < Data.define(:entrypoint, :args, :kwargs)
|
|
29
|
+
# Ruby constant-name pattern enforced on the +entrypoint+ Symbol
|
|
30
|
+
# ({docs/behavior.md E-25}[link:../../docs/behavior.md]). Parallel to
|
|
31
|
+
# +Kobako::Snippet::Table::NAME_PATTERN+; the two constants name the
|
|
32
|
+
# same regex but cover distinct surfaces (snippet identity vs.
|
|
33
|
+
# entrypoint resolution) so a future divergence stays local.
|
|
34
|
+
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
35
|
+
|
|
36
|
+
# steep:ignore:start
|
|
37
|
+
def initialize(entrypoint:, args: [], kwargs: {})
|
|
38
|
+
super(
|
|
39
|
+
entrypoint: normalize_entrypoint(entrypoint),
|
|
40
|
+
args: validate_args!(args),
|
|
41
|
+
kwargs: validate_kwargs!(kwargs)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
# steep:ignore:end
|
|
45
|
+
|
|
46
|
+
# Encode this Invocation to the msgpack bytes the guest's
|
|
47
|
+
# +__kobako_run+ entry point consumes as its command-buffer payload
|
|
48
|
+
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]).
|
|
49
|
+
# The Value Object's own invariants are the contract; this method
|
|
50
|
+
# does not re-check the shape. Layout: msgpack map with string keys
|
|
51
|
+
# +"entrypoint"+ (Symbol via ext 0x00), +"args"+ (Array), +"kwargs"+
|
|
52
|
+
# (Map with Symbol keys).
|
|
53
|
+
def encode
|
|
54
|
+
Codec::Encoder.encode(
|
|
55
|
+
"entrypoint" => entrypoint,
|
|
56
|
+
"args" => args,
|
|
57
|
+
"kwargs" => kwargs
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# steep:ignore:start
|
|
64
|
+
# E-24: target must be a Symbol or String (TypeError, not
|
|
65
|
+
# ArgumentError — the wrong-type case is a Host App programming
|
|
66
|
+
# error before the invocation reaches the guest). E-25: after
|
|
67
|
+
# +.to_s+ the value must match NAME_PATTERN (ArgumentError),
|
|
68
|
+
# rejecting +::+-segmented names and any non-constant form.
|
|
69
|
+
def normalize_entrypoint(target)
|
|
70
|
+
unless target.is_a?(Symbol) || target.is_a?(String)
|
|
71
|
+
raise TypeError, "Invocation entrypoint must be a Symbol or String, got #{target.class}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
target_str = target.to_s
|
|
75
|
+
unless NAME_PATTERN.match?(target_str)
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"Invocation entrypoint must match #{NAME_PATTERN.inspect} (got #{target.inspect})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
target_str.to_sym
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# E-29: +args+ must not contain a +Kobako::RPC::Handle+. Handles
|
|
84
|
+
# are per-invocation and cannot enter the next invocation through
|
|
85
|
+
# a control-plane channel; a guest that needs to call into a
|
|
86
|
+
# stateful host object must obtain a fresh Handle through a
|
|
87
|
+
# Service RPC inside the dispatched entrypoint.
|
|
88
|
+
def validate_args!(args)
|
|
89
|
+
raise ArgumentError, "Invocation args must be Array" unless args.is_a?(Array)
|
|
90
|
+
raise ArgumentError, "Invocation args must not contain a Kobako::RPC::Handle" if args.any?(Kobako::RPC::Handle)
|
|
91
|
+
|
|
92
|
+
args
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# E-30: +kwargs+ keys must be Symbols, mirroring the wire codec's
|
|
96
|
+
# Request kwargs rule. Validation lives here (not in the codec) so
|
|
97
|
+
# the Host App sees the host-side error message before any encode
|
|
98
|
+
# / decode boundary.
|
|
99
|
+
def validate_kwargs!(kwargs)
|
|
100
|
+
raise ArgumentError, "Invocation kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
bad_keys = kwargs.each_key.grep_v(Symbol)
|
|
103
|
+
unless bad_keys.empty?
|
|
104
|
+
raise ArgumentError,
|
|
105
|
+
"Invocation kwargs keys must be Symbols (got #{bad_keys.inspect})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
kwargs
|
|
109
|
+
end
|
|
110
|
+
# steep:ignore:end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/kobako/outcome/panic.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Outcome
|
|
5
|
-
#
|
|
6
|
-
# Envelope}[link:../../../
|
|
5
|
+
# Wire-contract Outcome Envelope → Panic envelope ({docs/wire-contract.md
|
|
6
|
+
# Outcome Envelope}[link:../../../docs/wire-contract.md]). Wire-shaped failure record
|
|
7
7
|
# carried in the OUTCOME_BUFFER when the guest run terminates with
|
|
8
8
|
# an uncaught top-level exception.
|
|
9
9
|
#
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -4,10 +4,10 @@ require_relative "outcome/panic"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
# Host-facing boundary for the OUTCOME_BUFFER produced by
|
|
7
|
-
# +
|
|
7
|
+
# +__kobako_eval+. Takes raw outcome bytes — a one-byte tag followed by
|
|
8
8
|
# the msgpack-encoded body — and maps them to either the unwrapped
|
|
9
9
|
# mruby return value or a raised three-layer
|
|
10
|
-
# ({
|
|
10
|
+
# ({docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]) exception.
|
|
11
11
|
#
|
|
12
12
|
# Self-contained: this module owns the wire framing (tag bytes,
|
|
13
13
|
# body decoding), and the +Panic+ wire record lives at
|
|
@@ -24,7 +24,7 @@ module Kobako
|
|
|
24
24
|
module Outcome
|
|
25
25
|
# First byte of the OUTCOME_BUFFER for the success branch — body is
|
|
26
26
|
# the bare msgpack encoding of the returned value
|
|
27
|
-
# ({
|
|
27
|
+
# ({docs/wire-contract.md Outcome Envelope}[link:../../docs/wire-contract.md]).
|
|
28
28
|
TYPE_VALUE = 0x01
|
|
29
29
|
# First byte of the OUTCOME_BUFFER for the failure branch — body is
|
|
30
30
|
# the msgpack Panic map.
|
|
@@ -45,8 +45,8 @@ module Kobako
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# TrapError for unknown or absent tag
|
|
48
|
-
# ({
|
|
49
|
-
# both walk the trap path).
|
|
48
|
+
# ({docs/wire-codec.md ABI Signatures}[link:../../docs/wire-codec.md]:
|
|
49
|
+
# len=0 and unknown-tag both walk the trap path).
|
|
50
50
|
def build_trap_error(tag)
|
|
51
51
|
return TrapError.new("guest exited without writing an outcome (len=0)") if tag.nil?
|
|
52
52
|
|
|
@@ -100,7 +100,7 @@ module Kobako
|
|
|
100
100
|
# Ruby exception. +origin == "service"+ → ServiceError (with the
|
|
101
101
|
# +::Disconnected+ subclass selected when the panic carries the
|
|
102
102
|
# disconnected sentinel —
|
|
103
|
-
# {
|
|
103
|
+
# {docs/behavior.md Error Classes}[link:../../docs/behavior.md]); everything else
|
|
104
104
|
# → SandboxError.
|
|
105
105
|
def build_panic_error(panic)
|
|
106
106
|
panic_target_class(panic).new(
|
|
@@ -112,14 +112,21 @@ module Kobako
|
|
|
112
112
|
)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
# {
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
115
|
+
# {docs/behavior.md Error Classes}[link:../../docs/behavior.md]: map
|
|
116
|
+
# the panic +class+ field to the matching Ruby exception subclass so
|
|
117
|
+
# callers can rescue specific failure paths. +origin="service"+ plus
|
|
118
|
+
# +class="Kobako::ServiceError::Disconnected"+ selects the
|
|
119
|
+
# +Disconnected+ subclass (E-14); +origin="sandbox"+ plus
|
|
120
|
+
# +class="Kobako::BytecodeError"+ selects the +BytecodeError+
|
|
121
|
+
# subclass (E-37 / E-38). Everything else falls back to the base
|
|
122
|
+
# class for the origin.
|
|
119
123
|
def panic_target_class(panic)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
case panic.origin
|
|
125
|
+
when Panic::ORIGIN_SERVICE
|
|
126
|
+
panic.klass == "Kobako::ServiceError::Disconnected" ? ServiceError::Disconnected : ServiceError
|
|
127
|
+
else
|
|
128
|
+
panic.klass == "Kobako::BytecodeError" ? BytecodeError : SandboxError
|
|
129
|
+
end
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
def build_wire_violation_error(message)
|
|
@@ -10,7 +10,7 @@ module Kobako
|
|
|
10
10
|
# The module is stateless — all mutable state is threaded through the
|
|
11
11
|
# +server+ argument so Dispatcher has no instance variables and no side
|
|
12
12
|
# effects beyond mutating the HandleTable via +alloc+ when a non-wire-
|
|
13
|
-
# representable return value must be wrapped ({
|
|
13
|
+
# representable return value must be wrapped ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
14
14
|
#
|
|
15
15
|
# Entry point:
|
|
16
16
|
#
|
|
@@ -22,12 +22,12 @@ module Kobako
|
|
|
22
22
|
# Internal sentinel raised when target resolution fails. Mapped to
|
|
23
23
|
# Response.error with type="undefined". Contained at the wire boundary —
|
|
24
24
|
# not part of the public Kobako error taxonomy
|
|
25
|
-
# ({
|
|
25
|
+
# ({docs/behavior.md E-xx}[link:../../../docs/behavior.md]).
|
|
26
26
|
class UndefinedTargetError < StandardError; end
|
|
27
27
|
|
|
28
28
|
# Internal sentinel raised when a Handle target resolves to the
|
|
29
29
|
# +:disconnected+ sentinel in the HandleTable (ABA protection,
|
|
30
|
-
# {
|
|
30
|
+
# {docs/behavior.md E-14}[link:../../../docs/behavior.md]). Mapped to Response.error with
|
|
31
31
|
# type="disconnected". Contained at the wire boundary.
|
|
32
32
|
class DisconnectedTargetError < StandardError; end
|
|
33
33
|
|
|
@@ -41,7 +41,7 @@ module Kobako
|
|
|
41
41
|
# during decode, lookup, or method invocation is reified as a
|
|
42
42
|
# Response.error envelope so the guest sees the failure as a normal RPC
|
|
43
43
|
# error rather than a wasm trap
|
|
44
|
-
# ({
|
|
44
|
+
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
45
45
|
def dispatch(request_bytes, server)
|
|
46
46
|
request = Kobako::RPC.decode_request(request_bytes)
|
|
47
47
|
handle_table = server.handle_table
|
|
@@ -57,7 +57,7 @@ module Kobako
|
|
|
57
57
|
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
58
58
|
# envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
|
|
59
59
|
# rescue. Returns a msgpack-encoded Response envelope (binary). Four
|
|
60
|
-
# error buckets ({
|
|
60
|
+
# error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
|
|
61
61
|
# +Kobako::Codec::Error+ → type="runtime" (wire decode failed);
|
|
62
62
|
# +DisconnectedTargetError+ → type="disconnected" (E-14);
|
|
63
63
|
# +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
|
|
@@ -86,7 +86,7 @@ module Kobako
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
# {
|
|
89
|
+
# {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An RPC::Handle arriving as a positional or keyword
|
|
90
90
|
# argument identifies a host-side object previously allocated by a prior
|
|
91
91
|
# RPC's Handle wrap (B-14). Resolve it back to the Ruby object before
|
|
92
92
|
# the dispatch reaches +public_send+. A Handle whose entry is the
|
|
@@ -139,10 +139,10 @@ module Kobako
|
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
142
|
-
# wire-representable per {
|
|
142
|
+
# wire-representable per {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
|
|
143
143
|
# mapping, the +UnsupportedType+ rescue routes it through the
|
|
144
144
|
# HandleTable via {#wrap_as_handle} and re-encodes with the Capability
|
|
145
|
-
# Handle in place ({
|
|
145
|
+
# Handle in place ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
|
|
146
146
|
# path encodes exactly once.
|
|
147
147
|
def encode_ok(value, server)
|
|
148
148
|
response = Kobako::RPC::Response.ok(value)
|
|
@@ -152,7 +152,7 @@ module Kobako
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Allocate +value+ in the Server's HandleTable and return a +Handle+
|
|
155
|
-
# that the wire codec can carry ({
|
|
155
|
+
# that the wire codec can carry ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
156
156
|
# Used as the fallback path of {#encode_ok} when +value+ has no wire
|
|
157
157
|
# representation.
|
|
158
158
|
def wrap_as_handle(value, server)
|
data/lib/kobako/rpc/envelope.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Kobako
|
|
|
8
8
|
# See lib/kobako/rpc.rb for the umbrella module doc; this file owns the
|
|
9
9
|
# Request / Response value objects and their encode/decode helpers.
|
|
10
10
|
module RPC
|
|
11
|
-
# ---------------- Response status bytes (
|
|
11
|
+
# ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
|
|
12
12
|
|
|
13
13
|
# Response variant marker for the success branch.
|
|
14
14
|
STATUS_OK = 0
|
|
@@ -16,7 +16,7 @@ module Kobako
|
|
|
16
16
|
STATUS_ERROR = 1
|
|
17
17
|
|
|
18
18
|
# Value object for a single guest-initiated RPC Request
|
|
19
|
-
# ({
|
|
19
|
+
# ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
|
|
20
20
|
#
|
|
21
21
|
# 4-element msgpack array: +[target, method, args, kwargs]+. +target+
|
|
22
22
|
# is either a +String+ (+"Namespace::Member"+) or a {Handle}. SPEC pins
|
|
@@ -66,7 +66,7 @@ module Kobako
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# Value object for a single host-side RPC Response
|
|
69
|
-
# ({
|
|
69
|
+
# ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
|
|
70
70
|
#
|
|
71
71
|
# 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
|
|
72
72
|
# (success) or 1 (fault). For success the second element is the return
|
data/lib/kobako/rpc/fault.rb
CHANGED
|
@@ -4,8 +4,9 @@ module Kobako
|
|
|
4
4
|
module RPC
|
|
5
5
|
# Wire-level value object for an ext-0x02 Exception envelope.
|
|
6
6
|
#
|
|
7
|
-
# SPEC pins the payload
|
|
8
|
-
#
|
|
7
|
+
# SPEC pins the payload
|
|
8
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
9
|
+
# → ext 0x02) to a msgpack map with exactly three keys:
|
|
9
10
|
# * "type" — one of "runtime", "argument", "disconnected", "undefined"
|
|
10
11
|
# * "message" — human-readable string
|
|
11
12
|
# * "details" — any wire-legal value, or nil when absent
|
data/lib/kobako/rpc/handle.rb
CHANGED
|
@@ -5,8 +5,9 @@ module Kobako
|
|
|
5
5
|
# Wire-level value object for an ext-0x01 Capability Handle.
|
|
6
6
|
#
|
|
7
7
|
# SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
|
|
8
|
-
# payload (
|
|
9
|
-
#
|
|
8
|
+
# payload ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
9
|
+
# § Ext Types → ext 0x01). ID 0 is reserved as the invalid sentinel;
|
|
10
|
+
# the maximum valid ID is 0x7fff_ffff (2^31 - 1).
|
|
10
11
|
#
|
|
11
12
|
# This is intentionally a thin value object built on +Data.define+ so
|
|
12
13
|
# equality, hash, and immutability are inherited. The runtime-facing
|