kobako 0.23.0 → 0.26.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 -1
- data/CHANGELOG.md +89 -0
- data/Cargo.lock +156 -110
- data/README.md +19 -18
- data/ROADMAP.md +12 -12
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/profile.rs +2 -2
- data/crates/kobako-transport/CHANGELOG.md +14 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +2 -0
- data/crates/kobako-transport/src/envelope/bytes.rs +6 -0
- data/crates/kobako-transport/src/envelope/call.rs +9 -0
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -0
- data/crates/kobako-transport/src/envelope/fault.rs +5 -0
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +9 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +11 -0
- data/crates/kobako-transport/src/envelope/reply.rs +9 -0
- data/crates/kobako-transport/src/envelope/run.rs +3 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
- data/crates/kobako-wasmtime/Cargo.toml +5 -5
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/ambient.rs +2 -0
- data/crates/kobako-wasmtime/src/capture.rs +8 -0
- data/crates/kobako-wasmtime/src/frames.rs +3 -1
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -0
- data/crates/kobako-wasmtime/src/invocation.rs +10 -0
- data/crates/kobako-wasmtime/src/trap.rs +6 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/lib/kobako/capture.rb +8 -22
- data/lib/kobako/catalog/handles.rb +33 -9
- data/lib/kobako/catalog/services.rb +9 -7
- data/lib/kobako/codec/encoder.rb +4 -13
- data/lib/kobako/codec/handle_walk.rb +1 -1
- data/lib/kobako/codec/nesting.rb +43 -0
- data/lib/kobako/codec.rb +4 -2
- data/lib/kobako/context.rb +8 -8
- data/lib/kobako/handle.rb +10 -40
- data/lib/kobako/sandbox.rb +5 -0
- data/lib/kobako/transport/dispatcher.rb +29 -26
- data/lib/kobako/transport/exposure.rb +100 -0
- data/lib/kobako/transport/reflection.rb +19 -20
- data/lib/kobako/transport/run.rb +5 -2
- data/lib/kobako/transport/yielder.rb +6 -4
- data/lib/kobako/transport.rb +4 -2
- data/lib/kobako/usage.rb +11 -22
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +1 -1
- data/sig/kobako/catalog/handles.rbs +2 -0
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +9 -0
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +7 -7
- data/sig/kobako/transport/exposure.rbs +43 -0
- data/sig/kobako/transport/reflection.rbs +1 -3
- data/sig/kobako/transport.rbs +11 -1
- metadata +5 -1
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -31,23 +31,14 @@ module Kobako
|
|
|
31
31
|
# same error: an integer outside i64..u64 (+RangeError+) and any
|
|
32
32
|
# packer-internal +NoMethodError+.
|
|
33
33
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# one that escapes every caller's rescue and traps the invocation.
|
|
39
|
-
#
|
|
40
|
-
# The refusal is spent once per thread: a thread that has absorbed one
|
|
41
|
-
# such overflow aborts on the next instead of raising, and a Hash cycle
|
|
42
|
-
# never reaches Ruby at all — the packer walks a Hash through C frames
|
|
43
|
-
# that carry no stack guard. Bounding the walk before the packer is
|
|
44
|
-
# handed the value is what would make the refusal repeatable.
|
|
34
|
+
# The caller bounds +value+'s nesting first (Nesting): the packer takes
|
|
35
|
+
# no depth limit and walks a list or map in frames that carry no stack
|
|
36
|
+
# guard, so a value nesting without end — a reference cycle necessarily
|
|
37
|
+
# does — exhausts the machine stack instead of raising.
|
|
45
38
|
def self.encode(value)
|
|
46
39
|
FACTORY.dump(value)
|
|
47
40
|
rescue ::RangeError, ::NoMethodError => e
|
|
48
41
|
raise UnsupportedTypeError, e.message
|
|
49
|
-
rescue ::SystemStackError
|
|
50
|
-
raise InvalidTypeError, "value nests deeper than this host can write (a reference cycle necessarily does)"
|
|
51
42
|
end
|
|
52
43
|
end
|
|
53
44
|
end
|
|
@@ -95,7 +95,7 @@ module Kobako
|
|
|
95
95
|
return unless depth > MAX_NESTING_DEPTH
|
|
96
96
|
|
|
97
97
|
raise Kobako::SandboxError,
|
|
98
|
-
"a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels and " \
|
|
98
|
+
"a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
|
|
99
99
|
"cannot cross the sandbox boundary (possible reference cycle)"
|
|
100
100
|
end
|
|
101
101
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error"
|
|
4
|
+
|
|
5
|
+
module Kobako
|
|
6
|
+
module Codec
|
|
7
|
+
# The structural nesting bound
|
|
8
|
+
# ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md]
|
|
9
|
+
# § Structural Nesting Depth), checked before a value reaches the
|
|
10
|
+
# packer. The packer takes no depth limit and walks a list or map in
|
|
11
|
+
# frames that carry no stack guard, so a value it cannot finish — a
|
|
12
|
+
# reference cycle necessarily is one — must be refused before it is
|
|
13
|
+
# handed over.
|
|
14
|
+
module Nesting
|
|
15
|
+
# Raise InvalidTypeError when +value+ nests past MAX_NESTING_DEPTH; a
|
|
16
|
+
# value nested exactly to the bound passes.
|
|
17
|
+
def self.assert_within_bound!(value, depth = 0)
|
|
18
|
+
case value
|
|
19
|
+
when ::Array then assert_members_within_bound!(value, depth)
|
|
20
|
+
when ::Hash
|
|
21
|
+
assert_members_within_bound!(value.keys, depth)
|
|
22
|
+
assert_members_within_bound!(value.values, depth)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Measure the members of one container at +depth+, each sitting one
|
|
27
|
+
# level deeper. A member is matched by class rather than asked about
|
|
28
|
+
# itself — it may be a BasicObject — and only one that could be a
|
|
29
|
+
# container is walked into.
|
|
30
|
+
def self.assert_members_within_bound!(members, depth)
|
|
31
|
+
return if members.empty?
|
|
32
|
+
if depth >= MAX_NESTING_DEPTH
|
|
33
|
+
raise InvalidTypeError,
|
|
34
|
+
"value nests deeper than #{MAX_NESTING_DEPTH} levels (a reference cycle necessarily does)"
|
|
35
|
+
end
|
|
36
|
+
return unless members.any?(::Enumerable)
|
|
37
|
+
|
|
38
|
+
members.grep(::Enumerable) { |member| assert_within_bound!(member, depth + 1) }
|
|
39
|
+
end
|
|
40
|
+
private_class_method :assert_members_within_bound!
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "codec/error"
|
|
4
4
|
require_relative "codec/utils"
|
|
5
5
|
require_relative "codec/handle_walk"
|
|
6
|
+
require_relative "codec/nesting"
|
|
6
7
|
require_relative "codec/state"
|
|
7
8
|
require_relative "codec/ext_types"
|
|
8
9
|
require_relative "codec/encoder"
|
|
@@ -29,8 +30,9 @@ module Kobako
|
|
|
29
30
|
module Codec
|
|
30
31
|
# The maximum structural nesting depth the wire represents (the
|
|
31
32
|
# MessagePack ecosystem's bound), shared with the guest +kobako_codec+
|
|
32
|
-
# so both sides cap identically. The host
|
|
33
|
-
#
|
|
33
|
+
# so both sides cap identically. The host refuses a value nesting past
|
|
34
|
+
# it — a reference cycle necessarily does — before handing it to the
|
|
35
|
+
# packer.
|
|
34
36
|
MAX_NESTING_DEPTH = 128
|
|
35
37
|
|
|
36
38
|
# Bracket a decode and return the block's result together with whether
|
data/lib/kobako/context.rb
CHANGED
|
@@ -30,8 +30,8 @@ module Kobako
|
|
|
30
30
|
@services = services
|
|
31
31
|
@snippets = snippets
|
|
32
32
|
@extensions = extensions
|
|
33
|
-
@resolved = {} # : Hash[String,
|
|
34
|
-
@overrides = {} # : Hash[String,
|
|
33
|
+
@resolved = {} # : Hash[String, Transport::Exposure]
|
|
34
|
+
@overrides = {} # : Hash[String, Transport::Exposure]
|
|
35
35
|
@spent = false
|
|
36
36
|
@handler = Catalog::Handles.new
|
|
37
37
|
@stdout_capture = @stderr_capture = Capture::EMPTY
|
|
@@ -52,11 +52,11 @@ module Kobako
|
|
|
52
52
|
key = path.to_s
|
|
53
53
|
raise ArgumentError, "cannot override undeclared path #{key.inspect}" unless @services.bound?(key)
|
|
54
54
|
|
|
55
|
-
@overrides[key] = object
|
|
55
|
+
@overrides[key] = Transport::Exposure.of(object)
|
|
56
56
|
self
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Resolve a Service +path+ to the
|
|
59
|
+
# Resolve a Service +path+ to the Exposure backing it this invocation,
|
|
60
60
|
# layering the per-eval +ctx.bind+ overrides over this Context's
|
|
61
61
|
# per-invocation provider results over the Sandbox's static base bindings.
|
|
62
62
|
# An unbound path raises +KeyError+; a fillable left unfilled resolves to
|
|
@@ -67,10 +67,10 @@ module Kobako
|
|
|
67
67
|
# handler is the sole caller.
|
|
68
68
|
def lookup(path)
|
|
69
69
|
key = path.to_s
|
|
70
|
-
|
|
71
|
-
raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(object)
|
|
70
|
+
exposure = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
|
|
71
|
+
raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(exposure.object)
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
exposure
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
# Execute a guest mruby source string in a fresh +mrb_state+ and return the
|
|
@@ -193,7 +193,7 @@ module Kobako
|
|
|
193
193
|
# return value. A could-not-start fault ran no invocation at all, so it
|
|
194
194
|
# carries no Execution and gains only the verb prefix.
|
|
195
195
|
def invoke!(verb, entrypoint: nil)
|
|
196
|
-
@resolved = @extensions.resolve
|
|
196
|
+
@resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
|
|
197
197
|
begin
|
|
198
198
|
snapshot = yield
|
|
199
199
|
rescue Kobako::TrapError => e
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -1,38 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
|
|
10
|
-
# payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
|
|
11
|
-
# § Ext Types → ext 0x01). ID 0 is reserved as the invalid sentinel;
|
|
12
|
-
# the maximum valid ID is 0x7fff_ffff (2^31 - 1).
|
|
13
|
-
#
|
|
14
|
-
# The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
|
|
15
|
-
# privatised so Host App code cannot fabricate a Handle from a bare
|
|
16
|
-
# integer; legitimate Handle instances enter Host App code only as
|
|
17
|
-
# fields on raised error objects. +#with+ — Data's copy-with-changes
|
|
18
|
-
# constructor — is removed for the same reason: a legitimate Handle
|
|
19
|
-
# must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
|
|
20
|
-
# Handles through +.restore+, which exists at exactly two call
|
|
21
|
-
# sites: +Kobako::Codec::ExtTypes#unpack_handle+ (wire decode) and
|
|
22
|
-
# +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
|
|
23
|
-
# (allocator paths). Both live inside +lib/kobako/+ and are not part
|
|
24
|
-
# of any public surface.
|
|
25
|
-
#
|
|
26
|
-
# The mruby counterpart +Kobako::Handle+ lives inside the Wasm guest
|
|
27
|
-
# under the same canonical name and shares neither code nor instances
|
|
28
|
-
# with this host-side class.
|
|
4
|
+
# A Capability Handle as it crosses the wire (ext 0x01, laid out in
|
|
5
|
+
# {docs/wire-codec.md}[link:../../docs/wire-codec.md]), in either
|
|
6
|
+
# direction. Host App code cannot construct one: +.new+ is private and
|
|
7
|
+
# +#with+ removed, so no Handle is ever made from a caller-chosen id.
|
|
8
|
+
# The guest's +Kobako::Handle+ shares only the name.
|
|
29
9
|
class Handle < Data.define(:id)
|
|
30
|
-
#
|
|
31
|
-
# the invalid sentinel and is never allocated.
|
|
10
|
+
# ID 0 is the invalid sentinel and is never allocated.
|
|
32
11
|
MIN_ID = 1
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# on either side of the wire without re-encoding.
|
|
12
|
+
# The positive half of a signed 32-bit integer, so an id fits either
|
|
13
|
+
# side of the wire unchanged.
|
|
36
14
|
MAX_ID = 0x7fff_ffff
|
|
37
15
|
|
|
38
16
|
def initialize(id:)
|
|
@@ -45,16 +23,8 @@ module Kobako
|
|
|
45
23
|
private_class_method :new
|
|
46
24
|
undef_method :with
|
|
47
25
|
|
|
48
|
-
# Host Gem
|
|
49
|
-
#
|
|
50
|
-
# invariant checks still run, while keeping the public +.new+
|
|
51
|
-
# privatised against Host App callers.
|
|
52
|
-
#
|
|
53
|
-
# Two collaborators call this: the codec when an ext 0x01 frame is
|
|
54
|
-
# decoded off the wire, and the allocator paths when a host-side
|
|
55
|
-
# Ruby object is registered into the Sandbox's +Catalog::Handles+. Both
|
|
56
|
-
# paths live inside +lib/kobako/+ and treat this method as a
|
|
57
|
-
# package-private constructor.
|
|
26
|
+
# The Host Gem's own constructor, for the wire decoder and the Handle
|
|
27
|
+
# allocator; it runs the same id checks +.new+ would.
|
|
58
28
|
def self.restore(id)
|
|
59
29
|
allocate.tap { |handle| handle.send(:initialize, id: id) }
|
|
60
30
|
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -66,6 +66,11 @@ module Kobako
|
|
|
66
66
|
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
67
67
|
# chaining.
|
|
68
68
|
#
|
|
69
|
+
# The guest reaches the public methods +object+'s own class and +object+
|
|
70
|
+
# itself define in source, fixed at this call — nothing inherited, mixed
|
|
71
|
+
# in, or built into the platform. An +object+ defining a private
|
|
72
|
+
# +respond_to_guest?(name)+ decides instead, asked on every call.
|
|
73
|
+
#
|
|
69
74
|
# Called with only a +path+, it declares a fillable Service:
|
|
70
75
|
# +bind(path)+ reserves the path for +Kobako::Unresolved+, so the guest
|
|
71
76
|
# sees the constant while the host defers the object it stands for. A
|
|
@@ -95,9 +95,9 @@ module Kobako
|
|
|
95
95
|
# so the reply-shaping and the failure boundary stay one glance wide.
|
|
96
96
|
def run(call, resolver, handler, yielder)
|
|
97
97
|
arguments, carried_handle = decode_arguments(call.payload)
|
|
98
|
-
|
|
98
|
+
exposure = resolve_target(call.target, resolver, handler)
|
|
99
99
|
args, kwargs = resolve_call_args(arguments, handler, carried_handle)
|
|
100
|
-
catch(BREAK_THROW) { invoke(
|
|
100
|
+
catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Decode the Call's payload into its arguments, reporting whether any
|
|
@@ -150,20 +150,22 @@ module Kobako
|
|
|
150
150
|
fault("runtime", "#{error.class}: #{error.message}")
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
# Dispatch +method+ on
|
|
154
|
-
#
|
|
155
|
-
# the
|
|
156
|
-
#
|
|
157
|
-
#
|
|
153
|
+
# Dispatch +method+ on the object behind +exposure+, once the Exposure
|
|
154
|
+
# the guest's reference carries permits it. +kwargs+ is already
|
|
155
|
+
# Symbol-keyed (the +Payload::Arguments+ invariant pins it). The
|
|
156
|
+
# empty-kwargs branch omits the +**+ splat so Ruby 3.x's strict kwargs
|
|
157
|
+
# separation does not reject calls to no-kwarg methods when the wire
|
|
158
|
+
# carries the uniform empty-map shape.
|
|
158
159
|
#
|
|
159
160
|
# +yielder+ is the host-side Yielder materialised when the guest
|
|
160
161
|
# call site supplied a block; its Yielder#to_proc
|
|
161
162
|
# rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
|
|
162
163
|
# so the same call site handles both cases without an explicit
|
|
163
164
|
# conditional.
|
|
164
|
-
def invoke(
|
|
165
|
+
def invoke(exposure, method, args, kwargs, yielder = nil)
|
|
165
166
|
name = method.to_sym
|
|
166
|
-
reject_unreachable!(
|
|
167
|
+
reject_unreachable!(exposure, name)
|
|
168
|
+
target = exposure.object
|
|
167
169
|
block = yielder&.to_proc
|
|
168
170
|
if kwargs.empty?
|
|
169
171
|
target.public_send(name, *args, &block)
|
|
@@ -173,12 +175,12 @@ module Kobako
|
|
|
173
175
|
end
|
|
174
176
|
|
|
175
177
|
# Guard the +public_send+ below: Reflection decides what counts as
|
|
176
|
-
# Service behaviour
|
|
177
|
-
# the guest's +undefined+ fault. Both the ambient-surface
|
|
178
|
-
# the
|
|
179
|
-
#
|
|
180
|
-
def reject_unreachable!(
|
|
181
|
-
reason = Reflection.refusal(
|
|
178
|
+
# Service behaviour through this reference, and its refusal reason
|
|
179
|
+
# becomes the guest's +undefined+ fault. Both the ambient-surface
|
|
180
|
+
# floor and the reference's Exposure answer through it, so a rejected
|
|
181
|
+
# name discloses nothing about which of the two refused.
|
|
182
|
+
def reject_unreachable!(exposure, name)
|
|
183
|
+
reason = Reflection.refusal(exposure, name)
|
|
182
184
|
raise UndefinedTargetError, reason if reason
|
|
183
185
|
end
|
|
184
186
|
|
|
@@ -192,18 +194,18 @@ module Kobako
|
|
|
192
194
|
raise UndefinedTargetError, e.message
|
|
193
195
|
end
|
|
194
196
|
|
|
195
|
-
# Resolve a Call target to the
|
|
196
|
-
# Catalog::Handles) holds. The native side already
|
|
197
|
-
# two forms off the core envelope's +kind+ tag: a
|
|
198
|
-
# constant's path, an Integer is a Capability Handle
|
|
199
|
-
# else-branch is needed — the envelope layer is the system
|
|
200
|
-
# that enforces the invariant.
|
|
197
|
+
# Resolve a Call target to the Exposure the path +resolver+ (or
|
|
198
|
+
# Catalog::Handles) holds for it. The native side already
|
|
199
|
+
# discriminated the two forms off the core envelope's +kind+ tag: a
|
|
200
|
+
# String is a bound constant's path, an Integer is a Capability Handle
|
|
201
|
+
# id. No else-branch is needed — the envelope layer is the system
|
|
202
|
+
# boundary that enforces the invariant.
|
|
201
203
|
def resolve_target(target, resolver, handler)
|
|
202
204
|
case target
|
|
203
205
|
when String
|
|
204
206
|
resolve_path(target, resolver)
|
|
205
207
|
when Integer
|
|
206
|
-
|
|
208
|
+
resolve_handle(target, handler)
|
|
207
209
|
end
|
|
208
210
|
end
|
|
209
211
|
|
|
@@ -213,10 +215,10 @@ module Kobako
|
|
|
213
215
|
raise UndefinedTargetError, e.message
|
|
214
216
|
end
|
|
215
217
|
|
|
216
|
-
# Resolve +id+
|
|
217
|
-
# surfaces as UndefinedTargetError.
|
|
218
|
-
def
|
|
219
|
-
handler.
|
|
218
|
+
# Resolve +id+ to the Exposure its Handle was minted with. An unknown
|
|
219
|
+
# id surfaces as UndefinedTargetError.
|
|
220
|
+
def resolve_handle(id, handler)
|
|
221
|
+
handler.exposure(id)
|
|
220
222
|
rescue Kobako::SandboxError => e
|
|
221
223
|
raise UndefinedTargetError, e.message
|
|
222
224
|
end
|
|
@@ -234,6 +236,7 @@ module Kobako
|
|
|
234
236
|
# known, instead of falling to the boundary's codec floor and reporting
|
|
235
237
|
# as an exchange that produced no Service outcome.
|
|
236
238
|
def encode_ok(value, handler)
|
|
239
|
+
Kobako::Codec::Nesting.assert_within_bound!(value)
|
|
237
240
|
Kobako::Codec::Encoder.encode(value)
|
|
238
241
|
rescue Kobako::Codec::UnsupportedTypeError
|
|
239
242
|
encode_ok(wrap_as_handle(value, handler), handler)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
require_relative "reflection"
|
|
6
|
+
|
|
7
|
+
module Kobako
|
|
8
|
+
module Transport
|
|
9
|
+
# The methods a host object lets the guest call through one reference
|
|
10
|
+
# to it — a bound path or a Capability Handle — fixed when the reference
|
|
11
|
+
# is made. An object defining its own private +respond_to_guest?(name)+
|
|
12
|
+
# is asked on every dispatch; any other exposes only what its own class
|
|
13
|
+
# and the object itself define in source, since the methods a Host App
|
|
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)
|
|
19
|
+
PREDICATE = :respond_to_guest?
|
|
20
|
+
private_constant :PREDICATE
|
|
21
|
+
|
|
22
|
+
# Kernel's own reflection, bound onto each object so a forwarder or a
|
|
23
|
+
# BasicObject answers from its own method table rather than through
|
|
24
|
+
# +method_missing+ / +respond_to_missing?+.
|
|
25
|
+
CLASS_OF = Kernel.instance_method(:class)
|
|
26
|
+
SINGLETON_CLASS_OF = Kernel.instance_method(:singleton_class)
|
|
27
|
+
SINGLETON_METHODS = Kernel.instance_method(:singleton_methods)
|
|
28
|
+
PRIVATE_METHODS = Kernel.instance_method(:private_methods)
|
|
29
|
+
private_constant :CLASS_OF, :SINGLETON_CLASS_OF, :SINGLETON_METHODS, :PRIVATE_METHODS
|
|
30
|
+
|
|
31
|
+
CALLABLE = Set.new(Reflection::CALLABLE_ALLOW).freeze
|
|
32
|
+
NOTHING = Set.new.freeze
|
|
33
|
+
private_constant :CALLABLE, :NOTHING
|
|
34
|
+
|
|
35
|
+
# The Exposure +object+ carries from this moment on. +surfaces+ caches
|
|
36
|
+
# each class's own surface for a caller that makes many references at
|
|
37
|
+
# once (a Handle table), so two objects of one class enumerate it once.
|
|
38
|
+
def self.of(object, surfaces = {})
|
|
39
|
+
klass = CLASS_OF.bind_call(object)
|
|
40
|
+
names = narrows_itself?(object, klass) ? nil : default_names(object, klass, surfaces)
|
|
41
|
+
new(object: object, names: names)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether the guest may call +name+ through this reference. The
|
|
45
|
+
# object's own predicate is consulted with the private surface
|
|
46
|
+
# included, so the guest's +public_send+ can never reach it.
|
|
47
|
+
def exposes?(name)
|
|
48
|
+
return names.include?(name) if names
|
|
49
|
+
|
|
50
|
+
object.__send__(PREDICATE, name) ? true : false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Whether +object+ defines the narrowing predicate itself — read off
|
|
57
|
+
# the method tables, so a forwarder's target or a catch-all
|
|
58
|
+
# +respond_to_missing?+ cannot answer for it.
|
|
59
|
+
def narrows_itself?(object, klass)
|
|
60
|
+
klass.method_defined?(PREDICATE) || klass.private_method_defined?(PREDICATE) ||
|
|
61
|
+
PRIVATE_METHODS.bind_call(object, false).include?(PREDICATE) ||
|
|
62
|
+
SINGLETON_METHODS.bind_call(object, false).include?(PREDICATE)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A callable keeps its callable names; a class-level surface or a
|
|
66
|
+
# forwarder has no surface of its own to expose.
|
|
67
|
+
def default_names(object, klass, surfaces)
|
|
68
|
+
case object
|
|
69
|
+
when Proc, Method then CALLABLE
|
|
70
|
+
when Module, Delegator then NOTHING
|
|
71
|
+
else with_singleton(object, surfaces[klass] ||= class_names(object, klass))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def class_names(object, klass)
|
|
76
|
+
names = authored(klass)
|
|
77
|
+
case object
|
|
78
|
+
when Struct, Data then names.concat(object.members)
|
|
79
|
+
end
|
|
80
|
+
Set.new(names).freeze
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def with_singleton(object, names)
|
|
84
|
+
return names if SINGLETON_METHODS.bind_call(object, false).empty?
|
|
85
|
+
|
|
86
|
+
names.union(authored(SINGLETON_CLASS_OF.bind_call(object))).freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The public methods +owner+ itself defines in source. A method with
|
|
90
|
+
# no source, or whose source is the platform's own, is built in.
|
|
91
|
+
def authored(owner)
|
|
92
|
+
owner.public_instance_methods(false).select do |name|
|
|
93
|
+
file, = owner.instance_method(name).source_location
|
|
94
|
+
file && !file.start_with?("<internal:")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -32,18 +32,19 @@ module Kobako
|
|
|
32
32
|
|
|
33
33
|
# The sole methods reachable on a GADGET_OWNERS target: invoking it
|
|
34
34
|
# (+call+ / +[]+ / +yield+) and the harmless +arity+ / +lambda?+
|
|
35
|
-
# describers that aid guest-side debugging.
|
|
35
|
+
# describers that aid guest-side debugging. Also the whole of what a
|
|
36
|
+
# callable exposes by default, so the floor and the Exposure agree.
|
|
36
37
|
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
37
|
-
private_constant :CALLABLE_ALLOW
|
|
38
38
|
|
|
39
39
|
module_function
|
|
40
40
|
|
|
41
|
-
# The reason +name+ is unreachable
|
|
42
|
-
# dispatch may proceed. Composes the ambient-surface floor with the
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
def refusal(
|
|
46
|
-
ambient_refusal(
|
|
41
|
+
# The reason +name+ is unreachable through +exposure+, or +nil+ when
|
|
42
|
+
# the dispatch may proceed. Composes the ambient-surface floor with the
|
|
43
|
+
# reference's Exposure, in that order: the Exposure only narrows and
|
|
44
|
+
# can never re-open what the floor rejects.
|
|
45
|
+
def refusal(exposure, name)
|
|
46
|
+
ambient_refusal(exposure.object, name) ||
|
|
47
|
+
(exposure.exposes?(name) ? nil : "method #{name.inspect} is not exposed to the guest")
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Guard against ambient reflection methods. A public method whose
|
|
@@ -55,7 +56,17 @@ module Kobako
|
|
|
55
56
|
# only when the target opts into it via +respond_to?+ (dynamic
|
|
56
57
|
# +method_missing+ Services), since the dangerous methods are all
|
|
57
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+.
|
|
58
67
|
def ambient_refusal(target, name)
|
|
68
|
+
return "method #{name.inspect} is not a Service method" if name == :method_missing
|
|
69
|
+
|
|
59
70
|
owner = target.public_method(name).owner
|
|
60
71
|
return nil unless ambient_owner?(owner, target)
|
|
61
72
|
return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
|
|
@@ -79,18 +90,6 @@ module Kobako
|
|
|
79
90
|
GADGET_OWNERS.include?(owner) ||
|
|
80
91
|
(target.is_a?(Module) && owner.singleton_class?)
|
|
81
92
|
end
|
|
82
|
-
|
|
83
|
-
# Consult the target's opt-in narrowing predicate. A bound object may
|
|
84
|
-
# define a private +respond_to_guest?(name)+ to restrict which of its
|
|
85
|
-
# methods the guest reaches; a falsy answer refuses the dispatch. It
|
|
86
|
-
# is consulted with the private surface included so the guest's
|
|
87
|
-
# +public_send+ dispatch can never reach +respond_to_guest?+ itself.
|
|
88
|
-
def narrowing_refusal(target, name)
|
|
89
|
-
return nil unless target.respond_to?(:respond_to_guest?, true)
|
|
90
|
-
return nil if target.__send__(:respond_to_guest?, name)
|
|
91
|
-
|
|
92
|
-
"method #{name.inspect} is not exposed to the guest"
|
|
93
|
-
end
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
95
|
end
|
data/lib/kobako/transport/run.rb
CHANGED
|
@@ -54,10 +54,13 @@ module Kobako
|
|
|
54
54
|
# invocation's table, sharing the same allocator the guest→host
|
|
55
55
|
# return path uses. A wrapped leaf rides as ext 0x01 in its
|
|
56
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.
|
|
57
60
|
def payload(handler)
|
|
58
61
|
Payload::Arguments.new(
|
|
59
|
-
args: Codec::HandleWalk.deep_wrap(args, handler),
|
|
60
|
-
kwargs: Codec::HandleWalk.deep_wrap(kwargs, handler)
|
|
62
|
+
args: Codec::HandleWalk.deep_wrap(args, handler, 1),
|
|
63
|
+
kwargs: Codec::HandleWalk.deep_wrap(kwargs, handler, 1)
|
|
61
64
|
).encode
|
|
62
65
|
end
|
|
63
66
|
|
|
@@ -94,11 +94,13 @@ module Kobako
|
|
|
94
94
|
|
|
95
95
|
private
|
|
96
96
|
|
|
97
|
-
# Encode what the Service is yielding.
|
|
98
|
-
# set
|
|
99
|
-
# the
|
|
100
|
-
# class it
|
|
97
|
+
# Encode what the Service is yielding. Arguments outside the wire
|
|
98
|
+
# type set, or nesting past the wire's bound as one list, fail here,
|
|
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.
|
|
101
102
|
def encode_args(args)
|
|
103
|
+
Kobako::Codec::Nesting.assert_within_bound!(args)
|
|
102
104
|
Kobako::Codec::Encoder.encode(args)
|
|
103
105
|
rescue Kobako::Codec::Error => e
|
|
104
106
|
raise YieldValueError, "Service yielded a value the block cannot receive: #{e.message}"
|
data/lib/kobako/transport.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "transport/call"
|
|
|
4
4
|
require_relative "transport/run"
|
|
5
5
|
require_relative "transport/yielder"
|
|
6
6
|
require_relative "transport/error"
|
|
7
|
+
require_relative "transport/exposure"
|
|
7
8
|
require_relative "transport/reflection"
|
|
8
9
|
require_relative "transport/dispatcher"
|
|
9
10
|
|
|
@@ -12,8 +13,9 @@ module Kobako
|
|
|
12
13
|
# host side of one Call/Reply exchange: the call value objects +Call+
|
|
13
14
|
# (guest→host, as the native side decoded it) and +Run+ (host→guest), the
|
|
14
15
|
# +Dispatcher+ that answers a routed Call, the +Yielder+ that re-enters
|
|
15
|
-
# the guest for a block, the +Reflection+ floor a dispatch must clear,
|
|
16
|
-
# +
|
|
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
|
|
17
19
|
# +Context+ composes them into the dispatch +Proc+ it passes +Runtime+ for
|
|
18
20
|
# that run.
|
|
19
21
|
#
|
data/lib/kobako/usage.rb
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# What one invocation spent against its caps, measured the way the caps
|
|
5
|
+
# measure it:
|
|
6
6
|
#
|
|
7
|
-
# * +wall_time+ —
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# readout — is excluded.
|
|
14
|
-
# * +memory_peak+ — the Integer high-water mark, in bytes, of the
|
|
15
|
-
# per-invocation +memory.grow+ delta past the linear-memory size
|
|
16
|
-
# captured at invocation entry. Same baseline accounting as
|
|
17
|
-
# +memory_limit+: the mruby image's initial allocation and any
|
|
18
|
-
# prior-invocation watermark sit outside the measurement. On
|
|
19
|
-
# +MemoryLimitError+ +memory_peak+ never exceeds the configured
|
|
20
|
-
# cap because the rejected +desired+ value is not promoted into
|
|
21
|
-
# the high-water.
|
|
7
|
+
# * +wall_time+ — Float seconds inside the guest, the span the
|
|
8
|
+
# +timeout+ deadline governs; Service callbacks count, reading the
|
|
9
|
+
# result and the captures afterwards does not.
|
|
10
|
+
# * +memory_peak+ — Integer bytes of memory the invocation grew,
|
|
11
|
+
# against the same baseline as +memory_limit+; it never exceeds the
|
|
12
|
+
# cap, even when the cap was hit.
|
|
22
13
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# after rescuing a trap to diagnose how much of the budget the failing
|
|
27
|
-
# invocation consumed.
|
|
14
|
+
# Filled on every outcome, traps included, so a Host App that rescues a
|
|
15
|
+
# trap can read from the error's +#execution+ how much of the budget the
|
|
16
|
+
# invocation used.
|
|
28
17
|
#
|
|
29
18
|
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
30
19
|
# Steep-friendly shape — see +.rubocop.yml+ for the rationale).
|
data/lib/kobako/version.rb
CHANGED
data/release-please-config.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"last-release-sha": "98509af508988f708bf0d7a76a718bb0428a177e",
|
|
6
6
|
"packages": {
|
|
7
7
|
".": {
|
|
8
|
-
"component": "kobako",
|
|
9
8
|
"include-component-in-tag": false,
|
|
10
9
|
"release-type": "ruby",
|
|
10
|
+
"version-file": "lib/kobako/version.rb",
|
|
11
11
|
"exclude-paths": ["wasm", "crates"]
|
|
12
12
|
},
|
|
13
13
|
"wasm/kobako-core": {
|