kobako 0.9.2 → 0.11.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +32 -0
  4. data/Cargo.lock +3 -1
  5. data/README.md +47 -19
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +12 -2
  8. data/ext/kobako/src/runtime/ambient.rs +1 -1
  9. data/ext/kobako/src/runtime/cache.rs +170 -6
  10. data/ext/kobako/src/runtime/capture.rs +1 -1
  11. data/ext/kobako/src/runtime/config.rs +3 -4
  12. data/ext/kobako/src/runtime/dispatch.rs +8 -8
  13. data/ext/kobako/src/runtime/exports.rs +32 -21
  14. data/ext/kobako/src/runtime/instance_pre.rs +97 -0
  15. data/ext/kobako/src/runtime/invocation.rs +36 -93
  16. data/ext/kobako/src/runtime/trap.rs +5 -5
  17. data/ext/kobako/src/runtime.rs +389 -403
  18. data/ext/kobako/src/snapshot.rs +2 -2
  19. data/lib/kobako/capture.rb +5 -7
  20. data/lib/kobako/catalog/handles.rb +28 -39
  21. data/lib/kobako/catalog/namespaces.rb +31 -20
  22. data/lib/kobako/catalog/snippets.rb +18 -16
  23. data/lib/kobako/codec/decoder.rb +5 -1
  24. data/lib/kobako/codec/utils.rb +6 -9
  25. data/lib/kobako/errors.rb +40 -36
  26. data/lib/kobako/handle.rb +2 -3
  27. data/lib/kobako/namespace.rb +17 -6
  28. data/lib/kobako/outcome.rb +12 -14
  29. data/lib/kobako/pool.rb +176 -0
  30. data/lib/kobako/sandbox.rb +68 -88
  31. data/lib/kobako/sandbox_options.rb +5 -9
  32. data/lib/kobako/snapshot.rb +2 -4
  33. data/lib/kobako/snippet/binary.rb +1 -3
  34. data/lib/kobako/snippet/source.rb +1 -2
  35. data/lib/kobako/snippet.rb +1 -2
  36. data/lib/kobako/transport/dispatcher.rb +39 -38
  37. data/lib/kobako/transport/request.rb +1 -1
  38. data/lib/kobako/transport/run.rb +23 -28
  39. data/lib/kobako/transport/yielder.rb +11 -17
  40. data/lib/kobako/transport.rb +2 -3
  41. data/lib/kobako/usage.rb +10 -13
  42. data/lib/kobako/version.rb +1 -1
  43. data/lib/kobako.rb +1 -0
  44. data/release-please-config.json +16 -1
  45. data/sig/kobako/catalog/handles.rbs +0 -2
  46. data/sig/kobako/errors.rbs +3 -0
  47. data/sig/kobako/namespace.rbs +2 -0
  48. data/sig/kobako/pool.rbs +44 -0
  49. data/sig/kobako/sandbox.rbs +2 -2
  50. data/sig/kobako/transport/dispatcher.rbs +2 -0
  51. metadata +4 -1
@@ -3,8 +3,8 @@
3
3
  //! Every successful `Kobako::Runtime#eval` / `#run` returns one of these.
4
4
  //! It carries every observable the host needs to surface after a guest
5
5
  //! invocation: the OUTCOME_BUFFER bytes (`return_bytes`), the captured
6
- //! stdout / stderr byte slices with their truncation flags (B-04), and
7
- //! the wall-clock + memory-peak figures from `Kobako::Usage` (B-35).
6
+ //! stdout / stderr byte slices with their truncation flags, and
7
+ //! the wall-clock + memory-peak figures from `Kobako::Usage`.
8
8
  //!
9
9
  //! Ruby callers see the seven raw readers registered below; the helper
10
10
  //! methods that pack them into `Kobako::Capture` / `Kobako::Usage`
@@ -4,7 +4,7 @@ module Kobako
4
4
  # Host-side captured prefix of guest stdout / stderr produced during a
5
5
  # single +Kobako::Sandbox+ invocation, paired with the truncation flag
6
6
  # the WASI pipe sets when the guest wrote past the configured per-channel
7
- # cap ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
7
+ # cap.
8
8
  #
9
9
  # Immutable value object: the captured bytes and the truncation flag
10
10
  # always travel together and the instance is frozen on construction.
@@ -30,14 +30,12 @@ module Kobako
30
30
  end
31
31
 
32
32
  # Returns +true+ iff the underlying capture channel exceeded its
33
- # configured cap during the originating +Sandbox+ invocation
34
- # ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
33
+ # configured cap during the originating +Sandbox+ invocation.
35
34
  def truncated? = @truncated
36
35
 
37
- # Pre-invocation sentinel ({docs/behavior.md B-05}[link:../../docs/behavior.md]).
38
- # Empty UTF-8 bytes and +truncated? == false+; reused by every fresh
39
- # +Sandbox+ and by +Sandbox+ between invocations to denote "no capture
40
- # yet".
36
+ # Pre-invocation sentinel. Empty UTF-8 bytes and +truncated? == false+;
37
+ # reused by every fresh +Sandbox+ and by +Sandbox+ between invocations
38
+ # to denote "no capture yet".
41
39
  EMPTY = new(bytes: "", truncated: false)
42
40
  end
43
41
  end
@@ -5,41 +5,29 @@ require_relative "../handle"
5
5
  module Kobako
6
6
  module Catalog
7
7
  # Host-side mapping from opaque integer Handle IDs to Ruby objects.
8
- # The table is owned by +Kobako::Sandbox+
9
- # ({docs/behavior.md B-19}[link:../../../docs/behavior.md]) and injected
8
+ # The table is owned by +Kobako::Sandbox+ and injected
10
9
  # into the per-Sandbox +Kobako::Catalog::Namespaces+ so guest→host dispatch
11
10
  # resolves Handle targets and arguments against the same table that
12
- # host→guest wire encoding allocates into
13
- # ({docs/behavior.md B-14, B-34}[link:../../../docs/behavior.md]).
11
+ # host→guest wire encoding allocates into.
14
12
  #
15
- # Lifecycle invariants ({docs/behavior.md}[link:../../../docs/behavior.md]):
13
+ # Lifecycle invariants:
16
14
  #
17
- # - {docs/behavior.md B-15}[link:../../../docs/behavior.md] Handle IDs
18
- # are allocated by a monotonically increasing counter scoped to a
19
- # single invocation. The first ID issued in an invocation is 1; ID 0
20
- # is reserved as the invalid sentinel and is never returned by
21
- # +#alloc+.
15
+ # - Handle IDs are allocated by a monotonically increasing counter
16
+ # scoped to a single invocation. The first ID issued in an
17
+ # invocation is 1; ID 0 is reserved as the invalid sentinel and is
18
+ # never returned by +#alloc+.
22
19
  #
23
- # - {docs/behavior.md B-19}[link:../../../docs/behavior.md] At every
24
- # invocation boundary (via +#reset!+), every Handle issued under the
25
- # old state becomes invalid. Reset applies uniformly regardless of
26
- # allocation source (B-14 Service return or B-34 host-injected
20
+ # - At every invocation boundary (via +#reset!+), every Handle issued
21
+ # under the old state becomes invalid. Reset applies uniformly
22
+ # regardless of allocation source (Service return or host-injected
27
23
  # argument).
28
24
  #
29
- # - {docs/behavior.md B-21}[link:../../../docs/behavior.md] The cap is
30
- # +0x7fff_ffff+ (2³¹ 1). Allocation beyond the cap raises
31
- # immediately — no silent truncation, no wrap, no ID reuse.
25
+ # - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
26
+ # raises immediately no silent truncation, no wrap, no ID reuse.
32
27
  class Handles
33
- # Reflective gadget types that are never minted as a Capability Handle
34
- # ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): wrapping a
35
- # +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a callable
36
- # proxy onto host reflection (a returned +Binding+ reaches +Binding#eval+).
37
- UNWRAPPABLE_TYPES = [Binding, Method, UnboundMethod].freeze
38
- private_constant :UNWRAPPABLE_TYPES
39
-
40
28
  # Build a fresh, empty table. +next_id+ is an internal seam that
41
- # sets the starting value of the monotonic counter (defaults to 1 per
42
- # B-15); tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
29
+ # sets the starting value of the monotonic counter (defaults to 1);
30
+ # tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
43
31
  # the cap-exhaustion path without 2³¹ allocations.
44
32
  def initialize(next_id: 1)
45
33
  @entries = {} # : Hash[Integer, untyped]
@@ -52,8 +40,7 @@ module Kobako
52
40
  # +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
53
41
  # +Kobako::HandlerExhaustedError+ if the next ID would exceed the
54
42
  # cap. The cap is anchored on +Kobako::Handle+ — the wire codec
55
- # and the allocator share the same invariant
56
- # ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
43
+ # and the allocator share the same invariant.
57
44
  #
58
45
  # Returning a Handle (rather than a bare Integer id) keeps the
59
46
  # allocator's output a domain entity; +Kobako::Handle.restore+
@@ -76,9 +63,8 @@ module Kobako
76
63
  @entries[id]
77
64
  end
78
65
 
79
- # Clear all entries AND reset the counter to 1. Called at the per-invocation
80
- # boundary by +Kobako::Sandbox+ see
81
- # {docs/behavior.md B-19}[link:../../../docs/behavior.md]. Returns +self+.
66
+ # Clear all entries AND reset the counter to 1. Called at the
67
+ # per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
82
68
  def reset!
83
69
  @entries.clear
84
70
  @next_id = 1
@@ -96,17 +82,20 @@ module Kobako
96
82
 
97
83
  private
98
84
 
99
- # Refuse to mint a Capability Handle for a reflective gadget
100
- # ({UNWRAPPABLE_TYPES}, B-43). Raising here keeps the rule at the single
101
- # mint point, so it holds on both the Service-return (B-14) and the
102
- # +#run+ host→guest auto-wrap (B-34) paths.
85
+ # Refuse to mint a Capability Handle for a reflective gadget:
86
+ # a +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a
87
+ # callable proxy onto host reflection (a returned +Binding+ reaches
88
+ # +Binding#eval+). Raising here keeps the rule at the single mint
89
+ # point, so it holds on both the Service-return and the +#run+
90
+ # host→guest auto-wrap paths.
103
91
  def reject_unwrappable!(object)
104
- return unless UNWRAPPABLE_TYPES.any? { |type| object.is_a?(type) }
105
-
106
- raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
92
+ case object
93
+ when Binding, Method, UnboundMethod
94
+ raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
95
+ end
107
96
  end
108
97
 
109
- # Guard {#alloc} against issuing an ID past the B-21 cap. Returns +nil+
98
+ # Guard {#alloc} against issuing an ID past the cap. Returns +nil+
110
99
  # on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
111
100
  def ensure_capacity!
112
101
  cap = Kobako::Handle::MAX_ID
@@ -9,8 +9,7 @@ module Kobako
9
9
  module Catalog
10
10
  # Kobako::Catalog::Namespaces — per-Sandbox registry of
11
11
  # +Kobako::Namespace+ entities. Holds the Namespace / Member bindings
12
- # and the preamble emitted on Frame 1
13
- # ({docs/behavior.md B-07..B-11}[link:../../../docs/behavior.md]).
12
+ # and the preamble emitted on Frame 1.
14
13
  #
15
14
  # Public API:
16
15
  #
@@ -24,29 +23,27 @@ module Kobako
24
23
  # +Kobako::Transport::Dispatcher+'s responsibility — the Dispatcher
25
24
  # receives this registry and the +Catalog::Handles+ as arguments from
26
25
  # the +Runtime#on_dispatch+ Proc that +Kobako::Sandbox#initialize+
27
- # installs ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
28
- # The registry holds an injected +Catalog::Handles+ reference so
26
+ # installs. The registry holds an injected +Catalog::Handles+ reference so
29
27
  # dispatch target resolution and host→guest auto-wrap share the same
30
- # Sandbox-owned allocator ({docs/behavior.md B-19}[link:../../../docs/behavior.md]).
28
+ # Sandbox-owned allocator.
31
29
  class Namespaces
32
30
  # Build a fresh registry. +handler+ is an internal seam that injects
33
31
  # a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
34
- # is pinned near +MAX_ID+ to exercise the B-21 cap-exhaustion path
32
+ # is pinned near +MAX_ID+ to exercise the cap-exhaustion path
35
33
  # without 2³¹ allocations. Production callers leave it at the default.
36
34
  def initialize(handler: Catalog::Handles.new)
37
35
  @namespaces = {} # : Hash[String, Kobako::Namespace]
38
36
  @handler = handler
39
37
  @sealed = false
38
+ @encoded = nil # : String?
40
39
  end
41
40
 
42
- # Declare or retrieve the Namespace named +name+ (idempotent
43
- # {docs/behavior.md B-10}[link:../../../docs/behavior.md]).
41
+ # Declare or retrieve the Namespace named +name+ (idempotent).
44
42
  # +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
45
43
  # +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
46
44
  # name, creating it if it does not exist. Raises +ArgumentError+ when
47
45
  # +name+ is malformed, or when called after the owning Sandbox has been
48
- # sealed by its first invocation
49
- # ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
46
+ # sealed by its first invocation.
50
47
  def define(name)
51
48
  raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
52
49
 
@@ -73,21 +70,35 @@ module Kobako
73
70
  namespace.fetch(member_name)
74
71
  end
75
72
 
76
- # Encode the preamble as msgpack bytes for stdin Frame 1 delivery
77
- # ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Routes through
78
- # {Kobako::Codec::Encoder} like every other host-side wire encode so
79
- # there is a single codec path; the preamble carries only Strings and
80
- # Arrays, so none of the kobako ext types actually fire. Structure:
81
- # +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
82
- # +String+ of msgpack bytes.
73
+ # Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
74
+ # Routes through {Kobako::Codec::Encoder} like every other host-side
75
+ # wire encode so there is a single codec path; the preamble carries
76
+ # only Strings and Arrays, so none of the kobako ext types actually
77
+ # fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
78
+ # Returns a binary +String+ of msgpack bytes.
79
+ #
80
+ # Once sealed, the bytes are computed once and reused for every
81
+ # subsequent invocation: sealing freezes Service registration at the
82
+ # first invocation, so the preamble is exactly the bindings that
83
+ # existed at that moment — a bind reaching a +Kobako::Namespace+
84
+ # after the seal raises +ArgumentError+ and never alters Frame 1.
83
85
  def encode
84
- Codec::Encoder.encode(@namespaces.values.map(&:to_preamble))
86
+ return @encoded if @encoded
87
+
88
+ bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
89
+ @encoded = bytes if @sealed
90
+ bytes
85
91
  end
86
92
 
87
- # Mark the registry as sealed. Called by +Sandbox+ on the first
88
- # invocation. After sealing, #define raises ArgumentError. Idempotent.
93
+ # Mark the registry as sealed and propagate the seal to every
94
+ # declared +Kobako::Namespace+. Called by +Sandbox+ on the first
95
+ # invocation. After sealing, both #define and +Namespace#bind+
96
+ # raise ArgumentError. Idempotent.
89
97
  def seal!
98
+ return self if @sealed
99
+
90
100
  @sealed = true
101
+ @namespaces.each_value(&:seal!)
91
102
  self
92
103
  end
93
104
 
@@ -6,8 +6,7 @@ require_relative "../snippet"
6
6
  module Kobako
7
7
  module Catalog
8
8
  # Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry
9
- # of preloaded snippets
10
- # ({docs/behavior.md B-32 / B-33}[link:../../../docs/behavior.md]).
9
+ # of preloaded snippets.
11
10
  #
12
11
  # Entries replay against the fresh +mrb_state+ before per-invocation
13
12
  # source / entrypoint resolution. Each +Snippet::Source+ entry's +name+
@@ -15,22 +14,21 @@ module Kobako
15
14
  # +debug_info+ that surfaces in every backtrace frame originating from
16
15
  # the snippet as +(snippet:Name):line+. Duplicate names within the
17
16
  # +code:+ form would produce ambiguous attribution and are rejected at
18
- # registration time
19
- # ({docs/behavior.md E-33}[link:../../../docs/behavior.md]).
17
+ # registration time.
20
18
  # +Snippet::Binary+ entries carry no host-side name — their canonical
21
19
  # name lives in the bytecode's +debug_info+ and is read by the guest at
22
20
  # load time; the host does not extract it.
23
21
  #
24
- # Sealing (B-33) is governed by the owning Sandbox — the registry itself
22
+ # Sealing is governed by the owning Sandbox — the registry itself
25
23
  # is append-only and exposes no mutation API beyond +#register+; the
26
24
  # Sandbox guards +#register+ behind the seal check before delegating.
27
25
  class Snippets
28
- # Ruby constant-name pattern enforced on snippet names
29
- # ({docs/behavior.md E-34}[link:../../../docs/behavior.md]).
26
+ # Ruby constant-name pattern enforced on snippet names.
30
27
  NAME_PATTERN = /\A[A-Z]\w*\z/
31
28
 
32
29
  def initialize
33
30
  @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
31
+ @encoded = nil # : String?
34
32
  end
35
33
 
36
34
  # Serialize the registered snippets to wire bytes. Each entry
@@ -42,12 +40,17 @@ module Kobako
42
40
  # carriers — this collection-tier method reads their attributes
43
41
  # externally via +entry_payload+ rather than asking each entry to
44
42
  # self-encode.
43
+ #
44
+ # The bytes are memoized — the table is replayed verbatim on every
45
+ # invocation after sealing, so Frame 3 never changes between
46
+ # encodes; {#register} drops the memo while the table is still open.
45
47
  def encode
46
- Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) })
48
+ return @encoded if @encoded
49
+
50
+ @encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
47
51
  end
48
52
 
49
- # Register one preloaded snippet in either of two forms
50
- # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
53
+ # Register one preloaded snippet in either of two forms.
51
54
  #
52
55
  # * Source form +register(code: src, name: Name)+ — +src+ is the
53
56
  # mruby source as a String; the bytes are re-encoded as UTF-8
@@ -58,16 +61,15 @@ module Kobako
58
61
  # precompiled RITE bytecode as a String, duplicated and forced
59
62
  # to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
60
63
  # +nil+ — bytecode entries are anonymous on the host side; any
61
- # structural validation
62
- # ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
63
- # is deferred to the guest at first replay.
64
+ # structural validation is deferred to the guest at first replay.
64
65
  #
65
66
  # The two forms are mutually exclusive: shape validation lives
66
67
  # here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
67
68
  # a single delegation. Raises +ArgumentError+ on mixed forms,
68
- # missing keywords, wrong types, malformed +name+ (E-34), or
69
- # duplicate +code:+ +name+ (E-33).
69
+ # missing keywords, wrong types, malformed +name+, or
70
+ # duplicate +code:+ +name+.
70
71
  def register(code: nil, name: nil, binary: nil)
72
+ @encoded = nil
71
73
  if binary
72
74
  raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
73
75
 
@@ -81,7 +83,7 @@ module Kobako
81
83
 
82
84
  # Source-form register path. Delegates argument-shape checks to
83
85
  # +ensure_source_args!+ (which returns the narrowed +[code, name]+
84
- # pair), normalises +name+ to a Symbol, rejects duplicates (E-33),
86
+ # pair), normalises +name+ to a Symbol, rejects duplicates,
85
87
  # and appends the Source entry.
86
88
  def register_source!(code, name)
87
89
  code, name = ensure_source_args!(code, name)
@@ -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
@@ -19,8 +19,7 @@ module Kobako
19
19
  # - Representability predicate ({representable?}) and the symmetric
20
20
  # host→guest +#run+ argument walk ({deep_wrap}) used by
21
21
  # +Kobako::Transport::Run#encode+ to route non-representable leaves
22
- # through the Sandbox's +Kobako::Catalog::Handles+
23
- # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]).
22
+ # through the Sandbox's +Kobako::Catalog::Handles+.
24
23
  #
25
24
  # All helpers are pure — they only inspect inputs, never mutate
26
25
  # them — except {deep_wrap}, whose only side effect is allocating
@@ -84,8 +83,7 @@ module Kobako
84
83
 
85
84
  # Deep-walk Array / Hash containers in +value+ and replace every
86
85
  # leaf that fails {representable?} with a +Kobako::Handle+
87
- # allocated from +handler+
88
- # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
86
+ # allocated from +handler+. The
89
87
  # walk only descends through representable container shapes
90
88
  # (Array, Hash) one structural level at a time; a non-representable
91
89
  # leaf is wrapped as-is without inspecting its internal structure.
@@ -116,8 +114,7 @@ module Kobako
116
114
 
117
115
  # Deep-walk Array / Hash containers in +value+ and replace every
118
116
  # +Kobako::Handle+ leaf with the host-side object +handler+ resolves
119
- # it to ({docs/behavior.md B-37}[link:../../../docs/behavior.md]).
120
- # The symmetric inverse of {deep_wrap}: that walk allocates objects
117
+ # it to. The symmetric inverse of {deep_wrap}: that walk allocates objects
121
118
  # into Handles on the host→guest argument path; this walk resolves
122
119
  # Handles back to their objects on every guest→host value path — the
123
120
  # +#eval+ / +#run+ result and the yield-block result alike. The walk
@@ -126,11 +123,11 @@ module Kobako
126
123
  # unchanged.
127
124
  #
128
125
  # +value+ is a decoded Ruby value (a Handle here is a wire-decoded
129
- # +Kobako::Handle+, never a guest-forged one — B-20); +handler+ must
126
+ # +Kobako::Handle+, never a guest-forged one); +handler+ must
130
127
  # respond to +#fetch(id) -> object+ (a host-side
131
128
  # +Kobako::Catalog::Handles+). +handler.fetch+ raises
132
- # +Kobako::SandboxError+ for an id with no live binding, which is the
133
- # corrupted-runtime fallback B-37 specifies.
129
+ # +Kobako::SandboxError+ for an id with no live binding, the
130
+ # corrupted-runtime fallback.
134
131
  def deep_restore(value, handler)
135
132
  case value
136
133
  when ::Array then value.map { |element| Utils.deep_restore(element, handler) }
data/lib/kobako/errors.rb CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  # Top-level Kobako namespace.
4
4
  module Kobako
5
- # Error taxonomy (docs/behavior.md § Error Scenarios).
5
+ # Error taxonomy.
6
6
  #
7
7
  # Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
8
8
  # exactly one of three invocation-outcome classes. Attribution is decided after the
9
- # guest binary returns control to the host (docs/behavior.md
10
- # "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
9
+ # guest binary returns control to the host: first the Wasm-trap layer, then
10
+ # the outcome-envelope tag.
11
11
  #
12
12
  # Three invocation-outcome branches:
13
13
  #
@@ -21,20 +21,21 @@ 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
28
- # configured +wasm_path+ before any invocation runs
29
- # ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
28
+ # configured +wasm_path+ before any invocation runs.
30
29
  # Not an invocation outcome, so it never passes
31
30
  # through the two-step attribution decision.
31
+ # * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
32
+ # when the checkout wait exceeds +checkout_timeout+.
32
33
  #
33
- # Subclasses pinned by docs/behavior.md Error Classes:
34
+ # Named subclasses:
34
35
  #
35
36
  # * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
36
- # at +wasm_path+ (E-40).
37
- # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit (B-21).
37
+ # at +wasm_path+.
38
+ # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit.
38
39
 
39
40
  # Base for all kobako-raised errors so callers that want to ignore the
40
41
  # taxonomy can rescue a single class.
@@ -43,13 +44,13 @@ module Kobako
43
44
  # Wasm engine layer. Raised when the Wasm execution engine crashed
44
45
  # (trap, OOM, unreachable) or when the wire layer detected a structural
45
46
  # violation that signals a corrupted guest execution environment
46
- # (zero-length OUTCOME_BUFFER, unknown outcome tag — SPEC E-02 / E-03).
47
+ # (zero-length OUTCOME_BUFFER, unknown outcome tag).
47
48
  #
48
- # Two named subclasses cover the configured per-invocation caps from B-01:
49
+ # Two named subclasses cover the configured per-invocation caps:
49
50
  #
50
- # * {TimeoutError} — wall-clock +timeout+ exceeded (E-19).
51
+ # * {TimeoutError} — wall-clock +timeout+ exceeded.
51
52
  # * {MemoryLimitError} — guest +memory.grow+ would exceed
52
- # +memory_limit+ (E-20).
53
+ # +memory_limit+.
53
54
  #
54
55
  # Host Apps that only care about "guest is unrecoverable, discard the
55
56
  # Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
@@ -57,24 +58,23 @@ module Kobako
57
58
  # first.
58
59
  class TrapError < Error; end
59
60
 
60
- # Wall-clock timeout cap exhausted. {docs/behavior.md E-19}[link:../../docs/behavior.md]:
61
- # the absolute deadline +entry_time + timeout+ passed and the next guest
62
- # wasm safepoint trapped. The Sandbox is unrecoverable after this point;
63
- # discard and recreate before another execution.
61
+ # Wall-clock timeout cap exhausted: the absolute deadline
62
+ # +entry_time + timeout+ passed and the next guest wasm safepoint
63
+ # trapped. The Sandbox is unrecoverable after this point; discard and
64
+ # recreate before another execution.
64
65
  class TimeoutError < TrapError; end
65
66
 
66
- # Linear-memory cap exhausted. {docs/behavior.md E-20}[link:../../docs/behavior.md]:
67
- # a guest +memory.grow+ would have pushed linear memory past the
68
- # configured +memory_limit+. The Sandbox is unrecoverable after this
69
- # point; discard and recreate before another execution.
67
+ # Linear-memory cap exhausted: a guest +memory.grow+ would have pushed
68
+ # linear memory past the configured +memory_limit+. The Sandbox is
69
+ # unrecoverable after this point; discard and recreate before another
70
+ # execution.
70
71
  class MemoryLimitError < TrapError; end
71
72
 
72
73
  # Construction-layer error raised by +Kobako::Sandbox.new+ /
73
74
  # +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
74
75
  # from the configured +wasm_path+ before any invocation runs —
75
76
  # an unreadable artifact, bytes that are not a valid Wasm module, or
76
- # engine / linker / instantiation setup failure
77
- # ({docs/behavior.md E-41}[link:../../docs/behavior.md]). Construction
77
+ # engine / linker / instantiation setup failure. Construction
78
78
  # is not an invocation, so +SetupError+ sits beside the invocation
79
79
  # taxonomy under +Kobako::Error+ rather than under +TrapError+: no
80
80
  # Sandbox is produced, so the +TrapError+ "discard and recreate"
@@ -83,8 +83,7 @@ module Kobako
83
83
 
84
84
  # The named +SetupError+ subclass for the common, actionable case:
85
85
  # the Guest Binary artifact is absent at +wasm_path+ — the pre-build
86
- # state on a fresh clone before +bundle exec rake compile+
87
- # ({docs/behavior.md E-40}[link:../../docs/behavior.md]). Host Apps
86
+ # state on a fresh clone before +bundle exec rake compile+. Host Apps
88
87
  # that only need "the Sandbox could not be set up" rescue +SetupError+;
89
88
  # those wanting to special-case the unbuilt-artifact state rescue
90
89
  # +ModuleNotBuiltError+ first.
@@ -120,21 +119,26 @@ module Kobako
120
119
  end
121
120
  end
122
121
 
123
- # docs/behavior.md Error Classes: HandlerExhaustedError is the canonical
124
- # SandboxError subclass for the id-cap-hit path (B-21). Raised when the
125
- # per-invocation Handle ID counter in Catalog::Handles reaches
126
- # +0x7fff_ffff+ (2³¹ − 1) and further allocation would exceed the cap.
122
+ # HandlerExhaustedError is the canonical SandboxError subclass for the
123
+ # id-cap-hit path. Raised when the per-invocation Handle ID counter in
124
+ # Catalog::Handles reaches +0x7fff_ffff+ (2³¹ 1) and further
125
+ # allocation would exceed the cap.
127
126
  class HandlerExhaustedError < SandboxError; end
128
127
 
129
- # docs/behavior.md Error Classes: BytecodeError is the SandboxError
130
- # subclass raised when a `#preload(binary:)` snippet fails structural
131
- # validation during the first invocation's snippet replay against a
132
- # fresh `mrb_state` (E-37 RITE version mismatch, E-38 corrupt body).
133
- # Bytecode that loads cleanly and then raises at top level is E-36
134
- # and surfaces as plain `SandboxError` with the natural mruby class
135
- # preserved. Inherits from SandboxError so a single
128
+ # BytecodeError is the SandboxError subclass raised when a
129
+ # `#preload(binary:)` snippet fails structural validation during the
130
+ # first invocation's snippet replay against a fresh `mrb_state` (RITE
131
+ # version mismatch or corrupt body). Bytecode that loads cleanly and
132
+ # then raises at top level surfaces as plain `SandboxError` with the
133
+ # natural mruby class preserved. Inherits from SandboxError so a single
136
134
  # `rescue Kobako::SandboxError` covers both source and bytecode
137
135
  # snippet failures while callers wanting bytecode-specific handling
138
136
  # can `rescue Kobako::BytecodeError` directly.
139
137
  class BytecodeError < SandboxError; end
138
+
139
+ # Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
140
+ # wait exceeded the configured +checkout_timeout+ while every slot was
141
+ # held. No Sandbox state is touched — retrying succeeds as soon as a holder
142
+ # returns its Sandbox.
143
+ class PoolTimeoutError < Error; end
140
144
  end
data/lib/kobako/handle.rb CHANGED
@@ -3,9 +3,8 @@
3
3
  module Kobako
4
4
  # Wire-level value object for an ext-0x01 Capability Handle, used in both
5
5
  # directions across the Sandbox boundary: as a Service method's return
6
- # value (guest→host return path; {docs/behavior.md B-14}[link:../../docs/behavior.md])
7
- # and as a +#run+ argument auto-wrapped by the host
8
- # ({docs/behavior.md B-34}[link:../../docs/behavior.md]).
6
+ # value (guest→host return path) and as a +#run+ argument auto-wrapped
7
+ # by the host.
9
8
  #
10
9
  # SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
11
10
  # payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # A named grouping of Members for one Sandbox
5
- # ({docs/behavior.md B-07..B-11}[link:../../docs/behavior.md]).
4
+ # A named grouping of Members for one Sandbox.
6
5
  # Returned by +Sandbox#define+. Each instance owns a flat name→object
7
6
  # table of Members; member binding is validated against {NAME_PATTERN}.
8
7
  class Namespace
9
- # Ruby constant-name pattern shared by Namespace and Member names
10
- # ({docs/behavior.md B-07/B-08 Notes}[link:../../docs/behavior.md]).
8
+ # Ruby constant-name pattern shared by Namespace and Member names.
11
9
  NAME_PATTERN = /\A[A-Z]\w*\z/
12
10
 
13
11
  attr_reader :name
@@ -18,15 +16,19 @@ module Kobako
18
16
  def initialize(name)
19
17
  @name = name
20
18
  @members = {} # : Hash[String, untyped]
19
+ @sealed = false
21
20
  end
22
21
 
23
22
  # Bind +object+ under +member+ inside this Namespace. +member+ is a
24
23
  # constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
25
24
  # object that responds to the methods guest code will invoke. Returns
26
25
  # +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]).
26
+ # match the constant pattern, when a Member of the same name is
27
+ # already bound, or when the owning Sandbox's first invocation has
28
+ # sealed Service registration.
29
29
  def bind(member, object)
30
+ raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
31
+
30
32
  member_str = validate_member_name!(member)
31
33
  raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
32
34
 
@@ -34,6 +36,15 @@ module Kobako
34
36
  self
35
37
  end
36
38
 
39
+ # Mark this Namespace as sealed. Called by
40
+ # +Kobako::Catalog::Namespaces#seal!+ on the owning Sandbox's first
41
+ # invocation; afterwards {#bind} raises +ArgumentError+. Idempotent;
42
+ # returns +self+.
43
+ def seal!
44
+ @sealed = true
45
+ self
46
+ end
47
+
37
48
  # Member lookup; raises +KeyError+ when no Member is registered
38
49
  # under +member+.
39
50
  def fetch(member)