kobako 0.26.0-aarch64-linux → 0.28.0-aarch64-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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +103 -0
- data/README.md +156 -118
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +38 -0
- data/crates/kobako-runtime/README.md +10 -12
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/README.md +8 -10
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +6 -3
- data/rust-toolchain.toml +0 -11
|
@@ -4,31 +4,17 @@ require_relative "../errors"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Transport
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# decode. Distinct from a Wasm trap (engine signalled the guest
|
|
11
|
-
# runtime is unrecoverable) and from a normal sandbox-layer failure
|
|
12
|
-
# (the script raised but the protocol was respected): a
|
|
13
|
-
# +Transport::Error+ always indicates the guest runtime is corrupted —
|
|
14
|
-
# the only safe recovery is to discard the Sandbox and start a new
|
|
15
|
-
# invocation.
|
|
16
|
-
#
|
|
17
|
-
# Inherits from +Kobako::SandboxError+ so a single
|
|
18
|
-
# +rescue Kobako::SandboxError+ still catches it; callers that want
|
|
19
|
-
# to distinguish wire-violation paths from script failures can
|
|
20
|
-
# +rescue Kobako::Transport::Error+ directly.
|
|
7
|
+
# The guest broke the exchange with the host: it produced a value that
|
|
8
|
+
# could not be decoded, or a dispatch it made could not complete. Being a
|
|
9
|
+
# SandboxError, it is caught by rescuing that too.
|
|
21
10
|
class Error < Kobako::SandboxError
|
|
22
|
-
def initialize(message, diagnostic: nil, **)
|
|
11
|
+
def initialize(message, diagnostic: nil, **) # :nodoc:
|
|
23
12
|
super(message, **)
|
|
24
13
|
@diagnostic = diagnostic
|
|
25
14
|
end
|
|
26
15
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# be …" wording, so it stays out of #message; an operator
|
|
30
|
-
# triaging a corrupted runtime still needs it, and
|
|
31
|
-
# +detailed_message+ is where Ruby puts text of exactly that kind.
|
|
16
|
+
# Appends the codec's own reason, which an operator needs and a caller
|
|
17
|
+
# cannot act on, so #message leaves it out.
|
|
32
18
|
def detailed_message(...)
|
|
33
19
|
rendered = super
|
|
34
20
|
return rendered if @diagnostic.nil?
|
|
@@ -6,16 +6,11 @@ require_relative "reflection"
|
|
|
6
6
|
|
|
7
7
|
module Kobako
|
|
8
8
|
module Transport
|
|
9
|
-
# The methods
|
|
10
|
-
#
|
|
11
|
-
# is
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
# cannot foresee handing over are the ones it never wrote.
|
|
15
|
-
#
|
|
16
|
-
# Built on the +class X < Data.define(...)+ subclass form so the class
|
|
17
|
-
# body is fully Steep-visible; see +.rubocop.yml+ for the rationale.
|
|
18
|
-
class Exposure < Data.define(:object, :names)
|
|
9
|
+
# The methods one reference lets the guest call, fixed when the reference
|
|
10
|
+
# is made. Only what the object's own class and the object define in
|
|
11
|
+
# source is exposed, since a Host App cannot foresee handing over what it
|
|
12
|
+
# never wrote.
|
|
13
|
+
class Exposure < Data.define(:object, :names) # :nodoc:
|
|
19
14
|
PREDICATE = :respond_to_guest?
|
|
20
15
|
private_constant :PREDICATE
|
|
21
16
|
|
|
@@ -2,15 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Transport
|
|
5
|
-
|
|
6
|
-
# Dispatcher reaches +public_send+: which method names on a resolved
|
|
7
|
-
# target count as Service behaviour, and which are Ruby's ambient
|
|
8
|
-
# metaprogramming surface.
|
|
9
|
-
#
|
|
10
|
-
# Answers with a refusal reason rather than raising, so the error
|
|
11
|
-
# taxonomy stays with the Dispatcher and this module holds only the
|
|
12
|
-
# policy.
|
|
13
|
-
module Reflection
|
|
5
|
+
module Reflection # :nodoc:
|
|
14
6
|
# Modules whose instance methods are ambient Ruby reflection /
|
|
15
7
|
# metaprogramming surface (+send+, +public_send+, +instance_eval+,
|
|
16
8
|
# +method+, +tap+, +instance_variable_get+, ...) rather than Service
|
|
@@ -36,59 +28,56 @@ module Kobako
|
|
|
36
28
|
# callable exposes by default, so the floor and the Exposure agree.
|
|
37
29
|
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
38
30
|
|
|
39
|
-
module_function
|
|
40
|
-
|
|
41
31
|
# The reason +name+ is unreachable through +exposure+, or +nil+ when
|
|
42
32
|
# the dispatch may proceed. Composes the ambient-surface floor with the
|
|
43
33
|
# reference's Exposure, in that order: the Exposure only narrows and
|
|
44
34
|
# can never re-open what the floor rejects.
|
|
45
|
-
def refusal(exposure, name)
|
|
35
|
+
def self.refusal(exposure, name)
|
|
46
36
|
ambient_refusal(exposure.object, name) ||
|
|
47
37
|
(exposure.exposes?(name) ? nil : "method #{name.inspect} is not exposed to the guest")
|
|
48
38
|
end
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# class, the owner of every class-level method (+File.popen+ /
|
|
53
|
-
# +Kernel.system+, unreachable via any fixed core-module list) — is
|
|
54
|
-
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
55
|
-
# stays invocable). A name with no concrete public method is allowed
|
|
56
|
-
# only when the target opts into it via +respond_to?+ (dynamic
|
|
57
|
-
# +method_missing+ Services), since the dangerous methods are all
|
|
58
|
-
# concretely defined and therefore never reach that branch.
|
|
59
|
-
#
|
|
60
|
-
# +method_missing+ named explicitly is refused whatever its owner:
|
|
61
|
-
# it is Ruby's dynamic-dispatch hook, and a public override (a
|
|
62
|
-
# +Delegator+) binds and calls the private method the guest passes as
|
|
63
|
-
# its first argument (+Kernel#system+). A dynamic +method_missing+
|
|
64
|
-
# Service is untouched — the guest reaches it by the virtual name,
|
|
65
|
-
# which resolves through the +NameError+ branch below, never by
|
|
66
|
-
# naming +method_missing+.
|
|
67
|
-
def ambient_refusal(target, name)
|
|
68
|
-
return "method #{name.inspect} is not a Service method" if name == :method_missing
|
|
40
|
+
class << self
|
|
41
|
+
private
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
# Guard against ambient reflection methods. A public method whose
|
|
44
|
+
# owner is a META_OWNERS or GADGET_OWNERS module — or a singleton
|
|
45
|
+
# class, the owner of every class-level method (+File.popen+ /
|
|
46
|
+
# +Kernel.system+, unreachable via any fixed core-module list) — is
|
|
47
|
+
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
48
|
+
# stays invocable). A name with no concrete public method is allowed
|
|
49
|
+
# only when the target opts into it via +respond_to?+ (dynamic
|
|
50
|
+
# +method_missing+ Services), since the dangerous methods are all
|
|
51
|
+
# concretely defined and therefore never reach that branch.
|
|
52
|
+
#
|
|
53
|
+
# +method_missing+ named explicitly is refused whatever its owner:
|
|
54
|
+
# it is Ruby's dynamic-dispatch hook, and a public override (a
|
|
55
|
+
# +Delegator+) binds and calls the private method the guest passes as
|
|
56
|
+
# its first argument (+Kernel#system+). A dynamic +method_missing+
|
|
57
|
+
# Service is untouched — the guest reaches it by the virtual name,
|
|
58
|
+
# which resolves through the +NameError+ branch below, never by
|
|
59
|
+
# naming +method_missing+.
|
|
60
|
+
def ambient_refusal(target, name)
|
|
61
|
+
return "method #{name.inspect} is not a Service method" if name == :method_missing
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
owner = target.public_method(name).owner
|
|
64
|
+
return nil unless ambient_owner?(owner, target)
|
|
65
|
+
return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
|
|
77
66
|
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
"method #{name.inspect} is not a Service method"
|
|
68
|
+
rescue NameError
|
|
69
|
+
return nil if target.respond_to?(name)
|
|
70
|
+
|
|
71
|
+
"no public method #{name.inspect} on target"
|
|
72
|
+
end
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def ambient_owner?(owner, target)
|
|
89
|
-
META_OWNERS.include?(owner) ||
|
|
90
|
-
GADGET_OWNERS.include?(owner) ||
|
|
91
|
-
(target.is_a?(Module) && owner.singleton_class?)
|
|
74
|
+
# A plain object's own singleton method (+def obj.x+) stays reachable,
|
|
75
|
+
# since only a Module target counts its singleton class as ambient.
|
|
76
|
+
def ambient_owner?(owner, target)
|
|
77
|
+
META_OWNERS.include?(owner) ||
|
|
78
|
+
GADGET_OWNERS.include?(owner) ||
|
|
79
|
+
(target.is_a?(Module) && owner.singleton_class?)
|
|
80
|
+
end
|
|
92
81
|
end
|
|
93
82
|
end
|
|
94
83
|
end
|
data/lib/kobako/transport/run.rb
CHANGED
|
@@ -1,44 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../constant_name"
|
|
3
4
|
require_relative "../handle"
|
|
4
5
|
require_relative "../codec"
|
|
5
6
|
require_relative "../payload"
|
|
6
7
|
|
|
7
8
|
module Kobako
|
|
8
|
-
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
9
|
-
# owns +Run+, the host-side value object for one +#run+ request — the
|
|
10
|
-
# native side frames it into the envelope +__kobako_run+ consumes.
|
|
11
9
|
module Transport
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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 (entrypoint type / name pattern, forged
|
|
18
|
-
# Handle, kwargs-key type) is enforced at construction so the Value
|
|
19
|
-
# Object is the single source of truth — anything that passes
|
|
20
|
-
# +Run.new+ is safe to ship to the guest.
|
|
21
|
-
#
|
|
22
|
-
# +#payload+ takes the invocation's +Catalog::Handles+ and routes any
|
|
23
|
-
# non-wire-representable +args+ / +kwargs+ leaf through it as a
|
|
24
|
-
# +Kobako::Handle+ — the symmetric counterpart of the guest→host wrap
|
|
25
|
-
# path in the dispatcher. A +Kobako::Handle+ that arrives **already
|
|
26
|
-
# constructed** in the caller's +args+ / +kwargs+ is rejected at
|
|
27
|
-
# construction: legitimate Handles only enter Host App code through
|
|
28
|
-
# error fields, so a Handle reaching the call site is by definition
|
|
29
|
-
# smuggled in. +Runtime#run+ frames that payload with the entrypoint
|
|
30
|
-
# into the Run envelope the +__kobako_run+ command buffer carries.
|
|
31
|
-
#
|
|
32
|
-
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
33
|
-
# Steep-friendly shape — see +.rubocop.yml+ for the rationale).
|
|
34
|
-
class Run < Data.define(:entrypoint, :args, :kwargs)
|
|
35
|
-
# Ruby constant-name pattern enforced on the +entrypoint+ Symbol.
|
|
36
|
-
# Parallel to
|
|
37
|
-
# +Kobako::Catalog::Snippets::NAME_PATTERN+; the two constants name the
|
|
38
|
-
# same regex but cover distinct surfaces (snippet identity vs.
|
|
39
|
-
# entrypoint resolution) so a future divergence stays local.
|
|
40
|
-
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
41
|
-
|
|
10
|
+
# A Handle already in the arguments is refused, since a caller never
|
|
11
|
+
# legitimately holds one.
|
|
12
|
+
class Run < Data.define(:entrypoint, :args, :kwargs) # :nodoc:
|
|
42
13
|
def initialize(entrypoint:, args: [], kwargs: {})
|
|
43
14
|
entrypoint = normalize_entrypoint(entrypoint)
|
|
44
15
|
validate_args!(args)
|
|
@@ -46,61 +17,42 @@ module Kobako
|
|
|
46
17
|
super
|
|
47
18
|
end
|
|
48
19
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
# invocation's table, sharing the same allocator the guest→host
|
|
55
|
-
# return path uses. A wrapped leaf rides as ext 0x01 in its
|
|
56
|
-
# original position (docs/wire/payload-msgpack.md § ext 0x01).
|
|
57
|
-
# The walk starts one level down, since +args+ and +kwargs+ ride
|
|
58
|
-
# inside the payload document the wire's nesting bound is counted
|
|
59
|
-
# from.
|
|
60
|
-
def payload(handler)
|
|
20
|
+
# A leaf with no wire form becomes a Capability Handle in the
|
|
21
|
+
# invocation's table. The walk starts one level down, since +args+ and
|
|
22
|
+
# +kwargs+ ride inside the payload document the wire's nesting bound
|
|
23
|
+
# is counted from.
|
|
24
|
+
def payload(handles)
|
|
61
25
|
Payload::Arguments.new(
|
|
62
|
-
args: Codec::HandleWalk.deep_wrap(args,
|
|
63
|
-
kwargs: Codec::HandleWalk.deep_wrap(kwargs,
|
|
26
|
+
args: Codec::HandleWalk.deep_wrap(args, handles, 1),
|
|
27
|
+
kwargs: Codec::HandleWalk.deep_wrap(kwargs, handles, 1)
|
|
64
28
|
).encode
|
|
65
29
|
end
|
|
66
30
|
|
|
67
31
|
private
|
|
68
32
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# error before the run reaches the guest). After +.to_s+
|
|
72
|
-
# the value must match NAME_PATTERN (ArgumentError), rejecting
|
|
73
|
-
# +::+-segmented names and any non-constant form.
|
|
33
|
+
# A target of the wrong type raises TypeError, a Host App programming
|
|
34
|
+
# error caught before the run reaches the guest.
|
|
74
35
|
def normalize_entrypoint(target)
|
|
75
36
|
unless target.is_a?(Symbol) || target.is_a?(String)
|
|
76
37
|
raise TypeError, "entrypoint must be a Symbol or String, got #{target.class}"
|
|
77
38
|
end
|
|
78
39
|
|
|
79
40
|
target_str = target.to_s
|
|
80
|
-
unless
|
|
41
|
+
unless CONSTANT_NAME.match?(target_str)
|
|
81
42
|
raise ArgumentError,
|
|
82
|
-
"entrypoint must match #{
|
|
43
|
+
"entrypoint must match #{CONSTANT_NAME.inspect} (got #{target.inspect})"
|
|
83
44
|
end
|
|
84
45
|
|
|
85
46
|
target_str.to_sym
|
|
86
47
|
end
|
|
87
48
|
|
|
88
|
-
# +args+ must not contain a +Kobako::Handle+. The Handle
|
|
89
|
-
# allocator lives inside the Host Gem; legitimate paths surface
|
|
90
|
-
# Handle objects only through raised error fields, so a Handle
|
|
91
|
-
# reaching +args+ is a forged or smuggled token. Non-wire-
|
|
92
|
-
# representable arguments that are not Handles are handled by
|
|
93
|
-
# auto-wrap inside +#payload+ — the reject path is reserved
|
|
94
|
-
# for Handle objects specifically.
|
|
95
49
|
def validate_args!(args)
|
|
96
50
|
raise ArgumentError, "arguments must be an Array" unless args.is_a?(Array)
|
|
97
51
|
raise ArgumentError, forged_handle_message("arguments") if args.any?(Kobako::Handle)
|
|
98
52
|
end
|
|
99
53
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# branch). Both checks live here so the Host App sees the
|
|
103
|
-
# host-side error message before any encode / decode boundary.
|
|
54
|
+
# Checked here so the Host App sees the host-side message before any
|
|
55
|
+
# encode boundary.
|
|
104
56
|
def validate_kwargs!(kwargs)
|
|
105
57
|
raise ArgumentError, "keyword arguments must be a Hash" unless kwargs.is_a?(Hash)
|
|
106
58
|
|
|
@@ -112,12 +64,6 @@ module Kobako
|
|
|
112
64
|
raise ArgumentError, forged_handle_message("keyword argument values") if kwargs.each_value.any?(Kobako::Handle)
|
|
113
65
|
end
|
|
114
66
|
|
|
115
|
-
# Single source of truth for the forged-Handle reject message so the
|
|
116
|
-
# args and kwargs branches stay phrased identically. Message stays in
|
|
117
|
-
# caller vocabulary: it names the affected slot and the reason
|
|
118
|
-
# without leaking internal SPEC identifiers or self-referential
|
|
119
|
-
# architecture terms — the error is raised BY kobako, so saying
|
|
120
|
-
# "allocated by the Host Gem" reads as third-person about self.
|
|
121
67
|
def forged_handle_message(slot)
|
|
122
68
|
"#{slot} must not contain a Kobako::Handle — " \
|
|
123
69
|
"Handles are created internally by the Sandbox and cannot be passed in"
|
|
@@ -4,51 +4,19 @@ require_relative "../codec"
|
|
|
4
4
|
require_relative "../errors"
|
|
5
5
|
|
|
6
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
7
|
module Transport
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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, and reifies the Yield Reply into Ruby
|
|
19
|
-
# control flow:
|
|
20
|
-
#
|
|
21
|
-
# * ok — return the decoded value to +yield+'s caller
|
|
22
|
-
# * break — +throw break_tag, value+ so the Dispatcher's +catch+
|
|
23
|
-
# frame unwinds the Service method
|
|
24
|
-
# * error — raise a BlockError at the Service's yield site,
|
|
25
|
-
# carrying the guest's class on +#klass+
|
|
26
|
-
#
|
|
27
|
-
# The Dispatcher calls #invalidate! from its +ensure+ block once
|
|
28
|
-
# dispatch completes; any later call to a stashed Yielder then raises
|
|
29
|
-
# +LocalJumpError+ — the observable shape of an escaped Yielder.
|
|
30
|
-
class Yielder
|
|
31
|
-
# +yield_to_guest+ is the ext's per-dispatch
|
|
32
|
-
# +Kobako::Runtime::GuestYielder+, which #yield invokes to re-enter
|
|
33
|
-
# the guest: it takes the argument payload and answers the reply
|
|
34
|
-
# already split into +[arm, body, class]+. +break_tag+ is the
|
|
35
|
-
# +catch+ throw tag the Dispatcher matches against to unwind the
|
|
36
|
-
# Service on a break. +handler+ is the invocation's +Kobako::Catalog::Handles+,
|
|
37
|
-
# used to restore a Capability Handle in the block's ok value back to
|
|
38
|
-
# its host object before it reaches the Service +yield+ site.
|
|
39
|
-
def initialize(yield_to_guest, break_tag, handler)
|
|
40
|
-
@yield_to_guest = yield_to_guest
|
|
8
|
+
class Yielder # :nodoc:
|
|
9
|
+
def initialize(guest_yielder, break_tag, handles)
|
|
10
|
+
@guest_yielder = guest_yielder
|
|
41
11
|
@break_tag = break_tag
|
|
42
|
-
@
|
|
12
|
+
@handles = handles
|
|
43
13
|
@active = true
|
|
44
14
|
@raised = nil
|
|
45
15
|
end
|
|
46
16
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# raised its own has reported something else, and the guest must
|
|
51
|
-
# hear about that instead.
|
|
17
|
+
# Identity rather than class: a Service that rescued the block's
|
|
18
|
+
# failure and raised its own has reported something else, and the
|
|
19
|
+
# guest must hear about that instead.
|
|
52
20
|
#
|
|
53
21
|
# The class travels with the message because not every block failure
|
|
54
22
|
# is an exception the guest holds — a block value the guest refused
|
|
@@ -60,17 +28,15 @@ module Kobako
|
|
|
60
28
|
"#{raised.klass}: #{raised.message}"
|
|
61
29
|
end
|
|
62
30
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# The break value unwinds past the Service back to the guest
|
|
68
|
-
# bound-constant call, so it passes through verbatim — a Handle stays a
|
|
69
|
-
# Handle and rides back on the same id rather than churning a new one.
|
|
31
|
+
# The ok value is consumed by the host Service, so a Capability Handle
|
|
32
|
+
# in it is restored. The break value unwinds back to the guest, so it
|
|
33
|
+
# passes through verbatim and rides back on the same id rather than
|
|
34
|
+
# churning a new one.
|
|
70
35
|
def yield(*args)
|
|
71
36
|
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
72
37
|
|
|
73
|
-
arm, body, klass = @
|
|
38
|
+
arm, body, klass = @guest_yielder.call(encode_args(args))
|
|
39
|
+
raise UnsupportedError, "the Sandbox runtime does not support yielding to a block" if arm == :unsupported
|
|
74
40
|
raise remember(BlockError.new(body, klass: klass)) if arm == :error
|
|
75
41
|
|
|
76
42
|
value, carried_handle = decode_body(body)
|
|
@@ -79,26 +45,18 @@ module Kobako
|
|
|
79
45
|
restore(value, carried_handle)
|
|
80
46
|
end
|
|
81
47
|
|
|
82
|
-
# The Proc the Dispatcher passes as +&block+, binding #yield so a
|
|
83
|
-
# Service method's +yield+ / +block.call+ drives the round-trip.
|
|
84
48
|
def to_proc
|
|
85
49
|
method(:yield).to_proc
|
|
86
50
|
end
|
|
87
51
|
|
|
88
|
-
# Mark this Yielder dead. Called by the Dispatcher's +ensure+ block
|
|
89
|
-
# when the originating dispatch frame returns; any later #yield
|
|
90
|
-
# call then raises +LocalJumpError+.
|
|
91
52
|
def invalidate!
|
|
92
53
|
@active = false
|
|
93
54
|
end
|
|
94
55
|
|
|
95
56
|
private
|
|
96
57
|
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# before the guest is re-entered, and are restated so the Service
|
|
100
|
-
# reads a refusal of its own argument rather than a codec class it
|
|
101
|
-
# never named.
|
|
58
|
+
# Restated so the Service reads a refusal of its own argument rather
|
|
59
|
+
# than a codec class it never named.
|
|
102
60
|
def encode_args(args)
|
|
103
61
|
Kobako::Codec::Nesting.assert_within_bound!(args)
|
|
104
62
|
Kobako::Codec::Encoder.encode(args)
|
|
@@ -106,33 +64,23 @@ module Kobako
|
|
|
106
64
|
raise YieldValueError, "Service yielded a value the block cannot receive: #{e.message}"
|
|
107
65
|
end
|
|
108
66
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
# as one statement. Only the newest is kept: a Service that rescued
|
|
112
|
-
# an earlier one and yielded again has already handled it.
|
|
67
|
+
# Only the newest is kept: a Service that rescued an earlier one and
|
|
68
|
+
# yielded again has already handled it.
|
|
113
69
|
def remember(error)
|
|
114
70
|
@raised = error
|
|
115
71
|
end
|
|
116
72
|
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
# nested dispatches whose own brackets would otherwise pollute the
|
|
121
|
-
# signal.
|
|
73
|
+
# The tracking bracket opens only around this decode: the guest
|
|
74
|
+
# re-entry may run nested dispatches whose own brackets would
|
|
75
|
+
# otherwise pollute the signal.
|
|
122
76
|
def decode_body(body)
|
|
123
77
|
Kobako::Codec.track_handles { Kobako::Codec::Decoder.decode(body) }
|
|
124
78
|
end
|
|
125
79
|
|
|
126
|
-
# Restore any Capability Handle in a block's ok value to its host
|
|
127
|
-
# object via the injected +Catalog::Handles+. Only the
|
|
128
|
-
# ok path calls this — host code consumes the ok value, whereas a
|
|
129
|
-
# break value returns to the guest and stays a Handle. A response
|
|
130
|
-
# whose decode carried no Handle resolves to itself, so the walk is
|
|
131
|
-
# skipped entirely.
|
|
132
80
|
def restore(value, carried_handle)
|
|
133
81
|
return value unless carried_handle
|
|
134
82
|
|
|
135
|
-
Kobako::Codec::HandleWalk.deep_restore(value, @
|
|
83
|
+
Kobako::Codec::HandleWalk.deep_restore(value, @handles)
|
|
136
84
|
end
|
|
137
85
|
end
|
|
138
86
|
end
|
data/lib/kobako/transport.rb
CHANGED
|
@@ -9,20 +9,8 @@ require_relative "transport/reflection"
|
|
|
9
9
|
require_relative "transport/dispatcher"
|
|
10
10
|
|
|
11
11
|
module Kobako
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
# +Dispatcher+ that answers a routed Call, the +Yielder+ that re-enters
|
|
16
|
-
# the guest for a block, the +Reflection+ floor a dispatch must clear, the
|
|
17
|
-
# +Exposure+ each reference authorizes a call against, and +Error+ for a
|
|
18
|
-
# wire violation the host detects. Each invocation's
|
|
19
|
-
# +Context+ composes them into the dispatch +Proc+ it passes +Runtime+ for
|
|
20
|
-
# that run.
|
|
21
|
-
#
|
|
22
|
-
# "RPC" was deliberately not chosen — it implies a cross-process boundary that
|
|
23
|
-
# kobako does not have, since host and guest share one OS thread and
|
|
24
|
-
# one wasm linear memory. See
|
|
25
|
-
# {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md].
|
|
26
|
-
module Transport
|
|
12
|
+
# The host side of the guest↔host exchange. Only Transport::Error is
|
|
13
|
+
# public; the rest serves one invocation's dispatch.
|
|
14
|
+
module Transport # :nodoc:
|
|
27
15
|
end
|
|
28
16
|
end
|
data/lib/kobako/unresolved.rb
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# is refused as an unresolved target and surfaces as +Kobako::ServiceError+
|
|
9
|
-
# when the guest leaves it unrescued — the same capability-failure channel
|
|
10
|
-
# as an idiom with no backend bound.
|
|
11
|
-
#
|
|
12
|
-
# A single shared value; only its identity distinguishes it, so the dispatch
|
|
13
|
-
# layer recognises it with +equal?+. A Host App may name it at a +bind+ site
|
|
14
|
-
# to declare a fillable explicitly.
|
|
4
|
+
# Stands for the object of a fillable Service, one declared with
|
|
5
|
+
# Sandbox#bind and no object. Context#bind supplies the object for an
|
|
6
|
+
# invocation; a call to one left unfilled fails as a ServiceError. Passing
|
|
7
|
+
# it to Sandbox#bind declares a fillable explicitly.
|
|
15
8
|
module Unresolved
|
|
16
9
|
end
|
|
17
10
|
end
|
data/lib/kobako/usage.rb
CHANGED
|
@@ -12,14 +12,10 @@ module Kobako
|
|
|
12
12
|
# cap, even when the cap was hit.
|
|
13
13
|
#
|
|
14
14
|
# Filled on every outcome, traps included, so a Host App that rescues a
|
|
15
|
-
# trap
|
|
15
|
+
# trap reads from the error's +execution+ how much of the budget the
|
|
16
16
|
# invocation used.
|
|
17
|
-
#
|
|
18
|
-
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
19
|
-
# Steep-friendly shape — see +.rubocop.yml+ for the rationale).
|
|
20
17
|
class Usage < Data.define(:wall_time, :memory_peak)
|
|
21
|
-
#
|
|
22
|
-
# runs, so an Execution's +#usage+ is never +nil+.
|
|
18
|
+
# The usage of a run that has not started: no time, no memory.
|
|
23
19
|
EMPTY = new(wall_time: 0.0, memory_peak: 0)
|
|
24
20
|
end
|
|
25
21
|
end
|
data/lib/kobako/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kobako
|
|
|
5
5
|
|
|
6
6
|
def install: (untyped extension, snippets: Kobako::Catalog::Snippets, services: Kobako::Catalog::Services) -> self
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def assert_dependencies!: () -> self
|
|
9
9
|
|
|
10
10
|
def resolve: () -> Hash[String, untyped]
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ module Kobako
|
|
|
17
17
|
|
|
18
18
|
def validate!: (untyped extension) -> void
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def unmet_dependency: () -> [untyped, untyped]?
|
|
21
21
|
|
|
22
22
|
def symbolize: (untyped name) -> untyped
|
|
23
23
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Kobako
|
|
2
|
+
module Catalog
|
|
3
|
+
class Flags
|
|
4
|
+
type tuple = [String, String, bool]
|
|
5
|
+
|
|
6
|
+
def initialize: () -> void
|
|
7
|
+
|
|
8
|
+
def entries: () -> Array[tuple]
|
|
9
|
+
|
|
10
|
+
def register: (Symbol | String name, String value, advisory: bool) -> nil
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def utf8!: (String text, String what) -> String
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
module Kobako
|
|
2
2
|
module Catalog
|
|
3
3
|
class Services
|
|
4
|
-
NAME_PATTERN: Regexp
|
|
5
|
-
|
|
6
4
|
def initialize: () -> void
|
|
7
5
|
|
|
8
6
|
def bind: (Symbol | String path, untyped object) -> self
|
|
@@ -13,10 +11,6 @@ module Kobako
|
|
|
13
11
|
|
|
14
12
|
def paths: () -> Array[String]
|
|
15
13
|
|
|
16
|
-
def seal!: () -> self
|
|
17
|
-
|
|
18
|
-
def sealed?: () -> bool
|
|
19
|
-
|
|
20
14
|
private
|
|
21
15
|
|
|
22
16
|
def validate_path!: (Symbol | String path) -> String
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
module Kobako
|
|
2
2
|
module Catalog
|
|
3
3
|
class Snippets
|
|
4
|
-
NAME_PATTERN: Regexp
|
|
5
|
-
|
|
6
4
|
type entry = Kobako::Snippet::Source | Kobako::Snippet::Binary
|
|
7
5
|
|
|
8
6
|
type tuple = [Symbol, String?, String]
|
|
9
7
|
|
|
10
8
|
def initialize: () -> void
|
|
11
9
|
|
|
12
|
-
def register: (?code: String?, ?name: (Symbol | String)?, ?binary: String?) ->
|
|
10
|
+
def register: (?code: String?, ?name: (Symbol | String)?, ?binary: String?) -> nil
|
|
13
11
|
|
|
14
12
|
def entries: () -> Array[tuple]
|
|
15
13
|
|
|
14
|
+
def compile: () { (Array[tuple] entries) -> Array[String]? } -> nil
|
|
15
|
+
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
-
def register_source!: (String? code, (Symbol | String)? name) ->
|
|
18
|
+
def register_source!: (String? code, (Symbol | String)? name) -> nil
|
|
19
19
|
|
|
20
20
|
def ensure_source_args!: (String? code, (Symbol | String)? name) -> [String, Symbol | String]
|
|
21
21
|
|