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
@@ -4,45 +4,17 @@ require_relative "error"
4
4
 
5
5
  module Kobako
6
6
  module Codec
7
- # Byte-boundary helpers shared by the host-side encoder and decoder.
8
- # Two concerns live here:
9
- #
10
- # - UTF-8 assertion at the codec boundary
11
- # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
12
- # § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
13
- # Decoder when walking +str+ family payloads and by ExtTypes
14
- # when validating the +ext 0x00+ Symbol payload.
15
- # - +ArgumentError+ translation at the codec boundary
16
- # (#with_boundary) so the public taxonomy stays
17
- # Kobako::Codec::Error.
18
- #
19
- # Both helpers are pure — they only inspect inputs, never mutate them.
20
- # The host↔guest Handle substitution walk lives in HandleWalk.
21
- module Utils
22
- module_function
23
-
24
- # Raise InvalidEncodingError unless +string+'s bytes are valid under
25
- # its current encoding tag. +label+ is the caller-supplied prefix
26
- # for the error message (e.g. +"str payload"+, +"Symbol payload"+).
27
- def assert_utf8!(string, label)
7
+ module Utils # :nodoc:
8
+ def self.assert_utf8!(string, label)
28
9
  return if string.valid_encoding?
29
10
 
30
11
  raise InvalidEncodingError, "#{label} is not valid UTF-8"
31
12
  end
32
13
 
33
- # Run +block+ at the codec boundary: a value object raises
34
- # +ArgumentError+ when an invariant is violated at construction, and
35
- # this helper surfaces that as InvalidTypeError so the public taxonomy
36
- # stays Kobako::Codec::Error and never leaks +ArgumentError+ from
37
- # the Ruby standard library.
38
- #
39
- # Reach for this only where a value object is constructed outside a
40
- # Decoder.decode block, whose rescue already performs the same
41
- # mapping (worked example: ExtTypes#unpack_handle building
42
- # +Handle.restore+ from a raw fixext payload). Do not use it for
43
- # general-purpose validation outside the codec boundary —
44
- # host-layer +ArgumentError+ values should propagate unchanged.
45
- def with_boundary
14
+ # Only for a value object built outside a Decoder.decode block, whose
15
+ # rescue already maps the same way; a host-layer +ArgumentError+
16
+ # elsewhere should propagate unchanged.
17
+ def self.with_boundary
46
18
  yield
47
19
  rescue ::ArgumentError => e
48
20
  raise InvalidTypeError, e.message
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Kobako
6
+ module Codec
7
+ # Counts the values a payload holds before the decoder builds any, so a
8
+ # guest cannot make the host build more objects than its budget allows
9
+ # (docs/wire/payload-msgpack.md § Value Budget). Every value takes at
10
+ # least one byte, so a payload no longer than the budget needs no count.
11
+ #
12
+ # The count stops at the first byte it cannot frame and leaves that
13
+ # payload for the decoder to refuse in its own words.
14
+ module ValueBudget # :nodoc:
15
+ # Marker to the body width of a scalar with no length field.
16
+ SCALARS = {
17
+ 0xc0 => 0, 0xc2 => 0, 0xc3 => 0, 0xca => 4, 0xcb => 8,
18
+ 0xcc => 1, 0xcd => 2, 0xce => 4, 0xcf => 8,
19
+ 0xd0 => 1, 0xd1 => 2, 0xd2 => 4, 0xd3 => 8,
20
+ 0xd4 => 2, 0xd5 => 3, 0xd6 => 5, 0xd7 => 9, 0xd8 => 17
21
+ }.freeze
22
+ # Marker to a container's count width and the values each entry holds.
23
+ COUNTS = { 0xdc => [2, 1], 0xdd => [4, 1], 0xde => [2, 2], 0xdf => [4, 2] }.freeze
24
+ # Marker to a str, bin, or ext's length width and type tag width.
25
+ LENGTHS = {
26
+ 0xc4 => [1, 0], 0xc5 => [2, 0], 0xc6 => [4, 0],
27
+ 0xd9 => [1, 0], 0xda => [2, 0], 0xdb => [4, 0],
28
+ 0xc7 => [1, 1], 0xc8 => [2, 1], 0xc9 => [4, 1]
29
+ }.freeze
30
+ UINT = { 1 => "C", 2 => "n", 4 => "N" }.freeze
31
+ private_constant :SCALARS, :COUNTS, :LENGTHS, :UINT
32
+
33
+ def self.assert_within!(bytes, budget)
34
+ return if budget.nil? || bytes.bytesize <= budget
35
+
36
+ remaining = budget
37
+ pending = 1
38
+ pos = 0
39
+ while pending.positive?
40
+ pos, children = step(bytes, pos)
41
+ return if pos.nil?
42
+ raise InvalidTypeError, "payload holds more than #{budget} values" if (remaining -= 1).negative?
43
+
44
+ pending += children - 1
45
+ end
46
+ end
47
+
48
+ # The position after the value at +pos+, nil for one the payload
49
+ # cannot frame, and the values it holds directly.
50
+ def self.step(bytes, pos)
51
+ marker = bytes.getbyte(pos) or return [nil, 0]
52
+ pos += 1
53
+ case marker
54
+ when 0x00..0x7f, 0xe0..0xff then [pos, 0]
55
+ when 0x80..0x8f then [pos, (marker & 0x0f) * 2]
56
+ when 0x90..0x9f then [pos, marker & 0x0f]
57
+ when 0xa0..0xbf then [pos + (marker & 0x1f), 0]
58
+ else step_framed(bytes, pos, marker)
59
+ end
60
+ end
61
+
62
+ def self.step_framed(bytes, pos, marker)
63
+ return [pos + SCALARS.fetch(marker), 0] if SCALARS.key?(marker)
64
+
65
+ width, per = COUNTS[marker]
66
+ return [pos + width, uint(bytes, pos, width) * per] if width && per
67
+
68
+ width, tag = LENGTHS[marker]
69
+ return [nil, 0] unless width && tag
70
+
71
+ [pos + width + tag + uint(bytes, pos, width), 0]
72
+ end
73
+
74
+ # A length cut short reads as zero, so the count runs off the end and
75
+ # stops there.
76
+ def self.uint(bytes, pos, width)
77
+ slice = bytes.byteslice(pos, width).to_s
78
+ return 0 if slice.bytesize < width
79
+
80
+ slice.unpack1(UINT.fetch(width)) # : Integer
81
+ end
82
+
83
+ private_class_method :step, :step_framed, :uint
84
+ end
85
+ end
86
+ end
data/lib/kobako/codec.rb CHANGED
@@ -5,29 +5,13 @@ require_relative "codec/utils"
5
5
  require_relative "codec/handle_walk"
6
6
  require_relative "codec/nesting"
7
7
  require_relative "codec/state"
8
+ require_relative "codec/value_budget"
8
9
  require_relative "codec/ext_types"
9
10
  require_relative "codec/encoder"
10
11
  require_relative "codec/decoder"
11
12
 
12
13
  module Kobako
13
- # Host-side MessagePack codec for the kobako wire contract — the
14
- # byte-level layer ({docs/wire-codec.md}[link:../../docs/wire-codec.md]).
15
- # This is the payload codec: the core envelope is framed natively, so
16
- # what reaches here is one codec document — an invocation's arguments
17
- # (+Kobako::Payload::Arguments+), a dispatch or yield value, or an
18
- # invocation's value and a Panic's diagnostics
19
- # (+Kobako::Outcome+). The ext-type leaves this layer
20
- # carries — +Kobako::Handle+ (0x01) — lives at the kobako root so the
21
- # codec can register it without depending upward on Transport.
22
- #
23
- # Backed by the official +msgpack+ gem: ExtTypes registers the two
24
- # kobako-specific ext types (0x00 Symbol, 0x01 Capability Handle) on
25
- # one process-wide +MessagePack::Factory+,
26
- # and Encoder / Decoder are thin wrappers over it. The Rust side
27
- # mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
28
- # the ext-code constants live as module-private values on ExtTypes
29
- # alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ on that side.
30
- module Codec
14
+ module Codec # :nodoc:
31
15
  # The maximum structural nesting depth the wire represents (the
32
16
  # MessagePack ecosystem's bound), shared with the guest +kobako_codec+
33
17
  # so both sides cap identically. The host refuses a value nesting past
@@ -35,12 +19,16 @@ module Kobako
35
19
  # packer.
36
20
  MAX_NESTING_DEPTH = 128
37
21
 
38
- # Bracket a decode and return the block's result together with whether
39
- # the decoded tree carried an ext 0x01 Capability Handle — the signal a
40
- # dispatch path uses to skip an all-identity Handle-resolution walk.
41
- # The tracking state is codec-internal; this is its only readout.
22
+ # Answers the block's result with whether the decode carried a
23
+ # Capability Handle, so a caller can skip an all-identity restore walk.
42
24
  def self.track_handles(&block)
43
25
  State.current.track_handles(&block)
44
26
  end
27
+
28
+ # Holds every decode the block runs on this thread to +budget+ values,
29
+ # or to none when +budget+ is nil.
30
+ def self.within_value_budget(budget, &block)
31
+ State.current.within_value_budget(budget, &block)
32
+ end
45
33
  end
46
34
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kobako
4
+ # A bind-path segment, a snippet name and a #run entrypoint all take the
5
+ # constant-name form, so they are refused alike. The guest interns each
6
+ # one, and mruby holds a symbol of at most 65534 bytes.
7
+ CONSTANT_NAME = /\A[A-Z]\w{0,65533}\z/ # :nodoc:
8
+ end
@@ -11,41 +11,30 @@ require_relative "transport"
11
11
  require_relative "catalog"
12
12
 
13
13
  module Kobako
14
- # Kobako::Context — the per-invocation driver behind a single +Sandbox#eval+
15
- # / +#run+. Each Context owns a fresh +Catalog::Handles+ table and drives one
16
- # guest invocation: it builds the dispatch handler, runs the guest, records
17
- # the run's captures and usage, and decodes the outcome. One Context per
18
- # invocation owns all per-invocation state, so the reusable +Sandbox+ holds
19
- # none and overlapping evals never touch each other's state.
14
+ # One invocation, as the block given to Sandbox#eval or Sandbox#run sees it.
15
+ # The block runs before the guest does, so it can choose the objects this
16
+ # invocation alone reaches:
17
+ #
18
+ # sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
20
19
  class Context
21
- # Build a Context over the Sandbox-owned config — the +Runtime+, the
22
- # sealed +Catalog::Services+ / +Catalog::Snippets+ registries, and the
23
- # +Catalog::Extensions+ whose callable backends this Context resolves for
24
- # its own run. The +Catalog::Handles+ table is this invocation's own, so
25
- # guest→host dispatch and host→guest auto-wrap share one allocator scoped
26
- # to the run; the resolved provider map is likewise per-invocation, so
27
- # concurrent invocations never share mutable state.
28
- def initialize(runtime:, services:, snippets:, extensions:)
20
+ def initialize(runtime:, services:, extensions:, grants:, preamble:) # :nodoc:
29
21
  @runtime = runtime
30
22
  @services = services
31
- @snippets = snippets
32
23
  @extensions = extensions
24
+ @grants = grants
25
+ @preamble = preamble
33
26
  @resolved = {} # : Hash[String, Transport::Exposure]
34
27
  @overrides = {} # : Hash[String, Transport::Exposure]
35
- @spent = false
36
- @handler = Catalog::Handles.new
28
+ @handles = Catalog::Handles.new
37
29
  @stdout_capture = @stderr_capture = Capture::EMPTY
38
30
  @usage = Usage::EMPTY
39
31
  end
40
32
 
41
- # Override the object bound at an already-declared +path+ for this
42
- # invocation only — the per-eval hook the +#eval+ / +#run+ block uses to
43
- # fill a fillable or shadow any static / per-invocation binding. +path+
44
- # must name a declared (Frame 1) binding; overriding an undeclared path
45
- # raises +ArgumentError+ so the Frame 1 key set stays fixed. Valid only
46
- # while the block runs — the Context is spent once the block returns, so a
47
- # captured +ctx+ used afterward raises +ArgumentError+ too, putting both
48
- # misuses on one rescuable channel. Returns +self+.
33
+ # Use +object+ for the Service at +path+ during this invocation only,
34
+ # filling a fillable Service or shadowing a bound one. Returns +self+.
35
+ #
36
+ # Raises ArgumentError when +path+ was never bound on the Sandbox, or when
37
+ # called after the block has returned.
49
38
  def bind(path, object)
50
39
  raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent
51
40
 
@@ -56,16 +45,29 @@ module Kobako
56
45
  self
57
46
  end
58
47
 
59
- # Resolve a Service +path+ to the Exposure backing it this invocation,
60
- # layering the per-eval +ctx.bind+ overrides over this Context's
61
- # per-invocation provider results over the Sandbox's static base bindings.
62
- # An unbound path raises +KeyError+; a fillable left unfilled resolves to
63
- # +Kobako::Unresolved+ and is reported the same way, so an unresolved
64
- # capability fails closed as an undefined target rather than dispatching to
65
- # the sentinel. The Dispatcher maps either +KeyError+ to an
66
- # undefined-target wire fault. Internal — the per-invocation dispatch
67
- # handler is the sole caller.
68
- def lookup(path)
48
+ # Use +filesystem+ for the mount at +at+ during this invocation only,
49
+ # with its own +access+ and block, as Sandbox#mount takes them. Returns
50
+ # +self+.
51
+ #
52
+ # Raises TypeError for a path that is no String or something other than
53
+ # a Filesystem, and ArgumentError when nothing was mounted at +at+ on the
54
+ # Sandbox, for a malformed path or an unknown access, or when called
55
+ # after the block has returned.
56
+ def mount(at, filesystem, access: :read, &policy)
57
+ raise ArgumentError, "Kobako::Context is spent; ctx.mount is only valid inside the #eval / #run block" if @spent
58
+
59
+ Runtime.validate_mount!(at, access, filesystem)
60
+ mount = Catalog::Mount.new(at: at, access: access, filesystem: filesystem, policy: policy)
61
+ raise ArgumentError, "cannot override unmounted path #{at.inspect}" unless @grants.include?(mount)
62
+
63
+ mounts = @mounts ||= {} # : Hash[String, Catalog::Mount]
64
+ mounts[mount.key] = mount
65
+ self
66
+ end
67
+
68
+ # A fillable left unfilled raises like an unbound path, so the guest's
69
+ # call fails closed instead of reaching the sentinel.
70
+ def lookup(path) # :nodoc:
69
71
  key = path.to_s
70
72
  exposure = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
71
73
  raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(exposure.object)
@@ -73,136 +75,100 @@ module Kobako
73
75
  exposure
74
76
  end
75
77
 
76
- # Execute a guest mruby source string in a fresh +mrb_state+ and return the
77
- # decoded last expression. A given +block+ runs first, receiving this
78
- # Context to collect +ctx.bind+ overrides before the guest drives.
79
- def eval(code, &block)
78
+ def eval(code, &block) # :nodoc:
80
79
  collect_overrides(&block) if block
81
- invoke!(:eval) do
82
- @runtime.eval(dispatch_handler, @services.paths, code.b, @snippets.entries)
83
- end
80
+ invoke!(:eval) { @runtime.eval(dispatch_handler, setup, code.b) }
84
81
  end
85
82
 
86
- # Dispatch a +Transport::Run+ into a preloaded entrypoint and return the
87
- # decoded result. A given +block+ runs first, receiving this Context to
88
- # collect +ctx.bind+ overrides before the guest drives.
89
- def run(request, &block)
83
+ def run(request, &block) # :nodoc:
90
84
  collect_overrides(&block) if block
91
85
  invoke!(:run, entrypoint: request.entrypoint) do
92
- @runtime.run(dispatch_handler, @services.paths, @snippets.entries,
93
- request.entrypoint.to_s, request.payload(@handler))
86
+ @runtime.run(dispatch_handler, setup, request.entrypoint.to_s, request.payload(@handles))
94
87
  end
95
88
  end
96
89
 
97
90
  private
98
91
 
99
- # Run the per-eval override +block+, handing it this Context so it can call
100
- # +ctx.bind+, then spend the Context so a captured +ctx+ used after the
101
- # block raises. A block that raises propagates before the guest drives, so
102
- # the guest never runs and no Execution is produced.
92
+ # What the Runtime builds the invocation from: the Preamble's parts,
93
+ # then this invocation's mounts.
94
+ def setup
95
+ paths, snippets, flags = @preamble
96
+ [paths, snippets, flags, @grants.entries(@mounts || {})]
97
+ end
98
+
99
+ # The Context is spent once the block returns, so a captured +ctx+ used
100
+ # later raises. A block that raises propagates before the guest drives,
101
+ # so the guest never runs and no Execution is produced.
103
102
  def collect_overrides
104
103
  yield self
105
104
  ensure
106
105
  @spent = true
107
106
  end
108
107
 
109
- # Build this invocation's guest→host dispatch handler — a +Proc+ routing
110
- # each guest→host call through the stateless +Transport::Dispatcher+,
111
- # capturing this Context as the path resolver (its +#lookup+ layers the
112
- # per-invocation providers over the static bindings) plus +@handler+. Handed to
113
- # +Runtime#eval+ / +#run+ as a call argument, so the Runtime holds no
114
- # dispatch state and the +Proc+ stays GC-rooted as a live argument for the
115
- # synchronous call. The ext hands the +Proc+ a per-dispatch +guest_yielder+
116
- # — a +String → String+ callable that re-enters the in-flight guest to run
117
- # a yielded block — which the +Dispatcher+ forwards to the
118
- # +Transport::Yielder+ it builds for the call.
108
+ # Handed to the Runtime as a call argument, so the Runtime holds no
109
+ # dispatch state and the Proc stays GC-rooted for the synchronous call.
110
+ # Built once per invocation, so every Call it answers shares one
111
+ # BlockFailure.
119
112
  def dispatch_handler
113
+ failure = Transport::BlockFailure.new
120
114
  lambda do |target, method_name, block_given, payload, guest_yielder|
121
115
  call = Transport::Call.new(target: target, method_name: method_name,
122
116
  block_given: block_given, payload: payload)
123
- Transport::Dispatcher.dispatch(call, self, @handler, guest_yielder)
117
+ Transport::Dispatcher.dispatch(call, self, @handles, failure, guest_yielder)
124
118
  end
125
119
  end
126
120
 
127
- # Record this invocation's usage and both output captures from the ext
128
- # +Snapshot+. Every Snapshot carries them — value return or trap alike — so
129
- # +#usage+ / +#stdout+ / +#stderr+ stay readable after a rescued trap.
130
- def populate_observability!(snapshot)
121
+ # Every Snapshot carries usage and captures, trap or not, so they stay
122
+ # readable after a rescued trap.
123
+ def populate_observability(snapshot)
131
124
  @usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
132
125
  @stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
133
126
  @stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
134
127
  end
135
128
 
136
- # Pick the +TrapError+ subclass to re-raise based on +err+'s actual class.
137
- # Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+) preserve their
138
- # named identity; everything else collapses to the base +Kobako::TrapError+,
139
- # so #invoke! can add the verb prefix without erasing the named subclass.
140
- def trap_class_for(err)
141
- case err
142
- when TimeoutError then TimeoutError
143
- when MemoryLimitError then MemoryLimitError
144
- else TrapError
145
- end
146
- end
147
-
148
- # Build the +TrapError+-family exception for a trapped +Snapshot+ from its
149
- # neutral trap kind, tagged with the verb — the cap subclasses
150
- # (+TimeoutError+ / +MemoryLimitError+) keep their identity, every other
151
- # engine fault is the base +TrapError+.
152
- def trap_error_for(snapshot, verb)
153
- klass = case snapshot.trap_kind
154
- when :timeout then TimeoutError
155
- when :memory_limit then MemoryLimitError
156
- else TrapError
157
- end
158
- klass.new("Sandbox##{verb} failed: #{snapshot.trap_message}")
129
+ # Every TrapError leaving an invocation names the verb that ran it; the
130
+ # cap subclasses keep their identity through the rebuild.
131
+ def with_verb(err, verb)
132
+ err.class.new("Sandbox##{verb} failed: #{err.message}")
159
133
  end
160
134
 
161
- # Freeze this run's observables plus +value+ (+nil+ on a failed run) into
162
- # the read-only +Execution+ the caller receives or the error carries.
163
- # +failed+ records the two apart so a +nil+ +value+ from a successful run
164
- # stays distinct from a failed one.
135
+ # +failed+ keeps a +nil+ value from a successful run apart from a failed
136
+ # run.
165
137
  def build_execution(value, failed:)
166
138
  Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
167
139
  end
168
140
 
169
- # Settle a completed run's outcome into its +Execution+. A Capability
170
- # Handle in the result is restored to its host object first. The settle
171
- # sits in the rescue so a wire-violation trap or a Panic both attach this
172
- # run's Execution, just like a guest-call trap does. +entrypoint+ is the
173
- # name +#run+ asked for, which the host knows and the wire never carries,
174
- # so an unresolved one names itself on the error it raises.
141
+ # The settle sits in the rescue so a wire-violation trap or a Panic both
142
+ # attach this run's Execution, just like a guest-call trap does.
143
+ # +entrypoint+ is known to the host and never carried by the wire, so an
144
+ # unresolved one names itself on the error it raises.
175
145
  def settle_outcome(snapshot, verb, entrypoint)
176
- kind, payload, panic = snapshot.outcome
177
- value, carried = Codec.track_handles { Outcome.reify(kind, payload, panic, entrypoint: entrypoint) }
178
- value = Codec::HandleWalk.deep_restore(value, @handler) if carried
146
+ payload, panic = snapshot.outcome
147
+ value, carried = Codec.track_handles { Outcome.reify(payload, panic, entrypoint: entrypoint) }
148
+ value = Codec::HandleWalk.deep_restore(value, @handles) if carried
179
149
  build_execution(value, failed: false)
180
150
  rescue Kobako::TrapError => e
181
- raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.message}").with_execution(build_execution(nil,
182
- failed: true))
151
+ raise with_verb(e, verb).with_execution(build_execution(nil, failed: true))
183
152
  rescue Kobako::SandboxError, Kobako::ServiceError => e
184
153
  raise e.with_execution(build_execution(nil, failed: true))
185
154
  end
186
155
 
187
- # Drive one invocation and settle it into a frozen +Execution+. +verb+
188
- # tags the TrapError message so the failing export is identifiable. This
189
- # invocation's callable Extension backends are resolved first — before the
190
- # guest runs — so a provider that raises propagates unwrapped and leaves
191
- # the guest unrun. Usage and captures are recorded before the trap check,
192
- # so a trapped Snapshot's error carries them just like a completed run's
193
- # return value. A could-not-start fault ran no invocation at all, so it
194
- # carries no Execution and gains only the verb prefix.
156
+ # Extension backends are resolved before the guest runs, so a provider
157
+ # that raises propagates unwrapped and leaves the guest unrun. A
158
+ # could-not-start fault ran no invocation at all, so it carries no
159
+ # Execution and gains only the verb prefix.
195
160
  def invoke!(verb, entrypoint: nil)
196
161
  @resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
197
162
  begin
198
163
  snapshot = yield
199
164
  rescue Kobako::TrapError => e
200
- raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
165
+ raise with_verb(e, verb)
201
166
  end
202
- populate_observability!(snapshot)
203
- return settle_outcome(snapshot, verb, entrypoint) unless snapshot.trapped?
167
+ populate_observability(snapshot)
168
+ trap = snapshot.trap_error
169
+ return settle_outcome(snapshot, verb, entrypoint) unless trap
204
170
 
205
- raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true))
171
+ raise with_verb(trap, verb).with_execution(build_execution(nil, failed: true))
206
172
  end
207
173
  end
208
174
  end