kobako 0.9.2 → 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +32 -0
  4. data/Cargo.lock +3 -1
  5. data/README.md +47 -19
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +12 -2
  8. data/ext/kobako/src/runtime/ambient.rs +1 -1
  9. data/ext/kobako/src/runtime/cache.rs +170 -6
  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 +8 -8
  13. data/ext/kobako/src/runtime/exports.rs +32 -21
  14. data/ext/kobako/src/runtime/instance_pre.rs +97 -0
  15. data/ext/kobako/src/runtime/invocation.rs +36 -93
  16. data/ext/kobako/src/runtime/trap.rs +5 -5
  17. data/ext/kobako/src/runtime.rs +389 -403
  18. data/ext/kobako/src/snapshot.rs +2 -2
  19. data/lib/kobako/capture.rb +5 -7
  20. data/lib/kobako/catalog/handles.rb +28 -39
  21. data/lib/kobako/catalog/namespaces.rb +31 -20
  22. data/lib/kobako/catalog/snippets.rb +18 -16
  23. data/lib/kobako/codec/decoder.rb +5 -1
  24. data/lib/kobako/codec/utils.rb +6 -9
  25. data/lib/kobako/errors.rb +40 -36
  26. data/lib/kobako/handle.rb +2 -3
  27. data/lib/kobako/namespace.rb +17 -6
  28. data/lib/kobako/outcome.rb +12 -14
  29. data/lib/kobako/pool.rb +176 -0
  30. data/lib/kobako/sandbox.rb +68 -88
  31. data/lib/kobako/sandbox_options.rb +5 -9
  32. data/lib/kobako/snapshot.rb +2 -4
  33. data/lib/kobako/snippet/binary.rb +1 -3
  34. data/lib/kobako/snippet/source.rb +1 -2
  35. data/lib/kobako/snippet.rb +1 -2
  36. data/lib/kobako/transport/dispatcher.rb +39 -38
  37. data/lib/kobako/transport/request.rb +1 -1
  38. data/lib/kobako/transport/run.rb +23 -28
  39. data/lib/kobako/transport/yielder.rb +11 -17
  40. data/lib/kobako/transport.rb +2 -3
  41. data/lib/kobako/usage.rb +10 -13
  42. data/lib/kobako/version.rb +1 -1
  43. data/lib/kobako.rb +1 -0
  44. data/release-please-config.json +16 -1
  45. data/sig/kobako/catalog/handles.rbs +0 -2
  46. data/sig/kobako/errors.rbs +3 -0
  47. data/sig/kobako/namespace.rbs +2 -0
  48. data/sig/kobako/pool.rbs +44 -0
  49. data/sig/kobako/sandbox.rbs +2 -2
  50. data/sig/kobako/transport/dispatcher.rbs +2 -0
  51. metadata +4 -1
@@ -7,8 +7,7 @@ module Kobako
7
7
  # Host-facing boundary for the OUTCOME_BUFFER produced by
8
8
  # +__kobako_eval+. Takes raw outcome bytes — a one-byte tag followed by
9
9
  # the msgpack-encoded body — and maps them to either the unwrapped
10
- # mruby return value or a raised three-layer
11
- # ({docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]) exception.
10
+ # mruby return value or a raised three-layer exception.
12
11
  #
13
12
  # Self-contained: this module owns the wire framing (tag bytes,
14
13
  # body decoding), and the +Panic+ wire record lives at
@@ -17,11 +16,11 @@ module Kobako
17
16
  # nothing in +Transport+ participates.
18
17
  #
19
18
  # * tag 0x01, decode OK → return decoded value
20
- # * tag 0x01, decode fails → SandboxError (E-09)
21
- # * tag 0x02, origin="service" → ServiceError (E-13)
22
- # * tag 0x02, origin="sandbox"/missing → SandboxError (E-04..E-07)
23
- # * tag 0x02, decode fails → SandboxError (E-08)
24
- # * unknown tag → TrapError (E-03)
19
+ # * tag 0x01, decode fails → SandboxError
20
+ # * tag 0x02, origin="service" → ServiceError
21
+ # * tag 0x02, origin="sandbox"/missing → SandboxError
22
+ # * tag 0x02, decode fails → SandboxError
23
+ # * unknown tag → TrapError
25
24
  module Outcome
26
25
  # First byte of the OUTCOME_BUFFER for the success branch — body is
27
26
  # the bare msgpack encoding of the returned value
@@ -71,7 +70,7 @@ module Kobako
71
70
  [tag, body]
72
71
  end
73
72
 
74
- # Decode failure on the success tag is a SandboxError (E-09): the
73
+ # Decode failure on the success tag is a SandboxError: the
75
74
  # framing was fine, but the carried value is unrepresentable. The
76
75
  # specific codec fault is stashed in +details+ rather
77
76
  # than spliced into the message — callers cannot act on the inner
@@ -86,7 +85,7 @@ module Kobako
86
85
  )
87
86
  end
88
87
 
89
- # Decode failure on the panic tag is a SandboxError (E-08). Either
88
+ # Decode failure on the panic tag is a SandboxError. Either
90
89
  # path raises — on success the decoded Panic is mapped to its three-
91
90
  # layer exception via +build_panic_error+ and raised; on wire-decode
92
91
  # failure the rescue path raises the wire-violation +SandboxError+.
@@ -130,13 +129,12 @@ module Kobako
130
129
  )
131
130
  end
132
131
 
133
- # {docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]: map
134
- # the panic +class+ field to the matching Ruby exception subclass so
135
- # callers can rescue specific failure paths. +origin="service"+ →
132
+ # Map the panic +class+ field to the matching Ruby exception subclass
133
+ # so callers can rescue specific failure paths. +origin="service"+
136
134
  # +ServiceError+; +origin="sandbox"+ plus
137
135
  # +class="Kobako::BytecodeError"+ selects the +BytecodeError+
138
- # subclass (E-37 / E-38). Everything else falls back to the base
139
- # class for the origin.
136
+ # subclass. Everything else falls back to the base class for the
137
+ # origin.
140
138
  def panic_target_class(panic)
141
139
  case panic.origin
142
140
  when Panic::ORIGIN_SERVICE
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "sandbox"
5
+
6
+ module Kobako
7
+ # Kobako::Pool — a bounded set of warm, identically set-up Sandboxes
8
+ # handed out one exclusive holder at a time.
9
+ #
10
+ # Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
11
+ # and holds the optional block as the per-Sandbox setup hook; a
12
+ # checkout prefers an idle Sandbox and constructs a new one only when
13
+ # none is idle and fewer than +slots+ exist. +#with+ blocks up
14
+ # to +checkout_timeout+ seconds when every slot is held, applies
15
+ # the +TrapError+ discard-and-recreate contract at checkin, and
16
+ # the Pool releases everything with its own reachability — there is no
17
+ # teardown verb.
18
+ class Pool
19
+ # The +#with+ wait bound applied when +checkout_timeout+ is not given.
20
+ DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
21
+
22
+ # Build a Pool of up to +slots+ Sandboxes. +slots+ is
23
+ # a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
24
+ # seconds (+nil+ waits indefinitely); every other keyword is
25
+ # forwarded verbatim to +Kobako::Sandbox.new+. The optional block
26
+ # runs exactly once per constructed Sandbox — it is the setup window
27
+ # for +#define+ / +#preload+ before that Sandbox's first checkout.
28
+ # No Sandbox is constructed here. Raises +ArgumentError+ for an
29
+ # invalid +slots+ / +checkout_timeout+.
30
+ def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
31
+ validate_slots!(slots)
32
+ @slots = slots
33
+ @checkout_timeout = normalize_checkout_timeout(checkout_timeout)
34
+ @sandbox_options = sandbox_options
35
+ @setup = setup
36
+ @idle = [] # : Array[Kobako::Sandbox]
37
+ @constructed = 0
38
+ @mutex = Mutex.new
39
+ @slot_freed = ConditionVariable.new
40
+ end
41
+
42
+ # Yield one exclusively-held Sandbox to the block and return the
43
+ # block's value. Blocks while every slot is held; raises
44
+ # +Kobako::PoolTimeoutError+ once the wait exceeds +checkout_timeout+.
45
+ # The Sandbox returns to the pool at block exit — unless the block raised
46
+ # +Kobako::TrapError+, in which case the unrecoverable Sandbox is
47
+ # discarded and its slot refills by a fresh construction on next
48
+ # demand.
49
+ def with
50
+ sandbox = checkout
51
+ begin
52
+ yield sandbox
53
+ rescue TrapError
54
+ release_capacity!
55
+ sandbox = nil
56
+ raise
57
+ ensure
58
+ checkin(sandbox) if sandbox
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Acquire a Sandbox and hand it over in pre-invocation state — empty
65
+ # output buffers and truncation predicates false.
66
+ def checkout
67
+ acquire.tap(&:reset_invocation_state!)
68
+ end
69
+
70
+ # The idle-first claim loop: an idle Sandbox wins, unclaimed
71
+ # capacity constructs, and a full pool waits for a checkin.
72
+ def acquire
73
+ timeout = @checkout_timeout
74
+ deadline = timeout && (monotonic_now + timeout)
75
+ loop do
76
+ action, sandbox = claim_or_wait(deadline)
77
+ return sandbox if action == :idle && sandbox
78
+ return construct_slot if action == :build
79
+ end
80
+ end
81
+
82
+ # Single locked decision point for one claim attempt. Waiting
83
+ # happens inside the lock (so a checkin can wake it); construction
84
+ # happens outside (so a slow setup block never holds the lock) —
85
+ # capacity is reserved here and released by +construct_slot+ on
86
+ # failure.
87
+ def claim_or_wait(deadline)
88
+ @mutex.synchronize do
89
+ return [:idle, @idle.pop] unless @idle.empty?
90
+
91
+ if @constructed < @slots
92
+ @constructed += 1
93
+ return [:build, nil]
94
+ end
95
+
96
+ await_slot!(deadline)
97
+ [:retry, nil]
98
+ end
99
+ end
100
+
101
+ # Wait for a checkin or freed capacity; raises
102
+ # +Kobako::PoolTimeoutError+ once +deadline+ has passed. Must
103
+ # run while holding +@mutex+.
104
+ def await_slot!(deadline)
105
+ remaining = deadline && (deadline - monotonic_now)
106
+ if remaining && remaining <= 0
107
+ raise PoolTimeoutError,
108
+ "no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held"
109
+ end
110
+
111
+ @slot_freed.wait(@mutex, remaining)
112
+ end
113
+
114
+ # Construct and set up one pooled Sandbox against the capacity
115
+ # reserved by +claim_or_wait+. Construction and setup-block errors
116
+ # propagate to the checkout caller unchanged; the reserved
117
+ # capacity is released so a later checkout can retry.
118
+ def construct_slot
119
+ done = false
120
+ sandbox = Sandbox.new(**@sandbox_options)
121
+ @setup&.call(sandbox)
122
+ done = true
123
+ sandbox
124
+ ensure
125
+ release_capacity! unless done
126
+ end
127
+
128
+ # Return a Sandbox to the idle list and wake one waiting checkout.
129
+ def checkin(sandbox)
130
+ @mutex.synchronize do
131
+ @idle.push(sandbox)
132
+ @slot_freed.signal
133
+ end
134
+ end
135
+
136
+ # Give back reserved-but-unfilled capacity — a failed construction or
137
+ # a discarded Sandbox — and wake one waiting checkout to claim it.
138
+ def release_capacity!
139
+ @mutex.synchronize do
140
+ @constructed -= 1
141
+ @slot_freed.signal
142
+ end
143
+ end
144
+
145
+ # The wait deadline runs on the monotonic clock so a wall-clock jump
146
+ # cannot stretch or cut the checkout wait.
147
+ def monotonic_now
148
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
149
+ end
150
+
151
+ # Pre-flight for +slots+ — no coercion, a positive Integer is
152
+ # the only accepted shape.
153
+ def validate_slots!(slots)
154
+ return if slots.is_a?(Integer) && slots.positive?
155
+
156
+ raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
157
+ end
158
+
159
+ # Coerce +checkout_timeout+ into the Float seconds the wait loop
160
+ # consumes, or +nil+ to wait indefinitely — the same normalisation
161
+ # idiom +SandboxOptions+ applies to +timeout+.
162
+ def normalize_checkout_timeout(checkout_timeout)
163
+ return nil if checkout_timeout.nil?
164
+ unless checkout_timeout.is_a?(Numeric)
165
+ raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}"
166
+ end
167
+
168
+ seconds = checkout_timeout.to_f
169
+ unless seconds.positive? && seconds.finite?
170
+ raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})"
171
+ end
172
+
173
+ seconds
174
+ end
175
+ end
176
+ end
@@ -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
 
@@ -206,16 +193,27 @@ module Kobako
206
193
  end
207
194
  end
208
195
 
196
+ # Reset all per-invocation observable state to its pre-invocation
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.
202
+ def reset_invocation_state!
203
+ @stdout_capture = Capture::EMPTY
204
+ @stderr_capture = Capture::EMPTY
205
+ @usage = Usage::EMPTY
206
+ end
207
+
209
208
  private
210
209
 
211
- # Configure the +Runtime+'s host↔guest dispatch wiring
212
- # ({docs/behavior.md B-12}[link:../../docs/behavior.md]). Builds a
210
+ # Configure the +Runtime+'s host↔guest dispatch wiring. Builds a
213
211
  # lambda that re-enters the guest via
214
- # +Runtime#yield_to_active_invocation+ (B-24) and a dispatch +Proc+
215
- # that routes guest→host calls through the stateless
216
- # +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
217
- # closure. Both are registered on the +Runtime+ once at construction
218
- # 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.
219
217
  def install_dispatch_proc!
220
218
  yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
221
219
  @runtime.on_dispatch = lambda do |request_bytes|
@@ -223,37 +221,21 @@ module Kobako
223
221
  end
224
222
  end
225
223
 
226
- # Per-invocation prologue ({docs/behavior.md B-03 / B-07 /
227
- # B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
228
- # registries on first call (idempotent) and zeros the per-invocation
229
- # 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
230
227
  # +Catalog::Handles+ counter — before the guest runs. The
231
- # +Catalog::Handles+ itself is held as +@handler+ and never exposed beyond
232
- # this class: SPEC.md Terminology pins it as "Not exposed to the
233
- # 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".
234
231
  def begin_invocation!
235
232
  @services.seal!
236
233
  @handler.reset!
237
234
  reset_invocation_state!
238
235
  end
239
236
 
240
- # Reset all per-invocation observable state to its pre-invocation
241
- # sentinels — both per-channel captures
242
- # ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
243
- # per-last-invocation usage record
244
- # ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
245
- # +#initialize+ (first-time setup) and +#begin_invocation!+
246
- # (between-invocation reset) so both paths agree on what
247
- # "pre-invocation state" means.
248
- def reset_invocation_state!
249
- @stdout_capture = Capture::EMPTY
250
- @stderr_capture = Capture::EMPTY
251
- @usage = Usage::EMPTY
252
- end
253
-
254
237
  # Read the per-last-invocation +wall_time+ and +memory_peak+ from
255
- # the ext and wrap them as a +Kobako::Usage+ value object
256
- # ({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
257
239
  # the +invoke!+ +ensure+ block so the usage record is populated on
258
240
  # every outcome — value return, +Kobako::TrapError+ (including
259
241
  # +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
@@ -271,8 +253,7 @@ module Kobako
271
253
  end
272
254
 
273
255
  # Pick the +TrapError+ subclass to re-raise based on +err+'s actual
274
- # class. Cap-trap subclasses
275
- # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
256
+ # class. Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+)
276
257
  # preserve their named identity; everything else collapses to the
277
258
  # base +Kobako::TrapError+. The ext already raises the right subclass
278
259
  # directly, so this is a pure re-attribution that lets +#invoke!+
@@ -296,9 +277,8 @@ module Kobako
296
277
  # +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
297
278
  # populated by the +ensure+ readout ({#read_usage!}) on every outcome.
298
279
  # The rescue chain is the single trap-translation boundary —
299
- # configured-cap paths
300
- # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
301
- # 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
302
282
  # the base +TrapError+.
303
283
  def invoke!(verb)
304
284
  begin_invocation!
@@ -307,7 +287,7 @@ module Kobako
307
287
  @stderr_capture = snapshot.stderr
308
288
  # A Capability Handle in the result is decoded as a Kobako::Handle
309
289
  # token; restore it to the host object the guest referenced before
310
- # 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
311
291
  # invocation's table — reset only happens at the next #begin_invocation!.
312
292
  Codec::Utils.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
313
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