kobako 0.4.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 +0 -1
- 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 +12 -16
- 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} +94 -86
- 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 -5
- data/lib/kobako/codec/factory.rb +12 -12
- data/lib/kobako/codec/utils.rb +56 -59
- 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 +4 -6
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +31 -35
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +83 -72
- 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/{rpc/wire_error.rb → transport/error.rb} +7 -6
- 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/version.rb +1 -1
- data/lib/kobako.rb +4 -4
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/catalog/handles.rbs +19 -0
- 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 +2 -2
- data/sig/kobako/codec/utils.rbs +5 -5
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +2 -3
- 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 +5 -8
- 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
- metadata +48 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -162
- data/ext/kobako/src/wasm/instance.rs +0 -873
- data/ext/kobako/src/wasm.rs +0 -126
- data/lib/kobako/handle_table.rb +0 -119
- data/lib/kobako/invocation.rb +0 -143
- data/lib/kobako/rpc/dispatcher.rb +0 -171
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -146
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/handle_table.rbs +0 -23
- data/sig/kobako/invocation.rbs +0 -25
- 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/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -31
- data/sig/kobako/rpc/wire_error.rbs +0 -6
- data/sig/kobako/wasm.rbs +0 -41
data/ext/kobako/src/wasm.rs
DELETED
|
@@ -1,126 +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
|
-
instance.define_method("usage", method!(Instance::usage, 0))?;
|
|
124
|
-
|
|
125
|
-
Ok(())
|
|
126
|
-
}
|
data/lib/kobako/handle_table.rb
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "handle"
|
|
4
|
-
|
|
5
|
-
module Kobako
|
|
6
|
-
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
7
|
-
# The table is owned by +Kobako::Sandbox+ ({docs/behavior.md B-19}[link:../../docs/behavior.md])
|
|
8
|
-
# and injected into the per-Sandbox +Kobako::RPC::Server+ so guest→host
|
|
9
|
-
# RPC dispatch resolves Handle targets and arguments against the same
|
|
10
|
-
# table that host→guest wire encoding allocates into
|
|
11
|
-
# ({docs/behavior.md B-14, B-34}[link:../../docs/behavior.md]).
|
|
12
|
-
#
|
|
13
|
-
# Lifecycle invariants ({docs/behavior.md}[link:../../docs/behavior.md]):
|
|
14
|
-
#
|
|
15
|
-
# - {docs/behavior.md B-15}[link:../../docs/behavior.md] — Handle IDs are
|
|
16
|
-
# allocated by a monotonically increasing counter scoped to a single
|
|
17
|
-
# invocation. The first ID issued in an invocation is 1; ID 0 is reserved
|
|
18
|
-
# as the invalid sentinel and is never returned by +#alloc+.
|
|
19
|
-
#
|
|
20
|
-
# - {docs/behavior.md B-19}[link:../../docs/behavior.md] — At every
|
|
21
|
-
# invocation boundary (via +#reset!+), every Handle issued under the
|
|
22
|
-
# old state becomes invalid. Reset applies uniformly regardless of
|
|
23
|
-
# allocation source (B-14 Service return or B-34 host-injected
|
|
24
|
-
# argument).
|
|
25
|
-
#
|
|
26
|
-
# - {docs/behavior.md B-21}[link:../../docs/behavior.md] — The cap is
|
|
27
|
-
# +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap raises immediately
|
|
28
|
-
# — no silent truncation, no wrap, no ID reuse.
|
|
29
|
-
class HandleTable
|
|
30
|
-
# Build a fresh, empty HandleTable. +next_id+ is an internal seam that
|
|
31
|
-
# sets the starting value of the monotonic counter (defaults to 1 per
|
|
32
|
-
# B-15); tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
33
|
-
# the cap-exhaustion path without 2³¹ allocations.
|
|
34
|
-
def initialize(next_id: 1)
|
|
35
|
-
@entries = {} # : Hash[Integer, untyped]
|
|
36
|
-
@next_id = next_id
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Bind +object+ in the table and return a +Kobako::Handle+ token
|
|
40
|
-
# for it. +object+ is any host-side Ruby object to bind. Returns a
|
|
41
|
-
# freshly-allocated +Kobako::Handle+ whose +#id+ falls in
|
|
42
|
-
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
43
|
-
# +Kobako::HandleTableExhausted+ if the next ID would exceed the
|
|
44
|
-
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
45
|
-
# and the allocator share the same invariant
|
|
46
|
-
# ({docs/behavior.md B-21}[link:../../docs/behavior.md]).
|
|
47
|
-
#
|
|
48
|
-
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
49
|
-
# allocator's output a domain entity; +Kobako::Handle.from_wire+
|
|
50
|
-
# is reserved for the codec's wire-decode path, where the id is
|
|
51
|
-
# the only thing the bytes carry.
|
|
52
|
-
def alloc(object)
|
|
53
|
-
id = @next_id
|
|
54
|
-
cap = Kobako::Handle::MAX_ID
|
|
55
|
-
if id > cap
|
|
56
|
-
raise HandleTableExhausted,
|
|
57
|
-
"Handle id space exhausted: allocation would assign id #{id}, exceeding the cap (#{cap})"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
@entries[id] = object
|
|
61
|
-
@next_id = id + 1
|
|
62
|
-
Kobako::Handle.from_wire(id)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Resolve a Handle ID to its bound object. +id+ is a Handle ID previously
|
|
66
|
-
# returned by +#alloc+. Returns the bound object. Raises
|
|
67
|
-
# +Kobako::HandleTableError+ if +id+ is not currently bound.
|
|
68
|
-
def fetch(id)
|
|
69
|
-
require_bound!(id)
|
|
70
|
-
@entries[id]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Remove and return the binding for +id+. +id+ is the Handle ID to
|
|
74
|
-
# release. Returns the previously-bound object. Raises
|
|
75
|
-
# +Kobako::HandleTableError+ if +id+ is not currently bound.
|
|
76
|
-
def release(id)
|
|
77
|
-
require_bound!(id)
|
|
78
|
-
@entries.delete(id)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Clear all entries AND reset the counter to 1. Called at the per-invocation
|
|
82
|
-
# boundary by +Kobako::Sandbox+ — see
|
|
83
|
-
# {docs/behavior.md B-19}[link:../../docs/behavior.md]. Returns +self+.
|
|
84
|
-
def reset!
|
|
85
|
-
@entries.clear
|
|
86
|
-
@next_id = 1
|
|
87
|
-
self
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Mark the entry at +id+ as disconnected (ABA protection). +id+ is the
|
|
91
|
-
# Handle ID to poison; silently ignored if +id+ is not currently bound.
|
|
92
|
-
# Returns +self+ for chainability, matching the convention of +#reset!+.
|
|
93
|
-
def mark_disconnected(id)
|
|
94
|
-
@entries[id] = :disconnected if @entries.key?(id)
|
|
95
|
-
self
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Returns the number of currently-bound entries.
|
|
99
|
-
def size
|
|
100
|
-
@entries.size
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Returns +true+ when +id+ is currently bound, +false+ otherwise.
|
|
104
|
-
def include?(id)
|
|
105
|
-
@entries.key?(id)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
# Single source of truth for the "unknown Handle id" raise shared by
|
|
111
|
-
# {#fetch} and {#release}. Returns +nil+ on success; raises
|
|
112
|
-
# +Kobako::HandleTableError+ when +id+ is not currently bound.
|
|
113
|
-
def require_bound!(id)
|
|
114
|
-
return if @entries.key?(id)
|
|
115
|
-
|
|
116
|
-
raise HandleTableError, "unknown Handle id: #{id.inspect}"
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
data/lib/kobako/invocation.rb
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "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
|
|
19
|
-
# is the guest→host capability channel (Server / Client / Request /
|
|
20
|
-
# Response); Invocation is the opposite direction (host→guest
|
|
21
|
-
# entrypoint dispatch). +#encode+ takes the Sandbox's HandleTable
|
|
22
|
-
# and routes any non-wire-representable +args+ / +kwargs+ leaf
|
|
23
|
-
# through it as a +Kobako::Handle+
|
|
24
|
-
# ({docs/behavior.md B-34}[link:../../docs/behavior.md]) — the
|
|
25
|
-
# symmetric counterpart of the guest→host wrap path in
|
|
26
|
-
# +Kobako::RPC::Dispatcher#wrap_as_handle+ (B-14). A
|
|
27
|
-
# +Kobako::Handle+ that arrives **already constructed** in the
|
|
28
|
-
# caller's +args+ / +kwargs+ is rejected at construction (E-29):
|
|
29
|
-
# legitimate Handles only enter Host App code through error fields,
|
|
30
|
-
# so a Handle reaching the call site is by definition smuggled in.
|
|
31
|
-
# The +#encode+ output is the "Invocation envelope" that ships
|
|
32
|
-
# through the +__kobako_run+ command buffer.
|
|
33
|
-
#
|
|
34
|
-
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
35
|
-
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
36
|
-
class Invocation < Data.define(:entrypoint, :args, :kwargs)
|
|
37
|
-
# Ruby constant-name pattern enforced on the +entrypoint+ Symbol
|
|
38
|
-
# ({docs/behavior.md E-25}[link:../../docs/behavior.md]). Parallel to
|
|
39
|
-
# +Kobako::Snippet::Table::NAME_PATTERN+; the two constants name the
|
|
40
|
-
# same regex but cover distinct surfaces (snippet identity vs.
|
|
41
|
-
# entrypoint resolution) so a future divergence stays local.
|
|
42
|
-
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
43
|
-
|
|
44
|
-
# steep:ignore:start
|
|
45
|
-
def initialize(entrypoint:, args: [], kwargs: {})
|
|
46
|
-
super(
|
|
47
|
-
entrypoint: normalize_entrypoint(entrypoint),
|
|
48
|
-
args: validate_args!(args),
|
|
49
|
-
kwargs: validate_kwargs!(kwargs)
|
|
50
|
-
)
|
|
51
|
-
end
|
|
52
|
-
# steep:ignore:end
|
|
53
|
-
|
|
54
|
-
# Encode this Invocation to the msgpack bytes the guest's
|
|
55
|
-
# +__kobako_run+ entry point consumes as its command-buffer payload
|
|
56
|
-
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]).
|
|
57
|
-
# Walks +args+ / +kwargs+ through {Codec::Utils.deep_wrap} so any
|
|
58
|
-
# non-wire-representable leaf is allocated into +handle_table+ and
|
|
59
|
-
# replaced with a +Kobako::Handle+
|
|
60
|
-
# ({docs/behavior.md B-34}[link:../../docs/behavior.md]); the
|
|
61
|
-
# +handle_table+ argument is the Sandbox's table, sharing the same
|
|
62
|
-
# allocator the guest→host return path (B-14) uses.
|
|
63
|
-
#
|
|
64
|
-
# Layout: msgpack map with string keys +"entrypoint"+ (Symbol via
|
|
65
|
-
# ext 0x00), +"args"+ (Array), +"kwargs"+ (Map with Symbol keys);
|
|
66
|
-
# any wrapped leaf rides as ext 0x01 in its original position
|
|
67
|
-
# (docs/wire-codec.md § ext 0x01 position rules).
|
|
68
|
-
def encode(handle_table)
|
|
69
|
-
Codec::Encoder.encode(
|
|
70
|
-
"entrypoint" => entrypoint,
|
|
71
|
-
"args" => Codec::Utils.deep_wrap(args, handle_table),
|
|
72
|
-
"kwargs" => Codec::Utils.deep_wrap(kwargs, handle_table)
|
|
73
|
-
)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
# steep:ignore:start
|
|
79
|
-
# E-24: target must be a Symbol or String (TypeError, not
|
|
80
|
-
# ArgumentError — the wrong-type case is a Host App programming
|
|
81
|
-
# error before the invocation reaches the guest). E-25: after
|
|
82
|
-
# +.to_s+ the value must match NAME_PATTERN (ArgumentError),
|
|
83
|
-
# rejecting +::+-segmented names and any non-constant form.
|
|
84
|
-
def normalize_entrypoint(target)
|
|
85
|
-
unless target.is_a?(Symbol) || target.is_a?(String)
|
|
86
|
-
raise TypeError, "Invocation entrypoint must be a Symbol or String, got #{target.class}"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
target_str = target.to_s
|
|
90
|
-
unless NAME_PATTERN.match?(target_str)
|
|
91
|
-
raise ArgumentError,
|
|
92
|
-
"Invocation entrypoint must match #{NAME_PATTERN.inspect} (got #{target.inspect})"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
target_str.to_sym
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# E-29: +args+ must not contain a +Kobako::Handle+. The Handle
|
|
99
|
-
# allocator lives inside the Host Gem; legitimate paths surface
|
|
100
|
-
# Handle objects only through raised error fields, so a Handle
|
|
101
|
-
# reaching +args+ is a forged or smuggled token. Non-wire-
|
|
102
|
-
# representable arguments that are not Handles are handled by
|
|
103
|
-
# auto-wrap inside +#encode+ (B-34) — the reject path is reserved
|
|
104
|
-
# for Handle objects specifically.
|
|
105
|
-
def validate_args!(args)
|
|
106
|
-
raise ArgumentError, "Invocation args must be Array" unless args.is_a?(Array)
|
|
107
|
-
raise ArgumentError, forged_handle_message("args") if args.any?(Kobako::Handle)
|
|
108
|
-
|
|
109
|
-
args
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# E-30 covers the non-Symbol kwargs-key case; E-29 also rejects a
|
|
113
|
-
# +Kobako::Handle+ arriving as a kwargs value (same forged-token
|
|
114
|
-
# principle as the +args+ branch). Both checks live here so the
|
|
115
|
-
# Host App sees the host-side error message before any encode /
|
|
116
|
-
# decode boundary.
|
|
117
|
-
def validate_kwargs!(kwargs)
|
|
118
|
-
raise ArgumentError, "Invocation kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
119
|
-
|
|
120
|
-
bad_keys = kwargs.each_key.grep_v(Symbol)
|
|
121
|
-
unless bad_keys.empty?
|
|
122
|
-
raise ArgumentError,
|
|
123
|
-
"Invocation kwargs keys must be Symbols (got #{bad_keys.inspect})"
|
|
124
|
-
end
|
|
125
|
-
raise ArgumentError, forged_handle_message("kwargs values") if kwargs.each_value.any?(Kobako::Handle)
|
|
126
|
-
|
|
127
|
-
kwargs
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Single source of truth for the E-29 reject message so the args
|
|
131
|
-
# and kwargs branches stay phrased identically. Message stays in
|
|
132
|
-
# caller vocabulary: it names the affected slot and the reason
|
|
133
|
-
# without leaking SPEC anchor identifiers (B-xx / E-xx live in
|
|
134
|
-
# source comments, not user-visible errors) or self-referential
|
|
135
|
-
# architecture terms — the error is raised BY kobako, so saying
|
|
136
|
-
# "allocated by the Host Gem" reads as third-person about self.
|
|
137
|
-
def forged_handle_message(slot)
|
|
138
|
-
"Invocation #{slot} must not contain a Kobako::Handle — " \
|
|
139
|
-
"Kobako::Handle instances are internal wire tokens, not caller-constructible"
|
|
140
|
-
end
|
|
141
|
-
# steep:ignore:end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
@@ -1,171 +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+ resolves path-based
|
|
38
|
-
# Member targets via +#lookup+. +handle_table+ is the Sandbox's
|
|
39
|
-
# HandleTable, injected separately so Dispatcher does not depend
|
|
40
|
-
# on Server publishing a Handle accessor — Handle is a
|
|
41
|
-
# Sandbox-level domain entity (B-19) and the dispatcher is its
|
|
42
|
-
# only consumer here. Always returns a binary String — never
|
|
43
|
-
# raises. Any failure during decode, lookup, or method invocation
|
|
44
|
-
# is reified as a Response.error envelope so the guest sees the
|
|
45
|
-
# failure as a normal RPC error rather than a wasm trap
|
|
46
|
-
# ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
47
|
-
def dispatch(request_bytes, server, handle_table)
|
|
48
|
-
request = Kobako::RPC.decode_request(request_bytes)
|
|
49
|
-
target = resolve_target(request.target, server, handle_table)
|
|
50
|
-
args = request.args.map { |v| resolve_arg(v, handle_table) }
|
|
51
|
-
kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handle_table) }
|
|
52
|
-
value = invoke(target, request.method_name, args, kwargs)
|
|
53
|
-
encode_ok(value, handle_table)
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
encode_caught_error(e)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
59
|
-
# envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
|
|
60
|
-
# rescue. Returns a msgpack-encoded Response envelope (binary). Four
|
|
61
|
-
# error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
|
|
62
|
-
# +Kobako::Codec::Error+ → type="runtime" (malformed RPC request);
|
|
63
|
-
# +DisconnectedTargetError+ → type="disconnected" (E-14);
|
|
64
|
-
# +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
|
|
65
|
-
# type="argument" (B-12 arity mismatch); everything else →
|
|
66
|
-
# type="runtime".
|
|
67
|
-
def encode_caught_error(error)
|
|
68
|
-
case error
|
|
69
|
-
when Kobako::Codec::Error then encode_error("runtime",
|
|
70
|
-
"Sandbox received a malformed RPC request: #{error.message}")
|
|
71
|
-
when DisconnectedTargetError then encode_error("disconnected", error.message)
|
|
72
|
-
when UndefinedTargetError then encode_error("undefined", error.message)
|
|
73
|
-
when ArgumentError then encode_error("argument", error.message)
|
|
74
|
-
else encode_error("runtime", "#{error.class}: #{error.message}")
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
|
|
79
|
-
# (the +Envelope::Request+ invariant pins it). The empty-kwargs
|
|
80
|
-
# branch omits the +**+ splat so Ruby 3.x's strict kwargs
|
|
81
|
-
# separation does not reject calls to no-kwarg methods when the
|
|
82
|
-
# wire carries the uniform empty-map shape.
|
|
83
|
-
def invoke(target, method, args, kwargs)
|
|
84
|
-
if kwargs.empty?
|
|
85
|
-
target.public_send(method.to_sym, *args)
|
|
86
|
-
else
|
|
87
|
-
target.public_send(method.to_sym, *args, **kwargs)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An Kobako::Handle arriving as a positional or keyword
|
|
92
|
-
# argument identifies a host-side object previously allocated by a prior
|
|
93
|
-
# RPC's Handle wrap (B-14). Resolve it back to the Ruby object before
|
|
94
|
-
# the dispatch reaches +public_send+. A Handle whose entry is the
|
|
95
|
-
# +:disconnected+ sentinel (E-14) raises DisconnectedTargetError so
|
|
96
|
-
# the dispatcher emits a Response.error with type="disconnected".
|
|
97
|
-
def resolve_arg(value, handle_table)
|
|
98
|
-
case value
|
|
99
|
-
when Kobako::Handle
|
|
100
|
-
require_live_object!(value.id, handle_table)
|
|
101
|
-
else
|
|
102
|
-
value
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Resolve a Request target to the Ruby object the Server (or
|
|
107
|
-
# HandleTable) holds. String targets go through the Server;
|
|
108
|
-
# Handle targets (ext 0x01) go through the HandleTable.
|
|
109
|
-
#
|
|
110
|
-
# Target type is already validated by +RPC.decode_request+
|
|
111
|
-
# before this method is reached, so no else-branch is needed here —
|
|
112
|
-
# the wire layer is the system boundary that enforces the invariant.
|
|
113
|
-
def resolve_target(target, server, handle_table)
|
|
114
|
-
case target
|
|
115
|
-
when String
|
|
116
|
-
resolve_path(target, server)
|
|
117
|
-
when Kobako::Handle
|
|
118
|
-
resolve_handle(target, handle_table)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def resolve_path(path, server)
|
|
123
|
-
server.lookup(path)
|
|
124
|
-
rescue KeyError => e
|
|
125
|
-
raise UndefinedTargetError, e.message
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def resolve_handle(handle, handle_table)
|
|
129
|
-
require_live_object!(handle.id, handle_table)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Resolve +id+ through the HandleTable, distinguishing the
|
|
133
|
-
# +:disconnected+ sentinel (E-14) from an unknown id (E-13).
|
|
134
|
-
def require_live_object!(id, handle_table)
|
|
135
|
-
object = handle_table.fetch(id)
|
|
136
|
-
raise DisconnectedTargetError, "Handle id #{id} is disconnected" if object == :disconnected
|
|
137
|
-
|
|
138
|
-
object
|
|
139
|
-
rescue Kobako::HandleTableError => e
|
|
140
|
-
raise UndefinedTargetError, e.message
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
144
|
-
# wire-representable per {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
|
|
145
|
-
# mapping, the +UnsupportedType+ rescue routes it through the
|
|
146
|
-
# HandleTable via {#wrap_as_handle} and re-encodes with the Capability
|
|
147
|
-
# Handle in place ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
|
|
148
|
-
# path encodes exactly once.
|
|
149
|
-
def encode_ok(value, handle_table)
|
|
150
|
-
response = Kobako::RPC::Response.ok(value)
|
|
151
|
-
Kobako::RPC.encode_response(response)
|
|
152
|
-
rescue Kobako::Codec::UnsupportedType
|
|
153
|
-
encode_ok(wrap_as_handle(value, handle_table), handle_table)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Allocate +value+ in the Sandbox's HandleTable and return a +Handle+
|
|
157
|
-
# that the wire codec can carry ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
158
|
-
# Used as the fallback path of {#encode_ok} when +value+ has no wire
|
|
159
|
-
# representation.
|
|
160
|
-
def wrap_as_handle(value, handle_table)
|
|
161
|
-
handle_table.alloc(value)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def encode_error(type, message)
|
|
165
|
-
fault = Kobako::RPC::Fault.new(type: type, message: message)
|
|
166
|
-
response = Kobako::RPC::Response.error(fault)
|
|
167
|
-
Kobako::RPC.encode_response(response)
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
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::Handle)
|
|
29
|
-
raise ArgumentError, "Request target must be String or Kobako::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 envelope is malformed (expected a 4-element array)"
|
|
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 (ok) or 1 (error), got #{status.inspect}"
|
|
91
|
-
end
|
|
92
|
-
if status == STATUS_ERROR && !payload.is_a?(Kobako::RPC::Fault)
|
|
93
|
-
raise ArgumentError, "Response with error status must carry a Kobako::RPC::Fault payload"
|
|
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 envelope is malformed (expected a 2-element array)"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
status, payload = arr
|
|
115
|
-
Codec::Utils.wire_boundary { Response.new(status: status, payload: payload) }
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|