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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +1 -1
  3. data/README.md +95 -60
  4. data/data/kobako.wasm +0 -0
  5. data/ext/kobako/Cargo.toml +1 -1
  6. data/ext/kobako/src/wasm/cache.rs +39 -1
  7. data/ext/kobako/src/wasm/dispatch.rs +20 -20
  8. data/ext/kobako/src/wasm/host_state.rs +261 -34
  9. data/ext/kobako/src/wasm/instance.rs +467 -272
  10. data/ext/kobako/src/wasm.rs +50 -19
  11. data/lib/kobako/capture.rb +46 -0
  12. data/lib/kobako/codec/decoder.rb +66 -0
  13. data/lib/kobako/codec/encoder.rb +37 -0
  14. data/lib/kobako/codec/error.rb +33 -0
  15. data/lib/kobako/codec/factory.rb +155 -0
  16. data/lib/kobako/codec/utils.rb +55 -0
  17. data/lib/kobako/codec.rb +27 -0
  18. data/lib/kobako/errors.rb +24 -1
  19. data/lib/kobako/outcome/panic.rb +42 -0
  20. data/lib/kobako/outcome.rb +133 -0
  21. data/lib/kobako/rpc/dispatcher.rb +169 -0
  22. data/lib/kobako/rpc/envelope.rb +118 -0
  23. data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
  24. data/lib/kobako/{wire → rpc}/handle.rb +4 -2
  25. data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
  26. data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
  27. data/lib/kobako/rpc/server.rb +156 -0
  28. data/lib/kobako/rpc.rb +11 -0
  29. data/lib/kobako/sandbox.rb +149 -69
  30. data/lib/kobako/version.rb +1 -1
  31. data/lib/kobako/wasm.rb +6 -16
  32. data/lib/kobako.rb +2 -0
  33. data/sig/kobako/capture.rbs +13 -0
  34. data/sig/kobako/codec/decoder.rbs +11 -0
  35. data/sig/kobako/codec/encoder.rbs +7 -0
  36. data/sig/kobako/codec/error.rbs +18 -0
  37. data/sig/kobako/codec/factory.rbs +31 -0
  38. data/sig/kobako/codec/utils.rbs +9 -0
  39. data/sig/kobako/errors.rbs +52 -0
  40. data/sig/kobako/outcome/panic.rbs +34 -0
  41. data/sig/kobako/outcome.rbs +24 -0
  42. data/sig/kobako/rpc/dispatcher.rbs +33 -0
  43. data/sig/kobako/rpc/envelope.rbs +51 -0
  44. data/sig/kobako/rpc/fault.rbs +20 -0
  45. data/sig/kobako/rpc/handle.rbs +19 -0
  46. data/sig/kobako/rpc/handle_table.rbs +25 -0
  47. data/sig/kobako/rpc/namespace.rbs +24 -0
  48. data/sig/kobako/rpc/server.rbs +37 -0
  49. data/sig/kobako/rpc.rbs +4 -0
  50. data/sig/kobako/sandbox.rbs +53 -0
  51. data/sig/kobako/wasm.rbs +37 -0
  52. data/sig/kobako.rbs +0 -1
  53. metadata +37 -17
  54. data/lib/kobako/registry/dispatcher.rb +0 -168
  55. data/lib/kobako/registry.rb +0 -160
  56. data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
  57. data/lib/kobako/sandbox/output_buffer.rb +0 -79
  58. data/lib/kobako/wire/codec/decoder.rb +0 -87
  59. data/lib/kobako/wire/codec/encoder.rb +0 -41
  60. data/lib/kobako/wire/codec/error.rb +0 -35
  61. data/lib/kobako/wire/codec/factory.rb +0 -136
  62. data/lib/kobako/wire/codec.rb +0 -44
  63. data/lib/kobako/wire/envelope/payloads.rb +0 -145
  64. data/lib/kobako/wire/envelope.rb +0 -147
  65. 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
@@ -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"