kobako 0.28.0 → 0.30.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 +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- 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 +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- 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 +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -8,7 +8,7 @@ module Kobako
|
|
|
8
8
|
def initialize(args: [], kwargs: {})
|
|
9
9
|
raise ArgumentError, "payload args must be Array" unless args.is_a?(Array)
|
|
10
10
|
|
|
11
|
-
validate_kwargs
|
|
11
|
+
validate_kwargs(kwargs)
|
|
12
12
|
super
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -32,7 +32,7 @@ module Kobako
|
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
-
def validate_kwargs
|
|
35
|
+
def validate_kwargs(kwargs)
|
|
36
36
|
raise ArgumentError, "payload kwargs must be Hash" unless kwargs.is_a?(Hash)
|
|
37
37
|
|
|
38
38
|
kwargs.each_key do |key|
|
data/lib/kobako/pool.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Kobako
|
|
|
20
20
|
# Raises ArgumentError when +slots+ is not a positive Integer or
|
|
21
21
|
# +checkout_timeout+ is not a positive finite number.
|
|
22
22
|
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
|
23
|
-
validate_slots
|
|
23
|
+
validate_slots(slots)
|
|
24
24
|
@slots = slots
|
|
25
25
|
@checkout_timeout = normalize_checkout_timeout(checkout_timeout)
|
|
26
26
|
@sandbox_options = sandbox_options
|
|
@@ -69,13 +69,13 @@ module Kobako
|
|
|
69
69
|
return [:build, nil]
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
await_slot
|
|
72
|
+
await_slot(deadline)
|
|
73
73
|
[:retry, nil]
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Must run while holding +@mutex+.
|
|
78
|
-
def await_slot
|
|
78
|
+
def await_slot(deadline)
|
|
79
79
|
remaining = deadline && (deadline - monotonic_now)
|
|
80
80
|
if remaining && remaining <= 0
|
|
81
81
|
raise PoolTimeoutError,
|
|
@@ -94,7 +94,7 @@ module Kobako
|
|
|
94
94
|
done = true
|
|
95
95
|
sandbox
|
|
96
96
|
ensure
|
|
97
|
-
release_capacity
|
|
97
|
+
release_capacity unless done
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def checkin(sandbox)
|
|
@@ -104,7 +104,7 @@ module Kobako
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def release_capacity
|
|
107
|
+
def release_capacity
|
|
108
108
|
@mutex.synchronize do
|
|
109
109
|
@constructed -= 1
|
|
110
110
|
@slot_freed.signal
|
|
@@ -117,7 +117,7 @@ module Kobako
|
|
|
117
117
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
def validate_slots
|
|
120
|
+
def validate_slots(slots)
|
|
121
121
|
return if slots.is_a?(Integer) && slots.positive?
|
|
122
122
|
|
|
123
123
|
raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -24,7 +24,8 @@ module Kobako
|
|
|
24
24
|
# readable on the Sandbox itself, as in #timeout.
|
|
25
25
|
attr_reader :options
|
|
26
26
|
|
|
27
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
27
|
+
def_delegators :@options, :timeout, :setup_timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
28
|
+
:open_file_limit, :profile, :gvl
|
|
28
29
|
|
|
29
30
|
# Build a Sandbox on the Guest Binary at +wasm_path+, the one bundled
|
|
30
31
|
# with the gem by default. The other keywords are the options
|
|
@@ -40,8 +41,9 @@ module Kobako
|
|
|
40
41
|
@snippets = Catalog::Snippets.new
|
|
41
42
|
@flags = Catalog::Flags.new
|
|
42
43
|
@extensions = Catalog::Extensions.new
|
|
44
|
+
@grants = Catalog::Grants.new
|
|
43
45
|
@sealed = false
|
|
44
|
-
@runtime = build_runtime
|
|
46
|
+
@runtime = build_runtime
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
# Make +object+ reachable from the guest as the constant at +path+, a
|
|
@@ -56,10 +58,11 @@ module Kobako
|
|
|
56
58
|
# the constant, and Context#bind supplies the object for each invocation.
|
|
57
59
|
# A call to one left unfilled fails as a ServiceError.
|
|
58
60
|
#
|
|
59
|
-
# Raises ArgumentError for a malformed path, a path
|
|
60
|
-
#
|
|
61
|
+
# Raises ArgumentError for a malformed path, a path under the reserved
|
|
62
|
+
# +Kobako+ namespace, a path that collides with an existing binding, or a
|
|
63
|
+
# call after the first invocation.
|
|
61
64
|
def bind(path, object = Unresolved)
|
|
62
|
-
refuse_once_invoked
|
|
65
|
+
refuse_once_invoked("bind")
|
|
63
66
|
@services.bind(path, object)
|
|
64
67
|
self
|
|
65
68
|
end
|
|
@@ -71,12 +74,37 @@ module Kobako
|
|
|
71
74
|
# Raises ArgumentError for a malformed Extension, a call after the first
|
|
72
75
|
# invocation, or, at the first invocation, a missing dependency.
|
|
73
76
|
def install(*extensions)
|
|
74
|
-
refuse_once_invoked
|
|
77
|
+
refuse_once_invoked("install")
|
|
75
78
|
|
|
76
79
|
extensions.each { |extension| @extensions.install(extension, snippets: @snippets, services: @services) }
|
|
77
80
|
self
|
|
78
81
|
end
|
|
79
82
|
|
|
83
|
+
# Mount +filesystem+, a Kobako::Filesystem, at the absolute guest path
|
|
84
|
+
# +at+, where the guest's +File+ reaches it. +access+ is +:read+ or
|
|
85
|
+
# +:read_write+; a write beneath a read-only mount fails as
|
|
86
|
+
# <tt>Errno::EROFS</tt> in the guest. Returns +self+.
|
|
87
|
+
#
|
|
88
|
+
# A given block judges each open and each change +access+ allows, asked
|
|
89
|
+
# with the guest path and +:read+ or +:read_write+, and can only refuse
|
|
90
|
+
# more; an open that may create asks +:read_write+ first, then what it
|
|
91
|
+
# opens for. A falsy answer,
|
|
92
|
+
# or a StandardError, fails that call as <tt>Errno::EACCES</tt> in the
|
|
93
|
+
# guest and leaves the script running.
|
|
94
|
+
#
|
|
95
|
+
# A path may sit beneath another mount, and the guest reaches the
|
|
96
|
+
# innermost one.
|
|
97
|
+
#
|
|
98
|
+
# Raises TypeError for a path that is no String or something other than
|
|
99
|
+
# a Filesystem, and ArgumentError for a malformed or duplicate path, an
|
|
100
|
+
# unknown access, or a call after the first invocation.
|
|
101
|
+
def mount(at, filesystem, access: :read, &policy)
|
|
102
|
+
refuse_once_invoked("mount")
|
|
103
|
+
Runtime.validate_mount(at, access, filesystem)
|
|
104
|
+
@grants.register(Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy))
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
80
108
|
# Register a snippet that every invocation runs, in the order registered,
|
|
81
109
|
# before its own code. Returns +self+.
|
|
82
110
|
#
|
|
@@ -90,7 +118,7 @@ module Kobako
|
|
|
90
118
|
# after the first invocation. A snippet that fails to load fails every
|
|
91
119
|
# invocation with SandboxError, or BytecodeError for bytecode.
|
|
92
120
|
def preload(code: nil, name: nil, binary: nil)
|
|
93
|
-
refuse_once_invoked
|
|
121
|
+
refuse_once_invoked("preload")
|
|
94
122
|
|
|
95
123
|
@snippets.register(code: code, name: name, binary: binary)
|
|
96
124
|
self
|
|
@@ -106,7 +134,7 @@ module Kobako
|
|
|
106
134
|
# Raises ArgumentError for a malformed or duplicate flag, or a call after
|
|
107
135
|
# the first invocation.
|
|
108
136
|
def flag(name, value = "", advisory: false)
|
|
109
|
-
refuse_once_invoked
|
|
137
|
+
refuse_once_invoked("flag")
|
|
110
138
|
|
|
111
139
|
@flags.register(name, value, advisory: advisory)
|
|
112
140
|
self
|
|
@@ -121,7 +149,7 @@ module Kobako
|
|
|
121
149
|
# Handle or a non-Symbol keyword. A failed run raises as #eval does.
|
|
122
150
|
def run(target, *args, **kwargs, &block)
|
|
123
151
|
request = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
124
|
-
|
|
152
|
+
Codec.within_value_budget(@options.value_budget) { begin_invocation.run(request, &block) }
|
|
125
153
|
end
|
|
126
154
|
|
|
127
155
|
# Evaluate +code+, mruby source as a String, and return the Execution,
|
|
@@ -136,42 +164,42 @@ module Kobako
|
|
|
136
164
|
def eval(code, &block)
|
|
137
165
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
138
166
|
|
|
139
|
-
|
|
167
|
+
Codec.within_value_budget(@options.value_budget) { begin_invocation.eval(code, &block) }
|
|
140
168
|
end
|
|
141
169
|
|
|
142
170
|
private
|
|
143
171
|
|
|
144
172
|
# A runtime whose declared posture falls below the request never runs
|
|
145
173
|
# guest code.
|
|
146
|
-
def build_runtime
|
|
147
|
-
runtime = Kobako::Runtime.from_path(@wasm_path,
|
|
148
|
-
|
|
149
|
-
@options.gvl)
|
|
150
|
-
@options.enforce_floor!(runtime.profile)
|
|
174
|
+
def build_runtime
|
|
175
|
+
runtime = Kobako::Runtime.from_path(@wasm_path, **@options.to_h)
|
|
176
|
+
@options.enforce_floor(runtime.profile)
|
|
151
177
|
runtime
|
|
152
178
|
end
|
|
153
179
|
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
Context.new(runtime: @runtime, services: @services, snippets: @snippets, flags: @flags,
|
|
159
|
-
extensions: @extensions)
|
|
180
|
+
# The one place the Preamble's parts are put together, for a compile
|
|
181
|
+
# and for every invocation alike.
|
|
182
|
+
def preamble(snippets)
|
|
183
|
+
[@services.paths, snippets, @flags.entries]
|
|
160
184
|
end
|
|
161
185
|
|
|
162
186
|
# Registration closes at the first invocation, so every invocation ships
|
|
163
|
-
# the same Services, snippets, flags and
|
|
164
|
-
def refuse_once_invoked
|
|
187
|
+
# the same Services, snippets, flags, Extensions and mounts.
|
|
188
|
+
def refuse_once_invoked(verb)
|
|
165
189
|
raise ArgumentError, "cannot #{verb} after first Sandbox invocation" if @sealed
|
|
166
190
|
end
|
|
167
191
|
|
|
168
192
|
# Sealing before the dependency check keeps a Sandbox whose check failed
|
|
169
193
|
# closed to further registration. The sealed snippets are compiled once,
|
|
170
|
-
# before the first invocation replays them.
|
|
171
|
-
|
|
194
|
+
# before the first invocation replays them. Per-invocation state lives on
|
|
195
|
+
# the Context it answers, so nothing an invocation does is written back
|
|
196
|
+
# onto the shared config.
|
|
197
|
+
def begin_invocation
|
|
172
198
|
@sealed = true
|
|
173
|
-
@extensions.assert_dependencies
|
|
174
|
-
@snippets.compile { |entries| @runtime.compile(
|
|
199
|
+
@extensions.assert_dependencies
|
|
200
|
+
@snippets.compile { |entries| @runtime.compile(preamble(entries)) }
|
|
201
|
+
Context.new(runtime: @runtime, services: @services, extensions: @extensions, grants: @grants,
|
|
202
|
+
preamble: preamble(@snippets.entries))
|
|
175
203
|
end
|
|
176
204
|
end
|
|
177
205
|
end
|
|
@@ -8,25 +8,38 @@ module Kobako
|
|
|
8
8
|
# default, and +nil+ switches it off.
|
|
9
9
|
#
|
|
10
10
|
# [+timeout+] Float seconds one invocation may run.
|
|
11
|
+
# [+setup_timeout+] Float seconds the Guest Binary may take to be set up
|
|
12
|
+
# the first time its path is loaded.
|
|
11
13
|
# [+memory_limit+] Integer bytes one invocation may grow guest memory by.
|
|
12
14
|
# [+stdout_limit+, +stderr_limit+]
|
|
13
15
|
# Integer bytes kept from each output channel.
|
|
16
|
+
# [+open_file_limit+]
|
|
17
|
+
# Integer descriptors one invocation may hold open, the
|
|
18
|
+
# standard ones and each Mount's included.
|
|
14
19
|
# [+profile+] The isolation the runtime must provide, one of PROFILES.
|
|
15
20
|
# [+gvl+] How an invocation holds Ruby's GVL, one of GVL_MODES.
|
|
16
21
|
#
|
|
17
22
|
# Raises ArgumentError for a value outside these shapes, and SetupError
|
|
18
23
|
# when the runtime provides less isolation than +profile+ asks for.
|
|
19
|
-
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
24
|
+
class SandboxOptions < Data.define(:timeout, :setup_timeout, :memory_limit, :stdout_limit, :stderr_limit,
|
|
25
|
+
:open_file_limit, :profile, :gvl)
|
|
20
26
|
# The default +timeout+: 60 seconds.
|
|
21
27
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
22
28
|
|
|
23
|
-
# The default +
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
# The default +setup_timeout+: 10 seconds.
|
|
30
|
+
DEFAULT_SETUP_TIMEOUT_SECONDS = 10.0
|
|
31
|
+
|
|
32
|
+
# The default +memory_limit+: 4 MiB of growth past the guest's starting
|
|
33
|
+
# memory, sized for a guest whose every value takes 16 bytes.
|
|
34
|
+
DEFAULT_MEMORY_LIMIT = 4 << 20
|
|
26
35
|
|
|
27
36
|
# The default +stdout_limit+ and +stderr_limit+: 1 MiB each.
|
|
28
37
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
29
38
|
|
|
39
|
+
# The default +open_file_limit+: 1024, the soft limit Linux gives a
|
|
40
|
+
# process.
|
|
41
|
+
DEFAULT_OPEN_FILE_LIMIT = 1024
|
|
42
|
+
|
|
30
43
|
# The isolation profiles, weakest first. +:hermetic+ denies the guest the
|
|
31
44
|
# clock and entropy; +:permissive+ leaves them live.
|
|
32
45
|
PROFILES = %i[permissive hermetic].freeze
|
|
@@ -42,23 +55,36 @@ module Kobako
|
|
|
42
55
|
DEFAULT_GVL = :hold
|
|
43
56
|
|
|
44
57
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
58
|
+
setup_timeout: DEFAULT_SETUP_TIMEOUT_SECONDS,
|
|
45
59
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
46
60
|
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
47
61
|
stderr_limit: DEFAULT_OUTPUT_LIMIT,
|
|
62
|
+
open_file_limit: DEFAULT_OPEN_FILE_LIMIT,
|
|
48
63
|
profile: DEFAULT_PROFILE,
|
|
49
64
|
gvl: DEFAULT_GVL)
|
|
50
|
-
timeout = normalize_timeout(timeout)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
timeout = normalize_timeout(timeout, "timeout")
|
|
66
|
+
setup_timeout = normalize_timeout(setup_timeout, "setup_timeout")
|
|
67
|
+
memory_limit = normalize_limit(memory_limit, "memory_limit")
|
|
68
|
+
stdout_limit = normalize_limit(stdout_limit, "stdout_limit")
|
|
69
|
+
stderr_limit = normalize_limit(stderr_limit, "stderr_limit")
|
|
70
|
+
open_file_limit = normalize_limit(open_file_limit, "open_file_limit")
|
|
54
71
|
profile = normalize_choice(profile, PROFILES, "profile")
|
|
55
72
|
gvl = normalize_choice(gvl, GVL_MODES, "gvl")
|
|
56
73
|
super
|
|
57
74
|
end
|
|
58
75
|
|
|
76
|
+
# The least guest memory one value occupies, so a guest payload may hold
|
|
77
|
+
# one value for each of these bytes of +memory_limit+.
|
|
78
|
+
VALUE_BYTES = 16
|
|
79
|
+
private_constant :VALUE_BYTES
|
|
80
|
+
|
|
81
|
+
def value_budget # :nodoc:
|
|
82
|
+
memory_limit&./(VALUE_BYTES)
|
|
83
|
+
end
|
|
84
|
+
|
|
59
85
|
# Both fallbacks fail closed: an unknown declaration ranks below every
|
|
60
86
|
# request, and an unknown request refuses every declaration.
|
|
61
|
-
def enforce_floor
|
|
87
|
+
def enforce_floor(declared) # :nodoc:
|
|
62
88
|
return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
|
|
63
89
|
|
|
64
90
|
raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
|
|
@@ -69,17 +95,17 @@ module Kobako
|
|
|
69
95
|
|
|
70
96
|
# A zero or negative timeout would either fire instantly or never, both
|
|
71
97
|
# more surprising than an early ArgumentError.
|
|
72
|
-
def normalize_timeout(timeout)
|
|
98
|
+
def normalize_timeout(timeout, name)
|
|
73
99
|
return nil if timeout.nil?
|
|
74
|
-
raise ArgumentError, "
|
|
100
|
+
raise ArgumentError, "#{name} must be Numeric or nil, got #{timeout.class}" unless timeout.is_a?(Numeric)
|
|
75
101
|
|
|
76
102
|
seconds = timeout.to_f
|
|
77
|
-
raise ArgumentError, "
|
|
103
|
+
raise ArgumentError, "#{name} must be > 0 (got #{timeout})" unless seconds.positive? && seconds.finite?
|
|
78
104
|
|
|
79
105
|
seconds
|
|
80
106
|
end
|
|
81
107
|
|
|
82
|
-
def
|
|
108
|
+
def normalize_limit(limit, name)
|
|
83
109
|
return nil if limit.nil?
|
|
84
110
|
unless limit.is_a?(Integer) && limit.positive?
|
|
85
111
|
raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
|
-
class
|
|
5
|
+
class Bytecode < Data.define(:body) # :nodoc:
|
|
6
6
|
# Names the snippet form the guest replays this entry as. The wire's
|
|
7
|
-
# discriminant byte is assigned by the
|
|
7
|
+
# discriminant byte is assigned by the Envelope, not here.
|
|
8
8
|
KIND = :bytecode
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Kobako
|
|
|
4
4
|
module Snippet
|
|
5
5
|
class Source < Data.define(:name, :body) # :nodoc:
|
|
6
6
|
# Names the snippet form the guest replays this entry as. The wire's
|
|
7
|
-
# discriminant byte is assigned by the
|
|
7
|
+
# discriminant byte is assigned by the Envelope, not here.
|
|
8
8
|
KIND = :source
|
|
9
9
|
end
|
|
10
10
|
end
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Transport
|
|
5
|
+
# The newest failure a guest block raised to a Service during one
|
|
6
|
+
# invocation, whichever open Call carried that block: a Service may run
|
|
7
|
+
# a block stored from an earlier Call, and its failure is still the
|
|
8
|
+
# guest's own.
|
|
9
|
+
class BlockFailure # :nodoc:
|
|
10
|
+
def initialize
|
|
11
|
+
@raised = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Only the newest is kept: a Service that rescued an earlier one and
|
|
15
|
+
# yielded again has already handled it.
|
|
16
|
+
def remember(error)
|
|
17
|
+
@raised = error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Identity rather than class: a Service that rescued the block's
|
|
21
|
+
# failure and raised its own has reported something else, and the
|
|
22
|
+
# guest must hear about that instead.
|
|
23
|
+
#
|
|
24
|
+
# The class travels with the message because not every block failure
|
|
25
|
+
# is an exception the guest holds — a block value the guest refused
|
|
26
|
+
# has a class to raise under and no object to continue.
|
|
27
|
+
def fault_text(error)
|
|
28
|
+
raised = @raised
|
|
29
|
+
return if raised.nil? || !error.equal?(raised)
|
|
30
|
+
|
|
31
|
+
"#{raised.klass}: #{raised.message}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
require_relative "../codec"
|
|
4
4
|
require_relative "../errors"
|
|
5
5
|
require_relative "../payload"
|
|
6
|
+
require_relative "block_failure"
|
|
6
7
|
require_relative "call"
|
|
7
8
|
require_relative "reflection"
|
|
8
9
|
require_relative "yielder"
|
|
9
10
|
|
|
10
11
|
module Kobako
|
|
11
12
|
module Transport
|
|
12
|
-
# Answers a routed Call with +[
|
|
13
|
-
# +[
|
|
14
|
-
#
|
|
13
|
+
# Answers a routed Call with one arm per Reply variant: +[:ok, body, nil]+,
|
|
14
|
+
# +[:fault, message, fault_type]+, or +[:break, "", nil]+ — a block
|
|
15
|
+
# exit the guest kept carries nothing. It never raises, so every
|
|
16
|
+
# failure reaches the guest as a fault.
|
|
15
17
|
module Dispatcher # :nodoc:
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
+
# Thrown rather than raised, so a Service unwound by a block exit runs
|
|
19
|
+
# its +ensure+ and cannot +rescue+ the exit. +private_constant+ is a
|
|
18
20
|
# convention boundary — not a defence.
|
|
19
21
|
BREAK_THROW = :__kobako_break__
|
|
20
22
|
private_constant :BREAK_THROW
|
|
@@ -23,15 +25,18 @@ module Kobako
|
|
|
23
25
|
|
|
24
26
|
class UnreadableRequestError < Kobako::Codec::Error; end # :nodoc:
|
|
25
27
|
|
|
26
|
-
# The category
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# The category each failure answering with its message alone takes,
|
|
29
|
+
# keyed by the class raised and ordered most specific first. kobako's
|
|
30
|
+
# own refusals are worded so none reads as a Service's exception, and
|
|
31
|
+
# +ArgumentError+, raised by a Service and by a call that will not
|
|
32
|
+
# bind alike, is named by +argument+ already. A class absent here is
|
|
33
|
+
# the Service's own exception, which answers under +runtime+ wearing
|
|
34
|
+
# the +<class>: <message>+ shape that names it.
|
|
30
35
|
#
|
|
31
36
|
# +Kobako::Codec::Error+ is the floor rather than a path of its own:
|
|
32
37
|
# a codec fault reaching the boundary unnamed is the exchange failing,
|
|
33
38
|
# and must not be dressed as something a Service raised.
|
|
34
|
-
|
|
39
|
+
BARE_FAULTS = {
|
|
35
40
|
HandleExhaustedError => "internal",
|
|
36
41
|
UndefinedTargetError => "undefined",
|
|
37
42
|
ArgumentError => "argument",
|
|
@@ -40,23 +45,24 @@ module Kobako
|
|
|
40
45
|
Kobako::Codec::Error => "internal",
|
|
41
46
|
Kobako::SandboxError => "runtime"
|
|
42
47
|
}.freeze
|
|
43
|
-
private_constant :
|
|
48
|
+
private_constant :BARE_FAULTS
|
|
44
49
|
|
|
45
50
|
# The per-invocation state arrives as arguments so the Dispatcher
|
|
46
51
|
# stays stateless and neither the resolver nor the Context publishes
|
|
47
52
|
# 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
|
-
[
|
|
53
|
+
def self.dispatch(call, resolver, handles, failure, guest_yielder)
|
|
54
|
+
yielder = Yielder.new(guest_yielder, BREAK_THROW, handles, failure) if call.block_given
|
|
55
|
+
catch(BREAK_THROW) { return [:ok, encode_ok(run(call, resolver, handles, yielder), handles), nil] }
|
|
56
|
+
[:break, "", nil] # : [Symbol, String, String?]
|
|
51
57
|
# StandardError is the boundary by intent: a Service method's
|
|
52
58
|
# application fault folds into a guest-rescuable fault, while a
|
|
53
59
|
# host-process failure (NoMemoryError, SignalException, a bare Exception)
|
|
54
60
|
# stays uncaught and traps the invocation rather than being masked as a
|
|
55
61
|
# rescuable fault.
|
|
56
62
|
rescue StandardError => e
|
|
57
|
-
[
|
|
63
|
+
[:fault, *fault_of(e, failure)] # : [Symbol, String, String?]
|
|
58
64
|
ensure
|
|
59
|
-
yielder&.invalidate
|
|
65
|
+
yielder&.invalidate
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
class << self
|
|
@@ -66,7 +72,7 @@ module Kobako
|
|
|
66
72
|
arguments, carried_handle = decode_arguments(call.payload)
|
|
67
73
|
exposure = resolve_target(call.target, resolver, handles)
|
|
68
74
|
args, kwargs = resolve_call_args(arguments, handles, carried_handle)
|
|
69
|
-
|
|
75
|
+
invoke(exposure, call.method_name, args, kwargs, yielder)
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
# A codec fault here is a request that never became a call, restated
|
|
@@ -84,21 +90,19 @@ module Kobako
|
|
|
84
90
|
arguments.kwargs.transform_values { |v| resolve_arg(v, handles) }]
|
|
85
91
|
end
|
|
86
92
|
|
|
87
|
-
# The class prefix marks a Service's own exception
|
|
88
|
-
# it is the +<class>: <message>+ shape a Host App is
|
|
89
|
-
# secrets out of, so wearing it says the Service raised.
|
|
90
|
-
# refusals answer under their own wording instead of borrowing that
|
|
91
|
-
# shape.
|
|
93
|
+
# The class prefix marks a Service's own exception that its category
|
|
94
|
+
# does not name: it is the +<class>: <message>+ shape a Host App is
|
|
95
|
+
# told to keep secrets out of, so wearing it says the Service raised.
|
|
92
96
|
#
|
|
93
97
|
# The guest's own block failing is not the Service's to report at
|
|
94
98
|
# all, so the Yielder that raised it is asked first — it recognises
|
|
95
99
|
# its own by identity and words the failure the guest's way.
|
|
96
|
-
def
|
|
97
|
-
block_failure =
|
|
100
|
+
def fault_of(error, failure)
|
|
101
|
+
block_failure = failure.fault_text(error)
|
|
98
102
|
return fault("block", block_failure) if block_failure
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
return fault(
|
|
104
|
+
bare = BARE_FAULTS.find { |klass, _| error.is_a?(klass) }
|
|
105
|
+
return fault(bare.last, error.message) if bare
|
|
102
106
|
|
|
103
107
|
fault("runtime", "#{error.class}: #{error.message}")
|
|
104
108
|
end
|
|
@@ -107,7 +111,7 @@ module Kobako
|
|
|
107
111
|
# unsplatted for methods that take no keywords.
|
|
108
112
|
def invoke(exposure, method, args, kwargs, yielder)
|
|
109
113
|
name = method.to_sym
|
|
110
|
-
reject_unreachable
|
|
114
|
+
reject_unreachable(exposure, name)
|
|
111
115
|
target = exposure.object
|
|
112
116
|
block = yielder&.to_proc
|
|
113
117
|
if kwargs.empty?
|
|
@@ -120,7 +124,7 @@ module Kobako
|
|
|
120
124
|
# Both the ambient-surface floor and the reference's Exposure answer
|
|
121
125
|
# through Reflection, so a rejected name discloses nothing about
|
|
122
126
|
# which of the two refused.
|
|
123
|
-
def reject_unreachable
|
|
127
|
+
def reject_unreachable(exposure, name)
|
|
124
128
|
reason = Reflection.refusal(exposure, name)
|
|
125
129
|
raise UndefinedTargetError, reason if reason
|
|
126
130
|
end
|
|
@@ -159,7 +163,7 @@ module Kobako
|
|
|
159
163
|
# Service can change — so it is named here, where the direction is
|
|
160
164
|
# known, instead of falling to the boundary's codec floor.
|
|
161
165
|
def encode_ok(value, handles)
|
|
162
|
-
Kobako::Codec::Nesting.assert_within_bound
|
|
166
|
+
Kobako::Codec::Nesting.assert_within_bound(value)
|
|
163
167
|
Kobako::Codec::Encoder.encode(value)
|
|
164
168
|
rescue Kobako::Codec::UnsupportedTypeError
|
|
165
169
|
encode_ok(handles.alloc(value), handles)
|
|
@@ -32,7 +32,7 @@ module Kobako
|
|
|
32
32
|
# once (a Handle table), so two objects of one class enumerate it once.
|
|
33
33
|
def self.of(object, surfaces = {})
|
|
34
34
|
klass = CLASS_OF.bind_call(object)
|
|
35
|
-
names = narrows_itself?(object, klass) ? nil :
|
|
35
|
+
names = narrows_itself?(object, klass) ? nil : resolve_default_names(object, klass, surfaces)
|
|
36
36
|
new(object: object, names: names)
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -59,7 +59,7 @@ module Kobako
|
|
|
59
59
|
|
|
60
60
|
# A callable keeps its callable names; a class-level surface or a
|
|
61
61
|
# forwarder has no surface of its own to expose.
|
|
62
|
-
def
|
|
62
|
+
def resolve_default_names(object, klass, surfaces)
|
|
63
63
|
case object
|
|
64
64
|
when Proc, Method then CALLABLE
|
|
65
65
|
when Module, Delegator then NOTHING
|
|
@@ -68,7 +68,7 @@ module Kobako
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def class_names(object, klass)
|
|
71
|
-
names =
|
|
71
|
+
names = authored_names(klass)
|
|
72
72
|
case object
|
|
73
73
|
when Struct, Data then names.concat(object.members)
|
|
74
74
|
end
|
|
@@ -78,15 +78,13 @@ module Kobako
|
|
|
78
78
|
def with_singleton(object, names)
|
|
79
79
|
return names if SINGLETON_METHODS.bind_call(object, false).empty?
|
|
80
80
|
|
|
81
|
-
names.union(
|
|
81
|
+
names.union(authored_names(SINGLETON_CLASS_OF.bind_call(object))).freeze
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# The public methods +owner+ itself defines in source.
|
|
85
|
-
|
|
86
|
-
def authored(owner)
|
|
84
|
+
# The public methods +owner+ itself defines in Ruby source.
|
|
85
|
+
def authored_names(owner)
|
|
87
86
|
owner.public_instance_methods(false).select do |name|
|
|
88
|
-
|
|
89
|
-
file && !file.start_with?("<internal:")
|
|
87
|
+
Reflection.authored?(owner.instance_method(name))
|
|
90
88
|
end
|
|
91
89
|
end
|
|
92
90
|
end
|