kobako 0.10.0 → 0.11.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 +7 -0
- data/Cargo.lock +1 -1
- data/README.md +14 -7
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/ambient.rs +1 -1
- data/ext/kobako/src/runtime/cache.rs +3 -3
- data/ext/kobako/src/runtime/capture.rs +1 -1
- data/ext/kobako/src/runtime/config.rs +3 -4
- data/ext/kobako/src/runtime/dispatch.rs +6 -6
- data/ext/kobako/src/runtime/exports.rs +1 -1
- data/ext/kobako/src/runtime/instance_pre.rs +1 -1
- data/ext/kobako/src/runtime/invocation.rs +27 -27
- data/ext/kobako/src/runtime/trap.rs +5 -5
- data/ext/kobako/src/runtime.rs +44 -45
- data/ext/kobako/src/snapshot.rs +2 -2
- data/lib/kobako/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +24 -31
- data/lib/kobako/catalog/namespaces.rb +19 -27
- data/lib/kobako/catalog/snippets.rb +10 -16
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +33 -39
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +8 -11
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +18 -24
- data/lib/kobako/sandbox.rb +61 -83
- data/lib/kobako/sandbox_options.rb +5 -9
- data/lib/kobako/snapshot.rb +2 -4
- data/lib/kobako/snippet/binary.rb +1 -3
- data/lib/kobako/snippet/source.rb +1 -2
- data/lib/kobako/snippet.rb +1 -2
- data/lib/kobako/transport/dispatcher.rb +39 -38
- data/lib/kobako/transport/request.rb +1 -1
- data/lib/kobako/transport/run.rb +23 -28
- data/lib/kobako/transport/yielder.rb +11 -17
- data/lib/kobako/transport.rb +2 -3
- data/lib/kobako/usage.rb +10 -13
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +1 -1
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -13,21 +13,19 @@ require_relative "catalog"
|
|
|
13
13
|
|
|
14
14
|
module Kobako
|
|
15
15
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
16
|
-
# scripts inside a wasmtime-hosted Wasm module
|
|
17
|
-
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
16
|
+
# scripts inside a wasmtime-hosted Wasm module.
|
|
18
17
|
#
|
|
19
18
|
# The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
|
|
20
|
-
# +Kobako::Catalog::Handles
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# Sandboxes amortises both costs automatically.
|
|
19
|
+
# +Kobako::Catalog::Handles+, the per-instance
|
|
20
|
+
# +Kobako::Catalog::Namespaces+ (which receives the +Catalog::Handles+ by
|
|
21
|
+
# injection so guest→host dispatch and host→guest auto-wrap share one
|
|
22
|
+
# allocator), and the dispatch +Proc+ / +yield_to_guest+ lambda installed
|
|
23
|
+
# on the Runtime via +Runtime#on_dispatch=+. The underlying wasmtime Engine
|
|
24
|
+
# and compiled Module are cached at process scope by the native ext and
|
|
25
|
+
# never surface to Ruby — constructing many Sandboxes amortises both costs
|
|
26
|
+
# automatically.
|
|
29
27
|
#
|
|
30
|
-
# Output capture policy
|
|
28
|
+
# Output capture policy: the
|
|
31
29
|
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
32
30
|
# WASI pipe — the host buffer stops growing at the cap, subsequent guest
|
|
33
31
|
# writes on that channel fail or are dropped, and +#run+ still returns
|
|
@@ -46,9 +44,8 @@ module Kobako
|
|
|
46
44
|
|
|
47
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
48
46
|
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
49
|
-
# any invocation
|
|
50
|
-
#
|
|
51
|
-
# observe overflow.
|
|
47
|
+
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
+
# so use +#stdout_truncated?+ to observe overflow.
|
|
52
49
|
def stdout
|
|
53
50
|
@stdout_capture.bytes
|
|
54
51
|
end
|
|
@@ -61,9 +58,8 @@ module Kobako
|
|
|
61
58
|
end
|
|
62
59
|
|
|
63
60
|
# Returns +true+ iff stdout capture during the most recent invocation
|
|
64
|
-
# exceeded +stdout_limit+
|
|
65
|
-
#
|
|
66
|
-
# ({docs/behavior.md B-03}[link:../../docs/behavior.md]).
|
|
61
|
+
# exceeded +stdout_limit+. Resets to +false+ at the start of the next
|
|
62
|
+
# invocation.
|
|
67
63
|
def stdout_truncated?
|
|
68
64
|
@stdout_capture.truncated?
|
|
69
65
|
end
|
|
@@ -75,8 +71,7 @@ module Kobako
|
|
|
75
71
|
end
|
|
76
72
|
|
|
77
73
|
# Returns the +Kobako::Usage+ value object for the most recent
|
|
78
|
-
# invocation (
|
|
79
|
-
# Carries +wall_time+ (Float seconds the guest export call spent
|
|
74
|
+
# invocation. Carries +wall_time+ (Float seconds the guest export call spent
|
|
80
75
|
# inside wasmtime) and +memory_peak+ (Integer bytes, high-water of
|
|
81
76
|
# the per-invocation +memory.grow+ delta past the entry-time
|
|
82
77
|
# baseline). Returns +Kobako::Usage::EMPTY+ before any invocation;
|
|
@@ -109,9 +104,8 @@ module Kobako
|
|
|
109
104
|
reset_invocation_state!
|
|
110
105
|
end
|
|
111
106
|
|
|
112
|
-
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
113
|
-
#
|
|
114
|
-
# Symbol or String in constant form. Returns the
|
|
107
|
+
# Declare or retrieve the Namespace named +name+ on this Sandbox. +name+
|
|
108
|
+
# must be a Symbol or String in constant form. Returns the
|
|
115
109
|
# +Kobako::Namespace+.
|
|
116
110
|
#
|
|
117
111
|
# Raises +ArgumentError+ when called after the first invocation, or
|
|
@@ -120,20 +114,18 @@ module Kobako
|
|
|
120
114
|
@services.define(name)
|
|
121
115
|
end
|
|
122
116
|
|
|
123
|
-
# Register a snippet on this Sandbox in one of two forms
|
|
124
|
-
# ({docs/behavior.md B-32}[link:../../docs/behavior.md]):
|
|
117
|
+
# Register a snippet on this Sandbox in one of two forms:
|
|
125
118
|
#
|
|
126
119
|
# * +preload(code: source, name: Name)+ — +source+ is mruby source
|
|
127
120
|
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. The +name+
|
|
128
121
|
# becomes the snippet's +(snippet:Name)+ backtrace filename and
|
|
129
|
-
# is the dedupe key
|
|
122
|
+
# is the dedupe key that rejects a duplicate +code:+ snippet.
|
|
130
123
|
# * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
|
|
131
124
|
# bytecode as a +String+. The canonical name, when present,
|
|
132
125
|
# lives in the bytecode's embedded +debug_info+ and is resolved
|
|
133
126
|
# by the guest at load time; the host treats the bytes as
|
|
134
|
-
# opaque. Structural failures
|
|
135
|
-
#
|
|
136
|
-
# surface as +Kobako::BytecodeError+ on the first invocation.
|
|
127
|
+
# opaque. Structural failures surface as +Kobako::BytecodeError+
|
|
128
|
+
# on the first invocation.
|
|
137
129
|
#
|
|
138
130
|
# Subsequent invocations (+#eval+ or +#run+) replay every registered
|
|
139
131
|
# snippet — in insertion order — against the fresh +mrb_state+
|
|
@@ -145,11 +137,9 @@ module Kobako
|
|
|
145
137
|
# supplied, when both forms are mixed (e.g., +code:+ and +binary:+
|
|
146
138
|
# together, or +binary:+ paired with +name:+), when +code+ / +bytes+
|
|
147
139
|
# is not a +String+, when +name+ does not match the constant
|
|
148
|
-
# pattern
|
|
149
|
-
# when
|
|
150
|
-
#
|
|
151
|
-
# called after the first invocation
|
|
152
|
-
# ({docs/behavior.md E-35, B-33}[link:../../docs/behavior.md]).
|
|
140
|
+
# pattern, when +name+ duplicates an already-registered +code:+ form
|
|
141
|
+
# snippet, or when called after the first invocation has sealed the
|
|
142
|
+
# snippet table.
|
|
153
143
|
def preload(code: nil, name: nil, binary: nil)
|
|
154
144
|
raise ArgumentError, "cannot preload after first Sandbox invocation" if @services.sealed?
|
|
155
145
|
|
|
@@ -157,17 +147,16 @@ module Kobako
|
|
|
157
147
|
self
|
|
158
148
|
end
|
|
159
149
|
|
|
160
|
-
# Dispatch into a preloaded entrypoint constant
|
|
161
|
-
# ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
|
|
150
|
+
# Dispatch into a preloaded entrypoint constant. Delegates host
|
|
162
151
|
# pre-flight and wire encoding to +Kobako::Transport::Run+ /
|
|
163
152
|
# +Kobako::Transport::Run#encode+: a non-Symbol/String +target+ raises
|
|
164
|
-
# +TypeError
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
153
|
+
# +TypeError+, while a +target+ failing the constant pattern, a forged
|
|
154
|
+
# +Kobako::Handle+ in +args+ / +kwargs+, or a non-Symbol +kwargs+ key
|
|
155
|
+
# raise +ArgumentError+. The guest resolves +target+ as a top-level
|
|
156
|
+
# constant, calls +#call+ on it with +args+ / +kwargs+, and returns the
|
|
157
|
+
# deserialized result. The first invocation seals the Service registry
|
|
158
|
+
# and snippet table. Runtime errors follow the same three-class
|
|
159
|
+
# taxonomy as +#eval+.
|
|
171
160
|
def run(target, *args, **kwargs)
|
|
172
161
|
run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
173
162
|
invoke!(:run) do
|
|
@@ -175,29 +164,27 @@ module Kobako
|
|
|
175
164
|
end
|
|
176
165
|
end
|
|
177
166
|
|
|
178
|
-
# Execute a guest mruby source string in a fresh +mrb_state+
|
|
179
|
-
#
|
|
180
|
-
# mruby source as a UTF-8 String. Returns the deserialized last
|
|
167
|
+
# Execute a guest mruby source string in a fresh +mrb_state+. +code+ is
|
|
168
|
+
# the mruby source as a UTF-8 String. Returns the deserialized last
|
|
181
169
|
# expression of the source.
|
|
182
170
|
#
|
|
183
171
|
# Source delivery uses the WASI stdin three-frame protocol
|
|
184
172
|
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
185
173
|
# Frame 1 carries the msgpack-encoded preamble (Namespace / Member
|
|
186
174
|
# registry snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
187
|
-
# Frame 3 carries the snippet table registered via +#preload
|
|
175
|
+
# Frame 3 carries the snippet table registered via +#preload+.
|
|
188
176
|
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
189
177
|
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
190
178
|
# array, never an absent frame.
|
|
191
179
|
#
|
|
192
|
-
# The first invocation seals the Service registry and snippet table
|
|
193
|
-
#
|
|
194
|
-
# +#define+ / +#preload+ calls raise +ArgumentError+.
|
|
180
|
+
# The first invocation seals the Service registry and snippet table;
|
|
181
|
+
# subsequent +#define+ / +#preload+ calls raise +ArgumentError+.
|
|
195
182
|
#
|
|
196
183
|
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
197
184
|
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
198
185
|
# (including when +code+ is +nil+ or not a String, or when a preloaded
|
|
199
|
-
# snippet's replay raises
|
|
200
|
-
#
|
|
186
|
+
# snippet's replay raises); +Kobako::ServiceError+ on an unrescued
|
|
187
|
+
# Service capability failure.
|
|
201
188
|
def eval(code)
|
|
202
189
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
203
190
|
|
|
@@ -207,15 +194,11 @@ module Kobako
|
|
|
207
194
|
end
|
|
208
195
|
|
|
209
196
|
# Reset all per-invocation observable state to its pre-invocation
|
|
210
|
-
# sentinels — both per-channel captures
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# (between-invocation reset) so both paths agree on what
|
|
216
|
-
# "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
|
|
217
|
-
# so a pooled Sandbox hands over empty output buffers
|
|
218
|
-
# ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
|
|
197
|
+
# sentinels — both per-channel captures and the per-last-invocation
|
|
198
|
+
# usage record. Shared by +#initialize+ (first-time setup) and
|
|
199
|
+
# +#begin_invocation!+ (between-invocation reset) so both paths agree on
|
|
200
|
+
# what "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
|
|
201
|
+
# so a pooled Sandbox hands over empty output buffers.
|
|
219
202
|
def reset_invocation_state!
|
|
220
203
|
@stdout_capture = Capture::EMPTY
|
|
221
204
|
@stderr_capture = Capture::EMPTY
|
|
@@ -224,14 +207,13 @@ module Kobako
|
|
|
224
207
|
|
|
225
208
|
private
|
|
226
209
|
|
|
227
|
-
# Configure the +Runtime+'s host↔guest dispatch wiring
|
|
228
|
-
# ({docs/behavior.md B-12}[link:../../docs/behavior.md]). Builds a
|
|
210
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring. Builds a
|
|
229
211
|
# lambda that re-enters the guest via
|
|
230
|
-
# +Runtime#yield_to_active_invocation+
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
212
|
+
# +Runtime#yield_to_active_invocation+ and a dispatch +Proc+ that routes
|
|
213
|
+
# guest→host calls through the stateless +Transport::Dispatcher+,
|
|
214
|
+
# capturing +@services+ / +@handler+ in the closure. Both are registered
|
|
215
|
+
# on the +Runtime+ once at construction time so the wasm ext callback can
|
|
216
|
+
# fire without further setup.
|
|
235
217
|
def install_dispatch_proc!
|
|
236
218
|
yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
|
|
237
219
|
@runtime.on_dispatch = lambda do |request_bytes|
|
|
@@ -239,14 +221,13 @@ module Kobako
|
|
|
239
221
|
end
|
|
240
222
|
end
|
|
241
223
|
|
|
242
|
-
# Per-invocation prologue
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
# capability state — capture buffers, truncation predicates, and the
|
|
224
|
+
# Per-invocation prologue. Seals the Service / snippet registries on
|
|
225
|
+
# first call (idempotent) and zeros the per-invocation capability
|
|
226
|
+
# state — capture buffers, truncation predicates, and the
|
|
246
227
|
# +Catalog::Handles+ counter — before the guest runs. The
|
|
247
|
-
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
248
|
-
# this class: SPEC.md Terminology pins it as "Not exposed to the
|
|
249
|
-
# Host App"
|
|
228
|
+
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
229
|
+
# beyond this class: SPEC.md Terminology pins it as "Not exposed to the
|
|
230
|
+
# Host App".
|
|
250
231
|
def begin_invocation!
|
|
251
232
|
@services.seal!
|
|
252
233
|
@handler.reset!
|
|
@@ -254,8 +235,7 @@ module Kobako
|
|
|
254
235
|
end
|
|
255
236
|
|
|
256
237
|
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
257
|
-
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
258
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
|
238
|
+
# the ext and wrap them as a +Kobako::Usage+ value object. Runs in
|
|
259
239
|
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
260
240
|
# every outcome — value return, +Kobako::TrapError+ (including
|
|
261
241
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
@@ -273,8 +253,7 @@ module Kobako
|
|
|
273
253
|
end
|
|
274
254
|
|
|
275
255
|
# Pick the +TrapError+ subclass to re-raise based on +err+'s actual
|
|
276
|
-
# class. Cap-trap subclasses
|
|
277
|
-
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
256
|
+
# class. Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+)
|
|
278
257
|
# preserve their named identity; everything else collapses to the
|
|
279
258
|
# base +Kobako::TrapError+. The ext already raises the right subclass
|
|
280
259
|
# directly, so this is a pure re-attribution that lets +#invoke!+
|
|
@@ -298,9 +277,8 @@ module Kobako
|
|
|
298
277
|
# +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
|
|
299
278
|
# populated by the +ensure+ readout ({#read_usage!}) on every outcome.
|
|
300
279
|
# The rescue chain is the single trap-translation boundary —
|
|
301
|
-
# configured-cap paths
|
|
302
|
-
# (
|
|
303
|
-
# surface as named TrapError subclasses; everything else surfaces as
|
|
280
|
+
# configured-cap paths surface as named TrapError subclasses
|
|
281
|
+
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
304
282
|
# the base +TrapError+.
|
|
305
283
|
def invoke!(verb)
|
|
306
284
|
begin_invocation!
|
|
@@ -309,7 +287,7 @@ module Kobako
|
|
|
309
287
|
@stderr_capture = snapshot.stderr
|
|
310
288
|
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
311
289
|
# token; restore it to the host object the guest referenced before
|
|
312
|
-
# handing the value to the Host App
|
|
290
|
+
# handing the value to the Host App. @handler still holds this
|
|
313
291
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
314
292
|
Codec::Utils.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
|
|
315
293
|
rescue Kobako::TrapError => e
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
# Kobako::SandboxOptions — immutable Value Object holding the four
|
|
5
|
-
# per-Sandbox configuration caps
|
|
6
|
-
# E-20}[link:../../docs/behavior.md]). Built on the +class X <
|
|
5
|
+
# per-Sandbox configuration caps. Built on the +class X <
|
|
7
6
|
# Data.define(...)+ subclass form (the Steep-friendly shape — see
|
|
8
7
|
# +lib/kobako/outcome/panic.rb+).
|
|
9
8
|
#
|
|
@@ -13,18 +12,15 @@ module Kobako
|
|
|
13
12
|
# +super+. Anything that survives +SandboxOptions.new+ is a wire-ready
|
|
14
13
|
# cap bundle the +Kobako::Runtime+ constructor consumes as-is.
|
|
15
14
|
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
|
|
16
|
-
# Default wall-clock timeout for a single invocation: 60 seconds
|
|
17
|
-
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
15
|
+
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
18
16
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
19
17
|
|
|
20
18
|
# Default cap on the per-invocation guest linear-memory delta:
|
|
21
|
-
# 1 MiB
|
|
22
|
-
#
|
|
23
|
-
# watermark sit outside this budget — see B-01 Notes.
|
|
19
|
+
# 1 MiB. The mruby image's initial allocation and prior invocations'
|
|
20
|
+
# watermark sit outside this budget.
|
|
24
21
|
DEFAULT_MEMORY_LIMIT = 1 << 20
|
|
25
22
|
|
|
26
|
-
# Default per-channel capture ceiling: 1 MiB
|
|
27
|
-
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
23
|
+
# Default per-channel capture ceiling: 1 MiB.
|
|
28
24
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
29
25
|
|
|
30
26
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
data/lib/kobako/snapshot.rb
CHANGED
|
@@ -16,8 +16,7 @@ module Kobako
|
|
|
16
16
|
# Host App.
|
|
17
17
|
class Snapshot
|
|
18
18
|
# Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
|
|
19
|
-
# as a +Kobako::Capture+ value object.
|
|
20
|
-
# B-04}[link:../../docs/behavior.md] — the byte content never carries
|
|
19
|
+
# as a +Kobako::Capture+ value object. The byte content never carries
|
|
21
20
|
# a truncation sentinel; +#truncated?+ is the only way to observe
|
|
22
21
|
# that the cap was hit.
|
|
23
22
|
def stdout
|
|
@@ -31,8 +30,7 @@ module Kobako
|
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
# Wrap the per-last-invocation usage pair (+wall_time+,
|
|
34
|
-
# +memory_peak+) as a +Kobako::Usage+ value object
|
|
35
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
|
|
33
|
+
# +memory_peak+) as a +Kobako::Usage+ value object.
|
|
36
34
|
def usage
|
|
37
35
|
Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
38
36
|
end
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
5
|
# Kobako::Snippet::Binary — value object representing a single
|
|
6
|
-
# +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets
|
|
7
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
6
|
+
# +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets+.
|
|
8
7
|
#
|
|
9
8
|
# The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
|
|
10
9
|
# +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
|
|
@@ -12,7 +11,6 @@ module Kobako
|
|
|
12
11
|
# The host treats the bytes as opaque — the snippet's canonical
|
|
13
12
|
# name, when present, lives in the bytecode's embedded +debug_info+
|
|
14
13
|
# and is resolved by the guest at load time; structural validation
|
|
15
|
-
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
16
14
|
# is deferred to the first invocation's guest replay.
|
|
17
15
|
#
|
|
18
16
|
# The class is a +Data.define+ subclass — frozen and value-equal.
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
5
|
# Kobako::Snippet::Source — value object representing a single
|
|
6
|
-
# +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets
|
|
7
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
6
|
+
# +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets+.
|
|
8
7
|
#
|
|
9
8
|
# +name+ is the canonical +Symbol+ identity baked into the loaded
|
|
10
9
|
# IREP's +debug_info+; backtrace frames originating in this snippet
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -5,8 +5,7 @@ require_relative "snippet/source"
|
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
7
|
# Kobako::Snippet — value-object family for preloaded snippet entries
|
|
8
|
-
# held by +Kobako::Catalog::Snippets
|
|
9
|
-
# ({docs/behavior.md B-32 / B-33}[link:../../docs/behavior.md]).
|
|
8
|
+
# held by +Kobako::Catalog::Snippets+.
|
|
10
9
|
#
|
|
11
10
|
# +Source+ represents a single +#preload(code:, name:)+ entry; +Binary+
|
|
12
11
|
# represents a single +#preload(binary:)+ entry. Both are plain value
|
|
@@ -20,8 +20,7 @@ module Kobako
|
|
|
20
20
|
# The module is stateless — all mutable state is threaded through
|
|
21
21
|
# arguments so Dispatcher has no instance variables and no side
|
|
22
22
|
# effects beyond mutating the Catalog::Handles via +alloc+ when a
|
|
23
|
-
# non-wire-representable return value must be wrapped
|
|
24
|
-
# ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
|
|
23
|
+
# non-wire-representable return value must be wrapped.
|
|
25
24
|
#
|
|
26
25
|
# Entry point:
|
|
27
26
|
#
|
|
@@ -29,7 +28,7 @@ module Kobako
|
|
|
29
28
|
# # => msgpack-encoded Response bytes (never raises)
|
|
30
29
|
module Dispatcher
|
|
31
30
|
# Throw tag for the {Yielder}'s break unwind back to the
|
|
32
|
-
# dispatcher's +catch+ frame
|
|
31
|
+
# dispatcher's +catch+ frame. +private_constant+ is a
|
|
33
32
|
# convention boundary — not a defence.
|
|
34
33
|
BREAK_THROW = :__kobako_break__
|
|
35
34
|
private_constant :BREAK_THROW
|
|
@@ -38,16 +37,14 @@ module Kobako
|
|
|
38
37
|
|
|
39
38
|
# Internal sentinel raised when target resolution fails. Mapped to
|
|
40
39
|
# Response.error with type="undefined". Contained at the wire boundary —
|
|
41
|
-
# not part of the public Kobako error taxonomy
|
|
42
|
-
# ({docs/behavior.md E-12}[link:../../../docs/behavior.md]).
|
|
40
|
+
# not part of the public Kobako error taxonomy.
|
|
43
41
|
class UndefinedTargetError < StandardError; end
|
|
44
42
|
|
|
45
43
|
# Modules whose instance methods are ambient Ruby reflection /
|
|
46
44
|
# metaprogramming surface (+send+, +public_send+, +instance_eval+,
|
|
47
45
|
# +method+, +tap+, +instance_variable_get+, ...) rather than Service
|
|
48
46
|
# behaviour. A guest-supplied method name resolving to one of these is
|
|
49
|
-
# rejected
|
|
50
|
-
# only methods the bound object itself exposes as Service behaviour are
|
|
47
|
+
# rejected: only methods the bound object itself exposes as Service behaviour are
|
|
51
48
|
# reachable, and +public_send(:send, ...)+ would otherwise let a guest
|
|
52
49
|
# pivot through +send+ into the private +Kernel#eval+ / +#system+
|
|
53
50
|
# surface (host RCE).
|
|
@@ -58,7 +55,7 @@ module Kobako
|
|
|
58
55
|
# (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
|
|
59
56
|
# hand back the underlying object) rather than Service behaviour. Only
|
|
60
57
|
# {CALLABLE_ALLOW} is reachable on a target of these types; a bound
|
|
61
|
-
# lambda stays invocable, its reflective surface does not
|
|
58
|
+
# lambda stays invocable, its reflective surface does not.
|
|
62
59
|
GADGET_OWNERS = [Proc, Method, UnboundMethod, Binding].freeze
|
|
63
60
|
private_constant :GADGET_OWNERS
|
|
64
61
|
|
|
@@ -69,8 +66,7 @@ module Kobako
|
|
|
69
66
|
private_constant :CALLABLE_ALLOW
|
|
70
67
|
|
|
71
68
|
# Dispatch a single transport request and return the encoded
|
|
72
|
-
# Response bytes
|
|
73
|
-
# Invoked from the +Runtime#on_dispatch+ Proc that
|
|
69
|
+
# Response bytes. Invoked from the +Runtime#on_dispatch+ Proc that
|
|
74
70
|
# +Kobako::Sandbox#initialize+ installs on the ext side; +namespaces+,
|
|
75
71
|
# +handler+, and +yield_to_guest+ are captured in that Proc's
|
|
76
72
|
# closure so the Dispatcher stays stateless and the registry doesn't
|
|
@@ -99,18 +95,17 @@ module Kobako
|
|
|
99
95
|
# round-trip back to the host-side Ruby object before the call
|
|
100
96
|
# reaches +public_send+.
|
|
101
97
|
def resolve_call_args(request, handler)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
[args, kwargs]
|
|
98
|
+
[request.args.map { |v| resolve_arg(v, handler) },
|
|
99
|
+
request.kwargs.transform_values { |v| resolve_arg(v, handler) }]
|
|
105
100
|
end
|
|
106
101
|
|
|
107
102
|
# Map an error caught at the dispatch boundary to a +Response.error+
|
|
108
103
|
# envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
|
|
109
|
-
# rescue. Returns a msgpack-encoded Response envelope (binary).
|
|
110
|
-
# error buckets
|
|
104
|
+
# rescue. Returns a msgpack-encoded Response envelope (binary). Four
|
|
105
|
+
# error buckets:
|
|
111
106
|
# +Kobako::Codec::Error+ → type="runtime" (malformed request);
|
|
112
|
-
# +UndefinedTargetError+ → type="undefined"
|
|
113
|
-
# type="argument" (
|
|
107
|
+
# +UndefinedTargetError+ → type="undefined"; +ArgumentError+ →
|
|
108
|
+
# type="argument" (arity mismatch); everything else →
|
|
114
109
|
# type="runtime".
|
|
115
110
|
def encode_caught_error(error)
|
|
116
111
|
case error
|
|
@@ -129,14 +124,14 @@ module Kobako
|
|
|
129
124
|
# uniform empty-map shape.
|
|
130
125
|
#
|
|
131
126
|
# +yielder+ is the host-side {Yielder} materialised when the guest
|
|
132
|
-
# call site supplied a block
|
|
133
|
-
# B-23}[link:../../../docs/behavior.md]); its {Yielder#to_proc}
|
|
127
|
+
# call site supplied a block; its {Yielder#to_proc}
|
|
134
128
|
# rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
|
|
135
129
|
# so the same call site handles both cases without an explicit
|
|
136
130
|
# conditional.
|
|
137
131
|
def invoke(target, method, args, kwargs, yielder = nil)
|
|
138
132
|
name = method.to_sym
|
|
139
133
|
reject_meta_method!(target, name)
|
|
134
|
+
reject_unexposed!(target, name)
|
|
140
135
|
block = yielder&.to_proc
|
|
141
136
|
if kwargs.empty?
|
|
142
137
|
target.public_send(name, *args, &block)
|
|
@@ -145,9 +140,8 @@ module Kobako
|
|
|
145
140
|
end
|
|
146
141
|
end
|
|
147
142
|
|
|
148
|
-
# Guard the +public_send+ below against ambient reflection methods
|
|
149
|
-
#
|
|
150
|
-
# method whose owner is a {META_OWNERS} or {GADGET_OWNERS} module is
|
|
143
|
+
# Guard the +public_send+ below against ambient reflection methods.
|
|
144
|
+
# A public method whose owner is a {META_OWNERS} or {GADGET_OWNERS} module is
|
|
151
145
|
# rejected, except {CALLABLE_ALLOW} on a gadget target (a bound lambda
|
|
152
146
|
# stays invocable). A name with no concrete public method is allowed
|
|
153
147
|
# only when the target opts into it via +respond_to?+ (dynamic
|
|
@@ -166,17 +160,26 @@ module Kobako
|
|
|
166
160
|
raise UndefinedTargetError, "no public method #{name.inspect} on target"
|
|
167
161
|
end
|
|
168
162
|
|
|
169
|
-
#
|
|
163
|
+
# Consult the target's opt-in narrowing predicate. A bound object
|
|
164
|
+
# may define a private +respond_to_guest?(name)+ to restrict which of its
|
|
165
|
+
# methods the guest reaches; a falsy answer rejects the dispatch.
|
|
166
|
+
# The predicate composes beneath {#reject_meta_method!} — it only narrows,
|
|
167
|
+
# never re-opening the reflection surface the floor rejects — and is
|
|
168
|
+
# consulted with the private surface included so the guest's +public_send+
|
|
169
|
+
# dispatch can never reach +respond_to_guest?+ itself.
|
|
170
|
+
def reject_unexposed!(target, name)
|
|
171
|
+
return unless target.respond_to?(:respond_to_guest?, true)
|
|
172
|
+
return if target.__send__(:respond_to_guest?, name)
|
|
173
|
+
|
|
174
|
+
raise UndefinedTargetError, "method #{name.inspect} is not exposed to the guest"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# A Kobako::Handle arriving as a positional or keyword
|
|
170
178
|
# argument identifies a host-side object previously allocated by a prior
|
|
171
|
-
# transport call's Handle wrap
|
|
179
|
+
# transport call's Handle wrap. Resolve it back to the Ruby object before
|
|
172
180
|
# the dispatch reaches +public_send+.
|
|
173
181
|
def resolve_arg(value, handler)
|
|
174
|
-
|
|
175
|
-
when Kobako::Handle
|
|
176
|
-
require_live_object!(value.id, handler)
|
|
177
|
-
else
|
|
178
|
-
value
|
|
179
|
-
end
|
|
182
|
+
value.is_a?(Kobako::Handle) ? require_live_object!(value.id, handler) : value
|
|
180
183
|
end
|
|
181
184
|
|
|
182
185
|
# Resolve a Request target to the Ruby object the registry (or
|
|
@@ -205,7 +208,7 @@ module Kobako
|
|
|
205
208
|
require_live_object!(handle.id, handler)
|
|
206
209
|
end
|
|
207
210
|
|
|
208
|
-
# Resolve +id+ through the Catalog::Handles. An unknown id
|
|
211
|
+
# Resolve +id+ through the Catalog::Handles. An unknown id
|
|
209
212
|
# surfaces as UndefinedTargetError.
|
|
210
213
|
def require_live_object!(id, handler)
|
|
211
214
|
handler.fetch(id)
|
|
@@ -214,11 +217,10 @@ module Kobako
|
|
|
214
217
|
end
|
|
215
218
|
|
|
216
219
|
# Encode +value+ as a +Response.ok+ envelope. When the value is not
|
|
217
|
-
# wire-representable per
|
|
218
|
-
#
|
|
220
|
+
# wire-representable per the codec's type mapping, the
|
|
221
|
+
# +UnsupportedType+ rescue routes it through the
|
|
219
222
|
# Catalog::Handles via {#wrap_as_handle} and re-encodes with the Capability
|
|
220
|
-
# Handle in place
|
|
221
|
-
# path encodes exactly once.
|
|
223
|
+
# Handle in place. The happy path encodes exactly once.
|
|
222
224
|
def encode_ok(value, handler)
|
|
223
225
|
response = Kobako::Transport::Response.ok(value)
|
|
224
226
|
response.encode
|
|
@@ -227,9 +229,8 @@ module Kobako
|
|
|
227
229
|
end
|
|
228
230
|
|
|
229
231
|
# Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
|
|
230
|
-
# that the wire codec can carry
|
|
231
|
-
#
|
|
232
|
-
# representation.
|
|
232
|
+
# that the wire codec can carry. Used as the fallback path of
|
|
233
|
+
# {#encode_ok} when +value+ has no wire representation.
|
|
233
234
|
def wrap_as_handle(value, handler)
|
|
234
235
|
handler.alloc(value)
|
|
235
236
|
end
|
|
@@ -16,7 +16,7 @@ module Kobako
|
|
|
16
16
|
# or a {Handle}. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
|
|
17
17
|
# enforced at construction so the Value Object is the single source of
|
|
18
18
|
# truth. +block_given+ is a Boolean signalling whether the guest call
|
|
19
|
-
# site supplied a block
|
|
19
|
+
# site supplied a block; the block body itself never crosses the
|
|
20
20
|
# wire.
|
|
21
21
|
#
|
|
22
22
|
# Built on the +class X < Data.define(...)+ subclass form so the
|