kobako 0.9.1 → 0.10.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.
@@ -53,14 +53,9 @@ module Kobako
53
53
  # is reserved for the codec's wire-decode path, where the id is
54
54
  # the only thing the bytes carry.
55
55
  def alloc(object)
56
+ reject_unwrappable!(object)
57
+ ensure_capacity!
56
58
  id = @next_id
57
- cap = Kobako::Handle::MAX_ID
58
- if id > cap
59
- raise HandlerExhaustedError,
60
- "Out of handle allocations: too many host objects were referenced " \
61
- "in a single invocation (limit #{cap})"
62
- end
63
-
64
59
  @entries[id] = object
65
60
  @next_id = id + 1
66
61
  Kobako::Handle.restore(id)
@@ -94,6 +89,30 @@ module Kobako
94
89
 
95
90
  private
96
91
 
92
+ # Refuse to mint a Capability Handle for a reflective gadget
93
+ # ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): a +Binding+ /
94
+ # +Method+ / +UnboundMethod+ would hand the guest a callable proxy onto
95
+ # host reflection (a returned +Binding+ reaches +Binding#eval+). Raising
96
+ # here keeps the rule at the single mint point, so it holds on both the
97
+ # Service-return (B-14) and the +#run+ host→guest auto-wrap (B-34) paths.
98
+ def reject_unwrappable!(object)
99
+ case object
100
+ when Binding, Method, UnboundMethod
101
+ raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
102
+ end
103
+ end
104
+
105
+ # Guard {#alloc} against issuing an ID past the B-21 cap. Returns +nil+
106
+ # on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
107
+ def ensure_capacity!
108
+ cap = Kobako::Handle::MAX_ID
109
+ return unless @next_id > cap
110
+
111
+ raise HandlerExhaustedError,
112
+ "Out of handle allocations: too many host objects were referenced " \
113
+ "in a single invocation (limit #{cap})"
114
+ end
115
+
97
116
  # Single source of truth for the "unknown Handle id" raise used by
98
117
  # {#fetch}. Returns +nil+ on success; raises +Kobako::SandboxError+
99
118
  # when +id+ is not currently bound.
@@ -3,7 +3,6 @@
3
3
  require_relative "handles"
4
4
  require_relative "../codec"
5
5
  require_relative "../errors"
6
- require_relative "../transport/request"
7
6
  require_relative "../namespace"
8
7
 
9
8
  module Kobako
@@ -38,6 +37,7 @@ module Kobako
38
37
  @namespaces = {} # : Hash[String, Kobako::Namespace]
39
38
  @handler = handler
40
39
  @sealed = false
40
+ @encoded = nil # : String?
41
41
  end
42
42
 
43
43
  # Declare or retrieve the Namespace named +name+ (idempotent —
@@ -81,14 +81,32 @@ module Kobako
81
81
  # Arrays, so none of the kobako ext types actually fire. Structure:
82
82
  # +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
83
83
  # +String+ of msgpack bytes.
84
+ #
85
+ # Once sealed, the bytes are computed once and reused for every
86
+ # subsequent invocation: B-33 seals Service registration (B-07 /
87
+ # B-08) at the first invocation, so the preamble is exactly the
88
+ # bindings that existed at that moment — a bind reaching a
89
+ # +Kobako::Namespace+ after the seal raises +ArgumentError+ (E-45)
90
+ # and never alters Frame 1.
84
91
  def encode
85
- Codec::Encoder.encode(@namespaces.values.map(&:to_preamble))
92
+ return @encoded if @encoded
93
+
94
+ bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
95
+ @encoded = bytes if @sealed
96
+ bytes
86
97
  end
87
98
 
88
- # Mark the registry as sealed. Called by +Sandbox+ on the first
89
- # invocation. After sealing, #define raises ArgumentError. Idempotent.
99
+ # Mark the registry as sealed and propagate the seal to every
100
+ # declared +Kobako::Namespace+
101
+ # ({docs/behavior.md B-33}[link:../../../docs/behavior.md]). Called
102
+ # by +Sandbox+ on the first invocation. After sealing, #define
103
+ # raises ArgumentError (E-18) and +Namespace#bind+ raises
104
+ # ArgumentError (E-45). Idempotent.
90
105
  def seal!
106
+ return self if @sealed
107
+
91
108
  @sealed = true
109
+ @namespaces.each_value(&:seal!)
92
110
  self
93
111
  end
94
112
 
@@ -31,6 +31,7 @@ module Kobako
31
31
 
32
32
  def initialize
33
33
  @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
34
+ @encoded = nil # : String?
34
35
  end
35
36
 
36
37
  # Serialize the registered snippets to wire bytes. Each entry
@@ -42,8 +43,14 @@ module Kobako
42
43
  # carriers — this collection-tier method reads their attributes
43
44
  # externally via +entry_payload+ rather than asking each entry to
44
45
  # self-encode.
46
+ #
47
+ # The bytes are memoized — the table is replayed verbatim on every
48
+ # invocation after B-33 seals it, so Frame 3 never changes between
49
+ # encodes; {#register} drops the memo while the table is still open.
45
50
  def encode
46
- Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) })
51
+ return @encoded if @encoded
52
+
53
+ @encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
47
54
  end
48
55
 
49
56
  # Register one preloaded snippet in either of two forms
@@ -68,6 +75,7 @@ module Kobako
68
75
  # missing keywords, wrong types, malformed +name+ (E-34), or
69
76
  # duplicate +code:+ +name+ (E-33).
70
77
  def register(code: nil, name: nil, binary: nil)
78
+ @encoded = nil
71
79
  if binary
72
80
  raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
73
81
 
@@ -64,7 +64,11 @@ module Kobako
64
64
  case value
65
65
  when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
66
66
  when Array then value.each { |v| validate_utf8!(v) }
67
- when Hash then value.each { |pair| validate_utf8!(pair) }
67
+ when Hash
68
+ value.each do |key, val|
69
+ validate_utf8!(key)
70
+ validate_utf8!(val)
71
+ end
68
72
  end
69
73
  end
70
74
  end
data/lib/kobako/codec.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "codec/error"
4
+ require_relative "codec/utils"
5
+ require_relative "codec/factory"
6
+ require_relative "codec/encoder"
7
+ require_relative "codec/decoder"
4
8
 
5
9
  module Kobako
6
10
  # Host-side MessagePack codec for the kobako wire contract — the
@@ -17,15 +21,10 @@ module Kobako
17
21
  # {Decoder} are thin wrappers that register the three kobako-specific
18
22
  # ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
19
23
  # envelope) on a single +MessagePack::Factory+ instance. The Rust side
20
- # mirrors this layer as the +codec+ module in the +kobako-wasm+ crate;
24
+ # mirrors this layer as the +codec+ module in the +kobako-core+ crate;
21
25
  # the ext-code constants live as module-private values on {Factory}
22
26
  # alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
23
27
  # +codec::EXT_ERRENV+ on that side.
24
28
  module Codec
25
29
  end
26
30
  end
27
-
28
- require_relative "codec/utils"
29
- require_relative "codec/factory"
30
- require_relative "codec/encoder"
31
- require_relative "codec/decoder"
data/lib/kobako/errors.rb CHANGED
@@ -21,7 +21,7 @@ module Kobako
21
21
  # call that failed and was not rescued inside the
22
22
  # script).
23
23
  #
24
- # A fourth branch sits outside the invocation taxonomy:
24
+ # Two further branches sit outside the invocation taxonomy:
25
25
  #
26
26
  # * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
27
27
  # when the wasm runtime cannot be built from the
@@ -29,6 +29,9 @@ module Kobako
29
29
  # ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
30
30
  # Not an invocation outcome, so it never passes
31
31
  # through the two-step attribution decision.
32
+ # * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
33
+ # when the checkout wait exceeds +checkout_timeout+
34
+ # ({docs/behavior.md E-46}[link:../../docs/behavior.md]).
32
35
  #
33
36
  # Subclasses pinned by docs/behavior.md Error Classes:
34
37
  #
@@ -137,4 +140,11 @@ module Kobako
137
140
  # snippet failures while callers wanting bytecode-specific handling
138
141
  # can `rescue Kobako::BytecodeError` directly.
139
142
  class BytecodeError < SandboxError; end
143
+
144
+ # Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
145
+ # wait exceeded the configured +checkout_timeout+ while every slot was
146
+ # held ({docs/behavior.md E-46}[link:../../docs/behavior.md]). No
147
+ # Sandbox state is touched — retrying succeeds as soon as a holder
148
+ # returns its Sandbox.
149
+ class PoolTimeoutError < Error; end
140
150
  end
@@ -18,15 +18,20 @@ module Kobako
18
18
  def initialize(name)
19
19
  @name = name
20
20
  @members = {} # : Hash[String, untyped]
21
+ @sealed = false
21
22
  end
22
23
 
23
24
  # Bind +object+ under +member+ inside this Namespace. +member+ is a
24
25
  # constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
25
26
  # object that responds to the methods guest code will invoke. Returns
26
27
  # +self+ for chaining. Raises +ArgumentError+ when +member+ does not
27
- # match the constant pattern, or a Member of the same name is already
28
- # bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]).
28
+ # match the constant pattern, when a Member of the same name is
29
+ # already bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]),
30
+ # or when the owning Sandbox's first invocation has sealed Service
31
+ # registration ({docs/behavior.md E-45}[link:../../docs/behavior.md]).
29
32
  def bind(member, object)
33
+ raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
34
+
30
35
  member_str = validate_member_name!(member)
31
36
  raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
32
37
 
@@ -34,6 +39,15 @@ module Kobako
34
39
  self
35
40
  end
36
41
 
42
+ # Mark this Namespace as sealed ({docs/behavior.md B-33}[link:../../docs/behavior.md]).
43
+ # Called by +Kobako::Catalog::Namespaces#seal!+ on the owning
44
+ # Sandbox's first invocation; afterwards {#bind} raises
45
+ # +ArgumentError+ (E-45). Idempotent; returns +self+.
46
+ def seal!
47
+ @sealed = true
48
+ self
49
+ end
50
+
37
51
  # Member lookup; raises +KeyError+ when no Member is registered
38
52
  # under +member+.
39
53
  def fetch(member)
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "sandbox"
5
+
6
+ module Kobako
7
+ # Kobako::Pool — a bounded set of warm, identically set-up Sandboxes
8
+ # handed out one exclusive holder at a time
9
+ # ({docs/behavior.md B-46..B-48}[link:../../docs/behavior.md]).
10
+ #
11
+ # Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
12
+ # and holds the optional block as the per-Sandbox setup hook; a
13
+ # checkout prefers an idle Sandbox and constructs a new one only when
14
+ # none is idle and fewer than +slots+ exist (B-46). +#with+ blocks up
15
+ # to +checkout_timeout+ seconds when every slot is held (E-46), applies
16
+ # the +TrapError+ discard-and-recreate contract at checkin (B-47), and
17
+ # the Pool releases everything with its own reachability — there is no
18
+ # teardown verb (B-48).
19
+ class Pool
20
+ # The +#with+ wait bound applied when +checkout_timeout+ is not given
21
+ # ({docs/behavior.md B-46}[link:../../docs/behavior.md]).
22
+ DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
23
+
24
+ # Build a Pool of up to +slots+ Sandboxes
25
+ # ({docs/behavior.md B-46}[link:../../docs/behavior.md]). +slots+ is
26
+ # a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
27
+ # seconds (+nil+ waits indefinitely); every other keyword is
28
+ # forwarded verbatim to +Kobako::Sandbox.new+. The optional block
29
+ # runs exactly once per constructed Sandbox — it is the setup window
30
+ # for +#define+ / +#preload+ before that Sandbox's first checkout.
31
+ # No Sandbox is constructed here. Raises +ArgumentError+ for an
32
+ # invalid +slots+ / +checkout_timeout+
33
+ # ({docs/behavior.md E-47}[link:../../docs/behavior.md]).
34
+ def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
35
+ validate_slots!(slots)
36
+ @slots = slots
37
+ @checkout_timeout = normalize_checkout_timeout(checkout_timeout)
38
+ @sandbox_options = sandbox_options
39
+ @setup = setup
40
+ @idle = [] # : Array[Kobako::Sandbox]
41
+ @constructed = 0
42
+ @mutex = Mutex.new
43
+ @slot_freed = ConditionVariable.new
44
+ end
45
+
46
+ # Yield one exclusively-held Sandbox to the block and return the
47
+ # block's value ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
48
+ # Blocks while every slot is held; raises +Kobako::PoolTimeoutError+
49
+ # once the wait exceeds +checkout_timeout+
50
+ # ({docs/behavior.md E-46}[link:../../docs/behavior.md]). The Sandbox
51
+ # returns to the pool at block exit — unless the block raised
52
+ # +Kobako::TrapError+, in which case the unrecoverable Sandbox is
53
+ # discarded and its slot refills by a fresh construction on next
54
+ # demand.
55
+ def with
56
+ sandbox = checkout
57
+ begin
58
+ yield sandbox
59
+ rescue TrapError
60
+ release_capacity!
61
+ sandbox = nil
62
+ raise
63
+ ensure
64
+ checkin(sandbox) if sandbox
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Acquire a Sandbox and hand it over in pre-invocation state — empty
71
+ # output buffers and truncation predicates false (B-47).
72
+ def checkout
73
+ acquire.tap(&:reset_invocation_state!)
74
+ end
75
+
76
+ # The idle-first claim loop (B-46): an idle Sandbox wins, unclaimed
77
+ # capacity constructs, and a full pool waits for a checkin.
78
+ def acquire
79
+ timeout = @checkout_timeout
80
+ deadline = timeout && (monotonic_now + timeout)
81
+ loop do
82
+ action, sandbox = claim_or_wait(deadline)
83
+ return sandbox if action == :idle && sandbox
84
+ return construct_slot if action == :build
85
+ end
86
+ end
87
+
88
+ # Single locked decision point for one claim attempt. Waiting
89
+ # happens inside the lock (so a checkin can wake it); construction
90
+ # happens outside (so a slow setup block never holds the lock) —
91
+ # capacity is reserved here and released by +construct_slot+ on
92
+ # failure.
93
+ def claim_or_wait(deadline)
94
+ @mutex.synchronize do
95
+ return [:idle, @idle.pop] unless @idle.empty?
96
+
97
+ if @constructed < @slots
98
+ @constructed += 1
99
+ return [:build, nil]
100
+ end
101
+
102
+ await_slot!(deadline)
103
+ [:retry, nil]
104
+ end
105
+ end
106
+
107
+ # Wait for a checkin or freed capacity; raises
108
+ # +Kobako::PoolTimeoutError+ once +deadline+ has passed (E-46). Must
109
+ # run while holding +@mutex+.
110
+ def await_slot!(deadline)
111
+ remaining = deadline && (deadline - monotonic_now)
112
+ if remaining && remaining <= 0
113
+ raise PoolTimeoutError,
114
+ "no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held"
115
+ end
116
+
117
+ @slot_freed.wait(@mutex, remaining)
118
+ end
119
+
120
+ # Construct and set up one pooled Sandbox against the capacity
121
+ # reserved by +claim_or_wait+. Construction and setup-block errors
122
+ # propagate to the checkout caller unchanged (B-46); the reserved
123
+ # capacity is released so a later checkout can retry.
124
+ def construct_slot
125
+ done = false
126
+ sandbox = Sandbox.new(**@sandbox_options)
127
+ @setup&.call(sandbox)
128
+ done = true
129
+ sandbox
130
+ ensure
131
+ release_capacity! unless done
132
+ end
133
+
134
+ # Return a Sandbox to the idle list and wake one waiting checkout.
135
+ def checkin(sandbox)
136
+ @mutex.synchronize do
137
+ @idle.push(sandbox)
138
+ @slot_freed.signal
139
+ end
140
+ end
141
+
142
+ # Give back reserved-but-unfilled capacity — a failed construction or
143
+ # a discarded Sandbox — and wake one waiting checkout to claim it.
144
+ def release_capacity!
145
+ @mutex.synchronize do
146
+ @constructed -= 1
147
+ @slot_freed.signal
148
+ end
149
+ end
150
+
151
+ # The wait deadline runs on the monotonic clock so a wall-clock jump
152
+ # cannot stretch or cut the checkout wait.
153
+ def monotonic_now
154
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
155
+ end
156
+
157
+ # E-47 pre-flight for +slots+ — no coercion, a positive Integer is
158
+ # the only accepted shape.
159
+ def validate_slots!(slots)
160
+ return if slots.is_a?(Integer) && slots.positive?
161
+
162
+ raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
163
+ end
164
+
165
+ # Coerce +checkout_timeout+ into the Float seconds the wait loop
166
+ # consumes, or +nil+ to wait indefinitely — the E-39 normalisation
167
+ # idiom applied to E-47.
168
+ def normalize_checkout_timeout(checkout_timeout)
169
+ return nil if checkout_timeout.nil?
170
+ unless checkout_timeout.is_a?(Numeric)
171
+ raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}"
172
+ end
173
+
174
+ seconds = checkout_timeout.to_f
175
+ unless seconds.positive? && seconds.finite?
176
+ raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})"
177
+ end
178
+
179
+ seconds
180
+ end
181
+ end
182
+ end
@@ -206,6 +206,22 @@ module Kobako
206
206
  end
207
207
  end
208
208
 
209
+ # Reset all per-invocation observable state to its pre-invocation
210
+ # sentinels — both per-channel captures
211
+ # ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
212
+ # per-last-invocation usage record
213
+ # ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
214
+ # +#initialize+ (first-time setup) and +#begin_invocation!+
215
+ # (between-invocation reset) so both paths agree on what
216
+ # "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
217
+ # so a pooled Sandbox hands over empty output buffers
218
+ # ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
219
+ def reset_invocation_state!
220
+ @stdout_capture = Capture::EMPTY
221
+ @stderr_capture = Capture::EMPTY
222
+ @usage = Usage::EMPTY
223
+ end
224
+
209
225
  private
210
226
 
211
227
  # Configure the +Runtime+'s host↔guest dispatch wiring
@@ -237,20 +253,6 @@ module Kobako
237
253
  reset_invocation_state!
238
254
  end
239
255
 
240
- # Reset all per-invocation observable state to its pre-invocation
241
- # sentinels — both per-channel captures
242
- # ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
243
- # per-last-invocation usage record
244
- # ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
245
- # +#initialize+ (first-time setup) and +#begin_invocation!+
246
- # (between-invocation reset) so both paths agree on what
247
- # "pre-invocation state" means.
248
- def reset_invocation_state!
249
- @stdout_capture = Capture::EMPTY
250
- @stderr_capture = Capture::EMPTY
251
- @usage = Usage::EMPTY
252
- end
253
-
254
256
  # Read the per-last-invocation +wall_time+ and +memory_peak+ from
255
257
  # the ext and wrap them as a +Kobako::Usage+ value object
256
258
  # ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
@@ -46,13 +46,28 @@ module Kobako
46
46
  # metaprogramming surface (+send+, +public_send+, +instance_eval+,
47
47
  # +method+, +tap+, +instance_variable_get+, ...) rather than Service
48
48
  # behaviour. A guest-supplied method name resolving to one of these is
49
- # rejected: the security contract is that only methods the bound object
50
- # itself defines are reachable, and +public_send(:send, ...)+ would
51
- # otherwise let a guest pivot through +send+ into the private
52
- # +Kernel#eval+ / +#system+ surface (host RCE).
49
+ # rejected ({docs/behavior.md B-42}[link:../../../docs/behavior.md]):
50
+ # only methods the bound object itself exposes as Service behaviour are
51
+ # reachable, and +public_send(:send, ...)+ would otherwise let a guest
52
+ # pivot through +send+ into the private +Kernel#eval+ / +#system+
53
+ # surface (host RCE).
53
54
  META_OWNERS = [BasicObject, Kernel, Object, Module, Class].freeze
54
55
  private_constant :META_OWNERS
55
56
 
57
+ # Callable gadget types whose own public methods are reflection surface
58
+ # (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
59
+ # hand back the underlying object) rather than Service behaviour. Only
60
+ # {CALLABLE_ALLOW} is reachable on a target of these types; a bound
61
+ # lambda stays invocable, its reflective surface does not (B-42).
62
+ GADGET_OWNERS = [Proc, Method, UnboundMethod, Binding].freeze
63
+ private_constant :GADGET_OWNERS
64
+
65
+ # The sole methods reachable on a {GADGET_OWNERS} target: invoking it
66
+ # (+call+ / +[]+ / +yield+) and the harmless +arity+ / +lambda?+
67
+ # describers that aid guest-side debugging.
68
+ CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
69
+ private_constant :CALLABLE_ALLOW
70
+
56
71
  # Dispatch a single transport request and return the encoded
57
72
  # Response bytes ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
58
73
  # Invoked from the +Runtime#on_dispatch+ Proc that
@@ -131,14 +146,18 @@ module Kobako
131
146
  end
132
147
 
133
148
  # Guard the +public_send+ below against ambient reflection methods
134
- # (see {META_OWNERS}). A concretely-defined public method whose owner
135
- # is a meta module is rejected; a name with no concrete public method
136
- # is allowed only when the target opts into it via +respond_to?+
137
- # (dynamic +method_missing+ Services), since the dangerous meta methods
138
- # are all concretely defined and therefore never reach that branch.
149
+ # ({docs/behavior.md B-42}[link:../../../docs/behavior.md]). A public
150
+ # method whose owner is a {META_OWNERS} or {GADGET_OWNERS} module is
151
+ # rejected, except {CALLABLE_ALLOW} on a gadget target (a bound lambda
152
+ # stays invocable). A name with no concrete public method is allowed
153
+ # only when the target opts into it via +respond_to?+ (dynamic
154
+ # +method_missing+ Services), since the dangerous methods are all
155
+ # concretely defined and therefore never reach that branch.
139
156
  def reject_meta_method!(target, name)
140
157
  owner = target.public_method(name).owner
141
- return unless META_OWNERS.include?(owner)
158
+ gadget = GADGET_OWNERS.include?(owner)
159
+ return unless META_OWNERS.include?(owner) || gadget
160
+ return if gadget && CALLABLE_ALLOW.include?(name)
142
161
 
143
162
  raise UndefinedTargetError, "method #{name.inspect} is not a Service method"
144
163
  rescue NameError
@@ -5,16 +5,8 @@ require_relative "../codec"
5
5
 
6
6
  module Kobako
7
7
  # See lib/kobako/transport.rb for the umbrella module doc; this file
8
- # owns the Request value object and its +#encode+ / +.decode+ codec,
9
- # plus the +STATUS_OK+ / +STATUS_ERROR+ constants shared with Response.
8
+ # owns the Request value object and its +#encode+ / +.decode+ codec.
10
9
  module Transport
11
- # ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
12
-
13
- # Response variant marker for the success branch.
14
- STATUS_OK = 0
15
- # Response variant marker for the fault branch.
16
- STATUS_ERROR = 1
17
-
18
10
  # Value object for a single guest-initiated Transport Request
19
11
  # ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
20
12
  #
@@ -2,12 +2,19 @@
2
2
 
3
3
  require_relative "../codec"
4
4
  require_relative "../fault"
5
- require_relative "request"
6
5
 
7
6
  module Kobako
8
7
  # See lib/kobako/transport.rb for the umbrella module doc; this file
9
- # owns the Response value object and its +#encode+ / +.decode+ codec.
8
+ # owns the Response value object and its +#encode+ / +.decode+ codec,
9
+ # plus the +STATUS_OK+ / +STATUS_ERROR+ status bytes.
10
10
  module Transport
11
+ # ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
12
+
13
+ # Response variant marker for the success branch.
14
+ STATUS_OK = 0
15
+ # Response variant marker for the fault branch.
16
+ STATUS_ERROR = 1
17
+
11
18
  # Value object for a single host-side Transport Response
12
19
  # ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
13
20
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.9.1"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/kobako.rb CHANGED
@@ -15,3 +15,4 @@ require_relative "kobako/catalog"
15
15
  require_relative "kobako/runtime"
16
16
  require_relative "kobako/snapshot"
17
17
  require_relative "kobako/sandbox"
18
+ require_relative "kobako/pool"
@@ -7,8 +7,7 @@
7
7
  "component": "kobako",
8
8
  "include-component-in-tag": false,
9
9
  "release-type": "ruby",
10
- "exclude-paths": ["wasm"],
11
- "release-as": "0.9.1"
10
+ "exclude-paths": ["wasm"]
12
11
  },
13
12
  "wasm/kobako-core": {
14
13
  "component": "kobako-core",
@@ -74,13 +73,28 @@
74
73
  "path": "/wasm/kobako-regexp/README.md"
75
74
  }
76
75
  ]
76
+ },
77
+ "wasm/kobako-baker": {
78
+ "component": "kobako-baker",
79
+ "release-type": "rust",
80
+ "extra-files": [
81
+ {
82
+ "type": "toml",
83
+ "path": "/wasm/kobako-baker/Cargo.lock",
84
+ "jsonpath": "$.package[?(@.name=='kobako-baker')].version"
85
+ },
86
+ {
87
+ "type": "generic",
88
+ "path": "/wasm/kobako-baker/README.md"
89
+ }
90
+ ]
77
91
  }
78
92
  },
79
93
  "plugins": [
80
94
  {
81
95
  "type": "linked-versions",
82
96
  "groupName": "kobako guest crates",
83
- "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp"]
97
+ "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp", "kobako-baker"]
84
98
  }
85
99
  ],
86
100
  "extra-files": [
@@ -13,6 +13,10 @@ module Kobako
13
13
 
14
14
  private
15
15
 
16
+ def reject_unwrappable!: (untyped object) -> void
17
+
18
+ def ensure_capacity!: () -> void
19
+
16
20
  def require_bound!: (Integer id) -> void
17
21
  end
18
22
  end
@@ -52,4 +52,7 @@ module Kobako
52
52
 
53
53
  class BytecodeError < SandboxError
54
54
  end
55
+
56
+ class PoolTimeoutError < Error
57
+ end
55
58
  end
@@ -8,6 +8,8 @@ module Kobako
8
8
 
9
9
  def bind: (Symbol | String member, untyped object) -> self
10
10
 
11
+ def seal!: () -> self
12
+
11
13
  def fetch: (Symbol | String member) -> untyped
12
14
 
13
15
  def to_preamble: () -> [String, Array[String]]