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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +7 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +14 -7
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +2 -2
  8. data/ext/kobako/src/runtime/ambient.rs +1 -1
  9. data/ext/kobako/src/runtime/cache.rs +3 -3
  10. data/ext/kobako/src/runtime/capture.rs +1 -1
  11. data/ext/kobako/src/runtime/config.rs +3 -4
  12. data/ext/kobako/src/runtime/dispatch.rs +6 -6
  13. data/ext/kobako/src/runtime/exports.rs +1 -1
  14. data/ext/kobako/src/runtime/instance_pre.rs +1 -1
  15. data/ext/kobako/src/runtime/invocation.rs +27 -27
  16. data/ext/kobako/src/runtime/trap.rs +5 -5
  17. data/ext/kobako/src/runtime.rs +44 -45
  18. data/ext/kobako/src/snapshot.rs +2 -2
  19. data/lib/kobako/capture.rb +5 -7
  20. data/lib/kobako/catalog/handles.rb +24 -31
  21. data/lib/kobako/catalog/namespaces.rb +19 -27
  22. data/lib/kobako/catalog/snippets.rb +10 -16
  23. data/lib/kobako/codec/utils.rb +6 -9
  24. data/lib/kobako/errors.rb +33 -39
  25. data/lib/kobako/handle.rb +2 -3
  26. data/lib/kobako/namespace.rb +8 -11
  27. data/lib/kobako/outcome.rb +12 -14
  28. data/lib/kobako/pool.rb +18 -24
  29. data/lib/kobako/sandbox.rb +61 -83
  30. data/lib/kobako/sandbox_options.rb +5 -9
  31. data/lib/kobako/snapshot.rb +2 -4
  32. data/lib/kobako/snippet/binary.rb +1 -3
  33. data/lib/kobako/snippet/source.rb +1 -2
  34. data/lib/kobako/snippet.rb +1 -2
  35. data/lib/kobako/transport/dispatcher.rb +39 -38
  36. data/lib/kobako/transport/request.rb +1 -1
  37. data/lib/kobako/transport/run.rb +23 -28
  38. data/lib/kobako/transport/yielder.rb +11 -17
  39. data/lib/kobako/transport.rb +2 -3
  40. data/lib/kobako/usage.rb +10 -13
  41. data/lib/kobako/version.rb +1 -1
  42. data/sig/kobako/transport/dispatcher.rbs +2 -0
  43. metadata +1 -1
@@ -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+ ({docs/behavior.md B-19}[link:../../docs/behavior.md]),
21
- # the per-instance +Kobako::Catalog::Namespaces+ (which receives the
22
- # +Catalog::Handles+ by injection so guest→host dispatch and host→guest
23
- # auto-wrap share one allocator), and the dispatch +Proc+ /
24
- # +yield_to_guest+ lambda installed on the Runtime via
25
- # +Runtime#on_dispatch=+ ({docs/behavior.md B-12}[link:../../docs/behavior.md]).
26
- # The underlying wasmtime Engine and compiled Module are cached at process
27
- # scope by the native ext and never surface to Ruby — constructing many
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 ({docs/behavior.md B-04}[link:../../docs/behavior.md]): the
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. {docs/behavior.md B-04}[link:../../docs/behavior.md] the byte
50
- # content never contains a truncation sentinel; use +#stdout_truncated?+ to
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+ ({docs/behavior.md B-04}[link:../../docs/behavior.md]). Resets
65
- # to +false+ at the start of the next invocation
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 ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
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
- # ({docs/behavior.md B-07, B-09, B-10}[link:../../docs/behavior.md]). +name+ must be a
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 for E-33.
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
- # ({docs/behavior.md E-37 / E-38}[link:../../docs/behavior.md])
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 ({docs/behavior.md E-34}[link:../../docs/behavior.md]),
149
- # when +name+ duplicates an already-registered +code:+ form snippet
150
- # ({docs/behavior.md E-33}[link:../../docs/behavior.md]), or when
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+ (E-24), while a +target+ failing the constant pattern
165
- # (E-25), a forged +Kobako::Handle+ in +args+ / +kwargs+ (E-29), or a
166
- # non-Symbol +kwargs+ key (E-30) raise +ArgumentError+. The guest
167
- # resolves +target+ as a top-level constant, calls +#call+ on it with
168
- # +args+ / +kwargs+, and returns the deserialized result. The first
169
- # invocation seals the Service registry and snippet table (B-07 /
170
- # B-33). Runtime errors follow the same three-class taxonomy as +#eval+.
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
- # ({docs/behavior.md B-02 / B-03 / B-06}[link:../../docs/behavior.md]). +code+ is the
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+ (B-32).
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
- # ({docs/behavior.md B-07 / B-33}[link:../../docs/behavior.md]); subsequent
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 E-36);
200
- # +Kobako::ServiceError+ on an unrescued Service capability failure.
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
- # ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
212
- # per-last-invocation usage record
213
- # ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
214
- # +#initialize+ (first-time setup) and +#begin_invocation!+
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+ (B-24) and a dispatch +Proc+
231
- # that routes guest→host calls through the stateless
232
- # +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
233
- # closure. Both are registered on the +Runtime+ once at construction
234
- # time so the wasm ext callback can fire without further setup.
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 ({docs/behavior.md B-03 / B-07 /
243
- # B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
244
- # registries on first call (idempotent) and zeros the per-invocation
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 beyond
248
- # this class: SPEC.md Terminology pins it as "Not exposed to the
249
- # Host App" (B-19 / B-20 / E-29).
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
- # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
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 (B-37). @handler still holds this
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 ({docs/behavior.md B-01,
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 ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
22
- # The mruby image's initial allocation and prior invocations'
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,
@@ -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. {docs/behavior.md
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
@@ -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 (B-25). +private_constant+ is a
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 ({docs/behavior.md B-42}[link:../../../docs/behavior.md]):
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 (B-42).
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 ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
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
- args = request.args.map { |v| resolve_arg(v, handler) }
103
- kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handler) }
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). Three
110
- # error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
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" (E-13); +ArgumentError+ →
113
- # type="argument" (B-12 arity mismatch); everything else →
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 ({docs/behavior.md
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
- # ({docs/behavior.md B-42}[link:../../../docs/behavior.md]). A public
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
- # {docs/behavior.md B-16}[link:../../../docs/behavior.md] A Kobako::Handle arriving as a positional or keyword
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 (B-14). Resolve it back to the Ruby object before
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
- case value
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 (E-13)
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 {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
218
- # mapping, the +UnsupportedType+ rescue routes it through the
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 ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
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 ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
231
- # Used as the fallback path of {#encode_ok} when +value+ has no wire
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 (B-23); the block body itself never crosses the
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