kobako 0.26.0 → 0.28.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +103 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +156 -118
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +38 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +10 -12
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +35 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +43 -27
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +8 -10
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -11
data/lib/kobako/codec.rb CHANGED
@@ -10,24 +10,7 @@ require_relative "codec/encoder"
10
10
  require_relative "codec/decoder"
11
11
 
12
12
  module Kobako
13
- # Host-side MessagePack codec for the kobako wire contract — the
14
- # byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
15
- # This is the payload codec: the core envelope is framed natively, so
16
- # what reaches here is one codec document — an invocation's arguments
17
- # (+Kobako::Payload::Arguments+), a dispatch or yield value, or an
18
- # invocation's value and a Panic's diagnostics
19
- # (+Kobako::Outcome+). The ext-type leaves this layer
20
- # carries — +Kobako::Handle+ (0x01) — lives at the kobako root so the
21
- # codec can register it without depending upward on Transport.
22
- #
23
- # Backed by the official +msgpack+ gem: ExtTypes registers the two
24
- # kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle) on
25
- # one process-wide +MessagePack::Factory+,
26
- # and Encoder / Decoder are thin wrappers over it. The Rust side
27
- # mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
28
- # the ext-code constants live as module-private values on ExtTypes
29
- # alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ on that side.
30
- module Codec
13
+ module Codec # :nodoc:
31
14
  # The maximum structural nesting depth the wire represents (the
32
15
  # MessagePack ecosystem's bound), shared with the guest +kobako_codec+
33
16
  # so both sides cap identically. The host refuses a value nesting past
@@ -35,10 +18,8 @@ module Kobako
35
18
  # packer.
36
19
  MAX_NESTING_DEPTH = 128
37
20
 
38
- # Bracket a decode and return the block's result together with whether
39
- # the decoded tree carried an ext 0x01 Capability Handle — the signal a
40
- # dispatch path uses to skip an all-identity Handle-resolution walk.
41
- # The tracking state is codec-internal; this is its only readout.
21
+ # Answers the block's result with whether the decode carried a
22
+ # Capability Handle, so a caller can skip an all-identity restore walk.
42
23
  def self.track_handles(&block)
43
24
  State.current.track_handles(&block)
44
25
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # A bind-path segment, a snippet name and a #run entrypoint all take the
5
+ # constant-name form, so they are refused alike. The guest interns each
6
+ # one, and mruby holds a symbol of at most 65534 bytes.
7
+ CONSTANT_NAME = /\A[A-Z]\w{0,65533}\z/ # :nodoc:
8
+ end
@@ -11,41 +11,30 @@ require_relative "transport"
11
11
  require_relative "catalog"
12
12
 
13
13
  module Kobako
14
- # Kobako::Context — the per-invocation driver behind a single +Sandbox#eval+
15
- # / +#run+. Each Context owns a fresh +Catalog::Handles+ table and drives one
16
- # guest invocation: it builds the dispatch handler, runs the guest, records
17
- # the run's captures and usage, and decodes the outcome. One Context per
18
- # invocation owns all per-invocation state, so the reusable +Sandbox+ holds
19
- # none and overlapping evals never touch each other's state.
14
+ # One invocation, as the block given to Sandbox#eval or Sandbox#run sees it.
15
+ # The block runs before the guest does, so it can choose the objects this
16
+ # invocation alone reaches:
17
+ #
18
+ # sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
20
19
  class Context
21
- # Build a Context over the Sandbox-owned config — the +Runtime+, the
22
- # sealed +Catalog::Services+ / +Catalog::Snippets+ registries, and the
23
- # +Catalog::Extensions+ whose callable backends this Context resolves for
24
- # its own run. The +Catalog::Handles+ table is this invocation's own, so
25
- # guest→host dispatch and host→guest auto-wrap share one allocator scoped
26
- # to the run; the resolved provider map is likewise per-invocation, so
27
- # concurrent invocations never share mutable state.
28
- def initialize(runtime:, services:, snippets:, extensions:)
20
+ def initialize(runtime:, services:, snippets:, flags:, extensions:) # :nodoc:
29
21
  @runtime = runtime
30
22
  @services = services
31
23
  @snippets = snippets
24
+ @flags = flags
32
25
  @extensions = extensions
33
26
  @resolved = {} # : Hash[String, Transport::Exposure]
34
27
  @overrides = {} # : Hash[String, Transport::Exposure]
35
- @spent = false
36
- @handler = Catalog::Handles.new
28
+ @handles = Catalog::Handles.new
37
29
  @stdout_capture = @stderr_capture = Capture::EMPTY
38
30
  @usage = Usage::EMPTY
39
31
  end
40
32
 
41
- # Override the object bound at an already-declared +path+ for this
42
- # invocation only — the per-eval hook the +#eval+ / +#run+ block uses to
43
- # fill a fillable or shadow any static / per-invocation binding. +path+
44
- # must name a declared (Frame 1) binding; overriding an undeclared path
45
- # raises +ArgumentError+ so the Frame 1 key set stays fixed. Valid only
46
- # while the block runs — the Context is spent once the block returns, so a
47
- # captured +ctx+ used afterward raises +ArgumentError+ too, putting both
48
- # misuses on one rescuable channel. Returns +self+.
33
+ # Use +object+ for the Service at +path+ during this invocation only,
34
+ # filling a fillable Service or shadowing a bound one. Returns +self+.
35
+ #
36
+ # Raises ArgumentError when +path+ was never bound on the Sandbox, or when
37
+ # called after the block has returned.
49
38
  def bind(path, object)
50
39
  raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent
51
40
 
@@ -56,16 +45,9 @@ module Kobako
56
45
  self
57
46
  end
58
47
 
59
- # Resolve a Service +path+ to the Exposure backing it this invocation,
60
- # layering the per-eval +ctx.bind+ overrides over this Context's
61
- # per-invocation provider results over the Sandbox's static base bindings.
62
- # An unbound path raises +KeyError+; a fillable left unfilled resolves to
63
- # +Kobako::Unresolved+ and is reported the same way, so an unresolved
64
- # capability fails closed as an undefined target rather than dispatching to
65
- # the sentinel. The Dispatcher maps either +KeyError+ to an
66
- # undefined-target wire fault. Internal — the per-invocation dispatch
67
- # handler is the sole caller.
68
- def lookup(path)
48
+ # A fillable left unfilled raises like an unbound path, so the guest's
49
+ # call fails closed instead of reaching the sentinel.
50
+ def lookup(path) # :nodoc:
69
51
  key = path.to_s
70
52
  exposure = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
71
53
  raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(exposure.object)
@@ -73,136 +55,93 @@ module Kobako
73
55
  exposure
74
56
  end
75
57
 
76
- # Execute a guest mruby source string in a fresh +mrb_state+ and return the
77
- # decoded last expression. A given +block+ runs first, receiving this
78
- # Context to collect +ctx.bind+ overrides before the guest drives.
79
- def eval(code, &block)
58
+ def eval(code, &block) # :nodoc:
80
59
  collect_overrides(&block) if block
81
60
  invoke!(:eval) do
82
- @runtime.eval(dispatch_handler, @services.paths, code.b, @snippets.entries)
61
+ @runtime.eval(dispatch_handler, @services.paths, @snippets.entries, @flags.entries, code.b)
83
62
  end
84
63
  end
85
64
 
86
- # Dispatch a +Transport::Run+ into a preloaded entrypoint and return the
87
- # decoded result. A given +block+ runs first, receiving this Context to
88
- # collect +ctx.bind+ overrides before the guest drives.
89
- def run(request, &block)
65
+ def run(request, &block) # :nodoc:
90
66
  collect_overrides(&block) if block
91
67
  invoke!(:run, entrypoint: request.entrypoint) do
92
- @runtime.run(dispatch_handler, @services.paths, @snippets.entries,
93
- request.entrypoint.to_s, request.payload(@handler))
68
+ @runtime.run(dispatch_handler, @services.paths, @snippets.entries, @flags.entries,
69
+ request.entrypoint.to_s, request.payload(@handles))
94
70
  end
95
71
  end
96
72
 
97
73
  private
98
74
 
99
- # Run the per-eval override +block+, handing it this Context so it can call
100
- # +ctx.bind+, then spend the Context so a captured +ctx+ used after the
101
- # block raises. A block that raises propagates before the guest drives, so
102
- # the guest never runs and no Execution is produced.
75
+ # The Context is spent once the block returns, so a captured +ctx+ used
76
+ # later raises. A block that raises propagates before the guest drives,
77
+ # so the guest never runs and no Execution is produced.
103
78
  def collect_overrides
104
79
  yield self
105
80
  ensure
106
81
  @spent = true
107
82
  end
108
83
 
109
- # Build this invocation's guest→host dispatch handler — a +Proc+ routing
110
- # each guest→host call through the stateless +Transport::Dispatcher+,
111
- # capturing this Context as the path resolver (its +#lookup+ layers the
112
- # per-invocation providers over the static bindings) plus +@handler+. Handed to
113
- # +Runtime#eval+ / +#run+ as a call argument, so the Runtime holds no
114
- # dispatch state and the +Proc+ stays GC-rooted as a live argument for the
115
- # synchronous call. The ext hands the +Proc+ a per-dispatch +guest_yielder+
116
- # — a +String → String+ callable that re-enters the in-flight guest to run
117
- # a yielded block — which the +Dispatcher+ forwards to the
118
- # +Transport::Yielder+ it builds for the call.
84
+ # Handed to the Runtime as a call argument, so the Runtime holds no
85
+ # dispatch state and the Proc stays GC-rooted for the synchronous call.
119
86
  def dispatch_handler
120
87
  lambda do |target, method_name, block_given, payload, guest_yielder|
121
88
  call = Transport::Call.new(target: target, method_name: method_name,
122
89
  block_given: block_given, payload: payload)
123
- Transport::Dispatcher.dispatch(call, self, @handler, guest_yielder)
90
+ Transport::Dispatcher.dispatch(call, self, @handles, guest_yielder)
124
91
  end
125
92
  end
126
93
 
127
- # Record this invocation's usage and both output captures from the ext
128
- # +Snapshot+. Every Snapshot carries them — value return or trap alike — so
129
- # +#usage+ / +#stdout+ / +#stderr+ stay readable after a rescued trap.
94
+ # Every Snapshot carries usage and captures, trap or not, so they stay
95
+ # readable after a rescued trap.
130
96
  def populate_observability!(snapshot)
131
97
  @usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
132
98
  @stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
133
99
  @stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
134
100
  end
135
101
 
136
- # Pick the +TrapError+ subclass to re-raise based on +err+'s actual class.
137
- # Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+) preserve their
138
- # named identity; everything else collapses to the base +Kobako::TrapError+,
139
- # so #invoke! can add the verb prefix without erasing the named subclass.
140
- def trap_class_for(err)
141
- case err
142
- when TimeoutError then TimeoutError
143
- when MemoryLimitError then MemoryLimitError
144
- else TrapError
145
- end
146
- end
147
-
148
- # Build the +TrapError+-family exception for a trapped +Snapshot+ from its
149
- # neutral trap kind, tagged with the verb — the cap subclasses
150
- # (+TimeoutError+ / +MemoryLimitError+) keep their identity, every other
151
- # engine fault is the base +TrapError+.
152
- def trap_error_for(snapshot, verb)
153
- klass = case snapshot.trap_kind
154
- when :timeout then TimeoutError
155
- when :memory_limit then MemoryLimitError
156
- else TrapError
157
- end
158
- klass.new("Sandbox##{verb} failed: #{snapshot.trap_message}")
102
+ # Every TrapError leaving an invocation names the verb that ran it; the
103
+ # cap subclasses keep their identity through the rebuild.
104
+ def with_verb(err, verb)
105
+ err.class.new("Sandbox##{verb} failed: #{err.message}")
159
106
  end
160
107
 
161
- # Freeze this run's observables plus +value+ (+nil+ on a failed run) into
162
- # the read-only +Execution+ the caller receives or the error carries.
163
- # +failed+ records the two apart so a +nil+ +value+ from a successful run
164
- # stays distinct from a failed one.
108
+ # +failed+ keeps a +nil+ value from a successful run apart from a failed
109
+ # run.
165
110
  def build_execution(value, failed:)
166
111
  Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
167
112
  end
168
113
 
169
- # Settle a completed run's outcome into its +Execution+. A Capability
170
- # Handle in the result is restored to its host object first. The settle
171
- # sits in the rescue so a wire-violation trap or a Panic both attach this
172
- # run's Execution, just like a guest-call trap does. +entrypoint+ is the
173
- # name +#run+ asked for, which the host knows and the wire never carries,
174
- # so an unresolved one names itself on the error it raises.
114
+ # The settle sits in the rescue so a wire-violation trap or a Panic both
115
+ # attach this run's Execution, just like a guest-call trap does.
116
+ # +entrypoint+ is known to the host and never carried by the wire, so an
117
+ # unresolved one names itself on the error it raises.
175
118
  def settle_outcome(snapshot, verb, entrypoint)
176
- kind, payload, panic = snapshot.outcome
177
- value, carried = Codec.track_handles { Outcome.reify(kind, payload, panic, entrypoint: entrypoint) }
178
- value = Codec::HandleWalk.deep_restore(value, @handler) if carried
119
+ payload, panic = snapshot.outcome
120
+ value, carried = Codec.track_handles { Outcome.reify(payload, panic, entrypoint: entrypoint) }
121
+ value = Codec::HandleWalk.deep_restore(value, @handles) if carried
179
122
  build_execution(value, failed: false)
180
123
  rescue Kobako::TrapError => e
181
- raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.message}").with_execution(build_execution(nil,
182
- failed: true))
124
+ raise with_verb(e, verb).with_execution(build_execution(nil, failed: true))
183
125
  rescue Kobako::SandboxError, Kobako::ServiceError => e
184
126
  raise e.with_execution(build_execution(nil, failed: true))
185
127
  end
186
128
 
187
- # Drive one invocation and settle it into a frozen +Execution+. +verb+
188
- # tags the TrapError message so the failing export is identifiable. This
189
- # invocation's callable Extension backends are resolved first — before the
190
- # guest runs — so a provider that raises propagates unwrapped and leaves
191
- # the guest unrun. Usage and captures are recorded before the trap check,
192
- # so a trapped Snapshot's error carries them just like a completed run's
193
- # return value. A could-not-start fault ran no invocation at all, so it
194
- # carries no Execution and gains only the verb prefix.
129
+ # Extension backends are resolved before the guest runs, so a provider
130
+ # that raises propagates unwrapped and leaves the guest unrun. A
131
+ # could-not-start fault ran no invocation at all, so it carries no
132
+ # Execution and gains only the verb prefix.
195
133
  def invoke!(verb, entrypoint: nil)
196
134
  @resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
197
135
  begin
198
136
  snapshot = yield
199
137
  rescue Kobako::TrapError => e
200
- raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
138
+ raise with_verb(e, verb)
201
139
  end
202
140
  populate_observability!(snapshot)
203
- return settle_outcome(snapshot, verb, entrypoint) unless snapshot.trapped?
141
+ trap = snapshot.trap_error
142
+ return settle_outcome(snapshot, verb, entrypoint) unless trap
204
143
 
205
- raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true))
144
+ raise with_verb(trap, verb).with_execution(build_execution(nil, failed: true))
206
145
  end
207
146
  end
208
147
  end
data/lib/kobako/errors.rb CHANGED
@@ -1,103 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Top-level Kobako namespace.
4
3
  module Kobako
5
- # Error taxonomy.
6
- #
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.
14
-
15
- # Base for all kobako-raised errors so callers that want to ignore the
16
- # taxonomy can rescue a single class.
4
+ # Every Sandbox#eval or Sandbox#run returns, or raises exactly one of
5
+ # TrapError, SandboxError, or ServiceError. SetupError and PoolTimeoutError
6
+ # arise outside any invocation.
7
+
8
+ # The base of every error kobako raises.
17
9
  class Error < StandardError; end
18
10
 
19
- # Carries the frozen +Kobako::Execution+ of the run that failed, so a
20
- # rescue reads its captures and usage exactly as a successful caller reads
21
- # them off the return value. Mixed only into the three invocation-outcome
22
- # classes (+TrapError+ / +SandboxError+ / +ServiceError+ and their
23
- # subclasses); +SetupError+ and +PoolTimeoutError+ arise outside any
24
- # invocation and carry no Execution. Defaults to +nil+ — a pre-flight
25
- # failure that ran no invocation leaves it unset.
11
+ # Gives an invocation failure the Execution of the run that failed, so a
12
+ # rescue reads its captures and usage as a successful caller would.
13
+ # +execution+ is +nil+ when the failure came before the guest ran.
26
14
  module CarriesExecution
15
+ # The Execution of the run that failed.
27
16
  attr_reader :execution
28
17
 
29
- # Attach the failed run's +Kobako::Execution+ and return +self+ so the
30
- # raise site reads +raise error.with_execution(execution)+.
31
- def with_execution(execution)
18
+ def with_execution(execution) # :nodoc:
32
19
  @execution = execution
33
20
  self
34
21
  end
35
22
  end
36
23
 
37
- # Wasm engine layer. Raised when the Wasm execution engine crashed
38
- # (trap, OOM, unreachable) or when the wire layer detected a structural
39
- # violation that signals a corrupted guest execution environment (an
40
- # outcome the core envelope cannot frame, an absent one included).
41
- #
42
- # Two named subclasses cover the configured per-invocation caps:
43
- #
44
- # * TimeoutError — wall-clock +timeout+ exceeded.
45
- # * MemoryLimitError — guest +memory.grow+ would exceed
46
- # +memory_limit+.
47
- #
48
- # Host Apps that only care about "guest is unrecoverable, discard the
49
- # Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
50
- # want to surface a specific reason to operators can rescue the subclass
51
- # first.
24
+ # The engine stopped the guest: a trap, a cap reached, or an outcome that
25
+ # could not be read. The next invocation on the same Sandbox runs as usual.
52
26
  class TrapError < Error
53
27
  include CarriesExecution
54
28
  end
55
29
 
56
- # Wall-clock timeout cap exhausted: the absolute deadline
57
- # +entry_time + timeout+ passed and the next guest wasm safepoint
58
- # trapped. The Sandbox is unrecoverable after this point; discard and
59
- # recreate before another execution.
30
+ # The invocation ran past its +timeout+.
60
31
  class TimeoutError < TrapError; end
61
32
 
62
- # Linear-memory cap exhausted: a guest +memory.grow+ would have pushed
63
- # linear memory past the configured +memory_limit+. The Sandbox is
64
- # unrecoverable after this point; discard and recreate before another
65
- # execution.
33
+ # The invocation grew guest memory past its +memory_limit+.
66
34
  class MemoryLimitError < TrapError; end
67
35
 
68
- # Construction-layer error raised by +Kobako::Sandbox.new+ /
69
- # +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
70
- # from the configured +wasm_path+ before any invocation runs —
71
- # an unreadable artifact, bytes that are not a valid Wasm module, or
72
- # engine / linker / instantiation setup failure. Construction
73
- # is not an invocation, so +SetupError+ sits beside the invocation
74
- # taxonomy under +Kobako::Error+ rather than under +TrapError+: no
75
- # Sandbox is produced, so the +TrapError+ "discard and recreate"
76
- # recovery contract does not apply.
36
+ # A Sandbox could not be built: the Guest Binary is unreadable or not a
37
+ # valid module, the runtime failed to start, or it provides less isolation
38
+ # than +profile+ asks for.
77
39
  class SetupError < Error; end
78
40
 
79
- # The named +SetupError+ subclass for the common, actionable case:
80
- # the Guest Binary artifact is absent at +wasm_path+ — the pre-build
81
- # state on a fresh clone before +bundle exec rake compile+. Host Apps
82
- # that only need "the Sandbox could not be set up" rescue +SetupError+;
83
- # those wanting to special-case the unbuilt-artifact state rescue
84
- # +ModuleNotBuiltError+ first.
41
+ # The Guest Binary is missing at +wasm_path+.
85
42
  class ModuleNotBuiltError < SetupError; end
86
43
 
87
- # The structured attribution the two invocation-failure classes carry
88
- # from a decoded guest exception — its +origin+, original +klass+, and
89
- # +backtrace_lines+ — so a Host App can inspect a failure beyond its
90
- # message. Mixed into both rather than promoted to a shared superclass
91
- # because +SandboxError+ and +ServiceError+ sit in distinct branches of
92
- # the invocation-outcome taxonomy under +Kobako::Error+.
93
- #
94
- # Data specific to one kind of failure rides a named reader on the
95
- # subclass that failure raises, the way Ruby pairs NameError#name with
96
- # +NameError#local_variables+ — see UndefinedEntrypointError.
44
+ # What the guest reported about a failure.
97
45
  module Diagnosable
46
+ # Where the failure came from (<tt>"sandbox"</tt> or <tt>"service"</tt>),
47
+ # the name of the class that raised it, and its backtrace.
98
48
  attr_reader :origin, :klass, :backtrace_lines
99
49
 
100
- def initialize(message, origin: nil, klass: nil, backtrace_lines: nil)
50
+ def initialize(message, origin: nil, klass: nil, backtrace_lines: nil) # :nodoc:
101
51
  super(message)
102
52
  @origin = origin
103
53
  @klass = klass
@@ -105,93 +55,67 @@ module Kobako
105
55
  end
106
56
  end
107
57
 
108
- # Sandbox / wire layer. Raised when the guest ran to completion but
109
- # execution failed due to a mruby script error, a protocol fault, or a
110
- # payload the host's codec could not decode out of a well-framed
111
- # outcome.
58
+ # The guest failed: the script raised or did not compile, the Guest Binary
59
+ # could not start the run, or what it produced could not be read.
112
60
  class SandboxError < Error
113
61
  include Diagnosable
114
62
  include CarriesExecution
115
63
  end
116
64
 
117
- # Service layer. Raised when a Service capability call inside a mruby
118
- # script reported an application-level failure that the script did not
119
- # rescue. The base class covers a Service that ran and raised; the two
120
- # subclasses below cover the calls that never reached one, so a Host App
121
- # routes them apart with +rescue+ instead of by reading the message.
65
+ # A Service call failed and the guest left it unrescued.
122
66
  class ServiceError < Error
123
67
  include Diagnosable
124
68
  include CarriesExecution
125
69
  end
126
70
 
127
- # The ServiceError subclass raised when the call reached no Service
128
- # method: the bound path holds nothing, the Capability Handle is not
129
- # live in this invocation, or the method is absent or outside the guest
130
- # surface. The causes stay indistinguishable — an opaque target must
131
- # disclose nothing about which methods it defines — so what a Host App
132
- # learns is that this call will not succeed by being retried.
71
+ # The call reached no Service method: nothing is bound there, the Handle
72
+ # is not live, or the guest may not call that method. The causes look the
73
+ # same on purpose, so a target discloses nothing about what it defines.
133
74
  class NoServiceError < ServiceError; end
134
75
 
135
- # The ServiceError subclass raised when the call reached the Service
136
- # method but its arguments did not fit — an unknown keyword, or an
137
- # arity mismatch.
76
+ # The call reached the Service method with arguments that did not fit.
138
77
  class ServiceArgumentError < ServiceError; end
139
78
 
140
- # Raised at a Service method's +yield+ site when the guest block it
141
- # yielded to raised. A Service rescues it the way it would rescue a
142
- # block's exception without a Sandbox between the two frames; leaving it
143
- # unrescued returns it to the guest, which re-raises the exception it
144
- # raised in the first place. It therefore never reaches the Host App as
145
- # an invocation outcome and carries no Execution. +klass+ names the
146
- # guest-side class, which has no host counterpart to rebuild.
79
+ # Raised at a Service's +yield+ when the guest block raised; rescue it as
80
+ # you would the block's own exception. Left unrescued, the guest sees its
81
+ # own exception again, so it never reaches the Host App.
147
82
  class BlockError < Error
148
83
  include Diagnosable
149
84
  end
150
85
 
151
- # Raised at a Service method's +yield+ site when a yield argument has no
152
- # wire representation, so the round-trip never left the host and the
153
- # block never ran. As BlockError, a Service rescues it at that site and
154
- # may go on, so it never reaches the Host App as an invocation outcome;
155
- # unrescued, the dispatch answers as that Service having failed.
86
+ # Raised at a Service's +yield+ when an argument cannot cross to the guest,
87
+ # so the block never ran. Left unrescued, the Service call fails.
156
88
  class YieldValueError < Error; end
157
89
 
158
- # HandleExhaustedError is the canonical SandboxError subclass for the
159
- # id-cap-hit path. Raised when the per-invocation Handle ID counter in
160
- # Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
161
- # allocation would exceed the cap.
90
+ # Raised at a Service's +yield+ when the guest serves no block, so the
91
+ # block never ran. Left unrescued, the Service call fails.
92
+ class UnsupportedError < Error; end
93
+
94
+ # One invocation issued more Handles than an id can number (2**31 - 1).
162
95
  class HandleExhaustedError < SandboxError; end
163
96
 
164
- # BytecodeError is the SandboxError subclass raised when a
165
- # +#preload(binary:)+ snippet fails structural validation during the
166
- # first invocation's snippet replay against a fresh +mrb_state+ (RITE
167
- # version mismatch or corrupt body). Bytecode that loads cleanly and
168
- # then raises at top level surfaces as plain +SandboxError+ with the
169
- # natural mruby class preserved. Inherits from SandboxError so a single
170
- # +rescue Kobako::SandboxError+ covers both source and bytecode
171
- # snippet failures while callers wanting bytecode-specific handling
172
- # can +rescue Kobako::BytecodeError+ directly.
97
+ # A <tt>preload(binary:)</tt> snippet failed to load. Bytecode that loads
98
+ # and then raises is a plain SandboxError.
173
99
  class BytecodeError < SandboxError; end
174
100
 
175
- # UndefinedEntrypointError is the SandboxError subclass raised when a
176
- # +#run+ target names no top-level constant in the guest. It carries the
177
- # +#name+ that was asked for and the +#available+ names it could have
178
- # been — the top-level constants the preloaded snippets contributed — so
179
- # a caller corrects the name from the error rather than by reading the
180
- # guest source. Ruby pairs NameError#name with
181
- # +NameError#local_variables+ for the same reason.
101
+ # The Guest Binary does not support a flag the Sandbox set, so no script
102
+ # ran. Retrying without the flag, or on a Guest Binary that supports it,
103
+ # can succeed.
104
+ class UnsupportedFlagError < SandboxError; end
105
+
106
+ # Sandbox#run named no top-level constant.
182
107
  class UndefinedEntrypointError < SandboxError
108
+ # The constant asked for, and the ones the preloaded snippets define.
183
109
  attr_reader :name, :available
184
110
 
185
- def initialize(message, name: nil, available: [], **)
111
+ def initialize(message, name: nil, available: [], **) # :nodoc:
186
112
  super(message, **)
187
113
  @name = name
188
114
  @available = available
189
115
  end
190
116
  end
191
117
 
192
- # Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
193
- # wait exceeded the configured +checkout_timeout+ while every slot was
194
- # held. No Sandbox state is touched — retrying succeeds as soon as a holder
195
- # returns its Sandbox.
118
+ # Pool#with waited past +checkout_timeout+ with every slot held. Retrying
119
+ # succeeds once a holder returns its Sandbox.
196
120
  class PoolTimeoutError < Error; end
197
121
  end
@@ -4,25 +4,20 @@ require_relative "capture"
4
4
  require_relative "usage"
5
5
 
6
6
  module Kobako
7
- # Kobako::Execution — the frozen result of one +Sandbox#eval+ / +#run+: the
8
- # decoded +#value+ plus the run's output captures and +#usage+. A successful
9
- # run returns it; a failed run raises an error carrying the same frozen
10
- # Execution on the error's +#execution+, so a rescue reads the captures and
11
- # usage exactly as a successful caller reads them off the return value. On a
12
- # failed run +#value+ is +nil+ — only the captures and usage are meaningful.
7
+ # The record of one Sandbox#eval or Sandbox#run: the guest's #value, its
8
+ # output, and its #usage. A failed run raises instead, and the error carries
9
+ # the same record on +execution+, where #value is +nil+.
13
10
  #
14
- # +#failed?+ tells the returned Execution from the carried one even when both
15
- # +#value+ are +nil+ (a run whose last expression was +nil+ versus a failed
16
- # one). It holds no reference to the raising error, so the error carries the
17
- # Execution but not the reverse.
11
+ # #failed? tells the two apart when both #value are +nil+: a script whose
12
+ # last expression was +nil+ versus one that failed.
18
13
  class Execution
19
- # The deserialized guest value the run produced; +nil+ on a failed run.
14
+ # The guest value the run produced; +nil+ on a failed run.
20
15
  attr_reader :value
21
16
 
22
- # The +Kobako::Usage+ resource accounting for this run.
17
+ # What the run spent against its caps, as a Usage.
23
18
  attr_reader :usage
24
19
 
25
- def initialize(value:, usage:, stdout:, stderr:, failed:)
20
+ def initialize(value:, usage:, stdout:, stderr:, failed:) # :nodoc:
26
21
  @value = value
27
22
  @usage = usage
28
23
  @stdout_capture = stdout
@@ -31,22 +26,22 @@ module Kobako
31
26
  freeze
32
27
  end
33
28
 
34
- # Returns +true+ iff the run failed — +false+ on the Execution +#eval+ /
35
- # +#run+ returned, +true+ on the one a raised error carries.
29
+ # Whether the run failed: +false+ on the Execution #eval or #run
30
+ # returned, +true+ on the one a raised error carries.
36
31
  def failed? = @failed
37
32
 
38
- # Bytes the guest wrote to stdout during this run as a UTF-8 String,
39
- # clipped at +stdout_limit+; the content carries no truncation sentinel,
40
- # so use +#stdout_truncated?+ to observe overflow.
33
+ # What the guest wrote to stdout, up to +stdout_limit+ bytes: a UTF-8
34
+ # String, or a binary one when the bytes are not valid UTF-8. The text
35
+ # carries no sign it was cut; #stdout_truncated? says so.
41
36
  def stdout = @stdout_capture.bytes
42
37
 
43
- # Bytes the guest wrote to stderr during this run. Mirror of #stdout.
38
+ # What the guest wrote to stderr; see #stdout.
44
39
  def stderr = @stderr_capture.bytes
45
40
 
46
- # Returns +true+ iff stdout capture reached +stdout_limit+ during this run.
41
+ # Whether stdout reached +stdout_limit+.
47
42
  def stdout_truncated? = @stdout_capture.truncated?
48
43
 
49
- # Returns +true+ iff stderr capture reached +stderr_limit+ during this run.
44
+ # Whether stderr reached +stderr_limit+.
50
45
  def stderr_truncated? = @stderr_capture.truncated?
51
46
  end
52
47
  end