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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +85 -6
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +22 -18
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +195 -81
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -7
- data/lib/kobako/codec/factory.rb +21 -18
- data/lib/kobako/codec/utils.rb +118 -29
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +60 -0
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +55 -29
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +131 -67
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/transport/error.rb +24 -0
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -3
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/{rpc/handle_table.rbs → catalog/handles.rbs} +3 -9
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +3 -3
- data/sig/kobako/codec/utils.rbs +11 -1
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +18 -0
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +10 -7
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- metadata +52 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -161
- data/ext/kobako/src/wasm/instance.rs +0 -771
- data/ext/kobako/src/wasm.rs +0 -125
- data/lib/kobako/invocation.rb +0 -112
- data/lib/kobako/rpc/dispatcher.rb +0 -169
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/handle.rb +0 -39
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -158
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/invocation.rbs +0 -23
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -37
- data/sig/kobako/wasm.rbs +0 -39
|
@@ -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
|