kobako 0.26.0-aarch64-linux → 0.28.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +103 -0
  4. data/README.md +156 -118
  5. data/ROADMAP.md +23 -22
  6. data/SECURITY.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +38 -0
  8. data/crates/kobako-runtime/README.md +10 -12
  9. data/crates/kobako-transport/CHANGELOG.md +35 -0
  10. data/crates/kobako-transport/README.md +43 -27
  11. data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
  12. data/crates/kobako-wasmtime/README.md +8 -10
  13. data/data/kobako.wasm +0 -0
  14. data/lib/kobako/3.3/kobako.so +0 -0
  15. data/lib/kobako/3.4/kobako.so +0 -0
  16. data/lib/kobako/4.0/kobako.so +0 -0
  17. data/lib/kobako/capture.rb +1 -4
  18. data/lib/kobako/catalog/extensions.rb +21 -59
  19. data/lib/kobako/catalog/flags.rb +41 -0
  20. data/lib/kobako/catalog/handles.rb +7 -54
  21. data/lib/kobako/catalog/services.rb +7 -61
  22. data/lib/kobako/catalog/snippets.rb +42 -66
  23. data/lib/kobako/catalog.rb +2 -10
  24. data/lib/kobako/codec/decoder.rb +9 -28
  25. data/lib/kobako/codec/encoder.rb +3 -22
  26. data/lib/kobako/codec/error.rb +6 -27
  27. data/lib/kobako/codec/ext_types.rb +72 -94
  28. data/lib/kobako/codec/handle_walk.rb +65 -130
  29. data/lib/kobako/codec/nesting.rb +3 -8
  30. data/lib/kobako/codec/state.rb +4 -10
  31. data/lib/kobako/codec/utils.rb +6 -34
  32. data/lib/kobako/codec.rb +3 -22
  33. data/lib/kobako/constant_name.rb +8 -0
  34. data/lib/kobako/context.rb +51 -112
  35. data/lib/kobako/errors.rb +51 -127
  36. data/lib/kobako/execution.rb +16 -21
  37. data/lib/kobako/extension.rb +23 -40
  38. data/lib/kobako/handle.rb +7 -12
  39. data/lib/kobako/outcome.rb +51 -80
  40. data/lib/kobako/payload/arguments.rb +1 -16
  41. data/lib/kobako/payload.rb +4 -12
  42. data/lib/kobako/pool.rb +23 -53
  43. data/lib/kobako/runtime.rb +7 -21
  44. data/lib/kobako/sandbox.rb +96 -133
  45. data/lib/kobako/sandbox_options.rb +38 -84
  46. data/lib/kobako/snippet/binary.rb +1 -16
  47. data/lib/kobako/snippet/source.rb +1 -16
  48. data/lib/kobako/snippet.rb +1 -9
  49. data/lib/kobako/transport/call.rb +1 -14
  50. data/lib/kobako/transport/dispatcher.rb +112 -195
  51. data/lib/kobako/transport/error.rb +6 -20
  52. data/lib/kobako/transport/exposure.rb +5 -10
  53. data/lib/kobako/transport/reflection.rb +39 -50
  54. data/lib/kobako/transport/run.rb +17 -71
  55. data/lib/kobako/transport/yielder.rb +21 -73
  56. data/lib/kobako/transport.rb +3 -15
  57. data/lib/kobako/unresolved.rb +4 -11
  58. data/lib/kobako/usage.rb +2 -6
  59. data/lib/kobako/version.rb +1 -1
  60. data/sig/kobako/catalog/extensions.rbs +2 -2
  61. data/sig/kobako/catalog/flags.rbs +17 -0
  62. data/sig/kobako/catalog/services.rbs +0 -6
  63. data/sig/kobako/catalog/snippets.rbs +4 -4
  64. data/sig/kobako/codec/ext_types.rbs +10 -8
  65. data/sig/kobako/codec/handle_walk.rbs +9 -7
  66. data/sig/kobako/codec/utils.rbs +2 -2
  67. data/sig/kobako/codec.rbs +2 -2
  68. data/sig/kobako/constant_name.rbs +3 -0
  69. data/sig/kobako/context.rbs +2 -3
  70. data/sig/kobako/errors.rbs +6 -0
  71. data/sig/kobako/outcome.rbs +6 -8
  72. data/sig/kobako/runtime.rbs +12 -8
  73. data/sig/kobako/sandbox.rbs +4 -0
  74. data/sig/kobako/sandbox_options.rbs +2 -6
  75. data/sig/kobako/transport/dispatcher.rbs +14 -18
  76. data/sig/kobako/transport/reflection.rbs +5 -3
  77. data/sig/kobako/transport/run.rbs +1 -3
  78. data/sig/kobako/transport/yielder.rbs +3 -3
  79. data/sig/kobako/transport.rbs +7 -6
  80. metadata +6 -3
  81. data/rust-toolchain.toml +0 -11
@@ -3,17 +3,9 @@
3
3
  require_relative "payload/arguments"
4
4
 
5
5
  module Kobako
6
- # The payload shapes that ride inside a core envelope's opaque payload
7
- # field. The envelope routes a message and the native side decodes it;
8
- # this layer decides what the resolved method receives, and the byte-level
9
- # encoding of that shape belongs to +Kobako::Codec+ beneath it.
10
- #
11
- # Keeping the two apart is what lets an endpoint with its own schema
12
- # replace this layer and keep the envelope, so nothing here may reach for
13
- # a routing field.
14
- #
15
- # +Kobako::Payload::Arguments+ is the wire-symmetric peer of
16
- # +kobako_codec::payload::Arguments+.
17
- module Payload
6
+ # What the resolved method receives, kept apart from the envelope so an
7
+ # endpoint with its own schema can replace it; nothing here reads a
8
+ # routing field.
9
+ module Payload # :nodoc:
18
10
  end
19
11
  end
data/lib/kobako/pool.rb CHANGED
@@ -4,29 +4,21 @@ require_relative "errors"
4
4
  require_relative "sandbox"
5
5
 
6
6
  module Kobako
7
- # Kobako::Pool — a bounded set of warm, identically set-up Sandboxes
8
- # handed out one exclusive holder at a time.
9
- #
10
- # Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
11
- # and holds the optional block as the per-Sandbox setup hook; a
12
- # checkout prefers an idle Sandbox and constructs a new one only when
13
- # none is idle and fewer than +slots+ exist. +#with+ blocks up
14
- # to +checkout_timeout+ seconds when every slot is held, applies
15
- # the +TrapError+ discard-and-recreate contract at checkin, and
16
- # the Pool releases everything with its own reachability — there is no
17
- # teardown verb.
7
+ # A bounded set of warm, identically set-up Sandboxes, handed out to one
8
+ # holder at a time. Every Sandbox it builds is kept: an invocation leaves
9
+ # nothing behind, whatever ended it.
18
10
  class Pool
19
- # The +#with+ wait bound applied when +checkout_timeout+ is not given.
11
+ # The default +checkout_timeout+: 5 seconds.
20
12
  DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
21
13
 
22
- # Build a Pool of up to +slots+ Sandboxes. +slots+ is
23
- # a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
24
- # seconds (+nil+ waits indefinitely); every other keyword is
25
- # forwarded verbatim to +Kobako::Sandbox.new+. The optional block
26
- # runs exactly once per constructed Sandbox — it is the setup window
27
- # for +#bind+ / +#preload+ before that Sandbox's first checkout.
28
- # No Sandbox is constructed here. Raises +ArgumentError+ for an
29
- # invalid +slots+ / +checkout_timeout+.
14
+ # Build a Pool of up to +slots+ Sandboxes, built on first demand with the
15
+ # other keywords as Sandbox.new takes them. +checkout_timeout+ bounds how
16
+ # many seconds #with waits, and +nil+ waits indefinitely. The block, if
17
+ # given, sets up each Sandbox once, as with Sandbox#bind and
18
+ # Sandbox#preload.
19
+ #
20
+ # Raises ArgumentError when +slots+ is not a positive Integer or
21
+ # +checkout_timeout+ is not a positive finite number.
30
22
  def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
31
23
  validate_slots!(slots)
32
24
  @slots = slots
@@ -40,29 +32,20 @@ module Kobako
40
32
  end
41
33
 
42
34
  # Yield one exclusively-held Sandbox to the block and return the
43
- # block's value. Blocks while every slot is held; raises
44
- # +Kobako::PoolTimeoutError+ once the wait exceeds +checkout_timeout+.
45
- # The Sandbox returns to the pool at block exit — unless the block raised
46
- # +Kobako::TrapError+, in which case the unrecoverable Sandbox is
47
- # discarded and its slot refills by a fresh construction on next
48
- # demand.
35
+ # block's value; the Sandbox returns to the pool however the block
36
+ # exits. Raises PoolTimeoutError once every slot has stayed held past
37
+ # +checkout_timeout+.
49
38
  def with
50
39
  sandbox = acquire
51
40
  begin
52
41
  yield sandbox
53
- rescue TrapError
54
- release_capacity!
55
- sandbox = nil
56
- raise
57
42
  ensure
58
- checkin(sandbox) if sandbox
43
+ checkin(sandbox)
59
44
  end
60
45
  end
61
46
 
62
47
  private
63
48
 
64
- # The idle-first claim loop: an idle Sandbox wins, unclaimed
65
- # capacity constructs, and a full pool waits for a checkin.
66
49
  def acquire
67
50
  timeout = @checkout_timeout
68
51
  deadline = timeout && (monotonic_now + timeout)
@@ -73,11 +56,10 @@ module Kobako
73
56
  end
74
57
  end
75
58
 
76
- # Single locked decision point for one claim attempt. Waiting
77
- # happens inside the lock (so a checkin can wake it); construction
78
- # happens outside (so a slow setup block never holds the lock) —
79
- # capacity is reserved here and released by +construct_slot+ on
80
- # failure.
59
+ # Waiting happens inside the lock (so a checkin can wake it);
60
+ # construction happens outside (so a slow setup block never holds the
61
+ # lock) — capacity is reserved here and released by +construct_slot+
62
+ # on failure.
81
63
  def claim_or_wait(deadline)
82
64
  @mutex.synchronize do
83
65
  return [:idle, @idle.pop] unless @idle.empty?
@@ -92,9 +74,7 @@ module Kobako
92
74
  end
93
75
  end
94
76
 
95
- # Wait for a checkin or freed capacity; raises
96
- # +Kobako::PoolTimeoutError+ once +deadline+ has passed. Must
97
- # run while holding +@mutex+.
77
+ # Must run while holding +@mutex+.
98
78
  def await_slot!(deadline)
99
79
  remaining = deadline && (deadline - monotonic_now)
100
80
  if remaining && remaining <= 0
@@ -105,10 +85,8 @@ module Kobako
105
85
  @slot_freed.wait(@mutex, remaining)
106
86
  end
107
87
 
108
- # Construct and set up one pooled Sandbox against the capacity
109
- # reserved by +claim_or_wait+. Construction and setup-block errors
110
- # propagate to the checkout caller unchanged; the reserved
111
- # capacity is released so a later checkout can retry.
88
+ # A failed construction releases its reserved capacity so a later
89
+ # checkout can retry.
112
90
  def construct_slot
113
91
  done = false
114
92
  sandbox = Sandbox.new(**@sandbox_options)
@@ -119,7 +97,6 @@ module Kobako
119
97
  release_capacity! unless done
120
98
  end
121
99
 
122
- # Return a Sandbox to the idle list and wake one waiting checkout.
123
100
  def checkin(sandbox)
124
101
  @mutex.synchronize do
125
102
  @idle.push(sandbox)
@@ -127,8 +104,6 @@ module Kobako
127
104
  end
128
105
  end
129
106
 
130
- # Give back reserved-but-unfilled capacity — a failed construction or
131
- # a discarded Sandbox — and wake one waiting checkout to claim it.
132
107
  def release_capacity!
133
108
  @mutex.synchronize do
134
109
  @constructed -= 1
@@ -142,17 +117,12 @@ module Kobako
142
117
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
143
118
  end
144
119
 
145
- # Pre-flight for +slots+ — no coercion, a positive Integer is
146
- # the only accepted shape.
147
120
  def validate_slots!(slots)
148
121
  return if slots.is_a?(Integer) && slots.positive?
149
122
 
150
123
  raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
151
124
  end
152
125
 
153
- # Coerce +checkout_timeout+ into the Float seconds the wait loop
154
- # consumes, or +nil+ to wait indefinitely — the same normalisation
155
- # idiom +SandboxOptions+ applies to +timeout+.
156
126
  def normalize_checkout_timeout(checkout_timeout)
157
127
  return nil if checkout_timeout.nil?
158
128
  unless checkout_timeout.is_a?(Numeric)
@@ -1,27 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # Host-side wasmtime runtime, surfaced as a Ruby class by the native ext
5
- # (see ext/kobako/src/runtime.rs). The +Kobako::Runtime+ class wraps the
6
- # wasmtime engine + compiled module + Store; it is the only Ruby-visible
7
- # wasmtime type and the foundational binding layer for +Kobako::Sandbox+.
8
- #
9
- # This file reopens the magnus-defined class only to add the pure-Ruby
10
- # +.default_path+ helper. Every other method (+#from_path+ singleton,
11
- # +#eval+ / +#run+, capture and usage readers) is registered from Rust
12
- # at ext load time.
13
- class Runtime
14
- # Absolute path to the gem-bundled +data/kobako.wasm+ artifact. Computed
15
- # from this file's location so it works for both +bundle exec+ (running
16
- # from the repo) and an installed gem (running from the gem dir).
17
- #
18
- # Returns a String regardless of whether the file currently exists —
19
- # call sites that need the file to be present should pass this through
20
- # +Kobako::Runtime.from_path+, which raises
21
- # +Kobako::ModuleNotBuiltError+ with a clear remediation message.
22
- # Raises +Kobako::SetupError+ if +__dir__+ is unavailable so that
23
- # +rescue Kobako::SetupError+ around +Kobako::Sandbox.new+ catches every
24
- # construction-layer failure uniformly, including this path-resolution one.
4
+ # The native wasmtime runtime, defined by the ext; this file adds only the
5
+ # default Guest Binary path.
6
+ class Runtime # :nodoc:
7
+ # The path is answered whether or not the file exists; from_path is
8
+ # where a missing artifact is refused. A missing +__dir__+ raises
9
+ # SetupError so one rescue around Sandbox.new covers every construction
10
+ # failure.
25
11
  def self.default_path
26
12
  dir = __dir__ or raise Kobako::SetupError, "Kobako::Runtime.default_path requires __dir__"
27
13
  File.expand_path("../../data/kobako.wasm", dir)
@@ -10,168 +10,129 @@ require_relative "catalog"
10
10
  require_relative "context"
11
11
 
12
12
  module Kobako
13
- # Kobako::Sandbox — the user-facing entry point for executing guest mruby
14
- # scripts inside a wasmtime-hosted Wasm module.
15
- #
16
- # The Sandbox owns the reusable configuration — the +Kobako::Runtime+ and
17
- # the +Kobako::Catalog::Services+ / +Catalog::Snippets+ / +Catalog::Extensions+
18
- # registries. Each +#eval+ / +#run+ seals that config on the first call and
19
- # drives one guest invocation through a fresh +Kobako::Context+ — its own
20
- # Handle table, dispatch +Proc+, captures, and usage — so the reusable
21
- # Sandbox holds no per-invocation state. The underlying wasmtime Engine and
22
- # compiled Module are cached at process scope by the native ext and never
23
- # surface to Ruby — constructing many Sandboxes amortises both costs
24
- # automatically.
25
- #
26
- # A run's observables — the captured +#stdout+ / +#stderr+ (bounded by
27
- # +stdout_limit+ / +stderr_limit+, enforced inside the WASI pipe) with their
28
- # +#stdout_truncated?+ / +#stderr_truncated?+ predicates, and +#usage+ — live
29
- # on the +Kobako::Execution+ each +#eval+ / +#run+ returns, or on the one its
30
- # raised error carries. The Sandbox itself keeps none of them, so nothing a
31
- # run observes carries into the next one.
13
+ # Runs untrusted mruby scripts in an isolated Wasm instance. Configure it
14
+ # once, binding Services and preloading snippets, then call #eval or #run as
15
+ # often as needed: each call returns an Execution and leaves nothing behind
16
+ # for the next.
32
17
  class Sandbox
33
18
  extend Forwardable
34
19
 
35
- attr_reader :wasm_path, :options
20
+ # The Guest Binary this Sandbox runs.
21
+ attr_reader :wasm_path
22
+
23
+ # The SandboxOptions this Sandbox was built with. Each option is also
24
+ # readable on the Sandbox itself, as in #timeout.
25
+ attr_reader :options
36
26
 
37
- # Per-option accessors forward to the immutable +SandboxOptions+ Value
38
- # Object so the Host App still reads them off Sandbox directly.
39
27
  def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile, :gvl
40
28
 
41
- # Build a fresh Sandbox.
29
+ # Build a Sandbox on the Guest Binary at +wasm_path+, the one bundled
30
+ # with the gem by default. The other keywords are the options
31
+ # SandboxOptions describes.
42
32
  #
43
- # +wasm_path+ is the absolute path to the Guest Binary; defaults to the
44
- # gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
45
- # (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
46
- # requested isolation profile (+profile+) — is forwarded verbatim to
47
- # +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
48
- # normalisation. The constructed +SandboxOptions+ is exposed as
49
- # +#options+ and every option remains readable directly on Sandbox via
50
- # +Forwardable+ delegation. The runtime builds the requested profile —
51
- # +:hermetic+ (the default) denies the guest ambient time and entropy,
52
- # +:permissive+ leaves them live — and construction refuses a runtime
53
- # whose declared profile falls below the request, raising
54
- # +Kobako::SetupError+ before any invocation entry point runs.
33
+ # Raises ArgumentError for an invalid option, and SetupError when the
34
+ # runtime cannot be built or provides less isolation than +profile+ asks
35
+ # for.
55
36
  def initialize(wasm_path: nil, **)
56
37
  @wasm_path = wasm_path || Kobako::Runtime.default_path
57
38
  @options = SandboxOptions.new(**)
58
39
  @services = Kobako::Catalog::Services.new
59
40
  @snippets = Catalog::Snippets.new
41
+ @flags = Catalog::Flags.new
60
42
  @extensions = Catalog::Extensions.new
43
+ @sealed = false
61
44
  @runtime = build_runtime!
62
45
  end
63
46
 
64
- # Bind +object+ as the Service reachable at +path+ — a Symbol or
65
- # String of one or more +::+-separated constant-form segments
66
- # (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
67
- # chaining.
47
+ # Make +object+ reachable from the guest as the constant at +path+, a
48
+ # Symbol or String such as <tt>"MyService::KV"</tt>. Returns +self+.
68
49
  #
69
- # The guest reaches the public methods +object+'s own class and +object+
70
- # itself define in source, fixed at this call — nothing inherited, mixed
71
- # in, or built into the platform. An +object+ defining a private
72
- # +respond_to_guest?(name)+ decides instead, asked on every call.
50
+ # The guest reaches the public methods that +object+'s own class and
51
+ # +object+ itself define in source, nothing inherited or mixed in. An
52
+ # +object+ defining a private <tt>respond_to_guest?(name)</tt> decides
53
+ # instead, on every call.
73
54
  #
74
- # Called with only a +path+, it declares a fillable Service:
75
- # +bind(path)+ reserves the path for +Kobako::Unresolved+, so the guest
76
- # sees the constant while the host defers the object it stands for. A
77
- # guest dispatch to an unfilled fillable surfaces as
78
- # +Kobako::ServiceError+ when left unrescued.
55
+ # Called with only +path+, it declares a fillable Service: the guest sees
56
+ # the constant, and Context#bind supplies the object for each invocation.
57
+ # A call to one left unfilled fails as a ServiceError.
79
58
  #
80
- # Raises +ArgumentError+ when a segment is malformed, when +path+
81
- # collides with an existing binding (a name is a bound Service or a
82
- # grouping prefix, never both), or when called after the first
83
- # invocation has sealed Service registration.
59
+ # Raises ArgumentError for a malformed path, a path that collides with an
60
+ # existing binding, or a call after the first invocation.
84
61
  def bind(path, object = Unresolved)
62
+ refuse_once_invoked!("bind")
85
63
  @services.bind(path, object)
86
64
  self
87
65
  end
88
66
 
89
- # Install one or more Extensions — each a guest idiom (+source+) paired
90
- # with an optional host +backend+, composed onto the Sandbox through
91
- # +#preload+ and +#bind+. An Extension is any object exposing
92
- # +name+ / +source+ / +backend+ / +depends_on+; +Kobako::Extension+ is
93
- # the bundled value type. Returns +self+.
67
+ # Install Extensions, each guest source paired with an optional host
68
+ # backend. Any object answering +name+, +source+, +backend+ and
69
+ # +depends_on+ works; Extension is the bundled one. Returns +self+.
94
70
  #
95
- # Raises +ArgumentError+ for a malformed Extension, a call after the
96
- # first invocation seals registration, or — at that first invocation —
97
- # an unmet +depends_on+.
71
+ # Raises ArgumentError for a malformed Extension, a call after the first
72
+ # invocation, or, at the first invocation, a missing dependency.
98
73
  def install(*extensions)
99
- raise ArgumentError, "cannot install after first Sandbox invocation" if @services.sealed?
74
+ refuse_once_invoked!("install")
100
75
 
101
76
  extensions.each { |extension| @extensions.install(extension, snippets: @snippets, services: @services) }
102
77
  self
103
78
  end
104
79
 
105
- # Register a snippet on this Sandbox in one of two forms:
106
- #
107
- # * +preload(code: source, name: Name)+ — +source+ is mruby source
108
- # as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. Compile
109
- # failures surface as +Kobako::SandboxError+ on the first
110
- # invocation's replay. The +name+
111
- # becomes the snippet's +(snippet:Name)+ backtrace filename and
112
- # is the dedupe key that rejects a duplicate +code:+ snippet.
113
- # * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
114
- # bytecode as a +String+. The canonical name, when present,
115
- # lives in the bytecode's embedded +debug_info+ and is resolved
116
- # by the guest at load time; the host treats the bytes as
117
- # opaque. Structural failures surface as +Kobako::BytecodeError+
118
- # on the first invocation.
80
+ # Register a snippet that every invocation runs, in the order registered,
81
+ # before its own code. Returns +self+.
119
82
  #
120
- # Subsequent invocations (+#eval+ or +#run+) replay every registered
121
- # snippet — in insertion order — against the fresh +mrb_state+
122
- # before per-invocation source or entrypoint resolution.
83
+ # [<tt>preload(code: source, name: Name)</tt>]
84
+ # mruby source, named <tt>(snippet:Name)</tt> in backtraces.
85
+ # [<tt>preload(binary: bytes)</tt>]
86
+ # Precompiled RITE bytecode, trusted as host code: the unmodified
87
+ # compiler output of the mruby release the Guest Binary is built on.
123
88
  #
124
- # Returns +self+ to allow chaining.
125
- #
126
- # Raises +ArgumentError+ when neither form's keyword set is
127
- # supplied, when both forms are mixed (e.g., +code:+ and +binary:+
128
- # together, or +binary:+ paired with +name:+), when +code+ / +bytes+
129
- # is not a +String+, when +name+ does not match the constant
130
- # pattern, when +name+ duplicates an already-registered +code:+ form
131
- # snippet, or when called after the first invocation has sealed the
132
- # snippet table.
89
+ # Raises ArgumentError for a malformed or duplicate snippet, or a call
90
+ # after the first invocation. A snippet that fails to load fails every
91
+ # invocation with SandboxError, or BytecodeError for bytecode.
133
92
  def preload(code: nil, name: nil, binary: nil)
134
- raise ArgumentError, "cannot preload after first Sandbox invocation" if @services.sealed?
93
+ refuse_once_invoked!("preload")
135
94
 
136
95
  @snippets.register(code: code, name: name, binary: binary)
137
96
  self
138
97
  end
139
98
 
140
- # Dispatch into a preloaded entrypoint constant. Delegates host
141
- # pre-flight and argument encoding to +Kobako::Transport::Run+ /
142
- # +Kobako::Transport::Run#payload+: a non-Symbol/String +target+ raises
143
- # +TypeError+, while a +target+ failing the constant pattern, a forged
144
- # +Kobako::Handle+ in +args+ / +kwargs+, or a non-Symbol +kwargs+ key
145
- # raise +ArgumentError+. The guest resolves +target+ as a top-level
146
- # constant, calls +#call+ on it with +args+ / +kwargs+, and returns the
147
- # deserialized result. The first invocation seals the Service registry
148
- # and snippet table. Runtime errors follow the same three-class
149
- # taxonomy as +#eval+.
99
+ # Set the flag +name+, a Symbol or String, to +value+ for every
100
+ # invocation; an empty +value+ is a plain switch. A Guest Binary that
101
+ # does not recognise the flag fails each invocation with
102
+ # UnsupportedFlagError, unless +advisory+ says the guest may skip it.
103
+ # Mark a flag advisory only when a script cannot tell it was skipped.
104
+ # Returns +self+.
105
+ #
106
+ # Raises ArgumentError for a malformed or duplicate flag, or a call after
107
+ # the first invocation.
108
+ def flag(name, value = "", advisory: false)
109
+ refuse_once_invoked!("flag")
110
+
111
+ @flags.register(name, value, advisory: advisory)
112
+ self
113
+ end
114
+
115
+ # Call +call+ on the preloaded top-level constant +target+ with +args+
116
+ # and +kwargs+, and return the Execution. A given block receives the
117
+ # invocation's Context before the guest runs.
118
+ #
119
+ # Raises TypeError when +target+ is not a Symbol or String, and
120
+ # ArgumentError when it is not a constant name or the arguments carry a
121
+ # Handle or a non-Symbol keyword. A failed run raises as #eval does.
150
122
  def run(target, *args, **kwargs, &block)
151
123
  request = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
152
124
  new_invocation.run(request, &block)
153
125
  end
154
126
 
155
- # Execute a guest mruby source string in a fresh +mrb_state+. +code+ is
156
- # the mruby source as a UTF-8 String. Returns the deserialized last
157
- # expression of the source.
158
- #
159
- # Source delivery uses the WASI stdin three-frame protocol
160
- # ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
161
- # Frame 1 carries the preamble (Service registry snapshot), Frame 2 the
162
- # user source UTF-8 bytes, and Frame 3 the snippet table registered via
163
- # +#preload+. Each frame is prefixed by a 4-byte big-endian u32 length;
164
- # Frame 1 and Frame 3 are mandatory-presence — an empty registry sends a
165
- # present, empty frame rather than an absent one.
127
+ # Evaluate +code+, mruby source as a String, and return the Execution,
128
+ # whose +value+ is the last expression. A given block receives the
129
+ # invocation's Context before the guest runs.
166
130
  #
167
- # The first invocation seals the Service registry and snippet table;
168
- # subsequent +#bind+ / +#preload+ calls raise +ArgumentError+.
169
- #
170
- # Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
171
- # +Kobako::SandboxError+ when the guest ran to completion but failed
172
- # (including when +code+ is +nil+ or not a String, or when a preloaded
173
- # snippet's replay raises); +Kobako::ServiceError+ on an unrescued
174
- # Service capability failure.
131
+ # Raises TrapError when the engine stopped the guest, a deadline or
132
+ # memory budget included; SandboxError when the guest failed, +code+ not
133
+ # being a String or the Guest Binary not starting the run included; and
134
+ # ServiceError when a Service call failed and the guest left it
135
+ # unrescued.
175
136
  def eval(code, &block)
176
137
  raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
177
138
 
@@ -180,10 +141,8 @@ module Kobako
180
141
 
181
142
  private
182
143
 
183
- # Construct the +Runtime+ with the requested isolation profile and
184
- # refuse one whose declared posture falls below the request —
185
- # +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
186
- # runtime that cannot honor the request never runs guest code.
144
+ # A runtime whose declared posture falls below the request never runs
145
+ # guest code.
187
146
  def build_runtime!
188
147
  runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
189
148
  @options.stdout_limit, @options.stderr_limit, @options.profile,
@@ -192,23 +151,27 @@ module Kobako
192
151
  runtime
193
152
  end
194
153
 
195
- # Seal the config on the first invocation and return a fresh
196
- # per-invocation +Context+ for the verb to drive. The Context owns this
197
- # run's Handle table, resolved Extension backends, captures, and usage, so
198
- # no per-invocation state is written back onto the shared config.
154
+ # Per-invocation state lives on the Context, so nothing an invocation
155
+ # does is written back onto the shared config.
199
156
  def new_invocation
200
157
  begin_invocation!
201
- Context.new(runtime: @runtime, services: @services, snippets: @snippets, extensions: @extensions)
158
+ Context.new(runtime: @runtime, services: @services, snippets: @snippets, flags: @flags,
159
+ extensions: @extensions)
160
+ end
161
+
162
+ # Registration closes at the first invocation, so every invocation ships
163
+ # the same Services, snippets, flags and Extensions.
164
+ def refuse_once_invoked!(verb)
165
+ raise ArgumentError, "cannot #{verb} after first Sandbox invocation" if @sealed
202
166
  end
203
167
 
204
- # Per-invocation prologue on the config tier: seals the Service and
205
- # Extension registries on the first call (idempotent — asserting Extension
206
- # dependencies then). The Service seal is the one +#bind+ / +#preload+ /
207
- # +#install+ all gate on. Per-invocation provider resolution and observable
208
- # state live on the +Context+, not here.
168
+ # Sealing before the dependency check keeps a Sandbox whose check failed
169
+ # closed to further registration. The sealed snippets are compiled once,
170
+ # before the first invocation replays them.
209
171
  def begin_invocation!
210
- @services.seal!
211
- @extensions.seal!
172
+ @sealed = true
173
+ @extensions.assert_dependencies!
174
+ @snippets.compile { |entries| @runtime.compile(@services.paths, entries, @flags.entries) }
212
175
  end
213
176
  end
214
177
  end