kobako 0.18.0-aarch64-linux → 0.20.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +4 -1
  5. data/crates/kobako-runtime/CHANGELOG.md +14 -0
  6. data/crates/kobako-runtime/README.md +1 -1
  7. data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
  8. data/crates/kobako-wasmtime/README.md +1 -1
  9. data/data/kobako.wasm +0 -0
  10. data/lib/kobako/3.3/kobako.so +0 -0
  11. data/lib/kobako/3.4/kobako.so +0 -0
  12. data/lib/kobako/4.0/kobako.so +0 -0
  13. data/lib/kobako/catalog/extensions.rb +50 -43
  14. data/lib/kobako/catalog/handles.rb +7 -16
  15. data/lib/kobako/catalog/services.rb +22 -36
  16. data/lib/kobako/catalog/snippets.rb +11 -10
  17. data/lib/kobako/context.rb +202 -0
  18. data/lib/kobako/errors.rb +26 -4
  19. data/lib/kobako/execution.rb +52 -0
  20. data/lib/kobako/extension.rb +24 -10
  21. data/lib/kobako/pool.rb +1 -7
  22. data/lib/kobako/sandbox.rb +43 -182
  23. data/lib/kobako/sandbox_options.rb +23 -2
  24. data/lib/kobako/transport/dispatcher.rb +16 -15
  25. data/lib/kobako/transport/yielder.rb +1 -1
  26. data/lib/kobako/unresolved.rb +17 -0
  27. data/lib/kobako/usage.rb +6 -6
  28. data/lib/kobako/version.rb +1 -1
  29. data/lib/kobako.rb +1 -0
  30. data/release-please-config.json +2 -1
  31. data/sig/kobako/catalog/extensions.rbs +4 -6
  32. data/sig/kobako/catalog/handles.rbs +0 -2
  33. data/sig/kobako/catalog/services.rbs +3 -3
  34. data/sig/kobako/catalog/snippets.rbs +2 -0
  35. data/sig/kobako/context.rbs +36 -0
  36. data/sig/kobako/errors.rbs +12 -3
  37. data/sig/kobako/execution.rbs +24 -0
  38. data/sig/kobako/extension.rbs +4 -2
  39. data/sig/kobako/runtime.rbs +37 -9
  40. data/sig/kobako/sandbox.rbs +5 -24
  41. data/sig/kobako/sandbox_options.rbs +9 -2
  42. data/sig/kobako/transport/dispatcher.rbs +3 -3
  43. data/sig/kobako/unresolved.rbs +4 -0
  44. metadata +8 -2
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capture"
4
+ require_relative "codec"
5
+ require_relative "errors"
6
+ require_relative "execution"
7
+ require_relative "unresolved"
8
+ require_relative "outcome"
9
+ require_relative "usage"
10
+ require_relative "transport"
11
+ require_relative "catalog"
12
+
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.
20
+ 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:)
29
+ @runtime = runtime
30
+ @services = services
31
+ @snippets = snippets
32
+ @extensions = extensions
33
+ @resolved = {} # : Hash[String, untyped]
34
+ @overrides = {} # : Hash[String, untyped]
35
+ @spent = false
36
+ @handler = Catalog::Handles.new
37
+ @stdout_capture = @stderr_capture = Capture::EMPTY
38
+ @usage = Usage::EMPTY
39
+ end
40
+
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+.
49
+ def bind(path, object)
50
+ raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent
51
+
52
+ key = path.to_s
53
+ raise ArgumentError, "cannot override undeclared path #{key.inspect}" unless @services.bound?(key)
54
+
55
+ @overrides[key] = object
56
+ self
57
+ end
58
+
59
+ # Resolve a Service +path+ to the object 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)
69
+ key = path.to_s
70
+ object = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
71
+ raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(object)
72
+
73
+ object
74
+ end
75
+
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)
80
+ collect_overrides(&block) if block
81
+ invoke!(:eval) do
82
+ @runtime.eval(dispatch_handler, @services.encode, code.b, @snippets.encode)
83
+ end
84
+ end
85
+
86
+ # Dispatch a +Transport::Run+ envelope into a preloaded entrypoint and
87
+ # return the decoded result. A given +block+ runs first, receiving this
88
+ # Context to collect +ctx.bind+ overrides before the guest drives.
89
+ def run(run_envelope, &block)
90
+ collect_overrides(&block) if block
91
+ invoke!(:run) do
92
+ @runtime.run(dispatch_handler, @services.encode, @snippets.encode, run_envelope.encode(@handler))
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ # Run the per-eval override +block+, handing it this Context so it can call
99
+ # +ctx.bind+, then spend the Context so a captured +ctx+ used after the
100
+ # block raises. A block that raises propagates before the guest drives, so
101
+ # the guest never runs and no Execution is produced.
102
+ def collect_overrides
103
+ yield self
104
+ ensure
105
+ @spent = true
106
+ end
107
+
108
+ # Build this invocation's guest→host dispatch handler — a +Proc+ routing
109
+ # each guest→host call through the stateless +Transport::Dispatcher+,
110
+ # capturing this Context as the path resolver (its +#lookup+ layers the
111
+ # per-invocation providers over the static bindings) plus +@handler+. Handed to
112
+ # +Runtime#eval+ / +#run+ as a call argument, so the Runtime holds no
113
+ # dispatch state and the +Proc+ stays GC-rooted as a live argument for the
114
+ # synchronous call. The ext hands the +Proc+ a per-dispatch +guest_yielder+
115
+ # — a +String → String+ callable that re-enters the in-flight guest to run
116
+ # a yielded block — which the +Dispatcher+ forwards to the
117
+ # +Transport::Yielder+ it builds for the call.
118
+ def dispatch_handler
119
+ lambda do |request_bytes, guest_yielder|
120
+ Transport::Dispatcher.dispatch(request_bytes, self, @handler, guest_yielder)
121
+ end
122
+ end
123
+
124
+ # Record this invocation's usage and both output captures from the ext
125
+ # +Snapshot+. Every Snapshot carries them — value return or trap alike — so
126
+ # +#usage+ / +#stdout+ / +#stderr+ stay readable after a rescued trap.
127
+ def populate_observability!(snapshot)
128
+ @usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
129
+ @stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
130
+ @stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
131
+ end
132
+
133
+ # Pick the +TrapError+ subclass to re-raise based on +err+'s actual class.
134
+ # Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+) preserve their
135
+ # named identity; everything else collapses to the base +Kobako::TrapError+,
136
+ # so #invoke! can add the verb prefix without erasing the named subclass.
137
+ def trap_class_for(err)
138
+ case err
139
+ when TimeoutError then TimeoutError
140
+ when MemoryLimitError then MemoryLimitError
141
+ else TrapError
142
+ end
143
+ end
144
+
145
+ # Build the +TrapError+-family exception for a trapped +Snapshot+ from its
146
+ # neutral trap kind, tagged with the verb — the cap subclasses
147
+ # (+TimeoutError+ / +MemoryLimitError+) keep their identity, every other
148
+ # engine fault is the base +TrapError+.
149
+ def trap_error_for(snapshot, verb)
150
+ klass = case snapshot.trap_kind
151
+ when :timeout then TimeoutError
152
+ when :memory_limit then MemoryLimitError
153
+ else TrapError
154
+ end
155
+ klass.new("Sandbox##{verb} failed: #{snapshot.trap_message}")
156
+ end
157
+
158
+ # Freeze this run's observables plus +value+ (+nil+ on a failed run) into
159
+ # the read-only +Execution+ the caller receives or the error carries.
160
+ # +failed+ records the two apart so a +nil+ +value+ from a successful run
161
+ # stays distinct from a failed one.
162
+ def build_execution(value, failed:)
163
+ Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
164
+ end
165
+
166
+ # Decode a completed run's outcome into its +Execution+. A Capability
167
+ # Handle in the result is restored to its host object first. Decode sits in
168
+ # the rescue so a wire-violation trap or a Panic envelope both attach this
169
+ # run's Execution, just like a guest-call trap does.
170
+ def settle_outcome(snapshot, verb)
171
+ value, carried = Codec.track_handles { Outcome.decode(snapshot.outcome) }
172
+ value = Codec::HandleWalk.deep_restore(value, @handler) if carried
173
+ build_execution(value, failed: false)
174
+ rescue Kobako::TrapError => e
175
+ raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.message}").with_execution(build_execution(nil,
176
+ failed: true))
177
+ rescue Kobako::SandboxError, Kobako::ServiceError => e
178
+ raise e.with_execution(build_execution(nil, failed: true))
179
+ end
180
+
181
+ # Drive one invocation and settle it into a frozen +Execution+. +verb+
182
+ # tags the TrapError message so the failing export is identifiable. This
183
+ # invocation's callable Extension backends are resolved first — before the
184
+ # guest runs — so a provider that raises propagates unwrapped and leaves
185
+ # the guest unrun. Usage and captures are recorded before the trap check,
186
+ # so a trapped Snapshot's error carries them just like a completed run's
187
+ # return value. A could-not-start fault ran no invocation at all, so it
188
+ # carries no Execution and gains only the verb prefix.
189
+ def invoke!(verb)
190
+ @resolved = @extensions.resolve
191
+ begin
192
+ snapshot = yield
193
+ rescue Kobako::TrapError => e
194
+ raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
195
+ end
196
+ populate_observability!(snapshot)
197
+ return settle_outcome(snapshot, verb) unless snapshot.trapped?
198
+
199
+ raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true))
200
+ end
201
+ end
202
+ end
data/lib/kobako/errors.rb CHANGED
@@ -16,6 +16,24 @@ module Kobako
16
16
  # taxonomy can rescue a single class.
17
17
  class Error < StandardError; end
18
18
 
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.
26
+ module CarriesExecution
27
+ attr_reader :execution
28
+
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)
32
+ @execution = execution
33
+ self
34
+ end
35
+ end
36
+
19
37
  # Wasm engine layer. Raised when the Wasm execution engine crashed
20
38
  # (trap, OOM, unreachable) or when the wire layer detected a structural
21
39
  # violation that signals a corrupted guest execution environment
@@ -31,7 +49,9 @@ module Kobako
31
49
  # Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
32
50
  # want to surface a specific reason to operators can rescue the subclass
33
51
  # first.
34
- class TrapError < Error; end
52
+ class TrapError < Error
53
+ include CarriesExecution
54
+ end
35
55
 
36
56
  # Wall-clock timeout cap exhausted: the absolute deadline
37
57
  # +entry_time + timeout+ passed and the next guest wasm safepoint
@@ -70,7 +90,7 @@ module Kobako
70
90
  # beyond its message. Mixed into both rather than promoted to a shared
71
91
  # superclass because +SandboxError+ and +ServiceError+ sit in distinct
72
92
  # branches of the invocation-outcome taxonomy under +Kobako::Error+.
73
- module StructuredError
93
+ module Diagnosable
74
94
  attr_reader :origin, :klass, :backtrace_lines, :details
75
95
 
76
96
  def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
@@ -86,14 +106,16 @@ module Kobako
86
106
  # execution failed due to a mruby script error, a protocol fault, or a
87
107
  # host-side wire decode failure on an otherwise valid outcome tag.
88
108
  class SandboxError < Error
89
- include StructuredError
109
+ include Diagnosable
110
+ include CarriesExecution
90
111
  end
91
112
 
92
113
  # Service layer. Raised when a Service capability call inside a mruby
93
114
  # script reported an application-level failure that the script did not
94
115
  # rescue.
95
116
  class ServiceError < Error
96
- include StructuredError
117
+ include Diagnosable
118
+ include CarriesExecution
97
119
  end
98
120
 
99
121
  # HandleExhaustedError is the canonical SandboxError subclass for the
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capture"
4
+ require_relative "usage"
5
+
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.
13
+ #
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.
18
+ class Execution
19
+ # The deserialized guest value the run produced; +nil+ on a failed run.
20
+ attr_reader :value
21
+
22
+ # The +Kobako::Usage+ resource accounting for this run.
23
+ attr_reader :usage
24
+
25
+ def initialize(value:, usage:, stdout:, stderr:, failed:)
26
+ @value = value
27
+ @usage = usage
28
+ @stdout_capture = stdout
29
+ @stderr_capture = stderr
30
+ @failed = failed
31
+ freeze
32
+ end
33
+
34
+ # Returns +true+ iff the run failed — +false+ on the Execution +#eval+ /
35
+ # +#run+ returned, +true+ on the one a raised error carries.
36
+ def failed? = @failed
37
+
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.
41
+ def stdout = @stdout_capture.bytes
42
+
43
+ # Bytes the guest wrote to stderr during this run. Mirror of #stdout.
44
+ def stderr = @stderr_capture.bytes
45
+
46
+ # Returns +true+ iff stdout capture reached +stdout_limit+ during this run.
47
+ def stdout_truncated? = @stdout_capture.truncated?
48
+
49
+ # Returns +true+ iff stderr capture reached +stderr_limit+ during this run.
50
+ def stderr_truncated? = @stderr_capture.truncated?
51
+ end
52
+ end
@@ -27,17 +27,31 @@ module Kobako
27
27
  # Kobako::Extension::Backend — the host attachment of an Extension,
28
28
  # pairing +path+ (the constant path the backend binds at, single-segment
29
29
  # +"File"+ or nested +"MyApp::Store"+, spelling the guest constant the
30
- # idiom routes to) with +provider+ (the source of the bound object).
30
+ # idiom routes to) with the source of the bound object, declared by
31
+ # explicit keyword:
31
32
  #
32
- # A +provider+ that is not itself callable is the bound object, resolved
33
- # once for the Sandbox's life; a callable provider is invoked once per
34
- # invocation to yield that invocation's object, so a fresh object backs
35
- # the path every invocation. Callability is the sole discriminator — a
36
- # fixed backend that is itself callable is supplied through a
37
- # non-callable wrapper. A callable provider that raises propagates its
38
- # exception to the invocation caller and leaves the guest unrun; the
39
- # next invocation resolves it afresh.
40
- class Backend < Data.define(:path, :provider)
33
+ # * +object:+ — a static object, fixed for the Sandbox's life.
34
+ # * +provider:+ — a no-argument callable invoked once per invocation to
35
+ # yield that invocation's object, so a fresh object backs the path
36
+ # every invocation. A provider that raises propagates its exception to
37
+ # the invocation caller and leaves the guest unrun; the next
38
+ # invocation resolves it afresh.
39
+ # * neither — a fillable, defaulting to +Kobako::Unresolved+ until the
40
+ # host supplies the invocation's object.
41
+ #
42
+ # The kind is chosen by keyword, never inferred from whether the value is
43
+ # callable, so a static object that is itself callable is expressed
44
+ # directly with +object:+. Giving both +object:+ and +provider:+ raises
45
+ # +ArgumentError+.
46
+ class Backend < Data.define(:path, :object, :provider)
47
+ def initialize(path:, object: nil, provider: nil)
48
+ if !object.nil? && !provider.nil?
49
+ raise ArgumentError,
50
+ "Extension::Backend accepts object: or provider:, not both"
51
+ end
52
+
53
+ super
54
+ end
41
55
  end
42
56
 
43
57
  # +backend+ and +depends_on+ default to absent so the common
data/lib/kobako/pool.rb CHANGED
@@ -47,7 +47,7 @@ module Kobako
47
47
  # discarded and its slot refills by a fresh construction on next
48
48
  # demand.
49
49
  def with
50
- sandbox = checkout
50
+ sandbox = acquire
51
51
  begin
52
52
  yield sandbox
53
53
  rescue TrapError
@@ -61,12 +61,6 @@ module Kobako
61
61
 
62
62
  private
63
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
64
  # The idle-first claim loop: an idle Sandbox wins, unclaimed
71
65
  # capacity constructs, and a full pool waits for a checkin.
72
66
  def acquire