kobako 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +85 -6
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +22 -18
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +195 -81
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -7
- data/lib/kobako/codec/factory.rb +21 -18
- data/lib/kobako/codec/utils.rb +118 -29
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- 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.rb +55 -29
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +131 -67
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- 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 +1 -1
- data/lib/kobako.rb +4 -3
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/{rpc/handle_table.rbs → catalog/handles.rbs} +3 -9
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +3 -3
- data/sig/kobako/codec/utils.rbs +11 -1
- data/sig/kobako/errors.rbs +7 -7
- 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.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +10 -7
- data/sig/kobako/snapshot.rbs +15 -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
- metadata +52 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -161
- data/ext/kobako/src/wasm/instance.rs +0 -771
- data/ext/kobako/src/wasm.rs +0 -125
- data/lib/kobako/invocation.rb +0 -112
- data/lib/kobako/rpc/dispatcher.rb +0 -169
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/handle.rb +0 -39
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -158
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/invocation.rbs +0 -23
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -37
- data/sig/kobako/wasm.rbs +0 -39
data/ext/kobako/src/wasm.rs
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// Host-side wasmtime wrapper.
|
|
2
|
-
//
|
|
3
|
-
// The only Ruby-visible class is
|
|
4
|
-
//
|
|
5
|
-
// Kobako::Wasm::Instance — wraps wasmtime::Instance + cached TypedFuncs
|
|
6
|
-
//
|
|
7
|
-
// constructed via `Kobako::Wasm::Instance.from_path(path, timeout, memory_limit,
|
|
8
|
-
// stdout_limit, stderr_limit)`.
|
|
9
|
-
// The underlying wasmtime Engine and compiled Module live in a process-scope
|
|
10
|
-
// cache (see the `cache` submodule) and never surface to Ruby (SPEC.md "Code
|
|
11
|
-
// Organization": `ext/` "exposes no Wasm engine types to the Host App or
|
|
12
|
-
// downstream gems").
|
|
13
|
-
//
|
|
14
|
-
// Module layout (per CLAUDE.md principle #2 — one responsibility per file):
|
|
15
|
-
//
|
|
16
|
-
// * `cache` — process-wide Engine + per-path Module cache and the
|
|
17
|
-
// process-singleton epoch ticker thread.
|
|
18
|
-
// * `host_state` — HostState (per-Store context), StoreCell wrapper, the
|
|
19
|
-
// [`KobakoLimiter`] memory cap, and the trap marker
|
|
20
|
-
// types ([`TimeoutTrap`] / [`MemoryLimitTrap`]).
|
|
21
|
-
// * `instance` — Kobako::Wasm::Instance and its run-path methods.
|
|
22
|
-
// * `dispatch` — `__kobako_dispatch` host-import dispatch helpers.
|
|
23
|
-
//
|
|
24
|
-
// This file is the façade: it owns the Ruby error class lazy-resolvers,
|
|
25
|
-
// the `wasm_err` / `timeout_err` / `memory_limit_err` constructors shared
|
|
26
|
-
// by every submodule, and the Ruby init() that registers
|
|
27
|
-
// `Kobako::Wasm::Instance` and its methods.
|
|
28
|
-
|
|
29
|
-
mod cache;
|
|
30
|
-
mod dispatch;
|
|
31
|
-
mod host_state;
|
|
32
|
-
mod instance;
|
|
33
|
-
|
|
34
|
-
use magnus::value::Lazy;
|
|
35
|
-
use magnus::{
|
|
36
|
-
function, method, prelude::*, Error as MagnusError, ExceptionClass, RModule, RString, Ruby,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
use instance::Instance;
|
|
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
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Error classes (lazy-resolved from Ruby once Kobako::Wasm is defined).
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
pub(crate) static MODULE_NOT_BUILT_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
57
|
-
let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
|
|
58
|
-
let wasm: RModule = kobako.const_get("Wasm").unwrap();
|
|
59
|
-
wasm.const_get("ModuleNotBuiltError").unwrap()
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
pub(crate) static WASM_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
63
|
-
let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
|
|
64
|
-
let wasm: RModule = kobako.const_get("Wasm").unwrap();
|
|
65
|
-
wasm.const_get("Error").unwrap()
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
pub(crate) static WASM_TIMEOUT_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
69
|
-
let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
|
|
70
|
-
let wasm: RModule = kobako.const_get("Wasm").unwrap();
|
|
71
|
-
wasm.const_get("TimeoutError").unwrap()
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
pub(crate) static WASM_MEMORY_LIMIT_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
|
|
75
|
-
let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
|
|
76
|
-
let wasm: RModule = kobako.const_get("Wasm").unwrap();
|
|
77
|
-
wasm.const_get("MemoryLimitError").unwrap()
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
pub(crate) fn wasm_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
81
|
-
MagnusError::new(ruby.get_inner(&WASM_ERROR), msg.into())
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/// Construct a `Kobako::Wasm::TimeoutError` magnus error. Surfaces the
|
|
85
|
-
/// docs/behavior.md E-19 wall-clock cap path so the Sandbox layer can rewrap it
|
|
86
|
-
/// as `Kobako::TimeoutError`.
|
|
87
|
-
pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
88
|
-
MagnusError::new(ruby.get_inner(&WASM_TIMEOUT_ERROR), msg.into())
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/// Construct a `Kobako::Wasm::MemoryLimitError` magnus error. Surfaces
|
|
92
|
-
/// the docs/behavior.md E-20 linear-memory cap path so the Sandbox layer can
|
|
93
|
-
/// rewrap it as `Kobako::MemoryLimitError`.
|
|
94
|
-
pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
95
|
-
MagnusError::new(ruby.get_inner(&WASM_MEMORY_LIMIT_ERROR), msg.into())
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Ruby init
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
103
|
-
let wasm = kobako.define_module("Wasm")?;
|
|
104
|
-
|
|
105
|
-
// Error hierarchy. ModuleNotBuiltError is the headline error for the
|
|
106
|
-
// common pre-build state where `data/kobako.wasm` has not yet been
|
|
107
|
-
// produced (e.g. fresh clone before `rake compile`). TimeoutError and
|
|
108
|
-
// MemoryLimitError carry the docs/behavior.md B-01 per-run cap paths up to the
|
|
109
|
-
// Sandbox layer.
|
|
110
|
-
let base_err = wasm.define_error("Error", ruby.exception_standard_error())?;
|
|
111
|
-
wasm.define_error("ModuleNotBuiltError", base_err)?;
|
|
112
|
-
wasm.define_error("TimeoutError", base_err)?;
|
|
113
|
-
wasm.define_error("MemoryLimitError", base_err)?;
|
|
114
|
-
|
|
115
|
-
let instance = wasm.define_class("Instance", ruby.class_object())?;
|
|
116
|
-
instance.define_singleton_method("from_path", function!(Instance::from_path, 5))?;
|
|
117
|
-
instance.define_method("server=", method!(Instance::set_server, 1))?;
|
|
118
|
-
instance.define_method("eval", method!(Instance::eval, 3))?;
|
|
119
|
-
instance.define_method("run", method!(Instance::run, 3))?;
|
|
120
|
-
instance.define_method("stdout", method!(Instance::stdout, 0))?;
|
|
121
|
-
instance.define_method("stderr", method!(Instance::stderr, 0))?;
|
|
122
|
-
instance.define_method("outcome!", method!(Instance::outcome, 0))?;
|
|
123
|
-
|
|
124
|
-
Ok(())
|
|
125
|
-
}
|
data/lib/kobako/invocation.rb
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
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
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kobako
|
|
4
|
-
module RPC
|
|
5
|
-
# Pure-function dispatcher for guest-initiated RPC calls. Decodes a
|
|
6
|
-
# msgpack-encoded Request envelope, resolves the target object through
|
|
7
|
-
# the Server (path lookup or HandleTable lookup), invokes the method,
|
|
8
|
-
# and returns a msgpack-encoded Response envelope.
|
|
9
|
-
#
|
|
10
|
-
# The module is stateless — all mutable state is threaded through the
|
|
11
|
-
# +server+ argument so Dispatcher has no instance variables and no side
|
|
12
|
-
# effects beyond mutating the HandleTable via +alloc+ when a non-wire-
|
|
13
|
-
# representable return value must be wrapped ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
14
|
-
#
|
|
15
|
-
# Entry point:
|
|
16
|
-
#
|
|
17
|
-
# Kobako::RPC::Dispatcher.dispatch(request_bytes, server)
|
|
18
|
-
# # => msgpack-encoded Response bytes (never raises)
|
|
19
|
-
module Dispatcher
|
|
20
|
-
module_function
|
|
21
|
-
|
|
22
|
-
# Internal sentinel raised when target resolution fails. Mapped to
|
|
23
|
-
# Response.error with type="undefined". Contained at the wire boundary —
|
|
24
|
-
# not part of the public Kobako error taxonomy
|
|
25
|
-
# ({docs/behavior.md E-xx}[link:../../../docs/behavior.md]).
|
|
26
|
-
class UndefinedTargetError < StandardError; end
|
|
27
|
-
|
|
28
|
-
# Internal sentinel raised when a Handle target resolves to the
|
|
29
|
-
# +:disconnected+ sentinel in the HandleTable (ABA protection,
|
|
30
|
-
# {docs/behavior.md E-14}[link:../../../docs/behavior.md]). Mapped to Response.error with
|
|
31
|
-
# type="disconnected". Contained at the wire boundary.
|
|
32
|
-
class DisconnectedTargetError < StandardError; end
|
|
33
|
-
|
|
34
|
-
# Dispatch a single RPC request and return the encoded response bytes.
|
|
35
|
-
# Called by +Kobako::RPC::Server#dispatch+ which is invoked from the
|
|
36
|
-
# Rust ext inside +__kobako_dispatch+. +request_bytes+ is the
|
|
37
|
-
# msgpack-encoded Request envelope. +server+ is the live Server for
|
|
38
|
-
# this run, used to resolve path-based targets via +#lookup+ and to
|
|
39
|
-
# access the +#handle_table+ for Handle-based targets and return-value
|
|
40
|
-
# wrapping. Always returns a binary String — never raises. Any failure
|
|
41
|
-
# during decode, lookup, or method invocation is reified as a
|
|
42
|
-
# Response.error envelope so the guest sees the failure as a normal RPC
|
|
43
|
-
# error rather than a wasm trap
|
|
44
|
-
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
45
|
-
def dispatch(request_bytes, server)
|
|
46
|
-
request = Kobako::RPC.decode_request(request_bytes)
|
|
47
|
-
handle_table = server.handle_table
|
|
48
|
-
target = resolve_target(request.target, server, handle_table)
|
|
49
|
-
args = request.args.map { |v| resolve_arg(v, handle_table) }
|
|
50
|
-
kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handle_table) }
|
|
51
|
-
value = invoke(target, request.method_name, args, kwargs)
|
|
52
|
-
encode_ok(value, server)
|
|
53
|
-
rescue StandardError => e
|
|
54
|
-
encode_caught_error(e)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
58
|
-
# envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
|
|
59
|
-
# rescue. Returns a msgpack-encoded Response envelope (binary). Four
|
|
60
|
-
# error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
|
|
61
|
-
# +Kobako::Codec::Error+ → type="runtime" (wire decode failed);
|
|
62
|
-
# +DisconnectedTargetError+ → type="disconnected" (E-14);
|
|
63
|
-
# +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
|
|
64
|
-
# type="argument" (B-12 arity mismatch); everything else →
|
|
65
|
-
# type="runtime".
|
|
66
|
-
def encode_caught_error(error)
|
|
67
|
-
case error
|
|
68
|
-
when Kobako::Codec::Error then encode_error("runtime", "wire decode failed: #{error.message}")
|
|
69
|
-
when DisconnectedTargetError then encode_error("disconnected", error.message)
|
|
70
|
-
when UndefinedTargetError then encode_error("undefined", error.message)
|
|
71
|
-
when ArgumentError then encode_error("argument", error.message)
|
|
72
|
-
else encode_error("runtime", "#{error.class}: #{error.message}")
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
|
|
77
|
-
# (the +Envelope::Request+ invariant pins it). The empty-kwargs
|
|
78
|
-
# branch omits the +**+ splat so Ruby 3.x's strict kwargs
|
|
79
|
-
# separation does not reject calls to no-kwarg methods when the
|
|
80
|
-
# wire carries the uniform empty-map shape.
|
|
81
|
-
def invoke(target, method, args, kwargs)
|
|
82
|
-
if kwargs.empty?
|
|
83
|
-
target.public_send(method.to_sym, *args)
|
|
84
|
-
else
|
|
85
|
-
target.public_send(method.to_sym, *args, **kwargs)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An RPC::Handle arriving as a positional or keyword
|
|
90
|
-
# argument identifies a host-side object previously allocated by a prior
|
|
91
|
-
# RPC's Handle wrap (B-14). Resolve it back to the Ruby object before
|
|
92
|
-
# the dispatch reaches +public_send+. A Handle whose entry is the
|
|
93
|
-
# +:disconnected+ sentinel (E-14) raises DisconnectedTargetError so
|
|
94
|
-
# the dispatcher emits a Response.error with type="disconnected".
|
|
95
|
-
def resolve_arg(value, handle_table)
|
|
96
|
-
case value
|
|
97
|
-
when Kobako::RPC::Handle
|
|
98
|
-
require_live_object!(value.id, handle_table)
|
|
99
|
-
else
|
|
100
|
-
value
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Resolve a Request target to the Ruby object the Server (or
|
|
105
|
-
# HandleTable) holds. String targets go through the Server;
|
|
106
|
-
# Handle targets (ext 0x01) go through the HandleTable.
|
|
107
|
-
#
|
|
108
|
-
# Target type is already validated by +RPC.decode_request+
|
|
109
|
-
# before this method is reached, so no else-branch is needed here —
|
|
110
|
-
# the wire layer is the system boundary that enforces the invariant.
|
|
111
|
-
def resolve_target(target, server, handle_table)
|
|
112
|
-
case target
|
|
113
|
-
when String
|
|
114
|
-
resolve_path(target, server)
|
|
115
|
-
when Kobako::RPC::Handle
|
|
116
|
-
resolve_handle(target, handle_table)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def resolve_path(path, server)
|
|
121
|
-
server.lookup(path)
|
|
122
|
-
rescue KeyError => e
|
|
123
|
-
raise UndefinedTargetError, e.message
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def resolve_handle(handle, handle_table)
|
|
127
|
-
require_live_object!(handle.id, handle_table)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Resolve +id+ through the HandleTable, distinguishing the
|
|
131
|
-
# +:disconnected+ sentinel (E-14) from an unknown id (E-13).
|
|
132
|
-
def require_live_object!(id, handle_table)
|
|
133
|
-
object = handle_table.fetch(id)
|
|
134
|
-
raise DisconnectedTargetError, "Handle id #{id} is disconnected" if object == :disconnected
|
|
135
|
-
|
|
136
|
-
object
|
|
137
|
-
rescue Kobako::HandleTableError => e
|
|
138
|
-
raise UndefinedTargetError, e.message
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
142
|
-
# wire-representable per {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
|
|
143
|
-
# mapping, the +UnsupportedType+ rescue routes it through the
|
|
144
|
-
# HandleTable via {#wrap_as_handle} and re-encodes with the Capability
|
|
145
|
-
# Handle in place ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
|
|
146
|
-
# path encodes exactly once.
|
|
147
|
-
def encode_ok(value, server)
|
|
148
|
-
response = Kobako::RPC::Response.ok(value)
|
|
149
|
-
Kobako::RPC.encode_response(response)
|
|
150
|
-
rescue Kobako::Codec::UnsupportedType
|
|
151
|
-
encode_ok(wrap_as_handle(value, server), server)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Allocate +value+ in the Server's HandleTable and return a +Handle+
|
|
155
|
-
# that the wire codec can carry ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
156
|
-
# Used as the fallback path of {#encode_ok} when +value+ has no wire
|
|
157
|
-
# representation.
|
|
158
|
-
def wrap_as_handle(value, server)
|
|
159
|
-
Kobako::RPC::Handle.new(server.handle_table.alloc(value))
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def encode_error(type, message)
|
|
163
|
-
fault = Kobako::RPC::Fault.new(type: type, message: message)
|
|
164
|
-
response = Kobako::RPC::Response.error(fault)
|
|
165
|
-
Kobako::RPC.encode_response(response)
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
data/lib/kobako/rpc/envelope.rb
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "handle"
|
|
4
|
-
require_relative "fault"
|
|
5
|
-
require_relative "../codec"
|
|
6
|
-
|
|
7
|
-
module Kobako
|
|
8
|
-
# See lib/kobako/rpc.rb for the umbrella module doc; this file owns the
|
|
9
|
-
# Request / Response value objects and their encode/decode helpers.
|
|
10
|
-
module RPC
|
|
11
|
-
# ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
|
|
12
|
-
|
|
13
|
-
# Response variant marker for the success branch.
|
|
14
|
-
STATUS_OK = 0
|
|
15
|
-
# Response variant marker for the fault branch.
|
|
16
|
-
STATUS_ERROR = 1
|
|
17
|
-
|
|
18
|
-
# Value object for a single guest-initiated RPC Request
|
|
19
|
-
# ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
|
|
20
|
-
#
|
|
21
|
-
# 4-element msgpack array: +[target, method, args, kwargs]+. +target+
|
|
22
|
-
# is either a +String+ (+"Namespace::Member"+) or a {Handle}. SPEC pins
|
|
23
|
-
# +kwargs+ map keys to ext 0x00 Symbol; enforced at construction so the
|
|
24
|
-
# Value Object is the single source of truth.
|
|
25
|
-
Request = Data.define(:target, :method_name, :args, :kwargs) do
|
|
26
|
-
# steep:ignore:start
|
|
27
|
-
def initialize(target:, method:, args: [], kwargs: {})
|
|
28
|
-
unless target.is_a?(String) || target.is_a?(Kobako::RPC::Handle)
|
|
29
|
-
raise ArgumentError, "Request target must be String or Kobako::RPC::Handle, got #{target.class}"
|
|
30
|
-
end
|
|
31
|
-
raise ArgumentError, "Request method must be String" unless method.is_a?(String)
|
|
32
|
-
raise ArgumentError, "Request args must be Array" unless args.is_a?(Array)
|
|
33
|
-
|
|
34
|
-
validate_kwargs!(kwargs)
|
|
35
|
-
super(target: target, method_name: method, args: args, kwargs: kwargs)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def validate_kwargs!(kwargs)
|
|
41
|
-
raise ArgumentError, "Request kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
42
|
-
|
|
43
|
-
kwargs.each_key do |k|
|
|
44
|
-
raise ArgumentError, "Request kwargs keys must be Symbol, got #{k.class}" unless k.is_a?(Symbol)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
# steep:ignore:end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Encode a {Request} to msgpack bytes. The Value Object's own
|
|
51
|
-
# invariants are the contract; this method does not re-check the shape.
|
|
52
|
-
def self.encode_request(request)
|
|
53
|
-
Codec::Encoder.encode([request.target, request.method_name, request.args, request.kwargs])
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def self.decode_request(bytes)
|
|
57
|
-
arr = Codec::Decoder.decode(bytes)
|
|
58
|
-
unless arr.is_a?(Array) && arr.length == 4
|
|
59
|
-
raise Codec::InvalidType, "Request must be a 4-element array, got #{arr.inspect}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
target, method_name, args, kwargs = arr
|
|
63
|
-
Codec::Utils.wire_boundary do
|
|
64
|
-
Request.new(target: target, method: method_name, args: args, kwargs: kwargs)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Value object for a single host-side RPC Response
|
|
69
|
-
# ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
|
|
70
|
-
#
|
|
71
|
-
# 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
|
|
72
|
-
# (success) or 1 (fault). For success the second element is the return
|
|
73
|
-
# value; for fault it is a {Fault} (ext 0x02 envelope).
|
|
74
|
-
Response = Data.define(:status, :payload) do
|
|
75
|
-
# steep:ignore:start
|
|
76
|
-
def self.ok(value)
|
|
77
|
-
new(status: STATUS_OK, payload: value)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def self.error(fault)
|
|
81
|
-
unless fault.is_a?(Kobako::RPC::Fault)
|
|
82
|
-
raise ArgumentError, "Response.error requires Kobako::RPC::Fault, got #{fault.class}"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
new(status: STATUS_ERROR, payload: fault)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def initialize(status:, payload:)
|
|
89
|
-
unless [STATUS_OK, STATUS_ERROR].include?(status)
|
|
90
|
-
raise ArgumentError, "Response status must be 0 or 1, got #{status.inspect}"
|
|
91
|
-
end
|
|
92
|
-
if status == STATUS_ERROR && !payload.is_a?(Kobako::RPC::Fault)
|
|
93
|
-
raise ArgumentError, "Response status=1 payload must be Kobako::RPC::Fault"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
super
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def ok? = status == STATUS_OK
|
|
100
|
-
def error? = status == STATUS_ERROR
|
|
101
|
-
# steep:ignore:end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def self.encode_response(response)
|
|
105
|
-
Codec::Encoder.encode([response.status, response.payload])
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def self.decode_response(bytes)
|
|
109
|
-
arr = Codec::Decoder.decode(bytes)
|
|
110
|
-
unless arr.is_a?(Array) && arr.length == 2
|
|
111
|
-
raise Codec::InvalidType, "Response must be a 2-element array, got #{arr.inspect}"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
status, payload = arr
|
|
115
|
-
Codec::Utils.wire_boundary { Response.new(status: status, payload: payload) }
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
data/lib/kobako/rpc/fault.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kobako
|
|
4
|
-
module RPC
|
|
5
|
-
# Wire-level value object for an ext-0x02 Exception envelope.
|
|
6
|
-
#
|
|
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:
|
|
10
|
-
# * "type" — one of "runtime", "argument", "disconnected", "undefined"
|
|
11
|
-
# * "message" — human-readable string
|
|
12
|
-
# * "details" — any wire-legal value, or nil when absent
|
|
13
|
-
#
|
|
14
|
-
# This object holds the *encoded* form. Reifying the corresponding Ruby
|
|
15
|
-
# exception class (RuntimeError, ArgumentError, Kobako::ServiceError, ...)
|
|
16
|
-
# is the responsibility of the dispatch layer, not the codec.
|
|
17
|
-
#
|
|
18
|
-
# Built on +Data.define+ so equality, hash, and immutability are
|
|
19
|
-
# inherited from the value-object machinery; only the field invariants
|
|
20
|
-
# ride on top.
|
|
21
|
-
Fault = Data.define(:type, :message, :details) do
|
|
22
|
-
# +VALID_TYPES+ is attached to the Exception class below this block.
|
|
23
|
-
# Reach it through +self.class::VALID_TYPES+ — Data.define's block
|
|
24
|
-
# scope resolves bare constants against the enclosing +Wire+ module,
|
|
25
|
-
# so a bare +VALID_TYPES+ would raise +NameError+. Same pattern as
|
|
26
|
-
# +RPC::Handle+.
|
|
27
|
-
# steep:ignore:start
|
|
28
|
-
def initialize(type:, message:, details: nil)
|
|
29
|
-
valid_types = self.class::VALID_TYPES
|
|
30
|
-
raise ArgumentError, "type must be String" unless type.is_a?(String)
|
|
31
|
-
raise ArgumentError, "message must be String" unless message.is_a?(String)
|
|
32
|
-
raise ArgumentError, "type=#{type.inspect} not one of #{valid_types.inspect}" unless valid_types.include?(type)
|
|
33
|
-
|
|
34
|
-
super
|
|
35
|
-
end
|
|
36
|
-
# steep:ignore:end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
Fault::VALID_TYPES = %w[runtime argument disconnected undefined].freeze
|
|
40
|
-
end
|
|
41
|
-
end
|
data/lib/kobako/rpc/handle.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kobako
|
|
4
|
-
module RPC
|
|
5
|
-
# Wire-level value object for an ext-0x01 Capability Handle.
|
|
6
|
-
#
|
|
7
|
-
# SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
|
|
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).
|
|
11
|
-
#
|
|
12
|
-
# This is intentionally a thin value object built on +Data.define+ so
|
|
13
|
-
# equality, hash, and immutability are inherited. The runtime-facing
|
|
14
|
-
# +Kobako::RPC::Handle+ class lives at a higher layer and may add behaviour
|
|
15
|
-
# (HandleTable bookkeeping, reset semantics). The codec only needs to
|
|
16
|
-
# carry the opaque integer ID across the wire.
|
|
17
|
-
Handle = Data.define(:id) do
|
|
18
|
-
# +MIN_ID+ / +MAX_ID+ live on the Handle class (defined below this
|
|
19
|
-
# block), not in this block's binding — Data.define's block scope
|
|
20
|
-
# resolves bare constants against the enclosing +Wire+ module, so
|
|
21
|
-
# +MIN_ID+ would raise +NameError+. Use +self.class::CONST+ to
|
|
22
|
-
# reach the constants attached to the Handle class itself. Do not
|
|
23
|
-
# "simplify" this back to bare +MIN_ID+/+MAX_ID+.
|
|
24
|
-
# steep:ignore:start
|
|
25
|
-
def initialize(id:)
|
|
26
|
-
min = self.class::MIN_ID
|
|
27
|
-
max = self.class::MAX_ID
|
|
28
|
-
raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
|
|
29
|
-
raise ArgumentError, "Handle id #{id} out of range [#{min}, #{max}]" unless id.between?(min, max)
|
|
30
|
-
|
|
31
|
-
super
|
|
32
|
-
end
|
|
33
|
-
# steep:ignore:end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
Handle::MIN_ID = 1
|
|
37
|
-
Handle::MAX_ID = 0x7fff_ffff
|
|
38
|
-
end
|
|
39
|
-
end
|