kobako 0.27.0 → 0.29.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 +241 -0
- data/Cargo.lock +132 -126
- data/README.md +240 -147
- data/ROADMAP.md +28 -22
- data/SECURITY.md +2 -2
- data/crates/kobako-runtime/CHANGELOG.md +117 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +16 -10
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +12 -2
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +43 -17
- data/crates/kobako-runtime/src/snapshot.rs +33 -15
- data/crates/kobako-runtime/src/yielder.rs +14 -6
- data/crates/kobako-transport/CHANGELOG.md +45 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +103 -61
- data/crates/kobako-transport/src/envelope/request.rs +305 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
- data/crates/kobako-wasmtime/Cargo.toml +23 -16
- data/crates/kobako-wasmtime/README.md +6 -4
- data/crates/kobako-wasmtime/src/abi.rs +67 -36
- data/crates/kobako-wasmtime/src/ambient.rs +64 -5
- data/crates/kobako-wasmtime/src/cache.rs +204 -127
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +69 -15
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
- data/crates/kobako-wasmtime/src/driver.rs +174 -125
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
- data/crates/kobako-wasmtime/src/invocation.rs +84 -412
- data/crates/kobako-wasmtime/src/io.rs +89 -0
- data/crates/kobako-wasmtime/src/lib.rs +7 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +66 -59
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +177 -113
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +11 -13
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +190 -312
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +29 -0
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +17 -60
- data/lib/kobako/catalog/snippets.rb +41 -69
- data/lib/kobako/catalog.rb +13 -10
- data/lib/kobako/codec/decoder.rb +10 -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 +21 -11
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +10 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +83 -117
- data/lib/kobako/errors.rb +57 -132
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/filesystem.rb +38 -0
- 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 +25 -55
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +128 -137
- data/lib/kobako/sandbox_options.rb +71 -90
- data/lib/kobako/snippet/bytecode.rb +11 -0
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +2 -10
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +127 -206
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +10 -17
- data/lib/kobako/transport/reflection.rb +108 -63
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +20 -94
- data/lib/kobako/transport.rb +4 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +13 -0
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +3 -5
- data/sig/kobako/catalog/snippets.rbs +5 -5
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +4 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +15 -8
- data/sig/kobako/errors.rbs +10 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -45
- data/sig/kobako/sandbox.rbs +11 -1
- data/sig/kobako/sandbox_options.rbs +25 -7
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +15 -19
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +19 -4
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +5 -9
- data/sig/kobako/transport.rbs +9 -8
- metadata +75 -10
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/lib/kobako/snippet/binary.rb +0 -26
- data/rust-toolchain.toml +0 -15
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -2,26 +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
|
|
14
|
-
# Modules whose instance methods are ambient Ruby reflection /
|
|
15
|
-
# metaprogramming surface (+send+, +public_send+, +instance_eval+,
|
|
16
|
-
# +method+, +tap+, +instance_variable_get+, ...) rather than Service
|
|
17
|
-
# behaviour. A guest-supplied method name resolving to one of these is
|
|
18
|
-
# rejected: only methods the bound object itself exposes as Service
|
|
19
|
-
# behaviour are reachable, and +public_send(:send, ...)+ would
|
|
20
|
-
# otherwise let a guest pivot through +send+ into the private
|
|
21
|
-
# +Kernel#eval+ / +#system+ surface (host RCE).
|
|
22
|
-
META_OWNERS = [BasicObject, Kernel, Object, Module, Class].freeze
|
|
23
|
-
private_constant :META_OWNERS
|
|
24
|
-
|
|
5
|
+
module Reflection # :nodoc:
|
|
25
6
|
# Callable gadget types whose own public methods are reflection surface
|
|
26
7
|
# (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
|
|
27
8
|
# hand back the underlying object) rather than Service behaviour. Only
|
|
@@ -36,59 +17,123 @@ module Kobako
|
|
|
36
17
|
# callable exposes by default, so the floor and the Exposure agree.
|
|
37
18
|
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
38
19
|
|
|
39
|
-
module_function
|
|
40
|
-
|
|
41
20
|
# The reason +name+ is unreachable through +exposure+, or +nil+ when
|
|
42
21
|
# the dispatch may proceed. Composes the ambient-surface floor with the
|
|
43
22
|
# reference's Exposure, in that order: the Exposure only narrows and
|
|
44
23
|
# can never re-open what the floor rejects.
|
|
45
|
-
def refusal(exposure, name)
|
|
24
|
+
def self.refusal(exposure, name)
|
|
46
25
|
ambient_refusal(exposure.object, name) ||
|
|
47
26
|
(exposure.exposes?(name) ? nil : "method #{name.inspect} is not exposed to the guest")
|
|
48
27
|
end
|
|
49
28
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
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+.
|
|
67
|
-
def ambient_refusal(target, name)
|
|
68
|
-
return "method #{name.inspect} is not a Service method" if name == :method_missing
|
|
69
|
-
|
|
70
|
-
owner = target.public_method(name).owner
|
|
71
|
-
return nil unless ambient_owner?(owner, target)
|
|
72
|
-
return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
|
|
73
|
-
|
|
74
|
-
"method #{name.inspect} is not a Service method"
|
|
75
|
-
rescue NameError
|
|
76
|
-
return nil if target.respond_to?(name)
|
|
77
|
-
|
|
78
|
-
"no public method #{name.inspect} on target"
|
|
29
|
+
# Whether +method+ is written in Ruby rather than built into the
|
|
30
|
+
# platform or copied from the surface every object shares. A native
|
|
31
|
+
# method has no source, and a built-in one names an +<internal:>+ file,
|
|
32
|
+
# so a copy, alias or rebinding of either keeps that mark wherever it
|
|
33
|
+
# is reached from.
|
|
34
|
+
def self.authored?(method)
|
|
35
|
+
file, = method.source_location
|
|
36
|
+
!file.nil? && !file.start_with?("<internal:") && !ambient_copy?(method)
|
|
79
37
|
end
|
|
80
38
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
39
|
+
class << self
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Whether +method+ is a copy, alias or rebinding of a Ruby method
|
|
43
|
+
# +Class+ inherits, such as the +name!+ alias OpenStruct makes of a
|
|
44
|
+
# mixed-in +try+. The copy is owned by the class that made it, but
|
|
45
|
+
# keeps the original's name and source.
|
|
46
|
+
def ambient_copy?(method)
|
|
47
|
+
name = method.original_name
|
|
48
|
+
Class.ancestors.any? do |owner|
|
|
49
|
+
(owner.method_defined?(name, false) || owner.private_method_defined?(name, false)) &&
|
|
50
|
+
owner.instance_method(name).source_location == method.source_location
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Guard against ambient reflection methods. A public method whose
|
|
55
|
+
# owner +Class+ inherits or a GADGET_OWNERS module — or a singleton
|
|
56
|
+
# class, the owner of every class-level method (+File.popen+ /
|
|
57
|
+
# +Kernel.system+, unreachable via any fixed core-module list) — is
|
|
58
|
+
# rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
|
|
59
|
+
# stays invocable). A name with no concrete public method is allowed
|
|
60
|
+
# only when the target opts into it via +respond_to?+ (dynamic
|
|
61
|
+
# +method_missing+ Services), since the dangerous methods are all
|
|
62
|
+
# concretely defined and therefore never reach that branch.
|
|
63
|
+
#
|
|
64
|
+
# +method_missing+ named explicitly is refused whatever its owner:
|
|
65
|
+
# it is Ruby's dynamic-dispatch hook, and a public override (a
|
|
66
|
+
# +Delegator+) binds and calls the private method the guest passes as
|
|
67
|
+
# its first argument (+Kernel#system+). A dynamic +method_missing+
|
|
68
|
+
# Service is untouched — the guest reaches it by the virtual name,
|
|
69
|
+
# which resolves through the +respond_to?+ opt-in, never by naming
|
|
70
|
+
# +method_missing+.
|
|
71
|
+
#
|
|
72
|
+
# A public method must also be authored, so a Service narrowing itself
|
|
73
|
+
# never reaches a built-in or ambient method by a copy, alias or
|
|
74
|
+
# rebinding an owner check cannot see; a non-public one is never
|
|
75
|
+
# reached through a dynamic opt-in either.
|
|
76
|
+
#
|
|
77
|
+
# A name with no method never reaches +instance_method+, which would
|
|
78
|
+
# keep a guest-chosen name as a Symbol for the life of the process.
|
|
79
|
+
def ambient_refusal(target, name)
|
|
80
|
+
return refused(name) if name == :method_missing
|
|
81
|
+
|
|
82
|
+
klass = lookup_class(target)
|
|
83
|
+
return defined_refusal(target, name, klass) if klass.public_method_defined?(name)
|
|
84
|
+
return "no public method #{name.inspect} on target" unless target.respond_to?(name)
|
|
85
|
+
return refused(name) if klass.method_defined?(name) || klass.private_method_defined?(name)
|
|
86
|
+
|
|
87
|
+
owner_refusal(target, name, klass)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def defined_refusal(target, name, klass)
|
|
91
|
+
method = klass.instance_method(name)
|
|
92
|
+
owner_refusal(target, name, method.owner) ||
|
|
93
|
+
(authored_for?(target, name, method, klass) ? nil : refused(name))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A bound callable keeps its callable names and a record its member
|
|
97
|
+
# readers, though the platform defines both.
|
|
98
|
+
def authored_for?(target, name, method, klass)
|
|
99
|
+
return true if Reflection.authored?(method)
|
|
100
|
+
return true if GADGET_OWNERS.include?(method.owner) && CALLABLE_ALLOW.include?(name)
|
|
101
|
+
|
|
102
|
+
(klass <= Struct || klass <= Data) && target.members.include?(name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def refused(name)
|
|
106
|
+
"method #{name.inspect} is not a Service method"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def owner_refusal(target, name, owner)
|
|
110
|
+
return nil unless ambient_owner?(owner, target)
|
|
111
|
+
return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
|
|
112
|
+
|
|
113
|
+
refused(name)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The class a method lookup on +target+ starts at. A plain object's
|
|
117
|
+
# singleton class is taken only when it already holds a public
|
|
118
|
+
# method, since asking for it otherwise would create one.
|
|
119
|
+
def lookup_class(target)
|
|
120
|
+
return target.singleton_class if target.is_a?(Module) || target.singleton_methods.any?
|
|
121
|
+
|
|
122
|
+
target.class
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Whatever +Class+ inherits is surface every object shares rather
|
|
126
|
+
# than Service behaviour: the reflection +BasicObject+ through +Class+
|
|
127
|
+
# define (+send+ pivots into +Kernel#eval+, a host RCE), and every
|
|
128
|
+
# module a library mixes into them, such as a +try+ dispatching by
|
|
129
|
+
# name or a +to_json+ dumping instance variables. A plain object's
|
|
130
|
+
# own singleton method (+def obj.x+) stays reachable, since only a
|
|
131
|
+
# Module target counts its singleton class as ambient.
|
|
132
|
+
def ambient_owner?(owner, target)
|
|
133
|
+
owner >= Class ||
|
|
134
|
+
GADGET_OWNERS.include?(owner) ||
|
|
135
|
+
(target.is_a?(Module) && owner.singleton_class?)
|
|
136
|
+
end
|
|
92
137
|
end
|
|
93
138
|
end
|
|
94
139
|
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,101 +4,43 @@ 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, failure)
|
|
10
|
+
@guest_yielder = guest_yielder
|
|
41
11
|
@break_tag = break_tag
|
|
42
|
-
@
|
|
12
|
+
@handles = handles
|
|
13
|
+
@failure = failure
|
|
43
14
|
@active = true
|
|
44
|
-
@raised = nil
|
|
45
15
|
end
|
|
46
16
|
|
|
47
|
-
# The
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# raised its own has reported something else, and the guest must
|
|
51
|
-
# hear about that instead.
|
|
52
|
-
#
|
|
53
|
-
# The class travels with the message because not every block failure
|
|
54
|
-
# is an exception the guest holds — a block value the guest refused
|
|
55
|
-
# has a class to raise under and no object to continue.
|
|
56
|
-
def fault_text(error)
|
|
57
|
-
raised = @raised
|
|
58
|
-
return if raised.nil? || !error.equal?(raised)
|
|
59
|
-
|
|
60
|
-
"#{raised.klass}: #{raised.message}"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Re-enter the guest with +args+ and reify the Yield Reply into
|
|
64
|
-
# Ruby control flow. Raises +LocalJumpError+ if called after
|
|
65
|
-
# #invalidate!. The ok value is consumed by the host Service
|
|
66
|
-
# method, so a Capability Handle in it is restored to its host object.
|
|
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.
|
|
17
|
+
# The ok value is consumed by the host Service, so a Capability Handle
|
|
18
|
+
# in it is restored. A block exit stays in the guest, so the Service
|
|
19
|
+
# is only unwound.
|
|
70
20
|
def yield(*args)
|
|
71
21
|
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
72
22
|
|
|
73
|
-
arm, body, klass = @
|
|
74
|
-
raise
|
|
23
|
+
arm, body, klass = @guest_yielder.call(encode_args(args))
|
|
24
|
+
raise UnsupportedError, "the Sandbox runtime does not support yielding to a block" if arm == :unsupported
|
|
25
|
+
raise @failure.remember(BlockError.new(body, klass: klass)) if arm == :error
|
|
75
26
|
|
|
76
|
-
|
|
77
|
-
throw @break_tag, value if arm == :break
|
|
27
|
+
throw @break_tag if arm == :break
|
|
78
28
|
|
|
79
|
-
restore(
|
|
29
|
+
restore(*decode_body(body))
|
|
80
30
|
end
|
|
81
31
|
|
|
82
|
-
# The Proc the Dispatcher passes as +&block+, binding #yield so a
|
|
83
|
-
# Service method's +yield+ / +block.call+ drives the round-trip.
|
|
84
32
|
def to_proc
|
|
85
33
|
method(:yield).to_proc
|
|
86
34
|
end
|
|
87
35
|
|
|
88
|
-
|
|
89
|
-
# when the originating dispatch frame returns; any later #yield
|
|
90
|
-
# call then raises +LocalJumpError+.
|
|
91
|
-
def invalidate!
|
|
36
|
+
def invalidate
|
|
92
37
|
@active = false
|
|
93
38
|
end
|
|
94
39
|
|
|
95
40
|
private
|
|
96
41
|
|
|
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.
|
|
42
|
+
# Restated so the Service reads a refusal of its own argument rather
|
|
43
|
+
# than a codec class it never named.
|
|
102
44
|
def encode_args(args)
|
|
103
45
|
Kobako::Codec::Nesting.assert_within_bound!(args)
|
|
104
46
|
Kobako::Codec::Encoder.encode(args)
|
|
@@ -106,33 +48,17 @@ module Kobako
|
|
|
106
48
|
raise YieldValueError, "Service yielded a value the block cannot receive: #{e.message}"
|
|
107
49
|
end
|
|
108
50
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# an earlier one and yielded again has already handled it.
|
|
113
|
-
def remember(error)
|
|
114
|
-
@raised = error
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Decode a value-carrying arm's payload, answering the value and
|
|
118
|
-
# whether the decode carried a Capability Handle. The tracking
|
|
119
|
-
# bracket opens only around this decode: the guest re-entry may run
|
|
120
|
-
# nested dispatches whose own brackets would otherwise pollute the
|
|
121
|
-
# signal.
|
|
51
|
+
# The tracking bracket opens only around this decode: the guest
|
|
52
|
+
# re-entry may run nested dispatches whose own brackets would
|
|
53
|
+
# otherwise pollute the signal.
|
|
122
54
|
def decode_body(body)
|
|
123
55
|
Kobako::Codec.track_handles { Kobako::Codec::Decoder.decode(body) }
|
|
124
56
|
end
|
|
125
57
|
|
|
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
58
|
def restore(value, carried_handle)
|
|
133
59
|
return value unless carried_handle
|
|
134
60
|
|
|
135
|
-
Kobako::Codec::HandleWalk.deep_restore(value, @
|
|
61
|
+
Kobako::Codec::HandleWalk.deep_restore(value, @handles)
|
|
136
62
|
end
|
|
137
63
|
end
|
|
138
64
|
end
|
data/lib/kobako/transport.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "transport/call"
|
|
4
4
|
require_relative "transport/run"
|
|
5
|
+
require_relative "transport/block_failure"
|
|
5
6
|
require_relative "transport/yielder"
|
|
6
7
|
require_relative "transport/error"
|
|
7
8
|
require_relative "transport/exposure"
|
|
@@ -9,20 +10,8 @@ require_relative "transport/reflection"
|
|
|
9
10
|
require_relative "transport/dispatcher"
|
|
10
11
|
|
|
11
12
|
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
|
|
13
|
+
# The host side of the guest↔host exchange. Only Transport::Error is
|
|
14
|
+
# public; the rest serves one invocation's dispatch.
|
|
15
|
+
module Transport # :nodoc:
|
|
27
16
|
end
|
|
28
17
|
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
data/lib/kobako.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
|