kobako 0.3.0 → 0.5.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +85 -6
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +1 -1
  8. data/ext/kobako/src/lib.rs +4 -2
  9. data/ext/kobako/src/{wasm → runtime}/cache.rs +22 -18
  10. data/ext/kobako/src/runtime/capture.rs +91 -0
  11. data/ext/kobako/src/runtime/config.rs +26 -0
  12. data/ext/kobako/src/runtime/dispatch.rs +211 -0
  13. data/ext/kobako/src/runtime/exports.rs +51 -0
  14. data/ext/kobako/src/runtime/guest_mem.rs +228 -0
  15. data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +195 -81
  16. data/ext/kobako/src/runtime/trap.rs +134 -0
  17. data/ext/kobako/src/runtime.rs +782 -0
  18. data/ext/kobako/src/snapshot.rs +110 -0
  19. data/lib/kobako/capture.rb +11 -16
  20. data/lib/kobako/catalog/handles.rb +107 -0
  21. data/lib/kobako/catalog/namespaces.rb +99 -0
  22. data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
  23. data/lib/kobako/catalog.rb +18 -0
  24. data/lib/kobako/codec/decoder.rb +13 -7
  25. data/lib/kobako/codec/factory.rb +21 -18
  26. data/lib/kobako/codec/utils.rb +118 -29
  27. data/lib/kobako/codec.rb +6 -3
  28. data/lib/kobako/errors.rb +45 -28
  29. data/lib/kobako/fault.rb +40 -0
  30. data/lib/kobako/handle.rb +60 -0
  31. data/lib/kobako/namespace.rb +67 -0
  32. data/lib/kobako/outcome.rb +55 -29
  33. data/lib/kobako/runtime.rb +30 -0
  34. data/lib/kobako/sandbox.rb +131 -67
  35. data/lib/kobako/sandbox_options.rb +6 -9
  36. data/lib/kobako/snapshot.rb +40 -0
  37. data/lib/kobako/snippet/binary.rb +6 -7
  38. data/lib/kobako/snippet/source.rb +8 -8
  39. data/lib/kobako/snippet.rb +7 -9
  40. data/lib/kobako/transport/dispatcher.rb +195 -0
  41. data/lib/kobako/transport/error.rb +24 -0
  42. data/lib/kobako/transport/request.rb +78 -0
  43. data/lib/kobako/transport/response.rb +69 -0
  44. data/lib/kobako/transport/run.rb +141 -0
  45. data/lib/kobako/transport/yield.rb +91 -0
  46. data/lib/kobako/transport/yielder.rb +89 -0
  47. data/lib/kobako/transport.rb +24 -0
  48. data/lib/kobako/usage.rb +41 -0
  49. data/lib/kobako/version.rb +1 -1
  50. data/lib/kobako.rb +4 -3
  51. data/release-please-config.json +24 -0
  52. data/sig/kobako/capture.rbs +0 -2
  53. data/sig/kobako/{rpc/handle_table.rbs → catalog/handles.rbs} +3 -9
  54. data/sig/kobako/catalog/namespaces.rbs +17 -0
  55. data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
  56. data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
  57. data/sig/kobako/codec/decoder.rbs +2 -1
  58. data/sig/kobako/codec/factory.rbs +3 -3
  59. data/sig/kobako/codec/utils.rbs +11 -1
  60. data/sig/kobako/errors.rbs +7 -7
  61. data/sig/kobako/fault.rbs +19 -0
  62. data/sig/kobako/handle.rbs +18 -0
  63. data/sig/kobako/namespace.rbs +19 -0
  64. data/sig/kobako/outcome.rbs +2 -2
  65. data/sig/kobako/runtime.rbs +23 -0
  66. data/sig/kobako/sandbox.rbs +10 -7
  67. data/sig/kobako/snapshot.rbs +15 -0
  68. data/sig/kobako/transport/dispatcher.rbs +34 -0
  69. data/sig/kobako/transport/error.rbs +6 -0
  70. data/sig/kobako/transport/request.rbs +32 -0
  71. data/sig/kobako/transport/response.rbs +30 -0
  72. data/sig/kobako/transport/run.rbs +27 -0
  73. data/sig/kobako/transport/yield.rbs +34 -0
  74. data/sig/kobako/transport/yielder.rbs +21 -0
  75. data/sig/kobako/transport.rbs +4 -0
  76. data/sig/kobako/usage.rbs +11 -0
  77. metadata +52 -30
  78. data/ext/kobako/src/wasm/dispatch.rs +0 -161
  79. data/ext/kobako/src/wasm/instance.rs +0 -771
  80. data/ext/kobako/src/wasm.rs +0 -125
  81. data/lib/kobako/invocation.rb +0 -112
  82. data/lib/kobako/rpc/dispatcher.rb +0 -169
  83. data/lib/kobako/rpc/envelope.rb +0 -118
  84. data/lib/kobako/rpc/fault.rb +0 -41
  85. data/lib/kobako/rpc/handle.rb +0 -39
  86. data/lib/kobako/rpc/handle_table.rb +0 -107
  87. data/lib/kobako/rpc/namespace.rb +0 -74
  88. data/lib/kobako/rpc/server.rb +0 -158
  89. data/lib/kobako/rpc.rb +0 -11
  90. data/lib/kobako/wasm.rb +0 -25
  91. data/sig/kobako/invocation.rbs +0 -23
  92. data/sig/kobako/rpc/dispatcher.rbs +0 -33
  93. data/sig/kobako/rpc/envelope.rbs +0 -51
  94. data/sig/kobako/rpc/fault.rbs +0 -20
  95. data/sig/kobako/rpc/handle.rbs +0 -19
  96. data/sig/kobako/rpc/namespace.rbs +0 -24
  97. data/sig/kobako/rpc/server.rbs +0 -37
  98. data/sig/kobako/wasm.rbs +0 -39
@@ -6,8 +6,8 @@ require "msgpack"
6
6
 
7
7
  require_relative "error"
8
8
  require_relative "utils"
9
- require_relative "../rpc/handle"
10
- require_relative "../rpc/fault"
9
+ require_relative "../handle"
10
+ require_relative "../fault"
11
11
 
12
12
  module Kobako
13
13
  module Codec
@@ -89,16 +89,18 @@ module Kobako
89
89
  # binary-encoding fallback that msgpack-gem's default unpacker
90
90
  # would otherwise apply. The re-tag step lives here because the
91
91
  # msgpack ext-type unpacker hands us binary bytes; the assertion
92
- # itself is shared with {Decoder} via {Utils.assert_utf8!}.
92
+ # itself is shared with {Decoder} via {Utils.assert_utf8!}. The
93
+ # +"Symbol"+ label keeps the error message in Ruby vocabulary
94
+ # rather than wire-ext-code vocabulary.
93
95
  def unpack_symbol(payload)
94
96
  name = payload.b.force_encoding(Encoding::UTF_8)
95
- Utils.assert_utf8!(name, "ext 0x00 payload")
97
+ Utils.assert_utf8!(name, "Symbol payload")
96
98
  name.to_sym
97
99
  end
98
100
 
99
101
  def register_handle
100
102
  @factory.register_type(
101
- EXT_HANDLE, RPC::Handle,
103
+ EXT_HANDLE, Kobako::Handle,
102
104
  packer: ->(handle) { [handle.id].pack("N") },
103
105
  unpacker: ->(payload) { unpack_handle(payload) }
104
106
  )
@@ -106,23 +108,24 @@ module Kobako
106
108
 
107
109
  def register_fault
108
110
  @factory.register_type(
109
- EXT_ERRENV, RPC::Fault,
111
+ EXT_ERRENV, Kobako::Fault,
110
112
  packer: ->(fault) { pack_fault(fault) },
111
113
  unpacker: ->(payload) { unpack_fault(payload) }
112
114
  )
113
115
  end
114
116
 
115
- # Peel off the fixext-4 frame, hand the bytes to +RPC::Handle.new+, and
116
- # translate the +ArgumentError+ raised by Handle's invariants into
117
- # a wire-layer +InvalidType+ via {Codec::Utils.wire_boundary}.
117
+ # Peel off the fixext-4 frame, hand the bytes to the
118
+ # Host-Gem-internal +Kobako::Handle.restore+ factory, and
119
+ # translate the +ArgumentError+ raised by Handle's invariants
120
+ # into a wire-layer +InvalidType+ via {Codec::Utils.with_boundary}.
118
121
  # The Value Object owns the id-range contract; this method only
119
122
  # owns the frame shape.
120
123
  def unpack_handle(payload)
121
124
  bytes = payload.b
122
- raise InvalidType, "ext 0x01 payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
125
+ raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
123
126
 
124
127
  id = bytes.unpack1("N") # : Integer
125
- Codec::Utils.wire_boundary { RPC::Handle.new(id) }
128
+ Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
126
129
  end
127
130
 
128
131
  # Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
@@ -133,9 +136,10 @@ module Kobako
133
136
  Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
134
137
  end
135
138
 
136
- # Peel the embedded msgpack map and hand it to +RPC::Fault.new+;
137
- # translate the value-object's +ArgumentError+ into +InvalidType+
138
- # at the wire boundary. Inner decode goes through {Decoder} (not
139
+ # Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
140
+ # inside {Decoder.decode}'s block form, so the value-object's
141
+ # +ArgumentError+ invariants surface as +InvalidType+ through the
142
+ # decoder boundary. Inner decode goes through {Decoder} (not
139
143
  # +factory.load+) so the embedded +str+ payloads flow through the
140
144
  # same UTF-8 validation as a top-level decode.
141
145
  #
@@ -147,11 +151,10 @@ module Kobako
147
151
  # +factory.load+ to "simplify": that path bypasses UTF-8 validation
148
152
  # and re-opens the Decoder's special case for Fault (removed in M5).
149
153
  def unpack_fault(payload)
150
- map = Decoder.decode(payload)
151
- raise InvalidType, "ext 0x02 payload must be a map" unless map.is_a?(Hash)
154
+ Decoder.decode(payload) do |map|
155
+ raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
152
156
 
153
- Codec::Utils.wire_boundary do
154
- RPC::Fault.new(type: map["type"], message: map["message"], details: map["details"])
157
+ Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
155
158
  end
156
159
  end
157
160
  end
@@ -1,56 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "error"
4
+ require_relative "../handle"
4
5
 
5
6
  module Kobako
6
7
  module Codec
7
- # Wire-codec helpers shared by the host-side encoders and decoders.
8
- # The single concern today is UTF-8 assertion at the wire boundary
9
- # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § str/bin
10
- # Encoding Rules and § Ext Types → ext 0x00). Two call sites lean on
11
- # this:
8
+ # Codec helpers shared by the host-side encoders and decoders.
9
+ # Three concerns live here today:
12
10
  #
13
- # - {Decoder} validates +str+ family payloads as it walks the
14
- # decoded value tree.
15
- # - {Factory} validates the +ext 0x00+ Symbol payload after
16
- # re-tagging the binary bytes as UTF-8.
11
+ # - UTF-8 assertion at the codec boundary
12
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
13
+ # § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
14
+ # {Decoder} when walking +str+ family payloads and by {Factory}
15
+ # when validating the +ext 0x00+ Symbol payload.
16
+ # - +ArgumentError+ translation at the codec boundary
17
+ # ({with_boundary}) so the public taxonomy stays
18
+ # {Kobako::Codec::Error}.
19
+ # - Representability predicate ({representable?}) and the symmetric
20
+ # host→guest +#run+ argument walk ({deep_wrap}) used by
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]).
17
24
  #
18
- # Encoding setup (re-tagging binary as UTF-8 when needed) stays at
19
- # the caller only the assertion shape is shared. The helper does
20
- # not mutate +string+; it only inspects +String#valid_encoding?+
21
- # against +string+'s current encoding tag.
25
+ # All helpers are pure they only inspect inputs, never mutate
26
+ # themexcept {deep_wrap}, whose only side effect is allocating
27
+ # new Handle ids into the supplied table.
22
28
  module Utils
23
29
  module_function
24
30
 
25
31
  # Raise {InvalidEncoding} unless +string+'s bytes are valid under
26
32
  # its current encoding tag. +label+ is the caller-supplied prefix
27
- # for the error message (e.g. +"str payload"+, +"ext 0x00 payload"+).
33
+ # for the error message (e.g. +"str payload"+, +"Symbol payload"+).
28
34
  def assert_utf8!(string, label)
29
35
  return if string.valid_encoding?
30
36
 
31
37
  raise InvalidEncoding, "#{label} is not valid UTF-8"
32
38
  end
33
39
 
34
- # Run +block+ at the wire boundary: every wire Value Object
35
- # (Handle / Fault / Request / Response / Panic) raises
36
- # +ArgumentError+ when an invariant is violated at construction,
37
- # and the wire boundary surfaces those violations to callers as
38
- # {InvalidType} so the public taxonomy stays
39
- # {Kobako::Codec::Error} and never leaks +ArgumentError+ from the
40
- # Ruby standard library.
40
+ # Run +block+ at the codec boundary: a value object raises
41
+ # +ArgumentError+ when an invariant is violated at construction, and
42
+ # this helper surfaces that as {InvalidType} so the public taxonomy
43
+ # stays {Kobako::Codec::Error} and never leaks +ArgumentError+ from
44
+ # the Ruby standard library.
41
45
  #
42
- # Wrap any block that constructs a wire Value Object from decoded
43
- # bytes with this helper to keep the five decode sites uniform
44
- # Request / Response in +Kobako::RPC+, Panic map in
45
- # +Kobako::Outcome+, and the Handle / Fault ext-type unpackers in
46
- # {Factory}. Do not use it for general-purpose validation outside
47
- # the wire boundary host-layer +ArgumentError+ values should
48
- # propagate unchanged.
49
- def wire_boundary
46
+ # Most construction sites no longer reach for this directly: a value
47
+ # object built inside a {Decoder.decode} block has its
48
+ # +ArgumentError+ mapped to {InvalidType} by the decoder's own
49
+ # rescue. The lone remaining caller is {Factory#unpack_handle}, which
50
+ # builds +Handle.restore+ from a raw 4-byte fixext payload without a
51
+ # {Decoder.decode} call. Do not use it for general-purpose validation
52
+ # outside the codec boundary — host-layer +ArgumentError+ values
53
+ # should propagate unchanged.
54
+ def with_boundary
50
55
  yield
51
56
  rescue ::ArgumentError => e
52
57
  raise InvalidType, e.message
53
58
  end
59
+
60
+ # Inclusive Integer range the msgpack gem encodes without raising
61
+ # +RangeError+ at encode time — signed +int 64+ minimum through
62
+ # unsigned +uint 64+ maximum
63
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
64
+ # Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
65
+ # Anchored as a +Range+ so {primitive_type?} stays a single
66
+ # dispatch line. This is the codec's encode domain — not to
67
+ # be confused with the Handle id range, which lives on
68
+ # +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
69
+ # represents a different concept entirely.
70
+ MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
71
+
72
+ # Codec-type predicate
73
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
74
+ # Mapping). Returns +true+ when +value+ belongs to the closed
75
+ # 12-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
76
+ # +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
77
+ # +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
78
+ # representable, or +Hash+ whose every key and value are
79
+ # representable. Integers outside the codec's signed-64 /
80
+ # unsigned-64 union are rejected so the predicate agrees with the
81
+ # msgpack gem's encode-time +RangeError+ behaviour the codec
82
+ # already surfaces as {UnsupportedType}.
83
+ def representable?(value)
84
+ primitive_type?(value) || container_representable?(value)
85
+ end
86
+
87
+ # Deep-walk Array / Hash containers in +value+ and replace every
88
+ # leaf that fails {representable?} with a +Kobako::Handle+
89
+ # allocated from +handler+
90
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]). The
91
+ # walk only descends through representable container shapes
92
+ # (Array, Hash) one structural level at a time; a non-representable
93
+ # leaf is wrapped as-is without inspecting its internal structure.
94
+ # An existing +Kobako::Handle+ is representable and passes through
95
+ # unchanged — auto-wrap never re-wraps a Handle.
96
+ #
97
+ # +value+ may be any Ruby value; +handler+ must respond to
98
+ # +#alloc(object) -> Kobako::Handle+ (a host-side
99
+ # +Kobako::Catalog::Handles+). Returns a structurally equivalent value
100
+ # whose leaves are either representable or +Kobako::Handle+
101
+ # tokens.
102
+ #
103
+ # The block bodies spell +Utils.deep_wrap+ explicitly rather than
104
+ # the unqualified +deep_wrap+ because +module_function+ makes the
105
+ # instance copy of these helpers private; an implicit receiver
106
+ # inside a block would resolve against the enclosing +self+
107
+ # (still +Utils+ at definition time, but the qualified form keeps
108
+ # the dispatch readable when the recursive call sits inside a
109
+ # Proc captured from elsewhere).
110
+ def deep_wrap(value, handler)
111
+ case value
112
+ when ::Array then value.map { |element| Utils.deep_wrap(element, handler) }
113
+ when ::Hash then value.transform_values { |val| Utils.deep_wrap(val, handler) }
114
+ else
115
+ representable?(value) ? value : handler.alloc(value)
116
+ end
117
+ end
118
+
119
+ # Predicate split out of {representable?} for cyclomatic
120
+ # budget — the closed-set non-container branch. Returns +true+ for
121
+ # the scalar leaves and an existing Handle. Not part of the
122
+ # public surface; reach for {representable?} instead.
123
+ def primitive_type?(value)
124
+ case value
125
+ when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
126
+ when ::Integer then MSGPACK_INT_RANGE.cover?(value)
127
+ else false
128
+ end
129
+ end
130
+
131
+ # Predicate split out of {representable?} for cyclomatic
132
+ # budget — the container branch. Recurses into Array elements and
133
+ # Hash key+value pairs through the public {representable?}.
134
+ # Not part of the public surface; reach for {representable?}
135
+ # instead.
136
+ def container_representable?(value)
137
+ case value
138
+ when ::Array then value.all? { |element| Utils.representable?(element) }
139
+ when ::Hash then value.all? { |key, val| Utils.representable?(key) && Utils.representable?(val) }
140
+ else false
141
+ end
142
+ end
54
143
  end
55
144
  end
56
145
  end
data/lib/kobako/codec.rb CHANGED
@@ -6,9 +6,12 @@ module Kobako
6
6
  # Host-side MessagePack codec for the kobako wire contract — the
7
7
  # byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
8
8
  # Two consumers sit on top:
9
- # +Kobako::RPC+ pins the RPC framing (Request / Response)
10
- # and +Kobako::Outcome+ owns the per-+#run+ outcome envelope (Result
11
- # body / Panic map).
9
+ # +Kobako::Transport+ pins the host↔guest framing (Request / Response /
10
+ # Run / Yield) and +Kobako::Outcome+ owns the per-+#run+ outcome
11
+ # envelope (Result body / Panic map). The ext-type leaves this layer
12
+ # carries — +Kobako::Handle+ (0x01) and +Kobako::Fault+ (0x02) — live at
13
+ # the kobako root so the codec can register them without depending
14
+ # upward on Transport.
12
15
  #
13
16
  # Backed by the official +msgpack+ gem via {Factory}; {Encoder} and
14
17
  # {Decoder} are thin wrappers that register the three kobako-specific
data/lib/kobako/errors.rb CHANGED
@@ -2,29 +2,39 @@
2
2
 
3
3
  # Top-level Kobako namespace.
4
4
  module Kobako
5
- # Three-class error taxonomy (docs/behavior.md § Error Scenarios).
5
+ # Error taxonomy (docs/behavior.md § Error Scenarios).
6
6
  #
7
7
  # Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
8
- # exactly one of these three classes. Attribution is decided after the
8
+ # exactly one of three invocation-outcome classes. Attribution is decided after the
9
9
  # guest binary returns control to the host (docs/behavior.md
10
10
  # "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
11
11
  #
12
- # Three top-level branches:
12
+ # Three invocation-outcome branches:
13
13
  #
14
14
  # * {TrapError} — Wasm engine layer (trap, OOM, unreachable, or a
15
15
  # wire-violation fallback signalling a corrupted
16
16
  # guest runtime).
17
17
  # * {SandboxError} — sandbox / wire layer (mruby script error,
18
18
  # wire-decode failure on an otherwise valid tag,
19
- # HandleTable exhaustion, output buffer overrun).
20
- # * {ServiceError} — service / capability layer (a Service RPC that
21
- # failed and was not rescued inside the script).
19
+ # Catalog::Handles exhaustion, output buffer overrun).
20
+ # * {ServiceError} — service / capability layer (a Service capability
21
+ # call that failed and was not rescued inside the
22
+ # script).
23
+ #
24
+ # A fourth branch sits outside the invocation taxonomy:
25
+ #
26
+ # * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
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). Not an invocation
30
+ # outcome, so it never passes through the two-step
31
+ # attribution decision.
22
32
  #
23
33
  # Subclasses pinned by docs/behavior.md Error Classes:
24
34
  #
25
- # * {HandleTableExhausted} < {SandboxError} id cap hit (B-21).
26
- # * {ServiceError::Disconnected} < {ServiceError} — `:disconnected`
27
- # sentinel hit on the HandleTable (E-14).
35
+ # * {ModuleNotBuiltError} < {SetupError} Guest Binary artifact absent
36
+ # at +wasm_path+ (E-40).
37
+ # * {HandlerExhaustedError} < {SandboxError} Handle id cap hit (B-21).
28
38
 
29
39
  # Base for all kobako-raised errors so callers that want to ignore the
30
40
  # taxonomy can rescue a single class.
@@ -59,6 +69,27 @@ module Kobako
59
69
  # point; discard and recreate before another execution.
60
70
  class MemoryLimitError < TrapError; end
61
71
 
72
+ # Construction-layer error raised by +Kobako::Sandbox.new+ /
73
+ # +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
74
+ # from the configured +wasm_path+ before any invocation runs —
75
+ # 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
78
+ # is not an invocation, so +SetupError+ sits beside the invocation
79
+ # taxonomy under +Kobako::Error+ rather than under +TrapError+: no
80
+ # Sandbox is produced, so the +TrapError+ "discard and recreate"
81
+ # recovery contract does not apply.
82
+ class SetupError < Error; end
83
+
84
+ # The named +SetupError+ subclass for the common, actionable case:
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
88
+ # that only need "the Sandbox could not be set up" rescue +SetupError+;
89
+ # those wanting to special-case the unbuilt-artifact state rescue
90
+ # +ModuleNotBuiltError+ first.
91
+ class ModuleNotBuiltError < SetupError; end
92
+
62
93
  # Sandbox / wire layer. Raised when the guest ran to completion but
63
94
  # execution failed due to a mruby script error, a protocol fault, or a
64
95
  # host-side wire decode failure on an otherwise valid outcome tag.
@@ -87,27 +118,13 @@ module Kobako
87
118
  @backtrace_lines = backtrace_lines
88
119
  @details = details
89
120
  end
90
-
91
- # docs/behavior.md Error Classes: ServiceError::Disconnected is raised
92
- # when the RPC target Handle resolves to the `:disconnected` sentinel
93
- # in the HandleTable (ABA protection rule — id exists but entry was
94
- # invalidated). E-14.
95
- class Disconnected < ServiceError; end
96
121
  end
97
122
 
98
- # HandleTable lookup-failure error (unknown id passed to #fetch /
99
- # #release). A SandboxError subclass: per the wire-layer rule, an
100
- # unknown Handle id surfaces as a `type="undefined"` Response.error
101
- # envelope inside RpcDispatcher and never reaches the Host App
102
- # directly; outside that path (e.g. tests poking the HandleTable
103
- # directly), it surfaces as a SandboxError.
104
- class HandleTableError < SandboxError; end
105
-
106
- # docs/behavior.md Error Classes: HandleTableExhausted is the canonical
107
- # SandboxError subclass for the id-cap-hit path (B-21). Inherits from
108
- # HandleTableError so a single `rescue Kobako::HandleTableError` covers
109
- # both lookup-failure and cap-exhaustion paths.
110
- class HandleTableExhausted < HandleTableError; end
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.
127
+ class HandlerExhaustedError < SandboxError; end
111
128
 
112
129
  # docs/behavior.md Error Classes: BytecodeError is the SandboxError
113
130
  # subclass raised when a `#preload(binary:)` snippet fails structural
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # Wire-level value object for an ext-0x02 Exception envelope.
5
+ #
6
+ # Top-level shared wire primitive: like +Kobako::Handle+ (ext 0x01),
7
+ # +Fault+ is a MessagePack ext-type leaf registered by
8
+ # +Kobako::Codec::Factory+ and rides nested inside other envelopes (a
9
+ # +Kobako::Transport::Response+ error payload, or another Fault's
10
+ # +details+). It lives at the kobako root rather than under +Transport+
11
+ # because the Codec layer must register it, and Codec must not depend
12
+ # upward on Transport.
13
+ #
14
+ # SPEC pins the payload
15
+ # ({docs/wire-codec.md}[link:../../docs/wire-codec.md] § Ext Types
16
+ # → ext 0x02) to a msgpack map with exactly three keys:
17
+ # * "type" — one of "runtime", "argument", "undefined"
18
+ # * "message" — human-readable string
19
+ # * "details" — any wire-legal value, or nil when absent
20
+ #
21
+ # This object holds the *encoded* form. Reifying the corresponding Ruby
22
+ # exception class (RuntimeError, ArgumentError, Kobako::ServiceError, ...)
23
+ # is the responsibility of the dispatch layer, not the codec.
24
+ #
25
+ # Built on the +class X < Data.define(...)+ subclass form so the
26
+ # class body is fully Steep-visible; ruby/rbs upstream documents
27
+ # this as the Steep-friendly shape and the +Style/DataInheritance+
28
+ # cop is disabled on that basis (see +.rubocop.yml+).
29
+ class Fault < Data.define(:type, :message, :details)
30
+ VALID_TYPES = %w[runtime argument undefined].freeze
31
+
32
+ def initialize(type:, message:, details: nil)
33
+ raise ArgumentError, "type must be String" unless type.is_a?(String)
34
+ raise ArgumentError, "message must be String" unless message.is_a?(String)
35
+ raise ArgumentError, "type=#{type.inspect} not one of #{VALID_TYPES.inspect}" unless VALID_TYPES.include?(type)
36
+
37
+ super
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # Wire-level value object for an ext-0x01 Capability Handle, used in both
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]).
9
+ #
10
+ # SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
11
+ # payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
12
+ # § Ext Types → ext 0x01). ID 0 is reserved as the invalid sentinel;
13
+ # the maximum valid ID is 0x7fff_ffff (2^31 - 1).
14
+ #
15
+ # The constructor is internal to the Host Gem. +Kobako::Handle.new+ is
16
+ # privatised so Host App code cannot fabricate a Handle from a bare
17
+ # integer; legitimate Handle instances enter Host App code only as
18
+ # fields on raised error objects. The Host Gem itself constructs
19
+ # Handles through {.restore}, which exists at exactly two call
20
+ # sites: +Kobako::Codec::Factory#unpack_handle+ (wire decode) and
21
+ # +Kobako::Codec::Utils.deep_wrap+ / +Kobako::Transport::Dispatcher#wrap_as_handle+
22
+ # (allocator paths). Both live inside +lib/kobako/+ and are not part
23
+ # of any public surface.
24
+ #
25
+ # The mruby counterpart +Kobako::Handle+ lives inside the Wasm guest
26
+ # under the same canonical name and shares neither code nor instances
27
+ # with this host-side class.
28
+ class Handle < Data.define(:id)
29
+ # Inclusive lower bound on the wire Handle ID. ID 0 is reserved as
30
+ # the invalid sentinel and is never allocated.
31
+ MIN_ID = 1
32
+ # Inclusive upper bound on the wire Handle ID. The cap matches the
33
+ # u32 signed-positive range so Handle IDs fit in a signed integer
34
+ # on either side of the wire without re-encoding.
35
+ MAX_ID = 0x7fff_ffff
36
+
37
+ def initialize(id:)
38
+ raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
39
+ raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
40
+
41
+ super
42
+ end
43
+
44
+ private_class_method :new
45
+
46
+ # Host Gem–internal factory. Allocates the Data instance through
47
+ # +Class#allocate+ and dispatches +#initialize+ explicitly so the
48
+ # invariant checks still run, while keeping the public +.new+
49
+ # privatised against Host App callers.
50
+ #
51
+ # Two collaborators call this: the codec when an ext 0x01 frame is
52
+ # decoded off the wire, and the allocator paths when a host-side
53
+ # Ruby object is registered into the Sandbox's +Catalog::Handles+. Both
54
+ # paths live inside +lib/kobako/+ and treat this method as a
55
+ # package-private constructor.
56
+ def self.restore(id)
57
+ allocate.tap { |handle| handle.send(:initialize, id: id) }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # A named grouping of Members for one Sandbox
5
+ # ({docs/behavior.md B-07..B-11}[link:../../docs/behavior.md]).
6
+ # Returned by +Sandbox#define+. Each instance owns a flat name→object
7
+ # table of Members; member binding is validated against {NAME_PATTERN}.
8
+ 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]).
11
+ NAME_PATTERN = /\A[A-Z]\w*\z/
12
+
13
+ attr_reader :name
14
+
15
+ # Build a new Namespace. +name+ is an already-validated Namespace
16
+ # name (must satisfy {NAME_PATTERN}; validation is the caller's
17
+ # responsibility).
18
+ def initialize(name)
19
+ @name = name
20
+ @members = {} # : Hash[String, untyped]
21
+ end
22
+
23
+ # Bind +object+ under +member+ inside this Namespace. +member+ is a
24
+ # constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
25
+ # object that responds to the methods guest code will invoke. Returns
26
+ # +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]).
29
+ def bind(member, object)
30
+ member_str = validate_member_name!(member)
31
+ raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
32
+
33
+ @members[member_str] = object
34
+ self
35
+ end
36
+
37
+ # Member lookup; raises +KeyError+ when no Member is registered
38
+ # under +member+.
39
+ def fetch(member)
40
+ member_str = member.to_s
41
+ unless @members.key?(member_str)
42
+ raise KeyError,
43
+ "no member named #{member_str.inspect} in namespace #{@name.inspect}"
44
+ end
45
+
46
+ @members[member_str]
47
+ end
48
+
49
+ # Structured description for the guest preamble (Frame 1). Returns a
50
+ # two-element array +[name, member_keys]+ suitable for msgpack encoding.
51
+ def to_preamble
52
+ [@name, @members.keys]
53
+ end
54
+
55
+ private
56
+
57
+ def validate_member_name!(member)
58
+ member_str = member.to_s
59
+ unless NAME_PATTERN.match?(member_str)
60
+ raise ArgumentError,
61
+ "MemberName must match #{NAME_PATTERN.inspect} (got #{member.inspect})"
62
+ end
63
+
64
+ member_str
65
+ end
66
+ end
67
+ end