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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ module Transport
5
+ # The newest failure a guest block raised to a Service during one
6
+ # invocation, whichever open Call carried that block: a Service may run
7
+ # a block stored from an earlier Call, and its failure is still the
8
+ # guest's own.
9
+ class BlockFailure # :nodoc:
10
+ def initialize
11
+ @raised = nil
12
+ end
13
+
14
+ # Only the newest is kept: a Service that rescued an earlier one and
15
+ # yielded again has already handled it.
16
+ def remember(error)
17
+ @raised = error
18
+ end
19
+
20
+ # Identity rather than class: a Service that rescued the block's
21
+ # failure and raised its own has reported something else, and the
22
+ # guest must hear about that instead.
23
+ #
24
+ # The class travels with the message because not every block failure
25
+ # is an exception the guest holds — a block value the guest refused
26
+ # has a class to raise under and no object to continue.
27
+ def fault_text(error)
28
+ raised = @raised
29
+ return if raised.nil? || !error.equal?(raised)
30
+
31
+ "#{raised.klass}: #{raised.message}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,20 +2,7 @@
2
2
 
3
3
  module Kobako
4
4
  module Transport
5
- # One guest-initiated dispatch, as the native side decoded it off the
6
- # core envelope. The routing fields arrive already decoded — a String
7
- # for a bound constant's path, an Integer for a Capability Handle id —
8
- # so the host resolves a target without interpreting a payload byte.
9
- #
10
- # +payload+ stays bytes here on purpose: routing never interprets a
11
- # payload byte, and the one place that does — the Dispatcher — must
12
- # decode it inside the Codec bracket that records whether a Handle
13
- # crossed.
14
- #
15
- # Built on the +class X < Data.define(...)+ subclass form so the class
16
- # body is fully Steep-visible; see +.rubocop.yml+ for the
17
- # rationale.
18
- class Call < Data.define(:target, :method_name, :block_given, :payload)
5
+ class Call < Data.define(:target, :method_name, :block_given, :payload) # :nodoc:
19
6
  end
20
7
  end
21
8
  end
@@ -3,259 +3,180 @@
3
3
  require_relative "../codec"
4
4
  require_relative "../errors"
5
5
  require_relative "../payload"
6
+ require_relative "block_failure"
6
7
  require_relative "call"
7
8
  require_relative "reflection"
8
9
  require_relative "yielder"
9
10
 
10
11
  module Kobako
11
- # See lib/kobako/transport.rb for the umbrella module doc; this file
12
- # owns the pure-function dispatcher that answers a routed Call.
13
12
  module Transport
14
- # Pure-function dispatcher for guest-initiated Calls. The native side
15
- # has already decoded the core envelope, so this resolves the target
16
- # through the per-invocation path +resolver+ (the +Context+, whose
17
- # +#lookup+ layers per-invocation providers over the static bindings)
18
- # or Catalog::Handles, decodes only the payload, invokes the method,
19
- # and answers +[ok, bytes]+ — which the native side puts on the
20
- # Reply's ok or fault arm. It never raises.
21
- #
22
- # The module is stateless — all mutable state is threaded through
23
- # arguments so Dispatcher has no instance variables and no side
24
- # effects beyond mutating the Catalog::Handles via +alloc+ when a
25
- # non-wire-representable return value must be wrapped.
26
- module Dispatcher
27
- # Throw tag for the Yielder's break unwind back to the
28
- # dispatcher's +catch+ frame. +private_constant+ is a
13
+ # Answers a routed Call with one arm per Reply variant: +[:ok, body, nil]+,
14
+ # +[:fault, message, fault_type]+, or +[:break, "", nil]+ — a block
15
+ # exit the guest kept carries nothing. It never raises, so every
16
+ # failure reaches the guest as a fault.
17
+ module Dispatcher # :nodoc:
18
+ # Thrown rather than raised, so a Service unwound by a block exit runs
19
+ # its +ensure+ and cannot +rescue+ the exit. +private_constant+ is a
29
20
  # convention boundary — not a defence.
30
21
  BREAK_THROW = :__kobako_break__
31
22
  private_constant :BREAK_THROW
32
23
 
33
- module_function
24
+ class UndefinedTargetError < StandardError; end # :nodoc:
34
25
 
35
- # Internal sentinel raised when target resolution fails. Becomes a
36
- # Fault with type="undefined". Contained at the wire boundary —
37
- # not part of the public Kobako error taxonomy.
38
- class UndefinedTargetError < StandardError; end
26
+ class UnreadableRequestError < Kobako::Codec::Error; end # :nodoc:
39
27
 
40
- # The codec fault of a request that never became a call, restated in
41
- # the vocabulary the guest can act on while staying inside the codec
42
- # taxonomy the fault boundary sorts on.
43
- class UnreadableRequestError < Kobako::Codec::Error; end
44
-
45
- # The category kobako's own refusals answer under, keyed by the class
46
- # each is raised as and ordered most specific first. A class absent
47
- # here is the Service's own exception, which answers under +runtime+
48
- # wearing the +<class>: <message>+ shape that says so.
28
+ # The category each failure answering with its message alone takes,
29
+ # keyed by the class raised and ordered most specific first. kobako's
30
+ # own refusals are worded so none reads as a Service's exception, and
31
+ # +ArgumentError+, raised by a Service and by a call that will not
32
+ # bind alike, is named by +argument+ already. A class absent here is
33
+ # the Service's own exception, which answers under +runtime+ wearing
34
+ # the +<class>: <message>+ shape that names it.
49
35
  #
50
36
  # +Kobako::Codec::Error+ is the floor rather than a path of its own:
51
37
  # a codec fault reaching the boundary unnamed is the exchange failing,
52
38
  # and must not be dressed as something a Service raised.
53
- OWN_FAULTS = {
39
+ BARE_FAULTS = {
54
40
  HandleExhaustedError => "internal",
55
41
  UndefinedTargetError => "undefined",
56
42
  ArgumentError => "argument",
57
43
  YieldValueError => "runtime",
44
+ UnsupportedError => "runtime",
58
45
  Kobako::Codec::Error => "internal",
59
46
  Kobako::SandboxError => "runtime"
60
47
  }.freeze
61
- private_constant :OWN_FAULTS
62
-
63
- # Answer a single routed Call with +[ok, bytes]+, which the native
64
- # side puts on the Reply's ok or fault arm. Invoked from the
65
- # per-invocation dispatch Proc that
66
- # +Kobako::Context+ hands to +Runtime#eval+ / +#run+; +resolver+,
67
- # +handler+, and +yield_to_guest+ are captured in that Proc's
68
- # closure so the Dispatcher stays stateless and neither the resolver
69
- # nor the Context needs to publish accessors for the per-invocation
70
- # +Catalog::Handles+ or +Runtime+. +yield_to_guest+ is a +String → String+ callable
71
- # (the ext's per-dispatch +Kobako::Runtime::GuestYielder+) used only
72
- # when the Call carries +block_given: true+. Never raises — every
73
- # failure path takes the fault arm instead, so the guest sees a
74
- # transport error rather than a wasm trap.
75
- #
76
- # The decode runs inside +Codec.track_handles+ so #resolve_call_args
77
- # can skip the argument walk when no Capability Handle crossed the
78
- # wire.
79
- def dispatch(call, resolver, handler, yield_to_guest)
80
- yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if call.block_given
81
- [true, encode_ok(run(call, resolver, handler, yielder), handler), nil] # : [bool, String, String?]
48
+ private_constant :BARE_FAULTS
49
+
50
+ # The per-invocation state arrives as arguments so the Dispatcher
51
+ # stays stateless and neither the resolver nor the Context publishes
52
+ # accessors for it.
53
+ def self.dispatch(call, resolver, handles, failure, guest_yielder)
54
+ yielder = Yielder.new(guest_yielder, BREAK_THROW, handles, failure) if call.block_given
55
+ catch(BREAK_THROW) { return [:ok, encode_ok(run(call, resolver, handles, yielder), handles), nil] }
56
+ [:break, "", nil] # : [Symbol, String, String?]
82
57
  # StandardError is the boundary by intent: a Service method's
83
58
  # application fault folds into a guest-rescuable fault, while a
84
59
  # host-process failure (NoMemoryError, SignalException, a bare Exception)
85
60
  # stays uncaught and traps the invocation rather than being masked as a
86
61
  # rescuable fault.
87
62
  rescue StandardError => e
88
- [false, *caught_fault(e, yielder)] # : [bool, String, String?]
63
+ [:fault, *caught_fault(e, failure)] # : [Symbol, String, String?]
89
64
  ensure
90
- yielder&.invalidate!
91
- end
92
-
93
- # Decode the payload, resolve the receiver, and run the method inside
94
- # the +catch+ frame a guest +break+ unwinds to. Split from #dispatch
95
- # so the reply-shaping and the failure boundary stay one glance wide.
96
- def run(call, resolver, handler, yielder)
97
- arguments, carried_handle = decode_arguments(call.payload)
98
- exposure = resolve_target(call.target, resolver, handler)
99
- args, kwargs = resolve_call_args(arguments, handler, carried_handle)
100
- catch(BREAK_THROW) { invoke(exposure, call.method_name, args, kwargs, yielder) }
101
- end
102
-
103
- # Decode the Call's payload into its arguments, reporting whether any
104
- # Capability Handle crossed. A codec fault here is a request that
105
- # never became a call, restated so it cannot read as an unwritable
106
- # reply — the same restatement #encode_ok makes in the other
107
- # direction.
108
- def decode_arguments(payload)
109
- Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
110
- rescue Kobako::Codec::Error => e
111
- raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
65
+ yielder&.invalidate
112
66
  end
113
67
 
114
- # Resolve positional and keyword arguments off the decoded payload in
115
- # one step. +carried_handle+ reports whether the decode carried any
116
- # Capability Handle; when it did not, every argument resolves to
117
- # itself, so the decoded values pass straight through and the walk is
118
- # skipped entirely. Otherwise both go through #resolve_arg so Handles
119
- # round-trip back to the host-side Ruby object before the call reaches
120
- # +public_send+.
121
- def resolve_call_args(arguments, handler, carried_handle)
122
- return [arguments.args, arguments.kwargs] unless carried_handle
68
+ class << self
69
+ private
123
70
 
124
- [arguments.args.map { |v| resolve_arg(v, handler) },
125
- arguments.kwargs.transform_values { |v| resolve_arg(v, handler) }]
126
- end
71
+ def run(call, resolver, handles, yielder)
72
+ arguments, carried_handle = decode_arguments(call.payload)
73
+ exposure = resolve_target(call.target, resolver, handles)
74
+ args, kwargs = resolve_call_args(arguments, handles, carried_handle)
75
+ invoke(exposure, call.method_name, args, kwargs, yielder)
76
+ end
127
77
 
128
- # Map an error caught at the dispatch boundary to the message and the
129
- # category the native side frames into the Reply's fault arm. +error+
130
- # is the +StandardError+ caught by #dispatch's rescue; the category
131
- # tells the guest which kind of failure it was so it can raise the
132
- # matching proxy-side error.
133
- #
134
- # The class prefix marks a Service's own exception and nothing else:
135
- # it is the +<class>: <message>+ shape a Host App is told to keep
136
- # secrets out of, so wearing it says the Service raised. kobako's own
137
- # refusals answer under their own wording instead of borrowing that
138
- # shape.
139
- #
140
- # The guest's own block failing is not the Service's to report at
141
- # all, so the Yielder that raised it is asked first — it recognises
142
- # its own by identity and words the failure the guest's way.
143
- def caught_fault(error, yielder)
144
- block_failure = yielder&.fault_text(error)
145
- return fault("block", block_failure) if block_failure
78
+ # A codec fault here is a request that never became a call, restated
79
+ # so it cannot read as an unwritable reply.
80
+ def decode_arguments(payload)
81
+ Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
82
+ rescue Kobako::Codec::Error => e
83
+ raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
84
+ end
146
85
 
147
- own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) }
148
- return fault(own.last, error.message) if own
86
+ def resolve_call_args(arguments, handles, carried_handle)
87
+ return [arguments.args, arguments.kwargs] unless carried_handle
149
88
 
150
- fault("runtime", "#{error.class}: #{error.message}")
151
- end
89
+ [arguments.args.map { |v| resolve_arg(v, handles) },
90
+ arguments.kwargs.transform_values { |v| resolve_arg(v, handles) }]
91
+ end
152
92
 
153
- # Dispatch +method+ on the object behind +exposure+, once the Exposure
154
- # the guest's reference carries permits it. +kwargs+ is already
155
- # Symbol-keyed (the +Payload::Arguments+ invariant pins it). The
156
- # empty-kwargs branch omits the +**+ splat so Ruby 3.x's strict kwargs
157
- # separation does not reject calls to no-kwarg methods when the wire
158
- # carries the uniform empty-map shape.
159
- #
160
- # +yielder+ is the host-side Yielder materialised when the guest
161
- # call site supplied a block; its Yielder#to_proc
162
- # rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
163
- # so the same call site handles both cases without an explicit
164
- # conditional.
165
- def invoke(exposure, method, args, kwargs, yielder = nil)
166
- name = method.to_sym
167
- reject_unreachable!(exposure, name)
168
- target = exposure.object
169
- block = yielder&.to_proc
170
- if kwargs.empty?
171
- target.public_send(name, *args, &block)
172
- else
173
- target.public_send(name, *args, **kwargs, &block)
93
+ # The class prefix marks a Service's own exception that its category
94
+ # does not name: it is the +<class>: <message>+ shape a Host App is
95
+ # told to keep secrets out of, so wearing it says the Service raised.
96
+ #
97
+ # The guest's own block failing is not the Service's to report at
98
+ # all, so the Yielder that raised it is asked first — it recognises
99
+ # its own by identity and words the failure the guest's way.
100
+ def caught_fault(error, failure)
101
+ block_failure = failure.fault_text(error)
102
+ return fault("block", block_failure) if block_failure
103
+
104
+ bare = BARE_FAULTS.find { |klass, _| error.is_a?(klass) }
105
+ return fault(bare.last, error.message) if bare
106
+
107
+ fault("runtime", "#{error.class}: #{error.message}")
174
108
  end
175
- end
176
109
 
177
- # Guard the +public_send+ below: Reflection decides what counts as
178
- # Service behaviour through this reference, and its refusal reason
179
- # becomes the guest's +undefined+ fault. Both the ambient-surface
180
- # floor and the reference's Exposure answer through it, so a rejected
181
- # name discloses nothing about which of the two refused.
182
- def reject_unreachable!(exposure, name)
183
- reason = Reflection.refusal(exposure, name)
184
- raise UndefinedTargetError, reason if reason
185
- end
110
+ # The wire always carries a keyword map, so an empty one is left
111
+ # unsplatted for methods that take no keywords.
112
+ def invoke(exposure, method, args, kwargs, yielder)
113
+ name = method.to_sym
114
+ reject_unreachable!(exposure, name)
115
+ target = exposure.object
116
+ block = yielder&.to_proc
117
+ if kwargs.empty?
118
+ target.public_send(name, *args, &block)
119
+ else
120
+ target.public_send(name, *args, **kwargs, &block)
121
+ end
122
+ end
186
123
 
187
- # Resolve every Kobako::Handle in an argument — bare or nested in an
188
- # Array / Hash — back to its host object before the dispatch reaches
189
- # +public_send+, symmetric with the guest→host return path. A Handle id
190
- # with no live entry surfaces as an unrecognized target.
191
- def resolve_arg(value, handler)
192
- Kobako::Codec::HandleWalk.deep_restore(value, handler)
193
- rescue Kobako::SandboxError => e
194
- raise UndefinedTargetError, e.message
195
- end
124
+ # Both the ambient-surface floor and the reference's Exposure answer
125
+ # through Reflection, so a rejected name discloses nothing about
126
+ # which of the two refused.
127
+ def reject_unreachable!(exposure, name)
128
+ reason = Reflection.refusal(exposure, name)
129
+ raise UndefinedTargetError, reason if reason
130
+ end
196
131
 
197
- # Resolve a Call target to the Exposure the path +resolver+ (or
198
- # Catalog::Handles) holds for it. The native side already
199
- # discriminated the two forms off the core envelope's +kind+ tag: a
200
- # String is a bound constant's path, an Integer is a Capability Handle
201
- # id. No else-branch is needed — the envelope layer is the system
202
- # boundary that enforces the invariant.
203
- def resolve_target(target, resolver, handler)
204
- case target
205
- when String
206
- resolve_path(target, resolver)
207
- when Integer
208
- resolve_handle(target, handler)
132
+ def resolve_arg(value, handles)
133
+ Kobako::Codec::HandleWalk.deep_restore(value, handles)
134
+ rescue Kobako::SandboxError => e
135
+ raise UndefinedTargetError, e.message
209
136
  end
210
- end
211
137
 
212
- def resolve_path(path, resolver)
213
- resolver.lookup(path)
214
- rescue KeyError => e
215
- raise UndefinedTargetError, e.message
216
- end
138
+ # The envelope already discriminated the two target forms, so no
139
+ # else-branch is needed.
140
+ def resolve_target(target, resolver, handles)
141
+ case target
142
+ when String
143
+ resolve_path(target, resolver)
144
+ when Integer
145
+ resolve_handle(target, handles)
146
+ end
147
+ end
217
148
 
218
- # Resolve +id+ to the Exposure its Handle was minted with. An unknown
219
- # id surfaces as UndefinedTargetError.
220
- def resolve_handle(id, handler)
221
- handler.exposure(id)
222
- rescue Kobako::SandboxError => e
223
- raise UndefinedTargetError, e.message
224
- end
149
+ def resolve_path(path, resolver)
150
+ resolver.lookup(path)
151
+ rescue KeyError => e
152
+ raise UndefinedTargetError, e.message
153
+ end
225
154
 
226
- # Encode +value+ as the body of a Reply's ok arm — the value alone,
227
- # since the envelope's tag already carries the success. A value that
228
- # is not wire-representable per the codec's type mapping raises
229
- # +UnsupportedTypeError+; the rescue routes it through the
230
- # Catalog::Handles via #wrap_as_handle and re-encodes with the
231
- # Capability Handle in place. The happy path encodes exactly once.
232
- #
233
- # Any other codec fault is the answer failing to encode rather than the
234
- # request failing to decode, and a Service is the only side that can
235
- # change what it returns — so it is named here, where the direction is
236
- # known, instead of falling to the boundary's codec floor and reporting
237
- # as an exchange that produced no Service outcome.
238
- def encode_ok(value, handler)
239
- Kobako::Codec::Nesting.assert_within_bound!(value)
240
- Kobako::Codec::Encoder.encode(value)
241
- rescue Kobako::Codec::UnsupportedTypeError
242
- encode_ok(wrap_as_handle(value, handler), handler)
243
- rescue Kobako::Codec::Error => e
244
- raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
245
- end
155
+ def resolve_handle(id, handles)
156
+ handles.exposure(id)
157
+ rescue Kobako::SandboxError => e
158
+ raise UndefinedTargetError, e.message
159
+ end
246
160
 
247
- # Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
248
- # that the wire codec can carry. Used as the fallback path of
249
- # #encode_ok when +value+ has no wire representation.
250
- def wrap_as_handle(value, handler)
251
- handler.alloc(value)
252
- end
161
+ # A value with no wire form goes back as a Capability Handle. Any
162
+ # other codec fault is the answer failing to encode, which only the
163
+ # Service can change — so it is named here, where the direction is
164
+ # known, instead of falling to the boundary's codec floor.
165
+ def encode_ok(value, handles)
166
+ Kobako::Codec::Nesting.assert_within_bound!(value)
167
+ Kobako::Codec::Encoder.encode(value)
168
+ rescue Kobako::Codec::UnsupportedTypeError
169
+ encode_ok(handles.alloc(value), handles)
170
+ rescue Kobako::Codec::Error => e
171
+ raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
172
+ end
253
173
 
254
- # +message+ folds to UTF-8 first: Ruby core builds some exception
255
- # messages as ASCII-8BIT (the arity ArgumentError, for one), and
256
- # the envelope requires UTF-8 of the text fields it frames.
257
- def fault(type, message)
258
- [message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), type] # : [String, String]
174
+ # Ruby core builds some exception messages as ASCII-8BIT (the arity
175
+ # ArgumentError, for one), and the envelope requires UTF-8 of the
176
+ # text fields it frames.
177
+ def fault(type, message)
178
+ [message.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), type] # : [String, String]
179
+ end
259
180
  end
260
181
  end
261
182
  end
@@ -4,31 +4,17 @@ require_relative "../errors"
4
4
 
5
5
  module Kobako
6
6
  module Transport
7
- # +Kobako::SandboxError+ subclass raised when the host detects a
8
- # structural violation of the wire contract while reading what the
9
- # guest produced — an invocation value the payload codec cannot
10
- # decode. Distinct from a Wasm trap (engine signalled the guest
11
- # runtime is unrecoverable) and from a normal sandbox-layer failure
12
- # (the script raised but the protocol was respected): a
13
- # +Transport::Error+ always indicates the guest runtime is corrupted —
14
- # the only safe recovery is to discard the Sandbox and start a new
15
- # invocation.
16
- #
17
- # Inherits from +Kobako::SandboxError+ so a single
18
- # +rescue Kobako::SandboxError+ still catches it; callers that want
19
- # to distinguish wire-violation paths from script failures can
20
- # +rescue Kobako::Transport::Error+ directly.
7
+ # The guest broke the exchange with the host: it produced a value that
8
+ # could not be decoded, or a dispatch it made could not complete. Being a
9
+ # SandboxError, it is caught by rescuing that too.
21
10
  class Error < Kobako::SandboxError
22
- def initialize(message, diagnostic: nil, **)
11
+ def initialize(message, diagnostic: nil, **) # :nodoc:
23
12
  super(message, **)
24
13
  @diagnostic = diagnostic
25
14
  end
26
15
 
27
- # The codec fault behind this violation, appended to Ruby's own
28
- # rendering. A caller cannot act on the inner "Symbol payload must
29
- # be …" wording, so it stays out of #message; an operator
30
- # triaging a corrupted runtime still needs it, and
31
- # +detailed_message+ is where Ruby puts text of exactly that kind.
16
+ # Appends the codec's own reason, which an operator needs and a caller
17
+ # cannot act on, so #message leaves it out.
32
18
  def detailed_message(...)
33
19
  rendered = super
34
20
  return rendered if @diagnostic.nil?
@@ -6,16 +6,11 @@ require_relative "reflection"
6
6
 
7
7
  module Kobako
8
8
  module Transport
9
- # The methods a host object lets the guest call through one reference
10
- # to it — a bound path or a Capability Handle — fixed when the reference
11
- # is made. An object defining its own private +respond_to_guest?(name)+
12
- # is asked on every dispatch; any other exposes only what its own class
13
- # and the object itself define in source, since the methods a Host App
14
- # cannot foresee handing over are the ones it never wrote.
15
- #
16
- # Built on the +class X < Data.define(...)+ subclass form so the class
17
- # body is fully Steep-visible; see +.rubocop.yml+ for the rationale.
18
- class Exposure < Data.define(:object, :names)
9
+ # The methods one reference lets the guest call, fixed when the reference
10
+ # is made. Only what the object's own class and the object define in
11
+ # source is exposed, since a Host App cannot foresee handing over what it
12
+ # never wrote.
13
+ class Exposure < Data.define(:object, :names) # :nodoc:
19
14
  PREDICATE = :respond_to_guest?
20
15
  private_constant :PREDICATE
21
16
 
@@ -73,7 +68,7 @@ module Kobako
73
68
  end
74
69
 
75
70
  def class_names(object, klass)
76
- names = authored(klass)
71
+ names = authored_names(klass)
77
72
  case object
78
73
  when Struct, Data then names.concat(object.members)
79
74
  end
@@ -83,15 +78,13 @@ module Kobako
83
78
  def with_singleton(object, names)
84
79
  return names if SINGLETON_METHODS.bind_call(object, false).empty?
85
80
 
86
- names.union(authored(SINGLETON_CLASS_OF.bind_call(object))).freeze
81
+ names.union(authored_names(SINGLETON_CLASS_OF.bind_call(object))).freeze
87
82
  end
88
83
 
89
- # The public methods +owner+ itself defines in source. A method with
90
- # no source, or whose source is the platform's own, is built in.
91
- def authored(owner)
84
+ # The public methods +owner+ itself defines in Ruby source.
85
+ def authored_names(owner)
92
86
  owner.public_instance_methods(false).select do |name|
93
- file, = owner.instance_method(name).source_location
94
- file && !file.start_with?("<internal:")
87
+ Reflection.authored?(owner.instance_method(name))
95
88
  end
96
89
  end
97
90
  end