kobako 0.4.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 +0 -1
- 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 +12 -16
- 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} +94 -86
- 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 -5
- data/lib/kobako/codec/factory.rb +12 -12
- data/lib/kobako/codec/utils.rb +56 -59
- 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 +4 -6
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +31 -35
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +83 -72
- 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/{rpc/wire_error.rb → transport/error.rb} +7 -6
- 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/version.rb +1 -1
- data/lib/kobako.rb +4 -4
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/catalog/handles.rbs +19 -0
- 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 +2 -2
- data/sig/kobako/codec/utils.rbs +5 -5
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +2 -3
- 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 +5 -8
- 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
- metadata +48 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -162
- data/ext/kobako/src/wasm/instance.rs +0 -873
- data/ext/kobako/src/wasm.rs +0 -126
- data/lib/kobako/handle_table.rb +0 -119
- data/lib/kobako/invocation.rb +0 -143
- data/lib/kobako/rpc/dispatcher.rb +0 -171
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -146
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/handle_table.rbs +0 -23
- data/sig/kobako/invocation.rbs +0 -25
- 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/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -31
- data/sig/kobako/rpc/wire_error.rbs +0 -6
- data/sig/kobako/wasm.rbs +0 -41
|
@@ -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
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../codec"
|
|
4
|
+
require_relative "yield"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
8
|
+
# owns the host-side object that materialises a guest-supplied block as
|
|
9
|
+
# a Ruby callable the Service method can yield into.
|
|
10
|
+
module Transport
|
|
11
|
+
# Host-side stand-in for a guest-supplied block (B-23).
|
|
12
|
+
#
|
|
13
|
+
# Each guest call that carries +block_given: true+ gets a Yielder
|
|
14
|
+
# that the Dispatcher hands to the Service method as +&block+. The
|
|
15
|
+
# Service method observes it as an ordinary Ruby Proc through
|
|
16
|
+
# {#to_proc}; +yield val+ / +block.call(val)+ invokes {#yield}, which
|
|
17
|
+
# serialises the positional args, re-enters the guest via the injected
|
|
18
|
+
# +yield_to_guest+ lambda
|
|
19
|
+
# ({docs/behavior.md B-24}[link:../../../docs/behavior.md]), and
|
|
20
|
+
# reifies the +YieldResponse+ into Ruby control flow:
|
|
21
|
+
#
|
|
22
|
+
# * +tag 0x01+ ok — return the decoded value to +yield+'s caller
|
|
23
|
+
# * +tag 0x02+ break — +throw break_tag, value+ so the Dispatcher's
|
|
24
|
+
# +catch+ frame unwinds the Service method
|
|
25
|
+
# ({docs/behavior.md B-25}[link:../../../docs/behavior.md])
|
|
26
|
+
# * +tag 0x04+ error — raise the +{class, message}+ payload at the
|
|
27
|
+
# Service's yield site
|
|
28
|
+
#
|
|
29
|
+
# The Dispatcher calls {#invalidate!} from its +ensure+ block once
|
|
30
|
+
# dispatch completes; any later call to a stashed Yielder then raises
|
|
31
|
+
# +LocalJumpError+ — the observable shape of
|
|
32
|
+
# {docs/behavior.md E-23}[link:../../../docs/behavior.md] (escaped
|
|
33
|
+
# Yielder).
|
|
34
|
+
class Yielder
|
|
35
|
+
# +yield_to_guest+ is a +String → String+ callable (typically
|
|
36
|
+
# +Runtime#yield_to_active_invocation+ bound through a lambda) that
|
|
37
|
+
# {#yield} invokes to re-enter the guest; +break_tag+ is the +catch+
|
|
38
|
+
# throw tag the Dispatcher matches against to unwind the Service on
|
|
39
|
+
# +tag 0x02+.
|
|
40
|
+
def initialize(yield_to_guest, break_tag)
|
|
41
|
+
@yield_to_guest = yield_to_guest
|
|
42
|
+
@break_tag = break_tag
|
|
43
|
+
@active = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Re-enter the guest with +args+ and reify the YieldResponse into
|
|
47
|
+
# Ruby control flow. Raises +LocalJumpError+ if called after
|
|
48
|
+
# {#invalidate!} (E-23).
|
|
49
|
+
def yield(*args)
|
|
50
|
+
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
51
|
+
|
|
52
|
+
response = Kobako::Transport::Yield.decode(@yield_to_guest.call(Kobako::Codec::Encoder.encode(args)))
|
|
53
|
+
return response.value if response.ok?
|
|
54
|
+
|
|
55
|
+
throw @break_tag, response.value if response.break?
|
|
56
|
+
|
|
57
|
+
raise yield_failure(response.value, default: "yield error")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The Proc the Dispatcher passes as +&block+, binding {#yield} so a
|
|
61
|
+
# Service method's +yield+ / +block.call+ drives the round-trip.
|
|
62
|
+
def to_proc
|
|
63
|
+
method(:yield).to_proc
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Mark this Yielder dead. Called by the Dispatcher's +ensure+ block
|
|
67
|
+
# when the originating dispatch frame returns; any later {#yield}
|
|
68
|
+
# call then raises +LocalJumpError+ (E-23).
|
|
69
|
+
def invalidate!
|
|
70
|
+
@active = false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Reify a +YieldResponse+ tag 0x04 payload into a +RuntimeError+ the
|
|
76
|
+
# Service method observes at its +yield+ site. The +{class, message,
|
|
77
|
+
# backtrace}+ shape mirrors the +Kobako::Transport::Yield+ tag 0x04
|
|
78
|
+
# payload; +default+ provides a fallback when the payload is not a
|
|
79
|
+
# Hash.
|
|
80
|
+
def yield_failure(payload, default:)
|
|
81
|
+
return RuntimeError.new(default) unless payload.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
klass = payload["class"] || "RuntimeError"
|
|
84
|
+
message = payload["message"] || default
|
|
85
|
+
RuntimeError.new("#{klass}: #{message}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "transport/request"
|
|
4
|
+
require_relative "transport/response"
|
|
5
|
+
require_relative "transport/run"
|
|
6
|
+
require_relative "transport/yield"
|
|
7
|
+
require_relative "transport/yielder"
|
|
8
|
+
require_relative "transport/error"
|
|
9
|
+
require_relative "transport/dispatcher"
|
|
10
|
+
|
|
11
|
+
module Kobako
|
|
12
|
+
# Kobako::Transport — host↔guest message transport namespace.
|
|
13
|
+
# Houses the envelope value objects (Request / Response / Run / Yield),
|
|
14
|
+
# the guest→host +Dispatcher+, and the host→guest +Yielder+.
|
|
15
|
+
# +Sandbox#initialize+ composes them onto the
|
|
16
|
+
# +Runtime+ as a dispatch +Proc+ + +yield_to_guest+ lambda pair
|
|
17
|
+
# ({docs/behavior.md B-12}[link:../../docs/behavior.md]). "RPC" was
|
|
18
|
+
# deliberately not chosen — it implies a cross-process boundary that
|
|
19
|
+
# kobako does not have, since host and guest share one OS thread and
|
|
20
|
+
# one wasm linear memory. See
|
|
21
|
+
# {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md].
|
|
22
|
+
module Transport
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require_relative "kobako/version"
|
|
4
4
|
require "kobako/kobako"
|
|
5
5
|
require_relative "kobako/errors"
|
|
6
|
-
require_relative "kobako/
|
|
7
|
-
require_relative "kobako/
|
|
8
|
-
require_relative "kobako/
|
|
9
|
-
require_relative "kobako/
|
|
6
|
+
require_relative "kobako/transport"
|
|
7
|
+
require_relative "kobako/catalog"
|
|
8
|
+
require_relative "kobako/runtime"
|
|
9
|
+
require_relative "kobako/snapshot"
|
|
10
10
|
require_relative "kobako/sandbox"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"release-type": "ruby",
|
|
4
|
+
"last-release-sha": "5694da60b08931ea260e13025689b8d8c47d767a",
|
|
5
|
+
"packages": {
|
|
6
|
+
".": {
|
|
7
|
+
"component": "kobako",
|
|
8
|
+
"include-component-in-tag": false,
|
|
9
|
+
"release-type": "ruby"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"extra-files": [
|
|
13
|
+
{
|
|
14
|
+
"type": "toml",
|
|
15
|
+
"path": "ext/kobako/Cargo.toml",
|
|
16
|
+
"jsonpath": "$.package.version"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "toml",
|
|
20
|
+
"path": "Cargo.lock",
|
|
21
|
+
"jsonpath": "$.package[?(@.name=='kobako')].version"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
data/sig/kobako/capture.rbs
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Kobako
|
|
2
|
+
module Catalog
|
|
3
|
+
class Handles
|
|
4
|
+
def initialize: (?next_id: Integer) -> void
|
|
5
|
+
|
|
6
|
+
def alloc: (untyped object) -> Kobako::Handle
|
|
7
|
+
|
|
8
|
+
def fetch: (Integer id) -> untyped
|
|
9
|
+
|
|
10
|
+
def reset!: () -> self
|
|
11
|
+
|
|
12
|
+
def size: () -> Integer
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def require_bound!: (Integer id) -> void
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Kobako
|
|
2
|
+
module Catalog
|
|
3
|
+
class Namespaces
|
|
4
|
+
def initialize: (?handler: Kobako::Catalog::Handles) -> void
|
|
5
|
+
|
|
6
|
+
def define: (Symbol | String name) -> Kobako::Namespace
|
|
7
|
+
|
|
8
|
+
def lookup: (String target) -> untyped
|
|
9
|
+
|
|
10
|
+
def encode: () -> String
|
|
11
|
+
|
|
12
|
+
def seal!: () -> self
|
|
13
|
+
|
|
14
|
+
def sealed?: () -> bool
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Kobako
|
|
2
|
-
module
|
|
3
|
-
class
|
|
2
|
+
module Catalog
|
|
3
|
+
class Snippets
|
|
4
4
|
NAME_PATTERN: Regexp
|
|
5
5
|
|
|
6
6
|
type entry = Kobako::Snippet::Source | Kobako::Snippet::Binary
|
|
@@ -11,15 +11,6 @@ module Kobako
|
|
|
11
11
|
|
|
12
12
|
def encode: () -> String
|
|
13
13
|
|
|
14
|
-
def each: () { (entry) -> void } -> Array[entry]
|
|
15
|
-
| () -> Enumerator[entry, Array[entry]]
|
|
16
|
-
|
|
17
|
-
def names: () -> Array[Symbol]
|
|
18
|
-
|
|
19
|
-
def size: () -> Integer
|
|
20
|
-
|
|
21
|
-
def empty?: () -> bool
|
|
22
|
-
|
|
23
14
|
private
|
|
24
15
|
|
|
25
16
|
def register_source!: (String? code, (Symbol | String)? name) -> Symbol
|
|
@@ -24,8 +24,8 @@ module Kobako
|
|
|
24
24
|
def register_handle: () -> void
|
|
25
25
|
def register_fault: () -> void
|
|
26
26
|
def unpack_handle: (String payload) -> Kobako::Handle
|
|
27
|
-
def pack_fault: (Kobako::
|
|
28
|
-
def unpack_fault: (String payload) -> Kobako::
|
|
27
|
+
def pack_fault: (Kobako::Fault fault) -> String
|
|
28
|
+
def unpack_fault: (String payload) -> Kobako::Fault
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
data/sig/kobako/codec/utils.rbs
CHANGED
|
@@ -5,15 +5,15 @@ module Kobako
|
|
|
5
5
|
|
|
6
6
|
def self?.assert_utf8!: (String string, String label) -> void
|
|
7
7
|
|
|
8
|
-
def self?.
|
|
8
|
+
def self?.with_boundary: [T] () { () -> T } -> T
|
|
9
9
|
|
|
10
|
-
def self?.
|
|
10
|
+
def self?.representable?: (untyped value) -> bool
|
|
11
11
|
|
|
12
|
-
def self?.deep_wrap: (untyped value, Kobako::
|
|
12
|
+
def self?.deep_wrap: (untyped value, Kobako::Catalog::Handles handler) -> untyped
|
|
13
13
|
|
|
14
|
-
def self?.
|
|
14
|
+
def self?.primitive_type?: (untyped value) -> bool
|
|
15
15
|
|
|
16
|
-
def self?.
|
|
16
|
+
def self?.container_representable?: (untyped value) -> bool
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|