kobako 0.5.0-x86_64-linux

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/LICENSE +201 -0
  5. data/README.md +408 -0
  6. data/data/kobako.wasm +0 -0
  7. data/lib/kobako/3.3/kobako.so +0 -0
  8. data/lib/kobako/3.4/kobako.so +0 -0
  9. data/lib/kobako/4.0/kobako.so +0 -0
  10. data/lib/kobako/capture.rb +43 -0
  11. data/lib/kobako/catalog/handles.rb +107 -0
  12. data/lib/kobako/catalog/namespaces.rb +99 -0
  13. data/lib/kobako/catalog/snippets.rb +149 -0
  14. data/lib/kobako/catalog.rb +18 -0
  15. data/lib/kobako/codec/decoder.rb +73 -0
  16. data/lib/kobako/codec/encoder.rb +37 -0
  17. data/lib/kobako/codec/error.rb +34 -0
  18. data/lib/kobako/codec/factory.rb +162 -0
  19. data/lib/kobako/codec/utils.rb +145 -0
  20. data/lib/kobako/codec.rb +31 -0
  21. data/lib/kobako/errors.rb +140 -0
  22. data/lib/kobako/fault.rb +40 -0
  23. data/lib/kobako/handle.rb +60 -0
  24. data/lib/kobako/namespace.rb +67 -0
  25. data/lib/kobako/outcome/panic.rb +42 -0
  26. data/lib/kobako/outcome.rb +166 -0
  27. data/lib/kobako/runtime.rb +30 -0
  28. data/lib/kobako/sandbox.rb +314 -0
  29. data/lib/kobako/sandbox_options.rb +70 -0
  30. data/lib/kobako/snapshot.rb +40 -0
  31. data/lib/kobako/snippet/binary.rb +29 -0
  32. data/lib/kobako/snippet/source.rb +28 -0
  33. data/lib/kobako/snippet.rb +18 -0
  34. data/lib/kobako/transport/dispatcher.rb +195 -0
  35. data/lib/kobako/transport/error.rb +24 -0
  36. data/lib/kobako/transport/request.rb +78 -0
  37. data/lib/kobako/transport/response.rb +69 -0
  38. data/lib/kobako/transport/run.rb +141 -0
  39. data/lib/kobako/transport/yield.rb +91 -0
  40. data/lib/kobako/transport/yielder.rb +89 -0
  41. data/lib/kobako/transport.rb +24 -0
  42. data/lib/kobako/usage.rb +41 -0
  43. data/lib/kobako/version.rb +5 -0
  44. data/lib/kobako.rb +10 -0
  45. data/release-please-config.json +24 -0
  46. data/sig/kobako/capture.rbs +11 -0
  47. data/sig/kobako/catalog/handles.rbs +19 -0
  48. data/sig/kobako/catalog/namespaces.rbs +17 -0
  49. data/sig/kobako/catalog/snippets.rbs +27 -0
  50. data/sig/kobako/catalog.rbs +4 -0
  51. data/sig/kobako/codec/decoder.rbs +12 -0
  52. data/sig/kobako/codec/encoder.rbs +7 -0
  53. data/sig/kobako/codec/error.rbs +18 -0
  54. data/sig/kobako/codec/factory.rbs +31 -0
  55. data/sig/kobako/codec/utils.rbs +19 -0
  56. data/sig/kobako/errors.rbs +55 -0
  57. data/sig/kobako/fault.rbs +19 -0
  58. data/sig/kobako/handle.rbs +18 -0
  59. data/sig/kobako/namespace.rbs +19 -0
  60. data/sig/kobako/outcome/panic.rbs +34 -0
  61. data/sig/kobako/outcome.rbs +24 -0
  62. data/sig/kobako/runtime.rbs +23 -0
  63. data/sig/kobako/sandbox.rbs +55 -0
  64. data/sig/kobako/sandbox_options.rbs +32 -0
  65. data/sig/kobako/snapshot.rbs +15 -0
  66. data/sig/kobako/snippet/binary.rbs +12 -0
  67. data/sig/kobako/snippet/source.rbs +13 -0
  68. data/sig/kobako/snippet.rbs +4 -0
  69. data/sig/kobako/transport/dispatcher.rbs +34 -0
  70. data/sig/kobako/transport/error.rbs +6 -0
  71. data/sig/kobako/transport/request.rbs +32 -0
  72. data/sig/kobako/transport/response.rbs +30 -0
  73. data/sig/kobako/transport/run.rbs +27 -0
  74. data/sig/kobako/transport/yield.rbs +34 -0
  75. data/sig/kobako/transport/yielder.rbs +21 -0
  76. data/sig/kobako/transport.rbs +4 -0
  77. data/sig/kobako/usage.rbs +11 -0
  78. data/sig/kobako.rbs +3 -0
  79. metadata +145 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Snippet
5
+ # Kobako::Snippet::Binary — value object representing a single
6
+ # +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets+
7
+ # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
8
+ #
9
+ # The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
10
+ # +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
11
+ # the wire ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
12
+ # The host treats the bytes as opaque — the snippet's canonical
13
+ # name, when present, lives in the bytecode's embedded +debug_info+
14
+ # and is resolved by the guest at load time; structural validation
15
+ # ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
16
+ # is deferred to the first invocation's guest replay.
17
+ #
18
+ # The class is a +Data.define+ subclass — frozen and value-equal.
19
+ # Callers (chiefly +Catalog::Snippets+) construct instances via keyword
20
+ # form +Binary.new(body: ...)+. Wire-form construction is the
21
+ # registry's responsibility.
22
+ class Binary < Data.define(:body)
23
+ # The +kind+ field value carried by bytecode snippets in their
24
+ # Frame 3 wire envelope entry
25
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
26
+ KIND = "bytecode"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Snippet
5
+ # Kobako::Snippet::Source — value object representing a single
6
+ # +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets+
7
+ # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
8
+ #
9
+ # +name+ is the canonical +Symbol+ identity baked into the loaded
10
+ # IREP's +debug_info+; backtrace frames originating in this snippet
11
+ # surface as +(snippet:Name):line+. +body+ is the UTF-8 mruby source
12
+ # detached from the caller's reference at +Catalog::Snippets#register+
13
+ # time so later mutation of the original String cannot bleed through.
14
+ #
15
+ # The class is a +Data.define+ subclass — frozen, value-equal, and
16
+ # carries no mutation API. Callers (chiefly +Catalog::Snippets+)
17
+ # construct instances via keyword form +Source.new(name: ..., body: ...)+.
18
+ # Wire-form construction is the registry's responsibility: as a leaf
19
+ # carrier this Source stays pure and +Catalog::Snippets#encode+ reads
20
+ # its attributes off the outside rather than asking it to self-encode.
21
+ class Source < Data.define(:name, :body)
22
+ # The +kind+ field value carried by source snippets in their Frame
23
+ # 3 wire envelope entry
24
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
25
+ KIND = "source"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "snippet/binary"
4
+ require_relative "snippet/source"
5
+
6
+ module Kobako
7
+ # Kobako::Snippet — value-object family for preloaded snippet entries
8
+ # held by +Kobako::Catalog::Snippets+
9
+ # ({docs/behavior.md B-32 / B-33}[link:../../docs/behavior.md]).
10
+ #
11
+ # +Source+ represents a single +#preload(code:, name:)+ entry; +Binary+
12
+ # represents a single +#preload(binary:)+ entry. Both are plain value
13
+ # objects with no dependency on the +Catalog::Snippets+ registry that
14
+ # holds them — the registry reads their attributes externally when
15
+ # encoding the wire envelope.
16
+ module Snippet
17
+ end
18
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+ require_relative "request"
5
+ require_relative "response"
6
+ require_relative "yield"
7
+ require_relative "yielder"
8
+
9
+ module Kobako
10
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
11
+ # owns the pure-function dispatcher that decodes guest-initiated
12
+ # Requests and produces encoded Responses.
13
+ module Transport
14
+ # Pure-function dispatcher for guest-initiated transport calls.
15
+ # Decodes a msgpack-encoded Request envelope, resolves the target
16
+ # object through the Catalog::Namespaces (path lookup) or
17
+ # Catalog::Handles (Handle lookup), invokes the method, and returns
18
+ # a msgpack-encoded Response envelope.
19
+ #
20
+ # The module is stateless — all mutable state is threaded through
21
+ # arguments so Dispatcher has no instance variables and no side
22
+ # effects beyond mutating the Catalog::Handles via +alloc+ when a
23
+ # non-wire-representable return value must be wrapped
24
+ # ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
25
+ #
26
+ # Entry point:
27
+ #
28
+ # Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
29
+ # # => msgpack-encoded Response bytes (never raises)
30
+ module Dispatcher
31
+ # Throw tag for the {Yielder}'s break unwind back to the
32
+ # dispatcher's +catch+ frame (B-25). +private_constant+ is a
33
+ # convention boundary — not a defence.
34
+ BREAK_THROW = :__kobako_break__
35
+ private_constant :BREAK_THROW
36
+
37
+ module_function
38
+
39
+ # Internal sentinel raised when target resolution fails. Mapped to
40
+ # Response.error with type="undefined". Contained at the wire boundary —
41
+ # not part of the public Kobako error taxonomy
42
+ # ({docs/behavior.md E-12}[link:../../../docs/behavior.md]).
43
+ class UndefinedTargetError < StandardError; end
44
+
45
+ # Dispatch a single transport request and return the encoded
46
+ # Response bytes ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
47
+ # Invoked from the +Runtime#on_dispatch+ Proc that
48
+ # +Kobako::Sandbox#initialize+ installs on the ext side; +namespaces+,
49
+ # +handler+, and +yield_to_guest+ are captured in that Proc's
50
+ # closure so the Dispatcher stays stateless and the registry doesn't
51
+ # need to publish accessors for the Sandbox-owned +Catalog::Handles+
52
+ # or +Runtime+. +yield_to_guest+ is a +String → String+ callable
53
+ # (typically +Runtime#yield_to_active_invocation+ bound as a lambda)
54
+ # used only when the Request carries +block_given: true+. Always
55
+ # returns a binary String — every failure path is reified as a
56
+ # Response.error envelope so the guest sees a transport error rather
57
+ # than a wasm trap.
58
+ def dispatch(request_bytes, namespaces, handler, yield_to_guest)
59
+ request = Kobako::Transport::Request.decode(request_bytes)
60
+ target = resolve_target(request.target, namespaces, handler)
61
+ args, kwargs = resolve_call_args(request, handler)
62
+ yielder = Yielder.new(yield_to_guest, BREAK_THROW) if request.block_given
63
+ value = catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }
64
+ encode_ok(value, handler)
65
+ rescue StandardError => e
66
+ encode_caught_error(e)
67
+ ensure
68
+ yielder&.invalidate!
69
+ end
70
+
71
+ # Resolve positional and keyword arguments off +request+ in one
72
+ # step. Both pass through {#resolve_arg} so Capability Handles
73
+ # round-trip back to the host-side Ruby object before the call
74
+ # reaches +public_send+.
75
+ def resolve_call_args(request, handler)
76
+ args = request.args.map { |v| resolve_arg(v, handler) }
77
+ kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handler) }
78
+ [args, kwargs]
79
+ end
80
+
81
+ # Map an error caught at the dispatch boundary to a +Response.error+
82
+ # envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
83
+ # rescue. Returns a msgpack-encoded Response envelope (binary). Three
84
+ # error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
85
+ # +Kobako::Codec::Error+ → type="runtime" (malformed request);
86
+ # +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
87
+ # type="argument" (B-12 arity mismatch); everything else →
88
+ # type="runtime".
89
+ def encode_caught_error(error)
90
+ case error
91
+ when Kobako::Codec::Error then encode_error("runtime",
92
+ "Sandbox received a malformed request: #{error.message}")
93
+ when UndefinedTargetError then encode_error("undefined", error.message)
94
+ when ArgumentError then encode_error("argument", error.message)
95
+ else encode_error("runtime", "#{error.class}: #{error.message}")
96
+ end
97
+ end
98
+
99
+ # Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
100
+ # (the +Request+ invariant pins it). The empty-kwargs branch omits
101
+ # the +**+ splat so Ruby 3.x's strict kwargs separation does not
102
+ # reject calls to no-kwarg methods when the wire carries the
103
+ # uniform empty-map shape.
104
+ #
105
+ # +yielder+ is the host-side {Yielder} materialised when the guest
106
+ # call site supplied a block ({docs/behavior.md
107
+ # B-23}[link:../../../docs/behavior.md]); its {Yielder#to_proc}
108
+ # rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
109
+ # so the same call site handles both cases without an explicit
110
+ # conditional.
111
+ def invoke(target, method, args, kwargs, yielder = nil)
112
+ block = yielder&.to_proc
113
+ if kwargs.empty?
114
+ target.public_send(method.to_sym, *args, &block)
115
+ else
116
+ target.public_send(method.to_sym, *args, **kwargs, &block)
117
+ end
118
+ end
119
+
120
+ # {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An Kobako::Handle arriving as a positional or keyword
121
+ # argument identifies a host-side object previously allocated by a prior
122
+ # transport call's Handle wrap (B-14). Resolve it back to the Ruby object before
123
+ # the dispatch reaches +public_send+.
124
+ def resolve_arg(value, handler)
125
+ case value
126
+ when Kobako::Handle
127
+ require_live_object!(value.id, handler)
128
+ else
129
+ value
130
+ end
131
+ end
132
+
133
+ # Resolve a Request target to the Ruby object the registry (or
134
+ # Catalog::Handles) holds. String targets go through the registry;
135
+ # Handle targets (ext 0x01) go through the Catalog::Handles.
136
+ #
137
+ # Target type is already validated by +Transport::Request.decode+
138
+ # before this method is reached, so no else-branch is needed here —
139
+ # the wire layer is the system boundary that enforces the invariant.
140
+ def resolve_target(target, namespaces, handler)
141
+ case target
142
+ when String
143
+ resolve_path(target, namespaces)
144
+ when Kobako::Handle
145
+ resolve_handle(target, handler)
146
+ end
147
+ end
148
+
149
+ def resolve_path(path, namespaces)
150
+ namespaces.lookup(path)
151
+ rescue KeyError => e
152
+ raise UndefinedTargetError, e.message
153
+ end
154
+
155
+ def resolve_handle(handle, handler)
156
+ require_live_object!(handle.id, handler)
157
+ end
158
+
159
+ # Resolve +id+ through the Catalog::Handles. An unknown id (E-13)
160
+ # surfaces as UndefinedTargetError.
161
+ def require_live_object!(id, handler)
162
+ handler.fetch(id)
163
+ rescue Kobako::SandboxError => e
164
+ raise UndefinedTargetError, e.message
165
+ end
166
+
167
+ # Encode +value+ as a +Response.ok+ envelope. When the value is not
168
+ # wire-representable per {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
169
+ # mapping, the +UnsupportedType+ rescue routes it through the
170
+ # Catalog::Handles via {#wrap_as_handle} and re-encodes with the Capability
171
+ # Handle in place ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
172
+ # path encodes exactly once.
173
+ def encode_ok(value, handler)
174
+ response = Kobako::Transport::Response.ok(value)
175
+ response.encode
176
+ rescue Kobako::Codec::UnsupportedType
177
+ encode_ok(wrap_as_handle(value, handler), handler)
178
+ end
179
+
180
+ # Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
181
+ # that the wire codec can carry ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
182
+ # Used as the fallback path of {#encode_ok} when +value+ has no wire
183
+ # representation.
184
+ def wrap_as_handle(value, handler)
185
+ handler.alloc(value)
186
+ end
187
+
188
+ def encode_error(type, message)
189
+ fault = Kobako::Fault.new(type: type, message: message)
190
+ response = Kobako::Transport::Response.error(fault)
191
+ response.encode
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module Kobako
6
+ module Transport
7
+ # +Kobako::SandboxError+ subclass raised when the host detects a
8
+ # structural violation of the wire contract while decoding bytes
9
+ # produced by the guest (a malformed Outcome envelope, a result body
10
+ # that fails msgpack decode, a Panic envelope missing required
11
+ # fields). Distinct from a Wasm trap (engine signalled the guest
12
+ # runtime is unrecoverable) and from a normal sandbox-layer failure
13
+ # (the script raised but the protocol was respected): a
14
+ # +Transport::Error+ always indicates the guest runtime is corrupted —
15
+ # the only safe recovery is to discard the Sandbox and start a new
16
+ # invocation.
17
+ #
18
+ # Inherits from +Kobako::SandboxError+ so a single
19
+ # +rescue Kobako::SandboxError+ still catches it; callers that want
20
+ # to distinguish wire-violation paths from script failures can
21
+ # +rescue Kobako::Transport::Error+ directly.
22
+ class Error < Kobako::SandboxError; end
23
+ end
24
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../handle"
4
+ require_relative "../codec"
5
+
6
+ module Kobako
7
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
8
+ # owns the Request value object and its +#encode+ / +.decode+ codec,
9
+ # plus the +STATUS_OK+ / +STATUS_ERROR+ constants shared with Response.
10
+ module Transport
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 Transport Request
19
+ # ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
20
+ #
21
+ # 5-element msgpack array:
22
+ # +[target, method_name, args, kwargs, block_given]+. +target+ is
23
+ # either a +String+ (+"Namespace::Member"+) or a {Handle}. SPEC pins
24
+ # +kwargs+ map keys to ext 0x00 Symbol; enforced at construction so
25
+ # the Value Object is the single source of truth. +block_given+ is a
26
+ # Boolean signalling whether the guest call site supplied a block
27
+ # (B-23); the block body itself never crosses the wire.
28
+ #
29
+ # Built on the +class X < Data.define(...)+ subclass form so the
30
+ # class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
31
+ # for the rationale.
32
+ class Request < Data.define(:target, :method_name, :args, :kwargs, :block_given)
33
+ def initialize(target:, method_name:, args: [], kwargs: {}, block_given: false)
34
+ unless target.is_a?(String) || target.is_a?(Kobako::Handle)
35
+ raise ArgumentError, "Request target must be String or Kobako::Handle, got #{target.class}"
36
+ end
37
+ raise ArgumentError, "Request method_name must be String" unless method_name.is_a?(String)
38
+ raise ArgumentError, "Request args must be Array" unless args.is_a?(Array)
39
+ unless block_given.is_a?(TrueClass) || block_given.is_a?(FalseClass)
40
+ raise ArgumentError, "Request block_given must be Boolean, got #{block_given.class}"
41
+ end
42
+
43
+ validate_kwargs!(kwargs)
44
+ super
45
+ end
46
+
47
+ # Encode this Request to msgpack bytes. The Value Object's own
48
+ # invariants are the contract; this method does not re-check the shape.
49
+ def encode
50
+ Codec::Encoder.encode([target, method_name, args, kwargs, block_given])
51
+ end
52
+
53
+ # Decode +bytes+ into a {Request}. Raises +Codec::InvalidType+ when the
54
+ # envelope is not the expected 5-element msgpack array, or when the
55
+ # Value Object's construction invariants reject the decoded fields.
56
+ def self.decode(bytes)
57
+ Codec::Decoder.decode(bytes) do |arr|
58
+ unless arr.is_a?(Array) && arr.length == 5
59
+ raise Codec::InvalidType, "Request envelope is malformed (expected a 5-element array)"
60
+ end
61
+
62
+ target, method_name, args, kwargs, block_given = arr
63
+ new(target: target, method_name: method_name, args: args, kwargs: kwargs, block_given: block_given)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def validate_kwargs!(kwargs)
70
+ raise ArgumentError, "Request kwargs must be Hash" unless kwargs.is_a?(Hash)
71
+
72
+ kwargs.each_key do |k|
73
+ raise ArgumentError, "Request kwargs keys must be Symbol, got #{k.class}" unless k.is_a?(Symbol)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+ require_relative "../fault"
5
+ require_relative "request"
6
+
7
+ module Kobako
8
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
9
+ # owns the Response value object and its +#encode+ / +.decode+ codec.
10
+ module Transport
11
+ # Value object for a single host-side Transport Response
12
+ # ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
13
+ #
14
+ # 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
15
+ # (success) or 1 (fault). For success the second element is the return
16
+ # value; for fault it is a {Fault} (ext 0x02 envelope).
17
+ #
18
+ # Built on the +class X < Data.define(...)+ subclass form so the
19
+ # class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
20
+ # for the rationale.
21
+ class Response < Data.define(:status, :payload)
22
+ def self.ok(value)
23
+ new(status: STATUS_OK, payload: value)
24
+ end
25
+
26
+ def self.error(fault)
27
+ unless fault.is_a?(Kobako::Fault)
28
+ raise ArgumentError, "Response.error requires Kobako::Fault, got #{fault.class}"
29
+ end
30
+
31
+ new(status: STATUS_ERROR, payload: fault)
32
+ end
33
+
34
+ # Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
35
+ # envelope is not the expected 2-element msgpack array, or when the
36
+ # Value Object's construction invariants reject the decoded fields.
37
+ def self.decode(bytes)
38
+ Codec::Decoder.decode(bytes) do |arr|
39
+ unless arr.is_a?(Array) && arr.length == 2
40
+ raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
41
+ end
42
+
43
+ status, payload = arr
44
+ new(status: status, payload: payload)
45
+ end
46
+ end
47
+
48
+ def initialize(status:, payload:)
49
+ unless [STATUS_OK, STATUS_ERROR].include?(status)
50
+ raise ArgumentError, "Response status must be 0 (ok) or 1 (error), got #{status.inspect}"
51
+ end
52
+ if status == STATUS_ERROR && !payload.is_a?(Kobako::Fault)
53
+ raise ArgumentError, "Response with error status must carry a Kobako::Fault payload"
54
+ end
55
+
56
+ super
57
+ end
58
+
59
+ def ok? = status == STATUS_OK
60
+ def error? = status == STATUS_ERROR
61
+
62
+ # Encode this Response to msgpack bytes as the 2-element
63
+ # +[status, payload]+ array.
64
+ def encode
65
+ Codec::Encoder.encode([status, payload])
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../handle"
4
+ require_relative "../codec"
5
+
6
+ module Kobako
7
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
8
+ # owns the +Run+ envelope value object — the host→guest request shape
9
+ # consumed by +__kobako_run+.
10
+ module Transport
11
+ # Host-side value object for a single +Sandbox#run+ invocation
12
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md];
13
+ # {docs/behavior.md B-31}[link:../../../docs/behavior.md]).
14
+ #
15
+ # A Run captures the host-layer concept of "a single +#run+
16
+ # call": the entrypoint constant name plus its positional and keyword
17
+ # arguments. Host pre-flight (E-24 / E-25 / E-29 / E-30) is enforced at
18
+ # construction so the Value Object is the single source of truth —
19
+ # anything that passes +Run.new+ is safe to encode and ship to
20
+ # the guest.
21
+ #
22
+ # Run is the host→guest entrypoint dispatch envelope (the +#run+
23
+ # request shape), the symmetric counterpart to the guest→host
24
+ # +Request+ envelope. +#encode+ takes the Sandbox's
25
+ # +Catalog::Handles+ and routes any non-wire-representable +args+ /
26
+ # +kwargs+ leaf through it as a +Kobako::Handle+
27
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]) — the
28
+ # symmetric counterpart of the guest→host wrap path in the
29
+ # dispatcher (B-14). A +Kobako::Handle+ that arrives **already
30
+ # constructed** in the caller's +args+ / +kwargs+ is rejected at
31
+ # construction (E-29): legitimate Handles only enter Host App code
32
+ # through error fields, so a Handle reaching the call site is by
33
+ # definition smuggled in. The +#encode+ output is the "Run envelope"
34
+ # that ships through the +__kobako_run+ command buffer.
35
+ #
36
+ # Built on the +class X < Data.define(...)+ subclass form (the
37
+ # Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
38
+ class Run < Data.define(:entrypoint, :args, :kwargs)
39
+ # Ruby constant-name pattern enforced on the +entrypoint+ Symbol
40
+ # ({docs/behavior.md E-25}[link:../../../docs/behavior.md]). Parallel to
41
+ # +Kobako::Catalog::Snippets::NAME_PATTERN+; the two constants name the
42
+ # same regex but cover distinct surfaces (snippet identity vs.
43
+ # entrypoint resolution) so a future divergence stays local.
44
+ NAME_PATTERN = /\A[A-Z]\w*\z/
45
+
46
+ def initialize(entrypoint:, args: [], kwargs: {})
47
+ entrypoint = normalize_entrypoint(entrypoint)
48
+ args = validate_args!(args)
49
+ kwargs = validate_kwargs!(kwargs)
50
+ super
51
+ end
52
+
53
+ # Encode this Run to the msgpack bytes the guest's +__kobako_run+
54
+ # entry point consumes as its command-buffer payload
55
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
56
+ # Walks +args+ / +kwargs+ through {Codec::Utils.deep_wrap} so any
57
+ # non-wire-representable leaf is allocated into +handler+ and
58
+ # replaced with a +Kobako::Handle+
59
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]); the
60
+ # +handler+ argument is the Sandbox's table, sharing the same
61
+ # allocator the guest→host return path (B-14) uses.
62
+ #
63
+ # Layout: msgpack map with string keys +"entrypoint"+ (Symbol via
64
+ # ext 0x00), +"args"+ (Array), +"kwargs"+ (Map with Symbol keys);
65
+ # any wrapped leaf rides as ext 0x01 in its original position
66
+ # (docs/wire-codec.md § ext 0x01 position rules).
67
+ def encode(handler)
68
+ Codec::Encoder.encode(
69
+ "entrypoint" => entrypoint,
70
+ "args" => Codec::Utils.deep_wrap(args, handler),
71
+ "kwargs" => Codec::Utils.deep_wrap(kwargs, handler)
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ # E-24: target must be a Symbol or String (TypeError, not
78
+ # ArgumentError — the wrong-type case is a Host App programming
79
+ # error before the run reaches the guest). E-25: after +.to_s+
80
+ # the value must match NAME_PATTERN (ArgumentError), rejecting
81
+ # +::+-segmented names and any non-constant form.
82
+ def normalize_entrypoint(target)
83
+ unless target.is_a?(Symbol) || target.is_a?(String)
84
+ raise TypeError, "entrypoint must be a Symbol or String, got #{target.class}"
85
+ end
86
+
87
+ target_str = target.to_s
88
+ unless NAME_PATTERN.match?(target_str)
89
+ raise ArgumentError,
90
+ "entrypoint must match #{NAME_PATTERN.inspect} (got #{target.inspect})"
91
+ end
92
+
93
+ target_str.to_sym
94
+ end
95
+
96
+ # E-29: +args+ must not contain a +Kobako::Handle+. The Handle
97
+ # allocator lives inside the Host Gem; legitimate paths surface
98
+ # Handle objects only through raised error fields, so a Handle
99
+ # reaching +args+ is a forged or smuggled token. Non-wire-
100
+ # representable arguments that are not Handles are handled by
101
+ # auto-wrap inside +#encode+ (B-34) — the reject path is reserved
102
+ # for Handle objects specifically.
103
+ def validate_args!(args)
104
+ raise ArgumentError, "arguments must be an Array" unless args.is_a?(Array)
105
+ raise ArgumentError, forged_handle_message("arguments") if args.any?(Kobako::Handle)
106
+
107
+ args
108
+ end
109
+
110
+ # E-30 covers the non-Symbol kwargs-key case; E-29 also rejects a
111
+ # +Kobako::Handle+ arriving as a kwargs value (same forged-token
112
+ # principle as the +args+ branch). Both checks live here so the
113
+ # Host App sees the host-side error message before any encode /
114
+ # decode boundary.
115
+ def validate_kwargs!(kwargs)
116
+ raise ArgumentError, "keyword arguments must be a Hash" unless kwargs.is_a?(Hash)
117
+
118
+ bad_keys = kwargs.each_key.grep_v(Symbol)
119
+ unless bad_keys.empty?
120
+ raise ArgumentError,
121
+ "keyword argument keys must be Symbols (got #{bad_keys.inspect})"
122
+ end
123
+ raise ArgumentError, forged_handle_message("keyword argument values") if kwargs.each_value.any?(Kobako::Handle)
124
+
125
+ kwargs
126
+ end
127
+
128
+ # Single source of truth for the E-29 reject message so the args
129
+ # and kwargs branches stay phrased identically. Message stays in
130
+ # caller vocabulary: it names the affected slot and the reason
131
+ # without leaking SPEC anchor identifiers (B-xx / E-xx live in
132
+ # source comments, not user-visible errors) or self-referential
133
+ # architecture terms — the error is raised BY kobako, so saying
134
+ # "allocated by the Host Gem" reads as third-person about self.
135
+ def forged_handle_message(slot)
136
+ "#{slot} must not contain a Kobako::Handle — " \
137
+ "Handles are created internally by the Sandbox and cannot be passed in"
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+
5
+ module Kobako
6
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
7
+ # owns the +Yield+ envelope value object plus its +#encode+ / +.decode+
8
+ # codec for the +__kobako_yield_to_block+ wire form.
9
+ module Transport
10
+ # First byte of the YieldResponse for the success branch — body is
11
+ # the block's return value encoded as a single msgpack value.
12
+ TAG_OK = 0x01
13
+ # First byte for `break val` — body is the break value.
14
+ TAG_BREAK = 0x02
15
+ # Reserved for future `return val` support; both sides reject this
16
+ # tag as a wire violation (YieldResponse envelope contract).
17
+ TAG_RESERVED = 0x03
18
+ # First byte for an error / fault outcome — body is a
19
+ # +{"class", "message", "backtrace"}+ Hash.
20
+ TAG_ERROR = 0x04
21
+
22
+ # Tags both sides currently accept on the wire.
23
+ LIVE_TAGS = [TAG_OK, TAG_BREAK, TAG_ERROR].freeze
24
+
25
+ # Value object for a single YieldResponse envelope
26
+ # ({docs/wire-codec.md YieldResponse Envelope}[link:../../../docs/wire-codec.md]).
27
+ #
28
+ # The wire form is a one-byte tag followed by an msgpack payload.
29
+ # The three live tags are +0x01+ (ok), +0x02+ (break), and +0x04+
30
+ # (error); +0x03+ is reserved and rejected by both sides.
31
+ #
32
+ # +value+ carries whatever the wire payload decoded to — a plain
33
+ # Ruby value for the +ok+ / +break+ tags, and a +{"class",
34
+ # "message", "backtrace"}+ Hash for the +error+ tag. No further
35
+ # shape constraint is enforced here; the host-side dispatcher
36
+ # decides how to translate each variant into Ruby control flow.
37
+ #
38
+ # Lives alongside the other envelope value objects (+Request+,
39
+ # +Response+) since it is the guest-to-host shape used
40
+ # mid-dispatch-frame to answer a +__kobako_yield_to_block+ re-entry.
41
+ class Yield < Data.define(:tag, :value)
42
+ def initialize(tag:, value:)
43
+ unless Kobako::Transport::LIVE_TAGS.include?(tag)
44
+ raise ArgumentError,
45
+ "Yield tag must be one of #{Kobako::Transport::LIVE_TAGS.inspect}, got #{tag.inspect}"
46
+ end
47
+
48
+ super
49
+ end
50
+
51
+ def ok? = tag == Kobako::Transport::TAG_OK
52
+ def break? = tag == Kobako::Transport::TAG_BREAK
53
+ def error? = tag == Kobako::Transport::TAG_ERROR
54
+
55
+ # Encode this Yield to YieldResponse bytes: one tag byte followed
56
+ # by an msgpack-encoded +value+.
57
+ def encode
58
+ [tag].pack("C") + Codec::Encoder.encode(value)
59
+ end
60
+
61
+ # Decode +bytes+ into a {Yield}. Rejects empty input, the reserved
62
+ # tag 0x03, and any tag outside +LIVE_TAGS+ by raising
63
+ # +Kobako::Codec::InvalidType+ — these are wire violations per the
64
+ # SPEC's YieldResponse envelope contract.
65
+ def self.decode(bytes)
66
+ bytes = bytes.b
67
+ raise Codec::InvalidType, "YieldResponse must carry at least one byte" if bytes.empty?
68
+
69
+ tag = bytes.getbyte(0) # : Integer
70
+ body = bytes.byteslice(1, bytes.bytesize - 1) || +""
71
+
72
+ reject_dead_tag!(tag)
73
+ new(tag: tag, value: Codec::Decoder.decode(body))
74
+ end
75
+
76
+ def self.reject_dead_tag!(tag)
77
+ return if LIVE_TAGS.include?(tag)
78
+
79
+ msg = if tag == TAG_RESERVED
80
+ "YieldResponse tag 0x03 is reserved"
81
+ else
82
+ format(
83
+ "YieldResponse tag 0x%02x is not recognised", tag
84
+ )
85
+ end
86
+ raise Codec::InvalidType, msg
87
+ end
88
+ private_class_method :reject_dead_tag!
89
+ end
90
+ end
91
+ end