kobako 0.26.0-aarch64-linux → 0.28.0-aarch64-linux

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +103 -0
  4. data/README.md +156 -118
  5. data/ROADMAP.md +23 -22
  6. data/SECURITY.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +38 -0
  8. data/crates/kobako-runtime/README.md +10 -12
  9. data/crates/kobako-transport/CHANGELOG.md +35 -0
  10. data/crates/kobako-transport/README.md +43 -27
  11. data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
  12. data/crates/kobako-wasmtime/README.md +8 -10
  13. data/data/kobako.wasm +0 -0
  14. data/lib/kobako/3.3/kobako.so +0 -0
  15. data/lib/kobako/3.4/kobako.so +0 -0
  16. data/lib/kobako/4.0/kobako.so +0 -0
  17. data/lib/kobako/capture.rb +1 -4
  18. data/lib/kobako/catalog/extensions.rb +21 -59
  19. data/lib/kobako/catalog/flags.rb +41 -0
  20. data/lib/kobako/catalog/handles.rb +7 -54
  21. data/lib/kobako/catalog/services.rb +7 -61
  22. data/lib/kobako/catalog/snippets.rb +42 -66
  23. data/lib/kobako/catalog.rb +2 -10
  24. data/lib/kobako/codec/decoder.rb +9 -28
  25. data/lib/kobako/codec/encoder.rb +3 -22
  26. data/lib/kobako/codec/error.rb +6 -27
  27. data/lib/kobako/codec/ext_types.rb +72 -94
  28. data/lib/kobako/codec/handle_walk.rb +65 -130
  29. data/lib/kobako/codec/nesting.rb +3 -8
  30. data/lib/kobako/codec/state.rb +4 -10
  31. data/lib/kobako/codec/utils.rb +6 -34
  32. data/lib/kobako/codec.rb +3 -22
  33. data/lib/kobako/constant_name.rb +8 -0
  34. data/lib/kobako/context.rb +51 -112
  35. data/lib/kobako/errors.rb +51 -127
  36. data/lib/kobako/execution.rb +16 -21
  37. data/lib/kobako/extension.rb +23 -40
  38. data/lib/kobako/handle.rb +7 -12
  39. data/lib/kobako/outcome.rb +51 -80
  40. data/lib/kobako/payload/arguments.rb +1 -16
  41. data/lib/kobako/payload.rb +4 -12
  42. data/lib/kobako/pool.rb +23 -53
  43. data/lib/kobako/runtime.rb +7 -21
  44. data/lib/kobako/sandbox.rb +96 -133
  45. data/lib/kobako/sandbox_options.rb +38 -84
  46. data/lib/kobako/snippet/binary.rb +1 -16
  47. data/lib/kobako/snippet/source.rb +1 -16
  48. data/lib/kobako/snippet.rb +1 -9
  49. data/lib/kobako/transport/call.rb +1 -14
  50. data/lib/kobako/transport/dispatcher.rb +112 -195
  51. data/lib/kobako/transport/error.rb +6 -20
  52. data/lib/kobako/transport/exposure.rb +5 -10
  53. data/lib/kobako/transport/reflection.rb +39 -50
  54. data/lib/kobako/transport/run.rb +17 -71
  55. data/lib/kobako/transport/yielder.rb +21 -73
  56. data/lib/kobako/transport.rb +3 -15
  57. data/lib/kobako/unresolved.rb +4 -11
  58. data/lib/kobako/usage.rb +2 -6
  59. data/lib/kobako/version.rb +1 -1
  60. data/sig/kobako/catalog/extensions.rbs +2 -2
  61. data/sig/kobako/catalog/flags.rbs +17 -0
  62. data/sig/kobako/catalog/services.rbs +0 -6
  63. data/sig/kobako/catalog/snippets.rbs +4 -4
  64. data/sig/kobako/codec/ext_types.rbs +10 -8
  65. data/sig/kobako/codec/handle_walk.rbs +9 -7
  66. data/sig/kobako/codec/utils.rbs +2 -2
  67. data/sig/kobako/codec.rbs +2 -2
  68. data/sig/kobako/constant_name.rbs +3 -0
  69. data/sig/kobako/context.rbs +2 -3
  70. data/sig/kobako/errors.rbs +6 -0
  71. data/sig/kobako/outcome.rbs +6 -8
  72. data/sig/kobako/runtime.rbs +12 -8
  73. data/sig/kobako/sandbox.rbs +4 -0
  74. data/sig/kobako/sandbox_options.rbs +2 -6
  75. data/sig/kobako/transport/dispatcher.rbs +14 -18
  76. data/sig/kobako/transport/reflection.rbs +5 -3
  77. data/sig/kobako/transport/run.rbs +1 -3
  78. data/sig/kobako/transport/yielder.rbs +3 -3
  79. data/sig/kobako/transport.rbs +7 -6
  80. metadata +6 -3
  81. data/rust-toolchain.toml +0 -11
@@ -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,9 @@
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, kept per
6
+ # thread so plain instance variables stay sound.
7
+ class State # :nodoc:
10
8
  # Thread-local slot holding the calling thread's State.
11
9
  STATE_KEY = :__kobako_codec_state__
12
10
  private_constant :STATE_KEY
@@ -22,12 +20,8 @@ module Kobako
22
20
  @carried_handle = false
23
21
  end
24
22
 
25
- # Bracket a decode and return the block's result together with
26
- # whether the decoded tree carried an ext 0x01 Capability Handle.
27
23
  # 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.
24
+ # through, so one decode pass records the whole tree.
31
25
  def track_handles
32
26
  @carried_handle = false
33
27
  result = yield
@@ -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
data/lib/kobako/codec.rb CHANGED
@@ -10,24 +10,7 @@ require_relative "codec/encoder"
10
10
  require_relative "codec/decoder"
11
11
 
12
12
  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
13
+ module Codec # :nodoc:
31
14
  # The maximum structural nesting depth the wire represents (the
32
15
  # MessagePack ecosystem's bound), shared with the guest +kobako_codec+
33
16
  # so both sides cap identically. The host refuses a value nesting past
@@ -35,10 +18,8 @@ module Kobako
35
18
  # packer.
36
19
  MAX_NESTING_DEPTH = 128
37
20
 
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.
21
+ # Answers the block's result with whether the decode carried a
22
+ # Capability Handle, so a caller can skip an all-identity restore walk.
42
23
  def self.track_handles(&block)
43
24
  State.current.track_handles(&block)
44
25
  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:, snippets:, flags:, extensions:) # :nodoc:
29
21
  @runtime = runtime
30
22
  @services = services
31
23
  @snippets = snippets
24
+ @flags = flags
32
25
  @extensions = extensions
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,9 @@ 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
+ # A fillable left unfilled raises like an unbound path, so the guest's
49
+ # call fails closed instead of reaching the sentinel.
50
+ def lookup(path) # :nodoc:
69
51
  key = path.to_s
70
52
  exposure = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
71
53
  raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(exposure.object)
@@ -73,136 +55,93 @@ module Kobako
73
55
  exposure
74
56
  end
75
57
 
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)
58
+ def eval(code, &block) # :nodoc:
80
59
  collect_overrides(&block) if block
81
60
  invoke!(:eval) do
82
- @runtime.eval(dispatch_handler, @services.paths, code.b, @snippets.entries)
61
+ @runtime.eval(dispatch_handler, @services.paths, @snippets.entries, @flags.entries, code.b)
83
62
  end
84
63
  end
85
64
 
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)
65
+ def run(request, &block) # :nodoc:
90
66
  collect_overrides(&block) if block
91
67
  invoke!(:run, entrypoint: request.entrypoint) do
92
- @runtime.run(dispatch_handler, @services.paths, @snippets.entries,
93
- request.entrypoint.to_s, request.payload(@handler))
68
+ @runtime.run(dispatch_handler, @services.paths, @snippets.entries, @flags.entries,
69
+ request.entrypoint.to_s, request.payload(@handles))
94
70
  end
95
71
  end
96
72
 
97
73
  private
98
74
 
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.
75
+ # The Context is spent once the block returns, so a captured +ctx+ used
76
+ # later raises. A block that raises propagates before the guest drives,
77
+ # so the guest never runs and no Execution is produced.
103
78
  def collect_overrides
104
79
  yield self
105
80
  ensure
106
81
  @spent = true
107
82
  end
108
83
 
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.
84
+ # Handed to the Runtime as a call argument, so the Runtime holds no
85
+ # dispatch state and the Proc stays GC-rooted for the synchronous call.
119
86
  def dispatch_handler
120
87
  lambda do |target, method_name, block_given, payload, guest_yielder|
121
88
  call = Transport::Call.new(target: target, method_name: method_name,
122
89
  block_given: block_given, payload: payload)
123
- Transport::Dispatcher.dispatch(call, self, @handler, guest_yielder)
90
+ Transport::Dispatcher.dispatch(call, self, @handles, guest_yielder)
124
91
  end
125
92
  end
126
93
 
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.
94
+ # Every Snapshot carries usage and captures, trap or not, so they stay
95
+ # readable after a rescued trap.
130
96
  def populate_observability!(snapshot)
131
97
  @usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
132
98
  @stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
133
99
  @stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
134
100
  end
135
101
 
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}")
102
+ # Every TrapError leaving an invocation names the verb that ran it; the
103
+ # cap subclasses keep their identity through the rebuild.
104
+ def with_verb(err, verb)
105
+ err.class.new("Sandbox##{verb} failed: #{err.message}")
159
106
  end
160
107
 
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.
108
+ # +failed+ keeps a +nil+ value from a successful run apart from a failed
109
+ # run.
165
110
  def build_execution(value, failed:)
166
111
  Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
167
112
  end
168
113
 
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.
114
+ # The settle sits in the rescue so a wire-violation trap or a Panic both
115
+ # attach this run's Execution, just like a guest-call trap does.
116
+ # +entrypoint+ is known to the host and never carried by the wire, so an
117
+ # unresolved one names itself on the error it raises.
175
118
  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
119
+ payload, panic = snapshot.outcome
120
+ value, carried = Codec.track_handles { Outcome.reify(payload, panic, entrypoint: entrypoint) }
121
+ value = Codec::HandleWalk.deep_restore(value, @handles) if carried
179
122
  build_execution(value, failed: false)
180
123
  rescue Kobako::TrapError => e
181
- raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.message}").with_execution(build_execution(nil,
182
- failed: true))
124
+ raise with_verb(e, verb).with_execution(build_execution(nil, failed: true))
183
125
  rescue Kobako::SandboxError, Kobako::ServiceError => e
184
126
  raise e.with_execution(build_execution(nil, failed: true))
185
127
  end
186
128
 
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.
129
+ # Extension backends are resolved before the guest runs, so a provider
130
+ # that raises propagates unwrapped and leaves the guest unrun. A
131
+ # could-not-start fault ran no invocation at all, so it carries no
132
+ # Execution and gains only the verb prefix.
195
133
  def invoke!(verb, entrypoint: nil)
196
134
  @resolved = @extensions.resolve.transform_values { |object| Transport::Exposure.of(object) }
197
135
  begin
198
136
  snapshot = yield
199
137
  rescue Kobako::TrapError => e
200
- raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
138
+ raise with_verb(e, verb)
201
139
  end
202
140
  populate_observability!(snapshot)
203
- return settle_outcome(snapshot, verb, entrypoint) unless snapshot.trapped?
141
+ trap = snapshot.trap_error
142
+ return settle_outcome(snapshot, verb, entrypoint) unless trap
204
143
 
205
- raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true))
144
+ raise with_verb(trap, verb).with_execution(build_execution(nil, failed: true))
206
145
  end
207
146
  end
208
147
  end