kobako 0.12.2-aarch64-linux → 0.14.0-aarch64-linux

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 +41 -0
  4. data/README.md +5 -3
  5. data/ROADMAP.md +26 -0
  6. data/crates/kobako-runtime/CHANGELOG.md +15 -0
  7. data/crates/kobako-runtime/README.md +1 -1
  8. data/crates/kobako-wasmtime/CHANGELOG.md +15 -0
  9. data/crates/kobako-wasmtime/README.md +1 -1
  10. data/data/kobako.wasm +0 -0
  11. data/lib/kobako/3.3/kobako.so +0 -0
  12. data/lib/kobako/3.4/kobako.so +0 -0
  13. data/lib/kobako/4.0/kobako.so +0 -0
  14. data/lib/kobako/catalog/handles.rb +2 -2
  15. data/lib/kobako/catalog/namespaces.rb +2 -2
  16. data/lib/kobako/catalog/snippets.rb +1 -1
  17. data/lib/kobako/codec/decoder.rb +7 -7
  18. data/lib/kobako/codec/encoder.rb +2 -2
  19. data/lib/kobako/codec/error.rb +3 -3
  20. data/lib/kobako/codec/factory.rb +10 -10
  21. data/lib/kobako/codec/handle_walk.rb +17 -21
  22. data/lib/kobako/codec/utils.rb +9 -9
  23. data/lib/kobako/codec.rb +4 -4
  24. data/lib/kobako/errors.rb +14 -39
  25. data/lib/kobako/handle.rb +5 -2
  26. data/lib/kobako/namespace.rb +3 -3
  27. data/lib/kobako/outcome.rb +1 -0
  28. data/lib/kobako/sandbox.rb +61 -33
  29. data/lib/kobako/sandbox_options.rb +51 -9
  30. data/lib/kobako/transport/dispatcher.rb +17 -14
  31. data/lib/kobako/transport/request.rb +2 -2
  32. data/lib/kobako/transport/response.rb +2 -2
  33. data/lib/kobako/transport/run.rb +1 -1
  34. data/lib/kobako/transport/yield.rb +3 -3
  35. data/lib/kobako/transport/yielder.rb +6 -6
  36. data/lib/kobako/version.rb +1 -1
  37. data/lib/kobako.rb +0 -1
  38. data/release-please-config.json +66 -6
  39. data/sig/kobako/codec/handle_walk.rbs +2 -2
  40. data/sig/kobako/codec.rbs +11 -0
  41. data/sig/kobako/handle.rbs +0 -2
  42. data/sig/kobako/runtime.rbs +8 -3
  43. data/sig/kobako/sandbox.rbs +9 -5
  44. data/sig/kobako/sandbox_options.rbs +11 -2
  45. data/sig/kobako/transport/dispatcher.rbs +8 -8
  46. data/sig/kobako/transport/run.rbs +1 -1
  47. data/sig/kobako/transport/yielder.rbs +2 -2
  48. data/sig/kobako/transport.rbs +8 -0
  49. metadata +3 -4
  50. data/lib/kobako/snapshot.rb +0 -32
  51. data/sig/kobako/snapshot.rbs +0 -12
data/lib/kobako/errors.rb CHANGED
@@ -4,38 +4,13 @@
4
4
  module Kobako
5
5
  # Error taxonomy.
6
6
  #
7
- # Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
8
- # exactly one of three invocation-outcome classes. Attribution is decided after the
9
- # guest binary returns control to the host: first the Wasm-trap layer, then
10
- # the outcome-envelope tag.
11
- #
12
- # Three invocation-outcome branches:
13
- #
14
- # * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
15
- # wire-violation fallback signalling a corrupted
16
- # guest runtime).
17
- # * {SandboxError} — sandbox / wire layer (mruby script error,
18
- # wire-decode failure on an otherwise valid tag,
19
- # Catalog::Handles exhaustion, output buffer overrun).
20
- # * {ServiceError} — service / capability layer (a Service capability
21
- # call that failed and was not rescued inside the
22
- # script).
23
- #
24
- # Two further branches sit outside the invocation taxonomy:
25
- #
26
- # * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
27
- # when the wasm runtime cannot be built from the
28
- # configured +wasm_path+ before any invocation runs.
29
- # Not an invocation outcome, so it never passes
30
- # through the two-step attribution decision.
31
- # * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
32
- # when the checkout wait exceeds +checkout_timeout+.
33
- #
34
- # Named subclasses:
35
- #
36
- # * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
37
- # at +wasm_path+.
38
- # * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
7
+ # Every +Kobako::Sandbox+ invocation (+#eval+ or +#run+) either returns a
8
+ # value or raises exactly one of TrapError, SandboxError, or ServiceError.
9
+ # Attribution is decided after the guest binary returns control to the
10
+ # host: first the Wasm-trap layer, then the outcome-envelope tag.
11
+ # SetupError and PoolTimeoutError sit outside the invocation taxonomy and
12
+ # never pass through that attribution decision. Each class below
13
+ # documents its own layer.
39
14
 
40
15
  # Base for all kobako-raised errors so callers that want to ignore the
41
16
  # taxonomy can rescue a single class.
@@ -48,8 +23,8 @@ module Kobako
48
23
  #
49
24
  # Two named subclasses cover the configured per-invocation caps:
50
25
  #
51
- # * {TimeoutError} — wall-clock +timeout+ exceeded.
52
- # * {MemoryLimitError} — guest +memory.grow+ would exceed
26
+ # * TimeoutError — wall-clock +timeout+ exceeded.
27
+ # * MemoryLimitError — guest +memory.grow+ would exceed
53
28
  # +memory_limit+.
54
29
  #
55
30
  # Host Apps that only care about "guest is unrecoverable, discard the
@@ -128,14 +103,14 @@ module Kobako
128
103
  class HandleExhaustedError < SandboxError; end
129
104
 
130
105
  # BytecodeError is the SandboxError subclass raised when a
131
- # `#preload(binary:)` snippet fails structural validation during the
132
- # first invocation's snippet replay against a fresh `mrb_state` (RITE
106
+ # +#preload(binary:)+ snippet fails structural validation during the
107
+ # first invocation's snippet replay against a fresh +mrb_state+ (RITE
133
108
  # version mismatch or corrupt body). Bytecode that loads cleanly and
134
- # then raises at top level surfaces as plain `SandboxError` with the
109
+ # then raises at top level surfaces as plain +SandboxError+ with the
135
110
  # natural mruby class preserved. Inherits from SandboxError so a single
136
- # `rescue Kobako::SandboxError` covers both source and bytecode
111
+ # +rescue Kobako::SandboxError+ covers both source and bytecode
137
112
  # snippet failures while callers wanting bytecode-specific handling
138
- # can `rescue Kobako::BytecodeError` directly.
113
+ # can +rescue Kobako::BytecodeError+ directly.
139
114
  class BytecodeError < SandboxError; end
140
115
 
141
116
  # Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
data/lib/kobako/handle.rb CHANGED
@@ -14,8 +14,10 @@ module Kobako
14
14
  # The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
15
15
  # privatised so Host App code cannot fabricate a Handle from a bare
16
16
  # integer; legitimate Handle instances enter Host App code only as
17
- # fields on raised error objects. The Host Gem itself constructs
18
- # Handles through {.restore}, which exists at exactly two call
17
+ # fields on raised error objects. +#with+ Data's copy-with-changes
18
+ # constructor is removed for the same reason: a legitimate Handle
19
+ # must not derive a sibling with a caller-chosen id. The Host Gem itself constructs
20
+ # Handles through +.restore+, which exists at exactly two call
19
21
  # sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
20
22
  # +Kobako::Codec::HandleWalk.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
21
23
  # (allocator paths). Both live inside +lib/kobako/+ and are not part
@@ -41,6 +43,7 @@ module Kobako
41
43
  end
42
44
 
43
45
  private_class_method :new
46
+ undef_method :with
44
47
 
45
48
  # Host Gem–internal factory. Allocates the Data instance through
46
49
  # +Class#allocate+ and dispatches +#initialize+ explicitly so the
@@ -3,7 +3,7 @@
3
3
  module Kobako
4
4
  # A named grouping of Members for one Sandbox.
5
5
  # Returned by +Sandbox#define+. Each instance owns a flat name→object
6
- # table of Members; member binding is validated against {NAME_PATTERN}.
6
+ # table of Members; member binding is validated against NAME_PATTERN.
7
7
  class Namespace
8
8
  # Ruby constant-name pattern shared by Namespace and Member names.
9
9
  NAME_PATTERN = /\A[A-Z]\w*\z/
@@ -11,7 +11,7 @@ module Kobako
11
11
  attr_reader :name
12
12
 
13
13
  # Build a new Namespace. +name+ is an already-validated Namespace
14
- # name (must satisfy {NAME_PATTERN}; validation is the caller's
14
+ # name (must satisfy NAME_PATTERN; validation is the caller's
15
15
  # responsibility).
16
16
  def initialize(name)
17
17
  @name = name
@@ -38,7 +38,7 @@ module Kobako
38
38
 
39
39
  # Mark this Namespace as sealed. Called by
40
40
  # +Kobako::Catalog::Namespaces#seal!+ on the owning Sandbox's first
41
- # invocation; afterwards {#bind} raises +ArgumentError+. Idempotent;
41
+ # invocation; afterwards #bind raises +ArgumentError+. Idempotent;
42
42
  # returns +self+.
43
43
  def seal!
44
44
  @sealed = true
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "codec"
3
4
  require_relative "outcome/panic"
4
5
  require_relative "transport/error"
5
6
 
@@ -38,14 +38,16 @@ module Kobako
38
38
 
39
39
  attr_reader :wasm_path, :options
40
40
 
41
- # Per-cap accessors forward to the immutable +SandboxOptions+ Value
41
+ # Per-option accessors forward to the immutable +SandboxOptions+ Value
42
42
  # Object so the Host App still reads them off Sandbox directly.
43
- def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
43
+ def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
44
44
 
45
45
  # Returns the bytes the guest wrote to stdout during the most recent
46
46
  # invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
47
47
  # any invocation; the byte content never contains a truncation sentinel,
48
- # so use +#stdout_truncated?+ to observe overflow.
48
+ # so use +#stdout_truncated?+ to observe overflow. Populated on every
49
+ # outcome — including a rescued +TrapError+, after which it holds the
50
+ # bytes written before the trap fired — mirroring +#usage+.
49
51
  def stdout
50
52
  @stdout_capture.bytes
51
53
  end
@@ -83,25 +85,24 @@ module Kobako
83
85
  # Build a fresh Sandbox.
84
86
  #
85
87
  # +wasm_path+ is the absolute path to the Guest Binary; defaults to the
86
- # gem-bundled +data/kobako.wasm+. The four caps (+stdout_limit+,
87
- # +stderr_limit+, +timeout+, +memory_limit+) are forwarded verbatim to
88
- # +Kobako::SandboxOptions+, which owns their DEFAULT fallback and
88
+ # gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
89
+ # (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
90
+ # requested isolation profile (+profile+) is forwarded verbatim to
91
+ # +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
89
92
  # normalisation. The constructed +SandboxOptions+ is exposed as
90
- # +#options+ and the four caps remain readable directly on Sandbox via
91
- # +Forwardable+ delegation.
92
- def initialize(wasm_path: nil,
93
- stdout_limit: SandboxOptions::DEFAULT_OUTPUT_LIMIT,
94
- stderr_limit: SandboxOptions::DEFAULT_OUTPUT_LIMIT,
95
- timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
96
- memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
93
+ # +#options+ and every option remains readable directly on Sandbox via
94
+ # +Forwardable+ delegation. The runtime builds the requested profile —
95
+ # +:hermetic+ (the default) denies the guest ambient time and entropy,
96
+ # +:permissive+ leaves them live — and construction refuses a runtime
97
+ # whose declared profile falls below the request, raising
98
+ # +Kobako::SetupError+ before any invocation entry point runs.
99
+ def initialize(wasm_path: nil, **)
97
100
  @wasm_path = wasm_path || Kobako::Runtime.default_path
98
- @options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
99
- stderr_limit: stderr_limit)
101
+ @options = SandboxOptions.new(**)
100
102
  @handler = Catalog::Handles.new
101
103
  @services = Kobako::Catalog::Namespaces.new(handler: @handler)
102
104
  @snippets = Catalog::Snippets.new
103
- @runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
104
- @options.stdout_limit, @options.stderr_limit)
105
+ @runtime = build_runtime!
105
106
  install_dispatch_proc!
106
107
  reset_invocation_state!
107
108
  end
@@ -119,7 +120,9 @@ module Kobako
119
120
  # Register a snippet on this Sandbox in one of two forms:
120
121
  #
121
122
  # * +preload(code: source, name: Name)+ — +source+ is mruby source
122
- # as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. The +name+
123
+ # as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. Compile
124
+ # failures surface as +Kobako::SandboxError+ on the first
125
+ # invocation's replay. The +name+
123
126
  # becomes the snippet's +(snippet:Name)+ backtrace filename and
124
127
  # is the dedupe key that rejects a duplicate +code:+ snippet.
125
128
  # * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
@@ -209,6 +212,17 @@ module Kobako
209
212
 
210
213
  private
211
214
 
215
+ # Construct the +Runtime+ with the requested isolation profile and
216
+ # refuse one whose declared posture falls below the request —
217
+ # +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
218
+ # runtime that cannot honor the request never runs guest code.
219
+ def build_runtime!
220
+ runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
221
+ @options.stdout_limit, @options.stderr_limit, @options.profile)
222
+ @options.enforce_floor!(runtime.profile)
223
+ runtime
224
+ end
225
+
212
226
  # Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
213
227
  # dispatch +Proc+ that routes guest→host calls through the stateless
214
228
  # +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
@@ -242,13 +256,11 @@ module Kobako
242
256
  # +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
243
257
  # and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
244
258
  # both paths: the figures are stashed in the ext on every outcome, so
245
- # unlike the +Snapshot+ (built only on success) the readout here also
246
- # covers the trap path.
259
+ # the readout here also covers the trap path, where +Runtime#eval+ /
260
+ # +#run+ raise instead of returning outcome bytes.
247
261
  #
248
- # The ext returns a positional 2-tuple +[wall_time, memory_peak]+
249
- # whose order matches the +Kobako::Usage+ field order; the
250
- # destructure-then-kwargs handoff below is the explicit
251
- # positional→keyword conversion point.
262
+ # The ext-side contract is positional: +Runtime#usage+ yields
263
+ # +[wall_time, memory_peak]+ in +Kobako::Usage+ field order.
252
264
  def read_usage!
253
265
  wall_time, memory_peak = @runtime.usage
254
266
  @usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
@@ -269,33 +281,49 @@ module Kobako
269
281
  end
270
282
  end
271
283
 
284
+ # Read the per-last-invocation output captures from the ext and wrap
285
+ # them as +Kobako::Capture+ value objects. Runs in the +invoke!+
286
+ # +ensure+ block next to #read_usage! for the same reason: the ext
287
+ # stashes the captures on every outcome, so the readout also covers
288
+ # the trap path, where +Runtime#eval+ / +#run+ raise instead of
289
+ # returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
290
+ # partial output readable after a rescue.
291
+ #
292
+ # The ext-side contract is positional: +Runtime#captures+ yields
293
+ # +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+.
294
+ def read_captures!
295
+ stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
296
+ @stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
297
+ @stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
298
+ end
299
+
272
300
  # Shared prologue / epilogue + trap-class translator for both
273
301
  # invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
274
302
  # TrapError message so the failing export is identifiable.
275
303
  #
276
- # The yielded block must return a +Kobako::Snapshot+ i.e. the
277
- # value of +Runtime#eval+ / +#run+. The success path unpacks
278
- # +#stdout+ / +#stderr+ into
279
- # +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
280
- # populated by the +ensure+ readout ({#read_usage!}) on every outcome.
304
+ # The yielded block must return the invocation's raw outcome bytes —
305
+ # i.e. the value of +Runtime#eval+ / +#run+ which the success path
306
+ # feeds to +Outcome.decode+. Captures and usage are populated by the
307
+ # +ensure+ readouts (#read_usage! / #read_captures!) on every
308
+ # outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
309
+ # rescued trap.
281
310
  # The rescue chain is the single trap-translation boundary —
282
311
  # configured-cap paths surface as named TrapError subclasses
283
312
  # (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
284
313
  # the base +TrapError+.
285
314
  def invoke!(verb)
286
315
  begin_invocation!
287
- snapshot = yield
288
- @stdout_capture = snapshot.stdout
289
- @stderr_capture = snapshot.stderr
316
+ return_bytes = yield
290
317
  # A Capability Handle in the result is decoded as a Kobako::Handle
291
318
  # token; restore it to the host object the guest referenced before
292
319
  # handing the value to the Host App. @handler still holds this
293
320
  # invocation's table — reset only happens at the next #begin_invocation!.
294
- Codec::HandleWalk.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
321
+ Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
295
322
  rescue Kobako::TrapError => e
296
323
  raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
297
324
  ensure
298
325
  read_usage!
326
+ read_captures!
299
327
  end
300
328
  end
301
329
  end
@@ -1,19 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "errors"
4
+
3
5
  module Kobako
4
6
  # Kobako::SandboxOptions — immutable Value Object holding the four
5
- # per-Sandbox configuration caps. Built on the +class X <
6
- # Data.define(...)+ subclass form (the Steep-friendly shape — see
7
- # +lib/kobako/outcome/panic.rb+).
7
+ # per-Sandbox configuration caps and the requested isolation profile.
8
+ # Built on the +class X < Data.define(...)+ subclass form (the
9
+ # Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
8
10
  #
9
- # The +initialize+ normalises every cap before delegating to Data's
11
+ # The +initialize+ normalises every option before delegating to Data's
10
12
  # +super+: +timeout+ to Float seconds, +memory_limit+ / +stdout_limit+ /
11
13
  # +stderr_limit+ to positive Integer bytes. Each cap is +nil+-disablable
12
14
  # (an absent argument takes its DEFAULT; an explicit +nil+ leaves the
13
- # bound off), so all four behave uniformly. Anything that survives
14
- # +SandboxOptions.new+ is a wire-ready cap bundle the +Kobako::Runtime+
15
- # constructor consumes as-is.
16
- class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
15
+ # bound off), so all four behave uniformly. +profile+ is the one
16
+ # non-cap: a Symbol on the PROFILES ladder naming the posture the
17
+ # runtime builds, which is also the weakest the Host App accepts —
18
+ # +nil+ is rejected because the weakest posture is requested as an
19
+ # explicit +:permissive+. Anything that survives +SandboxOptions.new+
20
+ # is a wire-ready bundle the +Kobako::Runtime+ constructor consumes
21
+ # as-is. The options also own the ladder comparison
22
+ # (+#enforce_floor!+) that +Kobako::Sandbox+ delegates its
23
+ # construction floor check to.
24
+ class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile)
17
25
  # Default wall-clock timeout for a single invocation: 60 seconds.
18
26
  DEFAULT_TIMEOUT_SECONDS = 60.0
19
27
 
@@ -25,17 +33,41 @@ module Kobako
25
33
  # Default per-channel capture ceiling: 1 MiB.
26
34
  DEFAULT_OUTPUT_LIMIT = 1 << 20
27
35
 
36
+ # The isolation ladder, weakest first — index order is rank order,
37
+ # so a floor check is an index comparison.
38
+ PROFILES = %i[permissive hermetic].freeze
39
+
40
+ # Default isolation profile: the strictest rung — opting down to
41
+ # +:permissive+ is the Host App's explicit trade.
42
+ DEFAULT_PROFILE = :hermetic
43
+
28
44
  def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
29
45
  memory_limit: DEFAULT_MEMORY_LIMIT,
30
46
  stdout_limit: DEFAULT_OUTPUT_LIMIT,
31
- stderr_limit: DEFAULT_OUTPUT_LIMIT)
47
+ stderr_limit: DEFAULT_OUTPUT_LIMIT,
48
+ profile: DEFAULT_PROFILE)
32
49
  timeout = normalize_timeout(timeout)
33
50
  memory_limit = normalize_memory_limit(memory_limit)
34
51
  stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
35
52
  stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
53
+ profile = normalize_profile(profile)
36
54
  super
37
55
  end
38
56
 
57
+ # Enforce the requested +profile+ as the floor against +declared+ —
58
+ # the posture a runtime reports having built — so a runtime that
59
+ # cannot honor the request fails construction with
60
+ # +Kobako::SetupError+ instead of weakening the posture silently.
61
+ # Both fallbacks fail closed: a declaration off the PROFILES ladder
62
+ # ranks below every request, and a request off the ladder
63
+ # (unreachable past +initialize+) refuses every declaration.
64
+ def enforce_floor!(declared)
65
+ return if (PROFILES.index(declared) || -1) >= (PROFILES.index(profile) || PROFILES.size)
66
+
67
+ raise Kobako::SetupError, "runtime declares isolation profile #{declared.inspect}, " \
68
+ "below the requested floor #{profile.inspect}"
69
+ end
70
+
39
71
  private
40
72
 
41
73
  # Coerce +timeout+ into the Float seconds the ext expects, or +nil+
@@ -78,5 +110,15 @@ module Kobako
78
110
 
79
111
  limit
80
112
  end
113
+
114
+ # Validate +profile+ against the PROFILES ladder. Unlike the caps
115
+ # there is no +nil+ form: the weakest posture is requested as an
116
+ # explicit +:permissive+, so anything off the ladder — +nil+
117
+ # included — is rejected.
118
+ def normalize_profile(profile)
119
+ return profile if PROFILES.include?(profile)
120
+
121
+ raise ArgumentError, "profile must be one of #{PROFILES.map(&:inspect).join(", ")}, got #{profile.inspect}"
122
+ end
81
123
  end
82
124
  end
@@ -27,7 +27,7 @@ module Kobako
27
27
  # Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
28
28
  # # => msgpack-encoded Response bytes (never raises)
29
29
  module Dispatcher
30
- # Throw tag for the {Yielder}'s break unwind back to the
30
+ # Throw tag for the Yielder's break unwind back to the
31
31
  # dispatcher's +catch+ frame. +private_constant+ is a
32
32
  # convention boundary — not a defence.
33
33
  BREAK_THROW = :__kobako_break__
@@ -54,12 +54,12 @@ module Kobako
54
54
  # Callable gadget types whose own public methods are reflection surface
55
55
  # (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
56
56
  # hand back the underlying object) rather than Service behaviour. Only
57
- # {CALLABLE_ALLOW} is reachable on a target of these types; a bound
57
+ # CALLABLE_ALLOW is reachable on a target of these types; a bound
58
58
  # lambda stays invocable, its reflective surface does not.
59
59
  GADGET_OWNERS = [Proc, Method, UnboundMethod, Binding].freeze
60
60
  private_constant :GADGET_OWNERS
61
61
 
62
- # The sole methods reachable on a {GADGET_OWNERS} target: invoking it
62
+ # The sole methods reachable on a GADGET_OWNERS target: invoking it
63
63
  # (+call+ / +[]+ / +yield+) and the harmless +arity+ / +lambda?+
64
64
  # describers that aid guest-side debugging.
65
65
  CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
@@ -91,7 +91,7 @@ module Kobako
91
91
  end
92
92
 
93
93
  # Resolve positional and keyword arguments off +request+ in one
94
- # step. Both pass through {#resolve_arg} so Capability Handles
94
+ # step. Both pass through #resolve_arg so Capability Handles
95
95
  # round-trip back to the host-side Ruby object before the call
96
96
  # reaches +public_send+.
97
97
  def resolve_call_args(request, handler)
@@ -101,7 +101,7 @@ module Kobako
101
101
 
102
102
  # Map an error caught at the dispatch boundary to a +Response.error+
103
103
  # envelope (binary msgpack). +error+ is the +StandardError+ caught by
104
- # {#dispatch}'s rescue; the +type+ field tells the guest which kind
104
+ # #dispatch's rescue; the +type+ field tells the guest which kind
105
105
  # of failure it was so it can raise the matching proxy-side error.
106
106
  def encode_caught_error(error)
107
107
  case error
@@ -119,8 +119,8 @@ module Kobako
119
119
  # reject calls to no-kwarg methods when the wire carries the
120
120
  # uniform empty-map shape.
121
121
  #
122
- # +yielder+ is the host-side {Yielder} materialised when the guest
123
- # call site supplied a block; its {Yielder#to_proc}
122
+ # +yielder+ is the host-side Yielder materialised when the guest
123
+ # call site supplied a block; its Yielder#to_proc
124
124
  # rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
125
125
  # so the same call site handles both cases without an explicit
126
126
  # conditional.
@@ -137,8 +137,8 @@ module Kobako
137
137
  end
138
138
 
139
139
  # Guard the +public_send+ below against ambient reflection methods.
140
- # A public method whose owner is a {META_OWNERS} or {GADGET_OWNERS} module is
141
- # rejected, except {CALLABLE_ALLOW} on a gadget target (a bound lambda
140
+ # A public method whose owner is a META_OWNERS or GADGET_OWNERS module is
141
+ # rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
142
142
  # stays invocable). A name with no concrete public method is allowed
143
143
  # only when the target opts into it via +respond_to?+ (dynamic
144
144
  # +method_missing+ Services), since the dangerous methods are all
@@ -159,7 +159,7 @@ module Kobako
159
159
  # Consult the target's opt-in narrowing predicate. A bound object
160
160
  # may define a private +respond_to_guest?(name)+ to restrict which of its
161
161
  # methods the guest reaches; a falsy answer rejects the dispatch.
162
- # The predicate composes beneath {#reject_meta_method!} — it only narrows,
162
+ # The predicate composes beneath #reject_meta_method! — it only narrows,
163
163
  # never re-opening the reflection surface the floor rejects — and is
164
164
  # consulted with the private surface included so the guest's +public_send+
165
165
  # dispatch can never reach +respond_to_guest?+ itself.
@@ -213,7 +213,7 @@ module Kobako
213
213
  # Encode +value+ as a +Response.ok+ envelope. When the value is not
214
214
  # wire-representable per the codec's type mapping, the
215
215
  # +UnsupportedType+ rescue routes it through the
216
- # Catalog::Handles via {#wrap_as_handle} and re-encodes with the Capability
216
+ # Catalog::Handles via #wrap_as_handle and re-encodes with the Capability
217
217
  # Handle in place. The happy path encodes exactly once.
218
218
  def encode_ok(value, handler)
219
219
  response = Kobako::Transport::Response.ok(value)
@@ -224,15 +224,18 @@ module Kobako
224
224
 
225
225
  # Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
226
226
  # that the wire codec can carry. Used as the fallback path of
227
- # {#encode_ok} when +value+ has no wire representation.
227
+ # #encode_ok when +value+ has no wire representation.
228
228
  def wrap_as_handle(value, handler)
229
229
  handler.alloc(value)
230
230
  end
231
231
 
232
+ # +message+ folds to UTF-8 first: Ruby core builds some exception
233
+ # messages as ASCII-8BIT (the arity ArgumentError, for one), and
234
+ # the codec would ride those as bin — a shape the guest refuses.
232
235
  def encode_error(type, message)
236
+ message = message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
233
237
  fault = Kobako::Fault.new(type: type, message: message)
234
- response = Kobako::Transport::Response.error(fault)
235
- response.encode
238
+ Kobako::Transport::Response.error(fault).encode
236
239
  end
237
240
  end
238
241
  end
@@ -13,7 +13,7 @@ module Kobako
13
13
  # 5-element msgpack array:
14
14
  # +[target, method_name, args, kwargs, block_given]+. +target+ is
15
15
  # either a +String+ (+"<Namespace>::<Member>"+, e.g. +"MyService::KV"+)
16
- # or a {Handle}. SPEC pins +kwargs+ map keys to ext 0x00 Symbol;
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
19
  # site supplied a block; the block body itself never crosses the
@@ -43,7 +43,7 @@ module Kobako
43
43
  Codec::Encoder.encode([target, method_name, args, kwargs, block_given])
44
44
  end
45
45
 
46
- # Decode +bytes+ into a {Request}. Raises +Codec::InvalidType+ when the
46
+ # Decode +bytes+ into a Request. Raises +Codec::InvalidType+ when the
47
47
  # envelope is not the expected 5-element msgpack array, or when the
48
48
  # Value Object's construction invariants reject the decoded fields.
49
49
  def self.decode(bytes)
@@ -20,7 +20,7 @@ module Kobako
20
20
  #
21
21
  # 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
22
22
  # (success) or 1 (fault). For success the second element is the return
23
- # value; for fault it is a {Fault} (ext 0x02 envelope).
23
+ # value; for fault it is a Fault (ext 0x02 envelope).
24
24
  #
25
25
  # Built on the +class X < Data.define(...)+ subclass form so the
26
26
  # class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
@@ -58,7 +58,7 @@ module Kobako
58
58
  Codec::Encoder.encode([status, payload])
59
59
  end
60
60
 
61
- # Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
61
+ # Decode +bytes+ into a Response. Raises +Codec::InvalidType+ when the
62
62
  # envelope is not the expected 2-element msgpack array, or when the
63
63
  # Value Object's construction invariants reject the decoded fields.
64
64
  def self.decode(bytes)
@@ -51,7 +51,7 @@ module Kobako
51
51
  # Encode this Run to the msgpack bytes the guest's +__kobako_run+
52
52
  # entry point consumes as its command-buffer payload
53
53
  # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
54
- # Walks +args+ / +kwargs+ through {Codec::HandleWalk.deep_wrap} so
54
+ # Walks +args+ / +kwargs+ through Codec::HandleWalk.deep_wrap so
55
55
  # any non-wire-representable leaf is allocated into +handler+ and
56
56
  # replaced with a +Kobako::Handle+; the
57
57
  # +handler+ argument is the Sandbox's table, sharing the same
@@ -10,9 +10,9 @@ module Kobako
10
10
  # First byte of the YieldResponse for the success branch — body is
11
11
  # the block's return value encoded as a single msgpack value.
12
12
  TAG_OK = 0x01
13
- # First byte for `break val` — body is the break value.
13
+ # First byte for +break val+ — body is the break value.
14
14
  TAG_BREAK = 0x02
15
- # Reserved for future `return val` support; both sides reject this
15
+ # Reserved for future +return val+ support; both sides reject this
16
16
  # tag as a wire violation (YieldResponse envelope contract).
17
17
  TAG_RESERVED = 0x03
18
18
  # First byte for an error / fault outcome — body is a
@@ -58,7 +58,7 @@ module Kobako
58
58
  [tag].pack("C") + Codec::Encoder.encode(value)
59
59
  end
60
60
 
61
- # Decode +bytes+ into a {Yield}. Rejects empty input, the reserved
61
+ # Decode +bytes+ into a Yield. Rejects empty input, the reserved
62
62
  # tag 0x03, and any tag outside +LIVE_TAGS+ by raising
63
63
  # +Kobako::Codec::InvalidType+ — these are wire violations per the
64
64
  # SPEC's YieldResponse envelope contract.
@@ -13,7 +13,7 @@ module Kobako
13
13
  # Each guest call that carries +block_given: true+ gets a Yielder
14
14
  # that the Dispatcher hands to the Service method as +&block+. The
15
15
  # Service method observes it as an ordinary Ruby Proc through
16
- # {#to_proc}; +yield val+ / +block.call(val)+ invokes {#yield}, which
16
+ # #to_proc; +yield val+ / +block.call(val)+ invokes #yield, which
17
17
  # serialises the positional args, re-enters the guest via the injected
18
18
  # +yield_to_guest+ lambda, and reifies the +YieldResponse+ into Ruby
19
19
  # control flow:
@@ -24,13 +24,13 @@ module Kobako
24
24
  # * +tag 0x04+ error — raise the +{class, message}+ payload at the
25
25
  # Service's yield site
26
26
  #
27
- # The Dispatcher calls {#invalidate!} from its +ensure+ block once
27
+ # The Dispatcher calls #invalidate! from its +ensure+ block once
28
28
  # dispatch completes; any later call to a stashed Yielder then raises
29
29
  # +LocalJumpError+ — the observable shape of an escaped Yielder.
30
30
  class Yielder
31
31
  # +yield_to_guest+ is a +String → String+ callable (the ext's
32
32
  # per-dispatch +Kobako::Runtime::GuestYielder+) that
33
- # {#yield} invokes to re-enter the guest; +break_tag+ is the +catch+
33
+ # #yield invokes to re-enter the guest; +break_tag+ is the +catch+
34
34
  # throw tag the Dispatcher matches against to unwind the Service on
35
35
  # +tag 0x02+. +handler+ is the Sandbox's +Kobako::Catalog::Handles+,
36
36
  # used to restore a Capability Handle in the block's ok value back to
@@ -44,7 +44,7 @@ module Kobako
44
44
 
45
45
  # Re-enter the guest with +args+ and reify the YieldResponse into
46
46
  # Ruby control flow. Raises +LocalJumpError+ if called after
47
- # {#invalidate!}. The ok value is consumed by the host Service
47
+ # #invalidate!. The ok value is consumed by the host Service
48
48
  # method, so a Capability Handle in it is restored to its host object.
49
49
  # The break value unwinds past the Service back to the guest
50
50
  # Member call, so it passes through verbatim — a Handle stays a
@@ -60,14 +60,14 @@ module Kobako
60
60
  raise yield_failure(response.value, default: "yield error")
61
61
  end
62
62
 
63
- # The Proc the Dispatcher passes as +&block+, binding {#yield} so a
63
+ # The Proc the Dispatcher passes as +&block+, binding #yield so a
64
64
  # Service method's +yield+ / +block.call+ drives the round-trip.
65
65
  def to_proc
66
66
  method(:yield).to_proc
67
67
  end
68
68
 
69
69
  # Mark this Yielder dead. Called by the Dispatcher's +ensure+ block
70
- # when the originating dispatch frame returns; any later {#yield}
70
+ # when the originating dispatch frame returns; any later #yield
71
71
  # call then raises +LocalJumpError+.
72
72
  def invalidate!
73
73
  @active = false
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.12.2"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/kobako.rb CHANGED
@@ -13,6 +13,5 @@ require_relative "kobako/errors"
13
13
  require_relative "kobako/transport"
14
14
  require_relative "kobako/catalog"
15
15
  require_relative "kobako/runtime"
16
- require_relative "kobako/snapshot"
17
16
  require_relative "kobako/sandbox"
18
17
  require_relative "kobako/pool"