kobako 0.27.0 → 0.29.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -8,36 +8,18 @@ require_relative "utils"
8
8
 
9
9
  module Kobako
10
10
  module Codec
11
- # Module-level entry point for the host side of the kobako wire
12
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
13
- #
14
- # Translates msgpack gem exceptions into the kobako error taxonomy
15
- # (TruncatedInputError, InvalidTypeError, InvalidEncodingError, UnsupportedTypeError) so
16
- # callers can pattern-match on the SPEC's wire-violation categories
17
- # without leaking the gem's internal exception classes.
18
- #
19
- # Public API is a single function — +.decode+. The decoder is
20
- # stateless; the +MessagePack::Unpacker+ instance is built per call
21
- # because callers always decode exactly one wire value at a time.
22
- module Decoder
23
- # Decode +bytes+ into one Ruby value and validate transitively
24
- # against the SPEC type mapping. Raises TruncatedInputError, InvalidTypeError,
25
- # or InvalidEncodingError on wire violations.
26
- #
27
- # When a block is given, the decoded value is yielded and the block's
28
- # result is returned — wire Value Objects use this to build themselves
29
- # from the decoded payload. The block runs inside this method's
30
- # rescue, so a Value Object's +ArgumentError+ invariant failure
31
- # surfaces as InvalidTypeError without a separate Utils.with_boundary
32
- # wrapper at the call site.
11
+ module Decoder # :nodoc:
12
+ # The block runs inside this method's rescue, so a Value Object built
13
+ # from the decoded payload reports a broken invariant as
14
+ # InvalidTypeError without its own Utils.with_boundary.
33
15
  def self.decode(bytes)
16
+ ValueBudget.assert_within!(bytes, State.current.value_budget)
34
17
  value = FACTORY.load(bytes.b)
35
18
  validate_utf8!(value)
36
19
  block_given? ? yield(value) : value
37
20
  # msgpack gem raises the format/type errors below; +ArgumentError+
38
- # comes from our ext-type validators (Handle id range, Exception type
39
- # whitelist) and from a yielded block's Value Object invariants — both
40
- # are wire violations, so both map to InvalidTypeError.
21
+ # covers a yielded block's Value Object invariants — a wire violation
22
+ # too, so it maps to InvalidTypeError.
41
23
  rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
42
24
  ::MessagePack::StackError, ::ArgumentError => e
43
25
  raise InvalidTypeError, e.message
@@ -48,9 +30,9 @@ module Kobako
48
30
  raise TruncatedInputError, e.message
49
31
  end
50
32
 
51
- # SPEC pins +str+ family payloads to UTF-8
52
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § str/bin
53
- # Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
33
+ # +str+ family payloads must be UTF-8
34
+ # (docs/wire/payload-msgpack.md § Text and Bytes). The msgpack gem
35
+ # returns UTF-8-tagged Strings for
54
36
  # str family but does not validate the bytes; +bin+ family decodes
55
37
  # to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
56
38
  # str-typed leaf via Utils.assert_utf8!.
@@ -7,28 +7,9 @@ require_relative "ext_types"
7
7
 
8
8
  module Kobako
9
9
  module Codec
10
- # Module-level entry point for the host side of the kobako wire
11
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
12
- #
13
- # The codec backbone is the official +msgpack+ gem: integers, floats,
14
- # strings, arrays, and maps go through the gem's narrowest-encoding
15
- # logic; the two kobako-specific ext types (0x00 Symbol, 0x01
16
- # Capability Handle) are registered by ExtTypes on the process-wide
17
- # factory.
18
- #
19
- # Public API is a single function — +.encode+. The codec is stateless;
20
- # there is no buffer accumulator and no streaming write API. Callers
21
- # that need to concatenate multiple encodings build the bytes
22
- # themselves.
23
- module Encoder
24
- # Encode +value+ to wire bytes (binary-encoded String).
25
- # SPEC's 11-entry type mapping is a closed set: a value outside it is
26
- # rejected as +UnsupportedTypeError+ by the factory's +BasicObject+ guard
27
- # (ExtTypes#register_unrepresentable), which raises before the msgpack
28
- # gem can route the value through +to_msgpack+ — so a permissive
29
- # +method_missing+ object cannot answer that probe and mis-encode. The
30
- # rescue below maps the two violations the guard does not reach onto the
31
- # same error: an integer outside i64..u64 (+RangeError+) and any
10
+ module Encoder # :nodoc:
11
+ # The rescue maps the two violations the factory's +BasicObject+ guard
12
+ # does not reach: an integer outside i64..u64 (+RangeError+) and any
32
13
  # packer-internal +NoMethodError+.
33
14
  #
34
15
  # The caller bounds +value+'s nesting first (Nesting): the packer takes
@@ -2,34 +2,13 @@
2
2
 
3
3
  module Kobako
4
4
  module Codec
5
- # Base class for all wire-codec faults raised by the pure-Ruby host codec.
6
- #
7
- # The wire codec implements the binary contract pinned in
8
- # {docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping.
9
- # Every wire violation surfaces as a
10
- # subclass of Error so callers can pattern-match on the specific
11
- # fault while still rescuing all codec faults via this base class.
12
- #
13
- # Higher layers (e.g. the Sandbox dispatch loop) translate these into
14
- # the public Kobako::SandboxError / Kobako::TrapError taxonomy.
15
- class Error < StandardError; end
5
+ class Error < StandardError; end # :nodoc:
6
+ class TruncatedInputError < Error; end # :nodoc:
16
7
 
17
- # Input ended before the type prefix or payload was fully consumed.
18
- class TruncatedInputError < Error; end
8
+ # Also raised on encode for a value nested past what the packer can walk.
9
+ class InvalidTypeError < Error; end # :nodoc:
19
10
 
20
- # The type byte at the current position is not in the 11-entry kobako
21
- # type mapping (e.g. an unknown ext code, or a reserved msgpack tag) —
22
- # or, on encode, a value nesting past what the packer can walk.
23
- class InvalidTypeError < Error; end
24
-
25
- # A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
26
- # payload was not valid UTF-8 — both are wire violations per SPEC.
27
- class InvalidEncodingError < Error; end
28
-
29
- # The encoder was handed a Ruby object whose type has no wire
30
- # representation (e.g. Range, Time). Higher layers may catch this
31
- # and re-route the value through Handle allocation, but at the
32
- # codec level it is a hard error.
33
- class UnsupportedTypeError < Error; end
11
+ class InvalidEncodingError < Error; end # :nodoc:
12
+ class UnsupportedTypeError < Error; end # :nodoc:
34
13
  end
35
14
  end
@@ -8,21 +8,16 @@ require_relative "state"
8
8
  require_relative "../handle"
9
9
 
10
10
  module Kobako
11
- module Codec
12
- # The kobako wire ext-type conversions
13
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types)
14
- # as pure functions: per-operation decode state is threaded in as an
15
- # argument, so the module itself holds nothing. #build_factory assembles
16
- # the one +MessagePack::Factory+ these conversions are registered on.
17
- module ExtTypes
11
+ module Codec # :nodoc:
12
+ module ExtTypes # :nodoc:
18
13
  # MessagePack ext type code reserved for Symbol
19
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types
20
- # → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
14
+ # (docs/wire/payload-msgpack.md § ext 0x00 — Symbol). Module-private —
15
+ # mirrors +codec::EXT_SYMBOL+ on the
21
16
  # Rust side.
22
17
  EXT_SYMBOL = 0x00
23
18
  # MessagePack ext type code reserved for Capability Handle
24
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types
25
- # → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
19
+ # (docs/wire/payload-msgpack.md § ext 0x01 — Handle). Module-private —
20
+ # mirrors +codec::EXT_HANDLE+ on the
26
21
  # Rust side.
27
22
  EXT_HANDLE = 0x01
28
23
  private_constant :EXT_SYMBOL, :EXT_HANDLE
@@ -31,19 +26,13 @@ module Kobako
31
26
  # never emitted (the guard's packer always raises) and never decoded
32
27
  # (no unpacker is registered, so the id stays an UnknownExtTypeError on
33
28
  # the wire), so it is not a wire ext type: deliberately not named
34
- # +EXT_*+ like the two real ext codes, since it has no Rust-side mirror
35
- # and must stay outside the wire-symmetry inventory.
29
+ # +EXT_*+ like the two real ext codes, since it has no Rust-side mirror.
36
30
  UNREPRESENTABLE_GUARD_ID = 0x7F
37
31
  private_constant :UNREPRESENTABLE_GUARD_ID
38
32
 
39
- module_function
40
-
41
- # Assemble a +MessagePack::Factory+ with the two kobako ext types plus
42
- # the unrepresentable-value guard registered, frozen because
43
- # registration is its only mutation and happens exactly once. The
44
- # stateful conversions resolve their per-operation state at call time,
45
- # so one registered factory serves every thread.
46
- def build_factory
33
+ # The stateful conversions resolve their per-operation state at call
34
+ # time, so one frozen factory serves every thread.
35
+ def self.build_factory
47
36
  factory = MessagePack::Factory.new
48
37
  register_symbol(factory)
49
38
  register_handle(factory)
@@ -51,89 +40,78 @@ module Kobako
51
40
  factory.freeze
52
41
  end
53
42
 
54
- # Symbol-to-name packer for the ext-0x00 registration.
55
- def pack_symbol(symbol)
56
- symbol.name
57
- end
43
+ class << self
44
+ private
58
45
 
59
- # Validate the ext-0x00 payload as UTF-8 and intern. Raises
60
- # InvalidEncodingError on invalid bytes — SPEC forbids the
61
- # binary-encoding fallback that msgpack-gem's default unpacker
62
- # would otherwise apply. The re-tag step lives here because the
63
- # msgpack ext-type unpacker hands us binary bytes; the assertion
64
- # itself is shared with Decoder via Utils.assert_utf8!. The
65
- # +"Symbol"+ label keeps the error message in Ruby vocabulary
66
- # rather than wire-ext-code vocabulary.
67
- def unpack_symbol(payload)
68
- name = payload.b.force_encoding(Encoding::UTF_8)
69
- Utils.assert_utf8!(name, "Symbol payload")
70
- name.to_sym
71
- end
46
+ def pack_symbol(symbol)
47
+ symbol.name
48
+ end
72
49
 
73
- # Handle-id packer for the ext-0x01 registration: the fixext-4
74
- # big-endian id frame.
75
- def pack_handle(handle)
76
- [handle.id].pack("N")
77
- end
50
+ # Refuses the binary-encoding fallback that msgpack-gem's default
51
+ # unpacker would otherwise apply to invalid bytes.
52
+ def unpack_symbol(payload)
53
+ name = payload.b.force_encoding(Encoding::UTF_8)
54
+ Utils.assert_utf8!(name, "Symbol payload")
55
+ name.to_sym
56
+ end
78
57
 
79
- # Peel off the fixext-4 frame, hand the bytes to the
80
- # Host-Gem-internal +Kobako::Handle.restore+ factory, and
81
- # translate the +ArgumentError+ raised by Handle's invariants
82
- # into a wire-layer +InvalidTypeError+ via Codec::Utils.with_boundary.
83
- # The Value Object owns the id-range contract; this method only
84
- # owns the frame shape. Records the Handle sighting on +state+ so a
85
- # Handle-free decode can skip the downstream resolution walk.
86
- def unpack_handle(payload, state)
87
- state.record_handle!
88
- bytes = payload.b
89
- raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
58
+ def pack_handle(handle)
59
+ [handle.id].pack("N")
60
+ end
90
61
 
91
- id = bytes.unpack1("N") # : Integer
92
- Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
93
- end
62
+ # Handle owns the id-range contract; this method owns only the frame
63
+ # shape. The sighting is recorded so a Handle-free decode can skip the
64
+ # downstream resolution walk.
65
+ def unpack_handle(payload, state)
66
+ state.record_handle
67
+ bytes = payload.b
68
+ raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
94
69
 
95
- def register_symbol(factory)
96
- factory.register_type(
97
- EXT_SYMBOL, Symbol,
98
- packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
99
- unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
100
- )
101
- end
70
+ id = bytes.unpack1("N") # : Integer
71
+ Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
72
+ end
102
73
 
103
- def register_handle(factory)
104
- factory.register_type(
105
- EXT_HANDLE, Kobako::Handle,
106
- packer: ->(handle) { ExtTypes.pack_handle(handle) },
107
- unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
108
- )
109
- end
74
+ def register_symbol(factory)
75
+ factory.register_type(
76
+ EXT_SYMBOL, Symbol,
77
+ packer: ->(symbol) { pack_symbol(symbol) },
78
+ unpacker: ->(payload) { unpack_symbol(payload) }
79
+ )
80
+ end
81
+
82
+ def register_handle(factory)
83
+ factory.register_type(
84
+ EXT_HANDLE, Kobako::Handle,
85
+ packer: ->(handle) { pack_handle(handle) },
86
+ unpacker: ->(payload) { unpack_handle(payload, State.current) }
87
+ )
88
+ end
110
89
 
111
- # A catch-all packer that rejects any value with no wire representation
112
- # as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
113
- # BasicObject-based proxies; the narrower Symbol / Handle
114
- # registrations still win by most-specific match, and native types never
115
- # reach it. Packer-only: the guard never writes bytes, so its id is inert
116
- # and the decode surface stays fail-closed.
117
- #
118
- # This makes the host's non-wire detection a positive allowlist — a value
119
- # outside the type set is rejected here rather than routed to +to_msgpack+
120
- # — matching the guest's classname allowlist and the Rust codec's closed
121
- # +Value+ enum. Without it, a value with a permissive +method_missing+
122
- # answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
123
- # of crossing as a Capability Handle.
124
- def register_unrepresentable(factory)
125
- factory.register_type(
126
- UNREPRESENTABLE_GUARD_ID, BasicObject,
127
- packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
128
- )
90
+ # A catch-all packer that rejects any value with no wire representation
91
+ # as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
92
+ # BasicObject-based proxies; the narrower Symbol / Handle
93
+ # registrations still win by most-specific match, and native types never
94
+ # reach it. Packer-only: the guard never writes bytes, so its id is inert
95
+ # and the decode surface stays fail-closed.
96
+ #
97
+ # This makes the host's non-wire detection a positive allowlist — a value
98
+ # outside the type set is rejected here rather than routed to +to_msgpack+
99
+ # — matching the guest's classname allowlist and the Rust codec's closed
100
+ # +Value+ enum. Without it, a value with a permissive +method_missing+
101
+ # answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
102
+ # of crossing as a Capability Handle.
103
+ def register_unrepresentable(factory)
104
+ factory.register_type(
105
+ UNREPRESENTABLE_GUARD_ID, BasicObject,
106
+ packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
107
+ )
108
+ end
129
109
  end
130
110
  end
131
111
 
132
- # The process-wide registered factory: ext registration is paid once at
133
- # load, and a registered +MessagePack::Factory+ only reads its type
134
- # registry afterwards, so every thread shares this instance for byte
135
- # work.
136
- FACTORY = ExtTypes.build_factory
112
+ # Registered once at load and only read afterwards, so every thread
113
+ # shares it.
114
+ FACTORY = ExtTypes.build_factory # :nodoc:
137
115
  private_constant :FACTORY
138
116
  end
139
117
  end
@@ -5,26 +5,11 @@ require_relative "../errors"
5
5
 
6
6
  module Kobako
7
7
  module Codec
8
- # Substitutes Capability Handles into and out of a Ruby value tree at
9
- # the host↔guest boundary. #deep_wrap allocates a +Kobako::Handle+ for
10
- # each non-wire-representable leaf on the host→guest +#run+ argument
11
- # path; #deep_restore resolves each wire-decoded Handle back to its
12
- # host object on every guest→host value path. #representable? is the
13
- # by-value codec-type predicate that decides which leaves #deep_wrap
14
- # must wrap: the closed 11-entry wire type set
15
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
16
- # Mapping).
17
- #
18
- # All helpers are pure except #deep_wrap, whose only side effect is
19
- # allocating new Handle ids into the supplied table.
20
- module HandleWalk
21
- module_function
22
-
8
+ module HandleWalk # :nodoc:
23
9
  # Inclusive Integer range the msgpack gem encodes without raising
24
10
  # +RangeError+ at encode time — signed +int 64+ minimum through
25
- # unsigned +uint 64+ maximum
26
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
27
- # Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
11
+ # unsigned +uint 64+ maximum (docs/wire/payload-msgpack.md § Integer
12
+ # Range).
28
13
  # Anchored as a +Range+ so #primitive_type? stays a single
29
14
  # dispatch line. This is the codec's encode domain — not to
30
15
  # be confused with the Handle id range, which lives on
@@ -32,141 +17,91 @@ module Kobako
32
17
  # represents a different concept entirely.
33
18
  MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
34
19
 
35
- # Codec-type predicate
36
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
37
- # Mapping). Returns +true+ when +value+ belongs to the closed
38
- # 11-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
39
- # +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
40
- # +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
41
- # representable, or +Hash+ whose every key and value are
42
- # representable. Integers outside the codec's signed-64 /
43
- # unsigned-64 union are rejected so the predicate agrees with the
44
- # msgpack gem's encode-time +RangeError+ behaviour the codec
45
- # already surfaces as UnsupportedTypeError.
46
- #
47
- # The optional +depth+ bounds the recursive descent at the wire's
48
- # structural nesting cap so a self-referential container — reachable
49
- # as a cyclic Hash key on the +#run+ argument path — reads as
50
- # non-representable rather than looping, letting #deep_wrap_hash
51
- # refuse it instead of overflowing this walk.
52
- def representable?(value, depth = 0)
20
+ # Whether +value+ falls in the codec's type set
21
+ # (docs/wire/payload-msgpack.md § Type Mapping). The +depth+ bound
22
+ # makes a self-referential container —
23
+ # reachable as a cyclic Hash key on the +#run+ argument path — read as
24
+ # non-representable rather than looping.
25
+ def self.representable?(value, depth = 0)
53
26
  return false if depth > MAX_NESTING_DEPTH
54
27
 
55
28
  primitive_type?(value) || container_representable?(value, depth)
56
29
  end
57
30
 
58
- # Deep-walk Array / Hash containers in +value+ and replace every
59
- # leaf that fails #representable? with a +Kobako::Handle+
60
- # allocated from +handler+. The
61
- # walk only descends through representable container shapes
62
- # (Array, Hash) one structural level at a time; a non-representable
63
- # leaf is wrapped as-is without inspecting its internal structure.
64
- # An existing +Kobako::Handle+ is representable and passes through
65
- # unchanged — auto-wrap never re-wraps a Handle. Only Hash *values*
66
- # are wrapped: a Hash *key* must already be wire-representable, since a
67
- # key is not auto-wrapped the way a value is — a non-representable key
68
- # raises +Kobako::SandboxError+ rather than crossing as an opaque token
69
- # the guest→host restore walk could not round-trip.
70
- #
71
- # +value+ may be any Ruby value; +handler+ must respond to
72
- # +#alloc(object) -> Kobako::Handle+ (a host-side
73
- # +Kobako::Catalog::Handles+). Returns a structurally equivalent value
74
- # whose leaves are either representable or +Kobako::Handle+
75
- # tokens.
76
- #
77
- # Recursive calls spell the +HandleWalk.+ receiver so the dispatch
78
- # stays valid even when a block is captured and run under a
79
- # different +self+ (+module_function+ privatizes the instance copies).
80
- def deep_wrap(value, handler, depth = 0)
31
+ # Only Hash values are wrapped: a non-representable key raises rather
32
+ # than crossing as a token the guest→host restore walk could not
33
+ # round-trip.
34
+ def self.deep_wrap(value, handles, depth = 0)
81
35
  guard_nesting!(depth)
82
36
  case value
83
- when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler, depth + 1) }
84
- when ::Hash then HandleWalk.deep_wrap_hash(value, handler, depth)
37
+ when ::Array then value.map { |element| deep_wrap(element, handles, depth + 1) }
38
+ when ::Hash then deep_wrap_hash(value, handles, depth)
85
39
  else
86
- representable?(value) ? value : handler.alloc(value)
87
- end
88
- end
89
-
90
- # Refuse a wrap walk that has descended past the wire's structural
91
- # nesting bound — a reference cycle necessarily trips it — so a +#run+
92
- # argument fails as a clean +Kobako::SandboxError+ rather than an
93
- # unbounded host recursion.
94
- def guard_nesting!(depth)
95
- return unless depth > MAX_NESTING_DEPTH
96
-
97
- raise Kobako::SandboxError,
98
- "a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
99
- "cannot cross the sandbox boundary (possible reference cycle)"
100
- end
101
-
102
- # Wrap a Hash on the host→guest argument path: each value's
103
- # non-representable leaves become Handles, while each key must be
104
- # wire-representable as-is. A stateful object may cross the boundary as
105
- # a Hash value but not as a key — the one deliberate asymmetry with the
106
- # guest→host restore walk, which resolves Handle keys the guest built.
107
- def deep_wrap_hash(hash, handler, depth)
108
- wrapped = {} # : Hash[untyped, untyped]
109
- hash.each do |key, val|
110
- unless HandleWalk.representable?(key, depth + 1)
111
- raise Kobako::SandboxError,
112
- "a Hash passed to #run has a key that cannot cross the sandbox boundary " \
113
- "(#{key.class}); only wire-representable values may be Hash keys"
114
- end
115
- wrapped[key] = HandleWalk.deep_wrap(val, handler, depth + 1)
40
+ representable?(value) ? value : handles.alloc(value)
116
41
  end
117
- wrapped
118
42
  end
119
43
 
120
- # Deep-walk Array / Hash containers in +value+ and replace every
121
- # +Kobako::Handle+ leaf with the host-side object +handler+ resolves
122
- # it to. The symmetric inverse of #deep_wrap: that walk allocates objects
123
- # into Handles on the host→guest argument path; this walk resolves
124
- # Handles back to their objects wherever a guest→host payload carries
125
- # one — an invocation result, a yield-block result, or a dispatch
126
- # argument. The walk descends through Array elements and Hash keys and
127
- # values one structural level at a time; any non-Handle leaf passes
128
- # through unchanged.
129
- #
130
- # +value+ is a decoded Ruby value (a Handle here is a wire-decoded
131
- # +Kobako::Handle+, never a guest-forged one); +handler+ must
132
- # respond to +#fetch(id) -> object+ (a host-side
133
- # +Kobako::Catalog::Handles+). +handler.fetch+ raises
134
- # +Kobako::SandboxError+ for an id with no live binding, the
135
- # corrupted-runtime fallback.
136
- def deep_restore(value, handler)
44
+ # The inverse of #deep_wrap. A Handle here was decoded off the wire,
45
+ # never forged by the guest, and +handles.fetch+ refuses an id with no
46
+ # live binding.
47
+ def self.deep_restore(value, handles)
137
48
  case value
138
- when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
49
+ when ::Array then value.map { |element| deep_restore(element, handles) }
139
50
  when ::Hash
140
51
  # Rebuilt with each key restored: two distinct Handle keys that
141
52
  # resolve to equal host objects collapse to the later pair, as in
142
53
  # any Ruby Hash. The guest authored this payload, so that collapse
143
54
  # is its own concern, not a fidelity guarantee the host owes it.
144
- value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
145
- when Kobako::Handle then handler.fetch(value.id)
55
+ value.to_h { |key, val| [deep_restore(key, handles), deep_restore(val, handles)] }
56
+ when Kobako::Handle then handles.fetch(value.id)
146
57
  else value
147
58
  end
148
59
  end
149
60
 
150
- # The non-container branch of #representable?: returns +true+ for
151
- # the scalar leaves and an existing Handle. Not part of the
152
- # public surface; reach for #representable? instead.
153
- def primitive_type?(value)
154
- case value
155
- when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
156
- when ::Integer then MSGPACK_INT_RANGE.cover?(value)
157
- else false
61
+ class << self
62
+ private
63
+
64
+ # A reference cycle necessarily trips the nesting bound, so a +#run+
65
+ # argument fails as a clean +Kobako::SandboxError+ rather than an
66
+ # unbounded host recursion.
67
+ def guard_nesting!(depth)
68
+ return unless depth > MAX_NESTING_DEPTH
69
+
70
+ raise Kobako::SandboxError,
71
+ "a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
72
+ "cannot cross the sandbox boundary (possible reference cycle)"
158
73
  end
159
- end
160
74
 
161
- # The container branch of #representable?: recurses into Array
162
- # elements and Hash key+value pairs through the public
163
- # #representable?. Not part of the public surface; reach for
164
- # #representable? instead.
165
- def container_representable?(value, depth)
166
- case value
167
- when ::Array then value.all? { |element| HandleWalk.representable?(element, depth + 1) }
168
- when ::Hash then value.all? { |pair| pair.all? { |member| HandleWalk.representable?(member, depth + 1) } }
169
- else false
75
+ # A stateful object may cross the boundary as a Hash value but not as
76
+ # a key — the one deliberate asymmetry with the guest→host restore
77
+ # walk, which resolves Handle keys the guest built.
78
+ def deep_wrap_hash(hash, handles, depth)
79
+ wrapped = {} # : Hash[untyped, untyped]
80
+ hash.each do |key, val|
81
+ unless representable?(key, depth + 1)
82
+ raise Kobako::SandboxError,
83
+ "a Hash passed to #run has a key that cannot cross the sandbox boundary " \
84
+ "(#{key.class}); only wire-representable values may be Hash keys"
85
+ end
86
+ wrapped[key] = deep_wrap(val, handles, depth + 1)
87
+ end
88
+ wrapped
89
+ end
90
+
91
+ def primitive_type?(value)
92
+ case value
93
+ when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
94
+ when ::Integer then MSGPACK_INT_RANGE.cover?(value)
95
+ else false
96
+ end
97
+ end
98
+
99
+ def container_representable?(value, depth)
100
+ case value
101
+ when ::Array then value.all? { |element| representable?(element, depth + 1) }
102
+ when ::Hash then value.all? { |pair| pair.all? { |member| representable?(member, depth + 1) } }
103
+ else false
104
+ end
170
105
  end
171
106
  end
172
107
  end
@@ -4,14 +4,9 @@ require_relative "error"
4
4
 
5
5
  module Kobako
6
6
  module Codec
7
- # The structural nesting bound
8
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md]
9
- # § Structural Nesting Depth), checked before a value reaches the
10
- # packer. The packer takes no depth limit and walks a list or map in
11
- # frames that carry no stack guard, so a value it cannot finish — a
12
- # reference cycle necessarily is one — must be refused before it is
13
- # handed over.
14
- module Nesting
7
+ # Refuses a value nested past the wire's bound before the packer sees it,
8
+ # since the packer walks without a stack guard and a cycle never ends.
9
+ module Nesting # :nodoc:
15
10
  # Raise InvalidTypeError when +value+ nests past MAX_NESTING_DEPTH; a
16
11
  # value nested exactly to the bound passes.
17
12
  def self.assert_within_bound!(value, depth = 0)
@@ -2,11 +2,10 @@
2
2
 
3
3
  module Kobako
4
4
  module Codec
5
- # Codec-internal, per-thread state of the operation in flight: whether
6
- # a Capability Handle crossed the current decode. Thread scoping is
7
- # what makes plain instance variables sound — host codec calls run
8
- # synchronously on their owning thread.
9
- class State
5
+ # Whether a Capability Handle crossed the decode in flight, and the
6
+ # value budget a decode is held to, kept per thread so plain instance
7
+ # variables stay sound.
8
+ class State # :nodoc:
10
9
  # Thread-local slot holding the calling thread's State.
11
10
  STATE_KEY = :__kobako_codec_state__
12
11
  private_constant :STATE_KEY
@@ -20,14 +19,25 @@ module Kobako
20
19
 
21
20
  def initialize
22
21
  @carried_handle = false
22
+ @value_budget = nil
23
+ end
24
+
25
+ # The most values a decode on this thread may build, or nil for no
26
+ # budget.
27
+ attr_reader :value_budget
28
+
29
+ # A nested invocation on the same thread restores the outer one's
30
+ # budget when it returns.
31
+ def within_value_budget(budget)
32
+ outer = @value_budget
33
+ @value_budget = budget
34
+ yield
35
+ ensure
36
+ @value_budget = outer
23
37
  end
24
38
 
25
- # Bracket a decode and return the block's result together with
26
- # whether the decoded tree carried an ext 0x01 Capability Handle.
27
39
  # ExtTypes#unpack_handle is the sole chokepoint every Handle passes
28
- # through, so one decode pass records the whole tree and a caller
29
- # can skip an all-identity Handle-resolution walk when none was
30
- # present.
40
+ # through, so one decode pass records the whole tree.
31
41
  def track_handles
32
42
  @carried_handle = false
33
43
  result = yield
@@ -36,7 +46,7 @@ module Kobako
36
46
 
37
47
  # Record that an ext 0x01 Capability Handle crossed the current
38
48
  # decode; #track_handles reports it to the bracketing caller.
39
- def record_handle!
49
+ def record_handle
40
50
  @carried_handle = true
41
51
  end
42
52
  end