kobako 0.27.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 +71 -0
- data/README.md +100 -125
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +31 -0
- data/crates/kobako-runtime/README.md +4 -3
- data/crates/kobako-transport/CHANGELOG.md +28 -0
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
- data/crates/kobako-wasmtime/README.md +2 -2
- 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 -15
|
@@ -3,52 +3,42 @@
|
|
|
3
3
|
require_relative "errors"
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Steep-friendly shape — see +.rubocop.yml+ for the rationale).
|
|
6
|
+
# The options a Sandbox was built with, read through Sandbox#options or
|
|
7
|
+
# directly on the Sandbox, as in Sandbox#timeout. A cap left out takes its
|
|
8
|
+
# default, and +nil+ switches it off.
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
# +
|
|
13
|
-
# +stderr_limit+
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# is a wire-ready bundle the +Kobako::Runtime+ constructor consumes
|
|
21
|
-
# as-is. The options also own the ladder comparison
|
|
22
|
-
# (+#enforce_floor!+) that +Kobako::Sandbox+ delegates its
|
|
23
|
-
# construction floor check to.
|
|
10
|
+
# [+timeout+] Float seconds one invocation may run.
|
|
11
|
+
# [+memory_limit+] Integer bytes one invocation may grow guest memory by.
|
|
12
|
+
# [+stdout_limit+, +stderr_limit+]
|
|
13
|
+
# Integer bytes kept from each output channel.
|
|
14
|
+
# [+profile+] The isolation the runtime must provide, one of PROFILES.
|
|
15
|
+
# [+gvl+] How an invocation holds Ruby's GVL, one of GVL_MODES.
|
|
16
|
+
#
|
|
17
|
+
# Raises ArgumentError for a value outside these shapes, and SetupError
|
|
18
|
+
# when the runtime provides less isolation than +profile+ asks for.
|
|
24
19
|
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile, :gvl)
|
|
25
|
-
#
|
|
20
|
+
# The default +timeout+: 60 seconds.
|
|
26
21
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
27
22
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# watermark sit outside this budget.
|
|
23
|
+
# The default +memory_limit+: 1 MiB of growth past the guest's starting
|
|
24
|
+
# memory.
|
|
31
25
|
DEFAULT_MEMORY_LIMIT = 1 << 20
|
|
32
26
|
|
|
33
|
-
#
|
|
27
|
+
# The default +stdout_limit+ and +stderr_limit+: 1 MiB each.
|
|
34
28
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
35
29
|
|
|
36
|
-
# The isolation
|
|
37
|
-
#
|
|
30
|
+
# The isolation profiles, weakest first. +:hermetic+ denies the guest the
|
|
31
|
+
# clock and entropy; +:permissive+ leaves them live.
|
|
38
32
|
PROFILES = %i[permissive hermetic].freeze
|
|
39
33
|
|
|
40
|
-
#
|
|
41
|
-
# +:permissive+ is the Host App's explicit trade.
|
|
34
|
+
# The default +profile+, the strictest.
|
|
42
35
|
DEFAULT_PROFILE = :hermetic
|
|
43
36
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# Sandboxes on distinct Threads run their guest code in parallel.
|
|
37
|
+
# +:hold+ keeps the GVL through the invocation; +:release+ drops it while
|
|
38
|
+
# the guest runs, so Sandboxes on different Threads run in parallel.
|
|
47
39
|
GVL_MODES = %i[hold release].freeze
|
|
48
40
|
|
|
49
|
-
#
|
|
50
|
-
# execution, so +:release+ is the Host App's explicit opt-in for
|
|
51
|
-
# host-parallel guest execution.
|
|
41
|
+
# The default +gvl+, +:hold+.
|
|
52
42
|
DEFAULT_GVL = :hold
|
|
53
43
|
|
|
54
44
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
@@ -58,22 +48,17 @@ module Kobako
|
|
|
58
48
|
profile: DEFAULT_PROFILE,
|
|
59
49
|
gvl: DEFAULT_GVL)
|
|
60
50
|
timeout = normalize_timeout(timeout)
|
|
61
|
-
memory_limit =
|
|
62
|
-
stdout_limit =
|
|
63
|
-
stderr_limit =
|
|
64
|
-
profile =
|
|
65
|
-
gvl =
|
|
51
|
+
memory_limit = normalize_byte_limit(memory_limit, "memory_limit")
|
|
52
|
+
stdout_limit = normalize_byte_limit(stdout_limit, "stdout_limit")
|
|
53
|
+
stderr_limit = normalize_byte_limit(stderr_limit, "stderr_limit")
|
|
54
|
+
profile = normalize_choice(profile, PROFILES, "profile")
|
|
55
|
+
gvl = normalize_choice(gvl, GVL_MODES, "gvl")
|
|
66
56
|
super
|
|
67
57
|
end
|
|
68
58
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
# +Kobako::SetupError+ instead of weakening the posture silently.
|
|
73
|
-
# Both fallbacks fail closed: a declaration off the PROFILES ladder
|
|
74
|
-
# ranks below every request, and a request off the ladder
|
|
75
|
-
# (unreachable past +initialize+) refuses every declaration.
|
|
76
|
-
def enforce_floor!(declared)
|
|
59
|
+
# Both fallbacks fail closed: an unknown declaration ranks below every
|
|
60
|
+
# request, and an unknown request refuses every declaration.
|
|
61
|
+
def enforce_floor!(declared) # :nodoc:
|
|
77
62
|
return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
|
|
78
63
|
|
|
79
64
|
raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
|
|
@@ -82,11 +67,8 @@ module Kobako
|
|
|
82
67
|
|
|
83
68
|
private
|
|
84
69
|
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# rejected — a zero or negative timeout would either fire instantly
|
|
88
|
-
# or never, both of which would surprise callers more than an early
|
|
89
|
-
# +ArgumentError+.
|
|
70
|
+
# A zero or negative timeout would either fire instantly or never, both
|
|
71
|
+
# more surprising than an early ArgumentError.
|
|
90
72
|
def normalize_timeout(timeout)
|
|
91
73
|
return nil if timeout.nil?
|
|
92
74
|
raise ArgumentError, "timeout must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
|
|
@@ -97,24 +79,7 @@ module Kobako
|
|
|
97
79
|
seconds
|
|
98
80
|
end
|
|
99
81
|
|
|
100
|
-
|
|
101
|
-
# to mean unbounded. Must be a positive Integer when set; Float or
|
|
102
|
-
# zero/negative values are rejected.
|
|
103
|
-
def normalize_memory_limit(memory_limit)
|
|
104
|
-
return nil if memory_limit.nil?
|
|
105
|
-
unless memory_limit.is_a?(Integer) && memory_limit.positive?
|
|
106
|
-
raise ArgumentError, "memory_limit must be a positive Integer or nil, got #{memory_limit.inspect}"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
memory_limit
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Coerce a per-channel output cap (+stdout_limit+ / +stderr_limit+)
|
|
113
|
-
# into the byte cap the ext expects, or +nil+ to leave the channel
|
|
114
|
-
# uncapped. Same shape as +normalize_memory_limit+: a positive Integer
|
|
115
|
-
# when set, Float / zero / negative rejected. +name+ tags the
|
|
116
|
-
# +ArgumentError+ with the offending keyword.
|
|
117
|
-
def normalize_output_limit(limit, name)
|
|
82
|
+
def normalize_byte_limit(limit, name)
|
|
118
83
|
return nil if limit.nil?
|
|
119
84
|
unless limit.is_a?(Integer) && limit.positive?
|
|
120
85
|
raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
|
|
@@ -123,23 +88,12 @@ module Kobako
|
|
|
123
88
|
limit
|
|
124
89
|
end
|
|
125
90
|
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def normalize_profile(profile)
|
|
131
|
-
return profile if PROFILES.include?(profile)
|
|
132
|
-
|
|
133
|
-
raise ArgumentError, "profile must be one of #{PROFILES.map(&:inspect).join(", ")}, got #{profile.inspect}"
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Validate +gvl+ against GVL_MODES. Like +profile+ it has no +nil+
|
|
137
|
-
# form: the scheduling mode is requested as an explicit Symbol, so
|
|
138
|
-
# anything off the set — +nil+ included — is rejected.
|
|
139
|
-
def normalize_gvl(gvl)
|
|
140
|
-
return gvl if GVL_MODES.include?(gvl)
|
|
91
|
+
# Unlike the caps there is no +nil+ form: every choice, the weakest
|
|
92
|
+
# included, is requested by name, as in +:permissive+.
|
|
93
|
+
def normalize_choice(value, choices, name)
|
|
94
|
+
return value if choices.include?(value)
|
|
141
95
|
|
|
142
|
-
raise ArgumentError, "
|
|
96
|
+
raise ArgumentError, "#{name} must be one of #{choices.map(&:inspect).join(", ")}, got #{value.inspect}"
|
|
143
97
|
end
|
|
144
98
|
end
|
|
145
99
|
end
|
|
@@ -2,22 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
|
-
|
|
6
|
-
# +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets+.
|
|
7
|
-
#
|
|
8
|
-
# The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
|
|
9
|
-
# +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
|
|
10
|
-
# the wire ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
|
|
11
|
-
# The host treats the bytes as opaque — the snippet's canonical
|
|
12
|
-
# name, when present, lives in the bytecode's embedded +debug_info+
|
|
13
|
-
# and is resolved by the guest at load time; structural validation
|
|
14
|
-
# is deferred to the first invocation's guest replay.
|
|
15
|
-
#
|
|
16
|
-
# The class is a +Data.define+ subclass — frozen and value-equal.
|
|
17
|
-
# Callers (chiefly +Catalog::Snippets+) construct instances via keyword
|
|
18
|
-
# form +Binary.new(body: ...)+. Wire-form construction is the
|
|
19
|
-
# registry's responsibility.
|
|
20
|
-
class Binary < Data.define(:body)
|
|
5
|
+
class Binary < Data.define(:body) # :nodoc:
|
|
21
6
|
# Names the snippet form the guest replays this entry as. The wire's
|
|
22
7
|
# discriminant byte is assigned by the core envelope, not here.
|
|
23
8
|
KIND = :bytecode
|
|
@@ -2,22 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
|
-
|
|
6
|
-
# +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets+.
|
|
7
|
-
#
|
|
8
|
-
# +name+ is the canonical +Symbol+ identity baked into the loaded
|
|
9
|
-
# IREP's +debug_info+; backtrace frames originating in this snippet
|
|
10
|
-
# surface as +(snippet:Name):line+. +body+ is the UTF-8 mruby source
|
|
11
|
-
# detached from the caller's reference at +Catalog::Snippets#register+
|
|
12
|
-
# time so later mutation of the original String cannot bleed through.
|
|
13
|
-
#
|
|
14
|
-
# The class is a +Data.define+ subclass — frozen, value-equal, and
|
|
15
|
-
# carries no mutation API. Callers (chiefly +Catalog::Snippets+)
|
|
16
|
-
# construct instances via keyword form +Source.new(name: ..., body: ...)+.
|
|
17
|
-
# Wire-form construction is the registry's responsibility: as a leaf
|
|
18
|
-
# carrier this Source stays pure and +Catalog::Snippets#entries+ reads
|
|
19
|
-
# its attributes off the outside rather than asking it to project itself.
|
|
20
|
-
class Source < Data.define(:name, :body)
|
|
5
|
+
class Source < Data.define(:name, :body) # :nodoc:
|
|
21
6
|
# Names the snippet form the guest replays this entry as. The wire's
|
|
22
7
|
# discriminant byte is assigned by the core envelope, not here.
|
|
23
8
|
KIND = :source
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -4,14 +4,6 @@ require_relative "snippet/binary"
|
|
|
4
4
|
require_relative "snippet/source"
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
|
-
|
|
8
|
-
# held by +Kobako::Catalog::Snippets+.
|
|
9
|
-
#
|
|
10
|
-
# +Source+ represents a single +#preload(code:, name:)+ entry; +Binary+
|
|
11
|
-
# represents a single +#preload(binary:)+ entry. Both are plain value
|
|
12
|
-
# objects with no dependency on the +Catalog::Snippets+ registry that
|
|
13
|
-
# holds them — the registry reads their attributes externally into the
|
|
14
|
-
# tuples the native side frames.
|
|
15
|
-
module Snippet
|
|
7
|
+
module Snippet # :nodoc:
|
|
16
8
|
end
|
|
17
9
|
end
|
|
@@ -2,20 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Transport
|
|
5
|
-
|
|
6
|
-
# core envelope. The routing fields arrive already decoded — a String
|
|
7
|
-
# for a bound constant's path, an Integer for a Capability Handle id —
|
|
8
|
-
# so the host resolves a target without interpreting a payload byte.
|
|
9
|
-
#
|
|
10
|
-
# +payload+ stays bytes here on purpose: routing never interprets a
|
|
11
|
-
# payload byte, and the one place that does — the Dispatcher — must
|
|
12
|
-
# decode it inside the Codec bracket that records whether a Handle
|
|
13
|
-
# crossed.
|
|
14
|
-
#
|
|
15
|
-
# Built on the +class X < Data.define(...)+ subclass form so the class
|
|
16
|
-
# body is fully Steep-visible; see +.rubocop.yml+ for the
|
|
17
|
-
# rationale.
|
|
18
|
-
class Call < Data.define(:target, :method_name, :block_given, :payload)
|
|
5
|
+
class Call < Data.define(:target, :method_name, :block_given, :payload) # :nodoc:
|
|
19
6
|
end
|
|
20
7
|
end
|
|
21
8
|
end
|
|
@@ -8,39 +8,20 @@ require_relative "reflection"
|
|
|
8
8
|
require_relative "yielder"
|
|
9
9
|
|
|
10
10
|
module Kobako
|
|
11
|
-
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
12
|
-
# owns the pure-function dispatcher that answers a routed Call.
|
|
13
11
|
module Transport
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
# or Catalog::Handles, decodes only the payload, invokes the method,
|
|
19
|
-
# and answers +[ok, bytes]+ — which the native side puts on the
|
|
20
|
-
# Reply's ok or fault arm. It never raises.
|
|
21
|
-
#
|
|
22
|
-
# The module is stateless — all mutable state is threaded through
|
|
23
|
-
# arguments so Dispatcher has no instance variables and no side
|
|
24
|
-
# effects beyond mutating the Catalog::Handles via +alloc+ when a
|
|
25
|
-
# non-wire-representable return value must be wrapped.
|
|
26
|
-
module Dispatcher
|
|
12
|
+
# Answers a routed Call with +[true, body, nil]+ or
|
|
13
|
+
# +[false, message, fault_type]+ and never raises, so every failure
|
|
14
|
+
# reaches the guest as a fault.
|
|
15
|
+
module Dispatcher # :nodoc:
|
|
27
16
|
# Throw tag for the Yielder's break unwind back to the
|
|
28
17
|
# dispatcher's +catch+ frame. +private_constant+ is a
|
|
29
18
|
# convention boundary — not a defence.
|
|
30
19
|
BREAK_THROW = :__kobako_break__
|
|
31
20
|
private_constant :BREAK_THROW
|
|
32
21
|
|
|
33
|
-
|
|
22
|
+
class UndefinedTargetError < StandardError; end # :nodoc:
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
# Fault with type="undefined". Contained at the wire boundary —
|
|
37
|
-
# not part of the public Kobako error taxonomy.
|
|
38
|
-
class UndefinedTargetError < StandardError; end
|
|
39
|
-
|
|
40
|
-
# The codec fault of a request that never became a call, restated in
|
|
41
|
-
# the vocabulary the guest can act on while staying inside the codec
|
|
42
|
-
# taxonomy the fault boundary sorts on.
|
|
43
|
-
class UnreadableRequestError < Kobako::Codec::Error; end
|
|
24
|
+
class UnreadableRequestError < Kobako::Codec::Error; end # :nodoc:
|
|
44
25
|
|
|
45
26
|
# The category kobako's own refusals answer under, keyed by the class
|
|
46
27
|
# each is raised as and ordered most specific first. A class absent
|
|
@@ -55,30 +36,18 @@ module Kobako
|
|
|
55
36
|
UndefinedTargetError => "undefined",
|
|
56
37
|
ArgumentError => "argument",
|
|
57
38
|
YieldValueError => "runtime",
|
|
39
|
+
UnsupportedError => "runtime",
|
|
58
40
|
Kobako::Codec::Error => "internal",
|
|
59
41
|
Kobako::SandboxError => "runtime"
|
|
60
42
|
}.freeze
|
|
61
43
|
private_constant :OWN_FAULTS
|
|
62
44
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# nor the Context needs to publish accessors for the per-invocation
|
|
70
|
-
# +Catalog::Handles+ or +Runtime+. +yield_to_guest+ is a +String → String+ callable
|
|
71
|
-
# (the ext's per-dispatch +Kobako::Runtime::GuestYielder+) used only
|
|
72
|
-
# when the Call carries +block_given: true+. Never raises — every
|
|
73
|
-
# failure path takes the fault arm instead, so the guest sees a
|
|
74
|
-
# transport error rather than a wasm trap.
|
|
75
|
-
#
|
|
76
|
-
# The decode runs inside +Codec.track_handles+ so #resolve_call_args
|
|
77
|
-
# can skip the argument walk when no Capability Handle crossed the
|
|
78
|
-
# wire.
|
|
79
|
-
def dispatch(call, resolver, handler, yield_to_guest)
|
|
80
|
-
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if call.block_given
|
|
81
|
-
[true, encode_ok(run(call, resolver, handler, yielder), handler), nil] # : [bool, String, String?]
|
|
45
|
+
# The per-invocation state arrives as arguments so the Dispatcher
|
|
46
|
+
# stays stateless and neither the resolver nor the Context publishes
|
|
47
|
+
# accessors for it.
|
|
48
|
+
def self.dispatch(call, resolver, handles, guest_yielder)
|
|
49
|
+
yielder = Yielder.new(guest_yielder, BREAK_THROW, handles) if call.block_given
|
|
50
|
+
[true, encode_ok(run(call, resolver, handles, yielder), handles), nil] # : [bool, String, String?]
|
|
82
51
|
# StandardError is the boundary by intent: a Service method's
|
|
83
52
|
# application fault folds into a guest-rescuable fault, while a
|
|
84
53
|
# host-process failure (NoMemoryError, SignalException, a bare Exception)
|
|
@@ -90,172 +59,120 @@ module Kobako
|
|
|
90
59
|
yielder&.invalidate!
|
|
91
60
|
end
|
|
92
61
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# so the reply-shaping and the failure boundary stay one glance wide.
|
|
96
|
-
def run(call, resolver, handler, yielder)
|
|
97
|
-
arguments, carried_handle = decode_arguments(call.payload)
|
|
98
|
-
exposure = resolve_target(call.target, resolver, handler)
|
|
99
|
-
args, kwargs = resolve_call_args(arguments, handler, carried_handle)
|
|
100
|
-
catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Decode the Call's payload into its arguments, reporting whether any
|
|
104
|
-
# Capability Handle crossed. A codec fault here is a request that
|
|
105
|
-
# never became a call, restated so it cannot read as an unwritable
|
|
106
|
-
# reply — the same restatement #encode_ok makes in the other
|
|
107
|
-
# direction.
|
|
108
|
-
def decode_arguments(payload)
|
|
109
|
-
Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
|
|
110
|
-
rescue Kobako::Codec::Error => e
|
|
111
|
-
raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Resolve positional and keyword arguments off the decoded payload in
|
|
115
|
-
# one step. +carried_handle+ reports whether the decode carried any
|
|
116
|
-
# Capability Handle; when it did not, every argument resolves to
|
|
117
|
-
# itself, so the decoded values pass straight through and the walk is
|
|
118
|
-
# skipped entirely. Otherwise both go through #resolve_arg so Handles
|
|
119
|
-
# round-trip back to the host-side Ruby object before the call reaches
|
|
120
|
-
# +public_send+.
|
|
121
|
-
def resolve_call_args(arguments, handler, carried_handle)
|
|
122
|
-
return [arguments.args, arguments.kwargs] unless carried_handle
|
|
62
|
+
class << self
|
|
63
|
+
private
|
|
123
64
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
65
|
+
def run(call, resolver, handles, yielder)
|
|
66
|
+
arguments, carried_handle = decode_arguments(call.payload)
|
|
67
|
+
exposure = resolve_target(call.target, resolver, handles)
|
|
68
|
+
args, kwargs = resolve_call_args(arguments, handles, carried_handle)
|
|
69
|
+
catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
|
|
70
|
+
end
|
|
127
71
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# it is the +<class>: <message>+ shape a Host App is told to keep
|
|
136
|
-
# secrets out of, so wearing it says the Service raised. kobako's own
|
|
137
|
-
# refusals answer under their own wording instead of borrowing that
|
|
138
|
-
# shape.
|
|
139
|
-
#
|
|
140
|
-
# The guest's own block failing is not the Service's to report at
|
|
141
|
-
# all, so the Yielder that raised it is asked first — it recognises
|
|
142
|
-
# its own by identity and words the failure the guest's way.
|
|
143
|
-
def caught_fault(error, yielder)
|
|
144
|
-
block_failure = yielder&.fault_text(error)
|
|
145
|
-
return fault("block", block_failure) if block_failure
|
|
72
|
+
# A codec fault here is a request that never became a call, restated
|
|
73
|
+
# so it cannot read as an unwritable reply.
|
|
74
|
+
def decode_arguments(payload)
|
|
75
|
+
Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
|
|
76
|
+
rescue Kobako::Codec::Error => e
|
|
77
|
+
raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
|
|
78
|
+
end
|
|
146
79
|
|
|
147
|
-
|
|
148
|
-
|
|
80
|
+
def resolve_call_args(arguments, handles, carried_handle)
|
|
81
|
+
return [arguments.args, arguments.kwargs] unless carried_handle
|
|
149
82
|
|
|
150
|
-
|
|
151
|
-
|
|
83
|
+
[arguments.args.map { |v| resolve_arg(v, handles) },
|
|
84
|
+
arguments.kwargs.transform_values { |v| resolve_arg(v, handles) }]
|
|
85
|
+
end
|
|
152
86
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if kwargs.empty?
|
|
171
|
-
target.public_send(name, *args, &block)
|
|
172
|
-
else
|
|
173
|
-
target.public_send(name, *args, **kwargs, &block)
|
|
87
|
+
# The class prefix marks a Service's own exception and nothing else:
|
|
88
|
+
# it is the +<class>: <message>+ shape a Host App is told to keep
|
|
89
|
+
# secrets out of, so wearing it says the Service raised. kobako's own
|
|
90
|
+
# refusals answer under their own wording instead of borrowing that
|
|
91
|
+
# shape.
|
|
92
|
+
#
|
|
93
|
+
# The guest's own block failing is not the Service's to report at
|
|
94
|
+
# all, so the Yielder that raised it is asked first — it recognises
|
|
95
|
+
# its own by identity and words the failure the guest's way.
|
|
96
|
+
def caught_fault(error, yielder)
|
|
97
|
+
block_failure = yielder&.fault_text(error)
|
|
98
|
+
return fault("block", block_failure) if block_failure
|
|
99
|
+
|
|
100
|
+
own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) }
|
|
101
|
+
return fault(own.last, error.message) if own
|
|
102
|
+
|
|
103
|
+
fault("runtime", "#{error.class}: #{error.message}")
|
|
174
104
|
end
|
|
175
|
-
end
|
|
176
105
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
106
|
+
# The wire always carries a keyword map, so an empty one is left
|
|
107
|
+
# unsplatted for methods that take no keywords.
|
|
108
|
+
def invoke(exposure, method, args, kwargs, yielder)
|
|
109
|
+
name = method.to_sym
|
|
110
|
+
reject_unreachable!(exposure, name)
|
|
111
|
+
target = exposure.object
|
|
112
|
+
block = yielder&.to_proc
|
|
113
|
+
if kwargs.empty?
|
|
114
|
+
target.public_send(name, *args, &block)
|
|
115
|
+
else
|
|
116
|
+
target.public_send(name, *args, **kwargs, &block)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
186
119
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
raise UndefinedTargetError, e.message
|
|
195
|
-
end
|
|
120
|
+
# Both the ambient-surface floor and the reference's Exposure answer
|
|
121
|
+
# through Reflection, so a rejected name discloses nothing about
|
|
122
|
+
# which of the two refused.
|
|
123
|
+
def reject_unreachable!(exposure, name)
|
|
124
|
+
reason = Reflection.refusal(exposure, name)
|
|
125
|
+
raise UndefinedTargetError, reason if reason
|
|
126
|
+
end
|
|
196
127
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# id. No else-branch is needed — the envelope layer is the system
|
|
202
|
-
# boundary that enforces the invariant.
|
|
203
|
-
def resolve_target(target, resolver, handler)
|
|
204
|
-
case target
|
|
205
|
-
when String
|
|
206
|
-
resolve_path(target, resolver)
|
|
207
|
-
when Integer
|
|
208
|
-
resolve_handle(target, handler)
|
|
128
|
+
def resolve_arg(value, handles)
|
|
129
|
+
Kobako::Codec::HandleWalk.deep_restore(value, handles)
|
|
130
|
+
rescue Kobako::SandboxError => e
|
|
131
|
+
raise UndefinedTargetError, e.message
|
|
209
132
|
end
|
|
210
|
-
end
|
|
211
133
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
134
|
+
# The envelope already discriminated the two target forms, so no
|
|
135
|
+
# else-branch is needed.
|
|
136
|
+
def resolve_target(target, resolver, handles)
|
|
137
|
+
case target
|
|
138
|
+
when String
|
|
139
|
+
resolve_path(target, resolver)
|
|
140
|
+
when Integer
|
|
141
|
+
resolve_handle(target, handles)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
217
144
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
raise UndefinedTargetError, e.message
|
|
224
|
-
end
|
|
145
|
+
def resolve_path(path, resolver)
|
|
146
|
+
resolver.lookup(path)
|
|
147
|
+
rescue KeyError => e
|
|
148
|
+
raise UndefinedTargetError, e.message
|
|
149
|
+
end
|
|
225
150
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# Capability Handle in place. The happy path encodes exactly once.
|
|
232
|
-
#
|
|
233
|
-
# Any other codec fault is the answer failing to encode rather than the
|
|
234
|
-
# request failing to decode, and a Service is the only side that can
|
|
235
|
-
# change what it returns — so it is named here, where the direction is
|
|
236
|
-
# known, instead of falling to the boundary's codec floor and reporting
|
|
237
|
-
# as an exchange that produced no Service outcome.
|
|
238
|
-
def encode_ok(value, handler)
|
|
239
|
-
Kobako::Codec::Nesting.assert_within_bound!(value)
|
|
240
|
-
Kobako::Codec::Encoder.encode(value)
|
|
241
|
-
rescue Kobako::Codec::UnsupportedTypeError
|
|
242
|
-
encode_ok(wrap_as_handle(value, handler), handler)
|
|
243
|
-
rescue Kobako::Codec::Error => e
|
|
244
|
-
raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
|
|
245
|
-
end
|
|
151
|
+
def resolve_handle(id, handles)
|
|
152
|
+
handles.exposure(id)
|
|
153
|
+
rescue Kobako::SandboxError => e
|
|
154
|
+
raise UndefinedTargetError, e.message
|
|
155
|
+
end
|
|
246
156
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
157
|
+
# A value with no wire form goes back as a Capability Handle. Any
|
|
158
|
+
# other codec fault is the answer failing to encode, which only the
|
|
159
|
+
# Service can change — so it is named here, where the direction is
|
|
160
|
+
# known, instead of falling to the boundary's codec floor.
|
|
161
|
+
def encode_ok(value, handles)
|
|
162
|
+
Kobako::Codec::Nesting.assert_within_bound!(value)
|
|
163
|
+
Kobako::Codec::Encoder.encode(value)
|
|
164
|
+
rescue Kobako::Codec::UnsupportedTypeError
|
|
165
|
+
encode_ok(handles.alloc(value), handles)
|
|
166
|
+
rescue Kobako::Codec::Error => e
|
|
167
|
+
raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
|
|
168
|
+
end
|
|
253
169
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
170
|
+
# Ruby core builds some exception messages as ASCII-8BIT (the arity
|
|
171
|
+
# ArgumentError, for one), and the envelope requires UTF-8 of the
|
|
172
|
+
# text fields it frames.
|
|
173
|
+
def fault(type, message)
|
|
174
|
+
[message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), type] # : [String, String]
|
|
175
|
+
end
|
|
259
176
|
end
|
|
260
177
|
end
|
|
261
178
|
end
|