kobako 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -1
- data/README.md +95 -60
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/wasm/cache.rs +39 -1
- data/ext/kobako/src/wasm/dispatch.rs +20 -20
- data/ext/kobako/src/wasm/host_state.rs +261 -34
- data/ext/kobako/src/wasm/instance.rs +467 -272
- data/ext/kobako/src/wasm.rs +50 -19
- data/lib/kobako/capture.rb +46 -0
- data/lib/kobako/codec/decoder.rb +66 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +33 -0
- data/lib/kobako/codec/factory.rb +155 -0
- data/lib/kobako/codec/utils.rb +55 -0
- data/lib/kobako/codec.rb +27 -0
- data/lib/kobako/errors.rb +24 -1
- data/lib/kobako/outcome/panic.rb +42 -0
- data/lib/kobako/outcome.rb +133 -0
- data/lib/kobako/rpc/dispatcher.rb +169 -0
- data/lib/kobako/rpc/envelope.rb +118 -0
- data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
- data/lib/kobako/{wire → rpc}/handle.rb +4 -2
- data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
- data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
- data/lib/kobako/rpc/server.rb +156 -0
- data/lib/kobako/rpc.rb +11 -0
- data/lib/kobako/sandbox.rb +149 -69
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako/wasm.rb +6 -16
- data/lib/kobako.rb +2 -0
- data/sig/kobako/capture.rbs +13 -0
- data/sig/kobako/codec/decoder.rbs +11 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +9 -0
- data/sig/kobako/errors.rbs +52 -0
- data/sig/kobako/outcome/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/rpc/dispatcher.rbs +33 -0
- data/sig/kobako/rpc/envelope.rbs +51 -0
- data/sig/kobako/rpc/fault.rbs +20 -0
- data/sig/kobako/rpc/handle.rbs +19 -0
- data/sig/kobako/rpc/handle_table.rbs +25 -0
- data/sig/kobako/rpc/namespace.rbs +24 -0
- data/sig/kobako/rpc/server.rbs +37 -0
- data/sig/kobako/rpc.rbs +4 -0
- data/sig/kobako/sandbox.rbs +53 -0
- data/sig/kobako/wasm.rbs +37 -0
- data/sig/kobako.rbs +0 -1
- metadata +37 -17
- data/lib/kobako/registry/dispatcher.rb +0 -168
- data/lib/kobako/registry.rb +0 -160
- data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
- data/lib/kobako/sandbox/output_buffer.rb +0 -79
- data/lib/kobako/wire/codec/decoder.rb +0 -87
- data/lib/kobako/wire/codec/encoder.rb +0 -41
- data/lib/kobako/wire/codec/error.rb +0 -35
- data/lib/kobako/wire/codec/factory.rb +0 -136
- data/lib/kobako/wire/codec.rb +0 -44
- data/lib/kobako/wire/envelope/payloads.rb +0 -145
- data/lib/kobako/wire/envelope.rb +0 -147
- data/lib/kobako/wire.rb +0 -40
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../codec"
|
|
4
|
-
|
|
5
|
-
module Kobako
|
|
6
|
-
module Wire
|
|
7
|
-
# Outcome-path envelopes (SPEC.md Outcome Envelope): Result and Panic
|
|
8
|
-
# value objects plus the tagged Outcome wrapper that frames them on
|
|
9
|
-
# the wire. The RPC-path counterparts (Request / Response) live in
|
|
10
|
-
# the parent +envelope.rb+ file.
|
|
11
|
-
module Envelope
|
|
12
|
-
# ============================================================
|
|
13
|
-
# Result (SPEC.md Outcome Envelope → Result)
|
|
14
|
-
# ============================================================
|
|
15
|
-
#
|
|
16
|
-
# Success Outcome payload. SPEC pins the Result envelope as a
|
|
17
|
-
# 1-element msgpack array carrying the value, keeping framing
|
|
18
|
-
# symmetric with the Panic envelope so the value position is never
|
|
19
|
-
# ambiguous.
|
|
20
|
-
Result = Data.define(:value)
|
|
21
|
-
|
|
22
|
-
def self.encode_result(value)
|
|
23
|
-
Codec::Encoder.encode([value])
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.decode_result(bytes)
|
|
27
|
-
arr = Codec::Decoder.decode(bytes)
|
|
28
|
-
unless arr.is_a?(Array) && arr.length == 1
|
|
29
|
-
raise Codec::InvalidType, "Result envelope must be a 1-element array, got #{arr.inspect}"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
Result.new(arr[0])
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# ============================================================
|
|
36
|
-
# Panic (SPEC.md Outcome Envelope → Panic)
|
|
37
|
-
# ============================================================
|
|
38
|
-
#
|
|
39
|
-
# Failure Outcome payload. Encoded as a msgpack **map** keyed by
|
|
40
|
-
# name (forward-compatibility — unknown keys are silently ignored).
|
|
41
|
-
# Required: "origin" / "class" / "message". Optional: "backtrace"
|
|
42
|
-
# (array of str), "details" (any wire-legal value).
|
|
43
|
-
Panic = Data.define(:origin, :klass, :message, :backtrace, :details) do
|
|
44
|
-
def initialize(origin:, klass:, message:, backtrace: [], details: nil)
|
|
45
|
-
raise ArgumentError, "Panic origin must be String" unless origin.is_a?(String)
|
|
46
|
-
raise ArgumentError, "Panic class must be String" unless klass.is_a?(String)
|
|
47
|
-
raise ArgumentError, "Panic message must be String" unless message.is_a?(String)
|
|
48
|
-
unless backtrace.is_a?(Array) && backtrace.all?(String)
|
|
49
|
-
raise ArgumentError, "Panic backtrace must be Array of String"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
super
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
Panic::ORIGIN_SANDBOX = "sandbox"
|
|
57
|
-
Panic::ORIGIN_SERVICE = "service"
|
|
58
|
-
|
|
59
|
-
def self.encode_panic(panic)
|
|
60
|
-
Codec::Encoder.encode(panic_map(panic))
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# SPEC: Panic is a msgpack MAP keyed by name. Required keys always
|
|
64
|
-
# emitted; "backtrace" emitted only when non-empty (keep the wire
|
|
65
|
-
# compact); "details" only when non-nil. Ruby Hash preserves
|
|
66
|
-
# insertion order so the resulting msgpack map carries the keys in
|
|
67
|
-
# the order added below.
|
|
68
|
-
def self.panic_map(panic)
|
|
69
|
-
map = { "origin" => panic.origin, "class" => panic.klass, "message" => panic.message }
|
|
70
|
-
map["backtrace"] = panic.backtrace unless panic.backtrace.empty?
|
|
71
|
-
map["details"] = panic.details unless panic.details.nil?
|
|
72
|
-
map
|
|
73
|
-
end
|
|
74
|
-
private_class_method :panic_map
|
|
75
|
-
|
|
76
|
-
def self.decode_panic(bytes)
|
|
77
|
-
map = Codec::Decoder.decode(bytes)
|
|
78
|
-
raise Codec::InvalidType, "Panic envelope must be a map, got #{map.class}" unless map.is_a?(Hash)
|
|
79
|
-
|
|
80
|
-
Codec.translate_value_object_error do
|
|
81
|
-
Panic.new(
|
|
82
|
-
origin: map["origin"], klass: map["class"], message: map["message"],
|
|
83
|
-
backtrace: map["backtrace"] || [], details: map["details"]
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# ============================================================
|
|
89
|
-
# Outcome (SPEC.md Outcome Envelope)
|
|
90
|
-
# ============================================================
|
|
91
|
-
#
|
|
92
|
-
# OUTCOME_BUFFER wrapper: one-byte tag (+0x01+ Result, +0x02+ Panic)
|
|
93
|
-
# followed by the msgpack payload of the corresponding envelope.
|
|
94
|
-
# Callers construct an +Outcome+ by wrapping the payload directly —
|
|
95
|
-
# +Outcome.new(Result.new(value))+ or +Outcome.new(panic)+ — so the
|
|
96
|
-
# contract reads symmetrically across both variants.
|
|
97
|
-
Outcome = Data.define(:payload) do
|
|
98
|
-
def initialize(payload:)
|
|
99
|
-
unless payload.is_a?(Result) || payload.is_a?(Panic)
|
|
100
|
-
raise ArgumentError, "Outcome payload must be Result or Panic, got #{payload.class}"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
super
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def result? = payload.is_a?(Result)
|
|
107
|
-
def panic? = payload.is_a?(Panic)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def self.encode_outcome(outcome)
|
|
111
|
-
tag, body = encode_outcome_payload(outcome.payload)
|
|
112
|
-
out = String.new(encoding: Encoding::ASCII_8BIT)
|
|
113
|
-
out << [tag].pack("C")
|
|
114
|
-
out << body
|
|
115
|
-
out
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def self.encode_outcome_payload(payload)
|
|
119
|
-
case payload
|
|
120
|
-
when Result then [OUTCOME_TAG_RESULT, encode_result(payload.value)]
|
|
121
|
-
when Panic then [OUTCOME_TAG_PANIC, encode_panic(payload)]
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
private_class_method :encode_outcome_payload
|
|
125
|
-
|
|
126
|
-
def self.decode_outcome(bytes)
|
|
127
|
-
bytes = bytes.b
|
|
128
|
-
raise Codec::InvalidType, "Outcome bytes must not be empty" if bytes.empty?
|
|
129
|
-
|
|
130
|
-
tag = bytes.getbyte(0)
|
|
131
|
-
body = bytes.byteslice(1, bytes.bytesize - 1)
|
|
132
|
-
Outcome.new(decode_outcome_payload(tag, body))
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def self.decode_outcome_payload(tag, body)
|
|
136
|
-
case tag
|
|
137
|
-
when OUTCOME_TAG_RESULT then decode_result(body)
|
|
138
|
-
when OUTCOME_TAG_PANIC then decode_panic(body)
|
|
139
|
-
else raise Codec::InvalidType, format("unknown outcome tag 0x%<tag>02x", tag: tag)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
private_class_method :decode_outcome_payload
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
data/lib/kobako/wire/envelope.rb
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "handle"
|
|
4
|
-
require_relative "exception"
|
|
5
|
-
require_relative "codec"
|
|
6
|
-
|
|
7
|
-
module Kobako
|
|
8
|
-
module Wire
|
|
9
|
-
# Envelope-layer encoders/decoders for the kobako wire contract.
|
|
10
|
-
#
|
|
11
|
-
# SPEC.md → Wire Contract pins the logical shape of every host↔guest
|
|
12
|
-
# message and SPEC.md → Wire Codec → Envelope Frame Layout pins the
|
|
13
|
-
# binary framing. This module assembles the four envelope kinds
|
|
14
|
-
# (Request, Response, Result, Panic) and the outer Outcome wrapper on
|
|
15
|
-
# top of the lower-level {Codec::Encoder} / {Codec::Decoder} primitives.
|
|
16
|
-
#
|
|
17
|
-
# The contract collapses into two wire paths:
|
|
18
|
-
#
|
|
19
|
-
# - **RPC path** (lives in this file): Request / Response — guest
|
|
20
|
-
# calls a Service, host returns a value or an Exception.
|
|
21
|
-
# - **Outcome path** (lives in +envelope/payloads.rb+): Result /
|
|
22
|
-
# Panic wrapped in an Outcome envelope — the host reads this
|
|
23
|
-
# after +__kobako_run+ to surface either the script's last
|
|
24
|
-
# expression or a Sandbox/Service panic.
|
|
25
|
-
#
|
|
26
|
-
# The envelope objects are plain Value Objects; they own the field
|
|
27
|
-
# invariants (raising +ArgumentError+ on violation). The encode/decode
|
|
28
|
-
# helpers around them own the msgpack framing and translate value-
|
|
29
|
-
# object faults into the wire-layer +Codec::InvalidType+ taxonomy.
|
|
30
|
-
module Envelope
|
|
31
|
-
# ---------------- Outcome tag bytes (SPEC.md Outcome Envelope) -----
|
|
32
|
-
|
|
33
|
-
# First byte of the OUTCOME_BUFFER for a Result envelope.
|
|
34
|
-
OUTCOME_TAG_RESULT = 0x01
|
|
35
|
-
# First byte of the OUTCOME_BUFFER for a Panic envelope.
|
|
36
|
-
OUTCOME_TAG_PANIC = 0x02
|
|
37
|
-
|
|
38
|
-
# ---------------- Response status bytes (SPEC.md Response Shape) ---
|
|
39
|
-
|
|
40
|
-
# Response variant marker for the success branch.
|
|
41
|
-
STATUS_OK = 0
|
|
42
|
-
# Response variant marker for the error branch.
|
|
43
|
-
STATUS_ERROR = 1
|
|
44
|
-
|
|
45
|
-
# ============================================================
|
|
46
|
-
# Request (SPEC.md Wire Codec → Request)
|
|
47
|
-
# ============================================================
|
|
48
|
-
#
|
|
49
|
-
# 4-element msgpack array: [target, method, args, kwargs]. +target+
|
|
50
|
-
# is either a String ("Group::Member") or a {Handle}. SPEC pins
|
|
51
|
-
# +kwargs+ map keys to ext 0x00 Symbol (→ Wire Codec → Ext Types);
|
|
52
|
-
# enforced at construction so the Value Object is the single source
|
|
53
|
-
# of truth.
|
|
54
|
-
Request = Data.define(:target, :method_name, :args, :kwargs) do
|
|
55
|
-
def initialize(target:, method:, args: [], kwargs: {})
|
|
56
|
-
unless target.is_a?(String) || target.is_a?(Handle)
|
|
57
|
-
raise ArgumentError, "Request target must be String or Handle, got #{target.class}"
|
|
58
|
-
end
|
|
59
|
-
raise ArgumentError, "Request method must be String" unless method.is_a?(String)
|
|
60
|
-
raise ArgumentError, "Request args must be Array" unless args.is_a?(Array)
|
|
61
|
-
|
|
62
|
-
validate_kwargs!(kwargs)
|
|
63
|
-
super(target: target, method_name: method, args: args, kwargs: kwargs)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def validate_kwargs!(kwargs)
|
|
69
|
-
raise ArgumentError, "Request kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
70
|
-
|
|
71
|
-
kwargs.each_key do |k|
|
|
72
|
-
raise ArgumentError, "Request kwargs keys must be Symbol, got #{k.class}" unless k.is_a?(Symbol)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Encode a {Request} to bytes. The Value Object's own invariants
|
|
78
|
-
# are the contract; this method does not re-check the shape.
|
|
79
|
-
def self.encode_request(request)
|
|
80
|
-
Codec::Encoder.encode([request.target, request.method_name, request.args, request.kwargs])
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.decode_request(bytes)
|
|
84
|
-
arr = Codec::Decoder.decode(bytes)
|
|
85
|
-
unless arr.is_a?(Array) && arr.length == 4
|
|
86
|
-
raise Codec::InvalidType, "Request must be a 4-element array, got #{arr.inspect}"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
target, method_name, args, kwargs = arr
|
|
90
|
-
Codec.translate_value_object_error do
|
|
91
|
-
Request.new(target: target, method: method_name, args: args, kwargs: kwargs)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# ============================================================
|
|
96
|
-
# Response (SPEC.md Wire Codec → Response)
|
|
97
|
-
# ============================================================
|
|
98
|
-
#
|
|
99
|
-
# 2-element msgpack array: [status, value-or-error]. +status+ is 0
|
|
100
|
-
# (success) or 1 (error). For success the second element is the
|
|
101
|
-
# return value; for error it is an {Exception} (ext 0x02 envelope).
|
|
102
|
-
Response = Data.define(:status, :payload) do
|
|
103
|
-
def self.ok(value)
|
|
104
|
-
new(status: STATUS_OK, payload: value)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def self.err(exception)
|
|
108
|
-
unless exception.is_a?(Exception)
|
|
109
|
-
raise ArgumentError, "Response.err requires Kobako::Wire::Exception, got #{exception.class}"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
new(status: STATUS_ERROR, payload: exception)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def initialize(status:, payload:)
|
|
116
|
-
unless [STATUS_OK, STATUS_ERROR].include?(status)
|
|
117
|
-
raise ArgumentError, "Response status must be 0 or 1, got #{status.inspect}"
|
|
118
|
-
end
|
|
119
|
-
if status == STATUS_ERROR && !payload.is_a?(Exception)
|
|
120
|
-
raise ArgumentError, "Response status=1 payload must be Kobako::Wire::Exception"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
super
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def ok? = status == STATUS_OK
|
|
127
|
-
def err? = status == STATUS_ERROR
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def self.encode_response(response)
|
|
131
|
-
Codec::Encoder.encode([response.status, response.payload])
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def self.decode_response(bytes)
|
|
135
|
-
arr = Codec::Decoder.decode(bytes)
|
|
136
|
-
unless arr.is_a?(Array) && arr.length == 2
|
|
137
|
-
raise Codec::InvalidType, "Response must be a 2-element array, got #{arr.inspect}"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
status, payload = arr
|
|
141
|
-
Codec.translate_value_object_error { Response.new(status: status, payload: payload) }
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
require_relative "envelope/payloads"
|
data/lib/kobako/wire.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Host-side namespace for the kobako wire contract (SPEC.md → Wire
|
|
4
|
-
# Contract). The wire is split into two layers, mirrored on the Rust
|
|
5
|
-
# side by the +codec+ / +envelope+ modules in the +kobako-wasm+ crate:
|
|
6
|
-
#
|
|
7
|
-
# - {Codec} — byte-level MessagePack codec (SPEC.md → Wire Codec):
|
|
8
|
-
# {Codec::Encoder}, {Codec::Decoder}, {Codec::Factory}, plus the
|
|
9
|
-
# {Codec::Error} taxonomy. This is the layer that emits and
|
|
10
|
-
# consumes raw bytes; ext types 0x01 (Capability Handle) and
|
|
11
|
-
# 0x02 (Exception envelope) are registered exactly once on the
|
|
12
|
-
# Factory, where the numeric codes live as module-private constants
|
|
13
|
-
# alongside the Rust-side +codec::EXT_HANDLE+ / +codec::EXT_ERRENV+.
|
|
14
|
-
#
|
|
15
|
-
# - {Envelope} — logical message framing (SPEC.md → Wire Contract):
|
|
16
|
-
# {Envelope::Request} / {Envelope::Response} / {Envelope::Result}
|
|
17
|
-
# / {Envelope::Panic} / {Envelope::Outcome} value objects and
|
|
18
|
-
# their encode/decode helpers, built on top of {Codec}.
|
|
19
|
-
#
|
|
20
|
-
# {Handle} and {Exception} are value objects that travel through both
|
|
21
|
-
# layers; they live directly under +Wire+ so neither layer "owns" them.
|
|
22
|
-
#
|
|
23
|
-
# The namespace is intentionally self-contained — it does not depend
|
|
24
|
-
# on the native extension or on +lib/kobako.rb+ — so it can be required
|
|
25
|
-
# directly from tests that run on a clean checkout (no compiled artifacts).
|
|
26
|
-
module Kobako
|
|
27
|
-
# See the file-level documentation above for the layer split. The
|
|
28
|
-
# module body is intentionally empty: the byte-level codec lives in
|
|
29
|
-
# {Wire::Codec}, the logical framing in {Wire::Envelope}, and the
|
|
30
|
-
# shared value objects ({Wire::Handle} / {Wire::Exception}) load
|
|
31
|
-
# themselves into this namespace via the +require_relative+ calls
|
|
32
|
-
# below.
|
|
33
|
-
module Wire
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
require_relative "wire/handle"
|
|
38
|
-
require_relative "wire/exception"
|
|
39
|
-
require_relative "wire/codec"
|
|
40
|
-
require_relative "wire/envelope"
|