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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +85 -6
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +1 -1
  8. data/ext/kobako/src/lib.rs +4 -2
  9. data/ext/kobako/src/{wasm → runtime}/cache.rs +22 -18
  10. data/ext/kobako/src/runtime/capture.rs +91 -0
  11. data/ext/kobako/src/runtime/config.rs +26 -0
  12. data/ext/kobako/src/runtime/dispatch.rs +211 -0
  13. data/ext/kobako/src/runtime/exports.rs +51 -0
  14. data/ext/kobako/src/runtime/guest_mem.rs +228 -0
  15. data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +195 -81
  16. data/ext/kobako/src/runtime/trap.rs +134 -0
  17. data/ext/kobako/src/runtime.rs +782 -0
  18. data/ext/kobako/src/snapshot.rs +110 -0
  19. data/lib/kobako/capture.rb +11 -16
  20. data/lib/kobako/catalog/handles.rb +107 -0
  21. data/lib/kobako/catalog/namespaces.rb +99 -0
  22. data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
  23. data/lib/kobako/catalog.rb +18 -0
  24. data/lib/kobako/codec/decoder.rb +13 -7
  25. data/lib/kobako/codec/factory.rb +21 -18
  26. data/lib/kobako/codec/utils.rb +118 -29
  27. data/lib/kobako/codec.rb +6 -3
  28. data/lib/kobako/errors.rb +45 -28
  29. data/lib/kobako/fault.rb +40 -0
  30. data/lib/kobako/handle.rb +60 -0
  31. data/lib/kobako/namespace.rb +67 -0
  32. data/lib/kobako/outcome.rb +55 -29
  33. data/lib/kobako/runtime.rb +30 -0
  34. data/lib/kobako/sandbox.rb +131 -67
  35. data/lib/kobako/sandbox_options.rb +6 -9
  36. data/lib/kobako/snapshot.rb +40 -0
  37. data/lib/kobako/snippet/binary.rb +6 -7
  38. data/lib/kobako/snippet/source.rb +8 -8
  39. data/lib/kobako/snippet.rb +7 -9
  40. data/lib/kobako/transport/dispatcher.rb +195 -0
  41. data/lib/kobako/transport/error.rb +24 -0
  42. data/lib/kobako/transport/request.rb +78 -0
  43. data/lib/kobako/transport/response.rb +69 -0
  44. data/lib/kobako/transport/run.rb +141 -0
  45. data/lib/kobako/transport/yield.rb +91 -0
  46. data/lib/kobako/transport/yielder.rb +89 -0
  47. data/lib/kobako/transport.rb +24 -0
  48. data/lib/kobako/usage.rb +41 -0
  49. data/lib/kobako/version.rb +1 -1
  50. data/lib/kobako.rb +4 -3
  51. data/release-please-config.json +24 -0
  52. data/sig/kobako/capture.rbs +0 -2
  53. data/sig/kobako/{rpc/handle_table.rbs → catalog/handles.rbs} +3 -9
  54. data/sig/kobako/catalog/namespaces.rbs +17 -0
  55. data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
  56. data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
  57. data/sig/kobako/codec/decoder.rbs +2 -1
  58. data/sig/kobako/codec/factory.rbs +3 -3
  59. data/sig/kobako/codec/utils.rbs +11 -1
  60. data/sig/kobako/errors.rbs +7 -7
  61. data/sig/kobako/fault.rbs +19 -0
  62. data/sig/kobako/handle.rbs +18 -0
  63. data/sig/kobako/namespace.rbs +19 -0
  64. data/sig/kobako/outcome.rbs +2 -2
  65. data/sig/kobako/runtime.rbs +23 -0
  66. data/sig/kobako/sandbox.rbs +10 -7
  67. data/sig/kobako/snapshot.rbs +15 -0
  68. data/sig/kobako/transport/dispatcher.rbs +34 -0
  69. data/sig/kobako/transport/error.rbs +6 -0
  70. data/sig/kobako/transport/request.rbs +32 -0
  71. data/sig/kobako/transport/response.rbs +30 -0
  72. data/sig/kobako/transport/run.rbs +27 -0
  73. data/sig/kobako/transport/yield.rbs +34 -0
  74. data/sig/kobako/transport/yielder.rbs +21 -0
  75. data/sig/kobako/transport.rbs +4 -0
  76. data/sig/kobako/usage.rbs +11 -0
  77. metadata +52 -30
  78. data/ext/kobako/src/wasm/dispatch.rs +0 -161
  79. data/ext/kobako/src/wasm/instance.rs +0 -771
  80. data/ext/kobako/src/wasm.rs +0 -125
  81. data/lib/kobako/invocation.rb +0 -112
  82. data/lib/kobako/rpc/dispatcher.rb +0 -169
  83. data/lib/kobako/rpc/envelope.rb +0 -118
  84. data/lib/kobako/rpc/fault.rb +0 -41
  85. data/lib/kobako/rpc/handle.rb +0 -39
  86. data/lib/kobako/rpc/handle_table.rb +0 -107
  87. data/lib/kobako/rpc/namespace.rb +0 -74
  88. data/lib/kobako/rpc/server.rb +0 -158
  89. data/lib/kobako/rpc.rb +0 -11
  90. data/lib/kobako/wasm.rb +0 -25
  91. data/sig/kobako/invocation.rbs +0 -23
  92. data/sig/kobako/rpc/dispatcher.rbs +0 -33
  93. data/sig/kobako/rpc/envelope.rbs +0 -51
  94. data/sig/kobako/rpc/fault.rbs +0 -20
  95. data/sig/kobako/rpc/handle.rbs +0 -19
  96. data/sig/kobako/rpc/namespace.rbs +0 -24
  97. data/sig/kobako/rpc/server.rbs +0 -37
  98. data/sig/kobako/wasm.rbs +0 -39
@@ -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
- }
@@ -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
@@ -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
@@ -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
@@ -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