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
@@ -2,26 +2,7 @@
2
2
 
3
3
  module Kobako
4
4
  module Transport
5
- # The reflection floor a guest→host dispatch must clear before the
6
- # Dispatcher reaches +public_send+: which method names on a resolved
7
- # target count as Service behaviour, and which are Ruby's ambient
8
- # metaprogramming surface.
9
- #
10
- # Answers with a refusal reason rather than raising, so the error
11
- # taxonomy stays with the Dispatcher and this module holds only the
12
- # policy.
13
- module Reflection
14
- # Modules whose instance methods are ambient Ruby reflection /
15
- # metaprogramming surface (+send+, +public_send+, +instance_eval+,
16
- # +method+, +tap+, +instance_variable_get+, ...) rather than Service
17
- # behaviour. A guest-supplied method name resolving to one of these is
18
- # rejected: only methods the bound object itself exposes as Service
19
- # behaviour are reachable, and +public_send(:send, ...)+ would
20
- # otherwise let a guest pivot through +send+ into the private
21
- # +Kernel#eval+ / +#system+ surface (host RCE).
22
- META_OWNERS = [BasicObject, Kernel, Object, Module, Class].freeze
23
- private_constant :META_OWNERS
24
-
5
+ module Reflection # :nodoc:
25
6
  # Callable gadget types whose own public methods are reflection surface
26
7
  # (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
27
8
  # hand back the underlying object) rather than Service behaviour. Only
@@ -36,59 +17,123 @@ module Kobako
36
17
  # callable exposes by default, so the floor and the Exposure agree.
37
18
  CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
38
19
 
39
- module_function
40
-
41
20
  # The reason +name+ is unreachable through +exposure+, or +nil+ when
42
21
  # the dispatch may proceed. Composes the ambient-surface floor with the
43
22
  # reference's Exposure, in that order: the Exposure only narrows and
44
23
  # can never re-open what the floor rejects.
45
- def refusal(exposure, name)
24
+ def self.refusal(exposure, name)
46
25
  ambient_refusal(exposure.object, name) ||
47
26
  (exposure.exposes?(name) ? nil : "method #{name.inspect} is not exposed to the guest")
48
27
  end
49
28
 
50
- # Guard against ambient reflection methods. A public method whose
51
- # owner is a META_OWNERS or GADGET_OWNERS module — or a singleton
52
- # class, the owner of every class-level method (+File.popen+ /
53
- # +Kernel.system+, unreachable via any fixed core-module list) — is
54
- # rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
55
- # stays invocable). A name with no concrete public method is allowed
56
- # only when the target opts into it via +respond_to?+ (dynamic
57
- # +method_missing+ Services), since the dangerous methods are all
58
- # concretely defined and therefore never reach that branch.
59
- #
60
- # +method_missing+ named explicitly is refused whatever its owner:
61
- # it is Ruby's dynamic-dispatch hook, and a public override (a
62
- # +Delegator+) binds and calls the private method the guest passes as
63
- # its first argument (+Kernel#system+). A dynamic +method_missing+
64
- # Service is untouched — the guest reaches it by the virtual name,
65
- # which resolves through the +NameError+ branch below, never by
66
- # naming +method_missing+.
67
- def ambient_refusal(target, name)
68
- return "method #{name.inspect} is not a Service method" if name == :method_missing
69
-
70
- owner = target.public_method(name).owner
71
- return nil unless ambient_owner?(owner, target)
72
- return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
73
-
74
- "method #{name.inspect} is not a Service method"
75
- rescue NameError
76
- return nil if target.respond_to?(name)
77
-
78
- "no public method #{name.inspect} on target"
29
+ # Whether +method+ is written in Ruby rather than built into the
30
+ # platform or copied from the surface every object shares. A native
31
+ # method has no source, and a built-in one names an +<internal:>+ file,
32
+ # so a copy, alias or rebinding of either keeps that mark wherever it
33
+ # is reached from.
34
+ def self.authored?(method)
35
+ file, = method.source_location
36
+ !file.nil? && !file.start_with?("<internal:") && !ambient_copy?(method)
79
37
  end
80
38
 
81
- # Whether +name+'s +owner+ is ambient surface rather than Service
82
- # behaviour: a core meta module, a callable gadget type, or — when
83
- # +target+ is itself a Class or Module bound as a Service — the
84
- # singleton class that owns its class-level API (+File.popen+ /
85
- # +Kernel.system+, unreachable via any fixed core-module list). A plain
86
- # object's own singleton method (+def obj.x+) stays reachable, since
87
- # +target+ is not a Module there.
88
- def ambient_owner?(owner, target)
89
- META_OWNERS.include?(owner) ||
90
- GADGET_OWNERS.include?(owner) ||
91
- (target.is_a?(Module) && owner.singleton_class?)
39
+ class << self
40
+ private
41
+
42
+ # Whether +method+ is a copy, alias or rebinding of a Ruby method
43
+ # +Class+ inherits, such as the +name!+ alias OpenStruct makes of a
44
+ # mixed-in +try+. The copy is owned by the class that made it, but
45
+ # keeps the original's name and source.
46
+ def ambient_copy?(method)
47
+ name = method.original_name
48
+ Class.ancestors.any? do |owner|
49
+ (owner.method_defined?(name, false) || owner.private_method_defined?(name, false)) &&
50
+ owner.instance_method(name).source_location == method.source_location
51
+ end
52
+ end
53
+
54
+ # Guard against ambient reflection methods. A public method whose
55
+ # owner +Class+ inherits or a GADGET_OWNERS module — or a singleton
56
+ # class, the owner of every class-level method (+File.popen+ /
57
+ # +Kernel.system+, unreachable via any fixed core-module list) — is
58
+ # rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda
59
+ # stays invocable). A name with no concrete public method is allowed
60
+ # only when the target opts into it via +respond_to?+ (dynamic
61
+ # +method_missing+ Services), since the dangerous methods are all
62
+ # concretely defined and therefore never reach that branch.
63
+ #
64
+ # +method_missing+ named explicitly is refused whatever its owner:
65
+ # it is Ruby's dynamic-dispatch hook, and a public override (a
66
+ # +Delegator+) binds and calls the private method the guest passes as
67
+ # its first argument (+Kernel#system+). A dynamic +method_missing+
68
+ # Service is untouched — the guest reaches it by the virtual name,
69
+ # which resolves through the +respond_to?+ opt-in, never by naming
70
+ # +method_missing+.
71
+ #
72
+ # A public method must also be authored, so a Service narrowing itself
73
+ # never reaches a built-in or ambient method by a copy, alias or
74
+ # rebinding an owner check cannot see; a non-public one is never
75
+ # reached through a dynamic opt-in either.
76
+ #
77
+ # A name with no method never reaches +instance_method+, which would
78
+ # keep a guest-chosen name as a Symbol for the life of the process.
79
+ def ambient_refusal(target, name)
80
+ return refused(name) if name == :method_missing
81
+
82
+ klass = lookup_class(target)
83
+ return defined_refusal(target, name, klass) if klass.public_method_defined?(name)
84
+ return "no public method #{name.inspect} on target" unless target.respond_to?(name)
85
+ return refused(name) if klass.method_defined?(name) || klass.private_method_defined?(name)
86
+
87
+ owner_refusal(target, name, klass)
88
+ end
89
+
90
+ def defined_refusal(target, name, klass)
91
+ method = klass.instance_method(name)
92
+ owner_refusal(target, name, method.owner) ||
93
+ (authored_for?(target, name, method, klass) ? nil : refused(name))
94
+ end
95
+
96
+ # A bound callable keeps its callable names and a record its member
97
+ # readers, though the platform defines both.
98
+ def authored_for?(target, name, method, klass)
99
+ return true if Reflection.authored?(method)
100
+ return true if GADGET_OWNERS.include?(method.owner) && CALLABLE_ALLOW.include?(name)
101
+
102
+ (klass <= Struct || klass <= Data) && target.members.include?(name)
103
+ end
104
+
105
+ def refused(name)
106
+ "method #{name.inspect} is not a Service method"
107
+ end
108
+
109
+ def owner_refusal(target, name, owner)
110
+ return nil unless ambient_owner?(owner, target)
111
+ return nil if GADGET_OWNERS.include?(owner) && CALLABLE_ALLOW.include?(name)
112
+
113
+ refused(name)
114
+ end
115
+
116
+ # The class a method lookup on +target+ starts at. A plain object's
117
+ # singleton class is taken only when it already holds a public
118
+ # method, since asking for it otherwise would create one.
119
+ def lookup_class(target)
120
+ return target.singleton_class if target.is_a?(Module) || target.singleton_methods.any?
121
+
122
+ target.class
123
+ end
124
+
125
+ # Whatever +Class+ inherits is surface every object shares rather
126
+ # than Service behaviour: the reflection +BasicObject+ through +Class+
127
+ # define (+send+ pivots into +Kernel#eval+, a host RCE), and every
128
+ # module a library mixes into them, such as a +try+ dispatching by
129
+ # name or a +to_json+ dumping instance variables. A plain object's
130
+ # own singleton method (+def obj.x+) stays reachable, since only a
131
+ # Module target counts its singleton class as ambient.
132
+ def ambient_owner?(owner, target)
133
+ owner >= Class ||
134
+ GADGET_OWNERS.include?(owner) ||
135
+ (target.is_a?(Module) && owner.singleton_class?)
136
+ end
92
137
  end
93
138
  end
94
139
  end
@@ -1,44 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../constant_name"
3
4
  require_relative "../handle"
4
5
  require_relative "../codec"
5
6
  require_relative "../payload"
6
7
 
7
8
  module Kobako
8
- # See lib/kobako/transport.rb for the umbrella module doc; this file
9
- # owns +Run+, the host-side value object for one +#run+ request — the
10
- # native side frames it into the envelope +__kobako_run+ consumes.
11
9
  module Transport
12
- # Host-side value object for a single +Sandbox#run+ invocation
13
- # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
14
- #
15
- # A Run captures the host-layer concept of "a single +#run+
16
- # call": the entrypoint constant name plus its positional and keyword
17
- # arguments. Host pre-flight (entrypoint type / name pattern, forged
18
- # Handle, kwargs-key type) is enforced at construction so the Value
19
- # Object is the single source of truth — anything that passes
20
- # +Run.new+ is safe to ship to the guest.
21
- #
22
- # +#payload+ takes the invocation's +Catalog::Handles+ and routes any
23
- # non-wire-representable +args+ / +kwargs+ leaf through it as a
24
- # +Kobako::Handle+ — the symmetric counterpart of the guest→host wrap
25
- # path in the dispatcher. A +Kobako::Handle+ that arrives **already
26
- # constructed** in the caller's +args+ / +kwargs+ is rejected at
27
- # construction: legitimate Handles only enter Host App code through
28
- # error fields, so a Handle reaching the call site is by definition
29
- # smuggled in. +Runtime#run+ frames that payload with the entrypoint
30
- # into the Run envelope the +__kobako_run+ command buffer carries.
31
- #
32
- # Built on the +class X < Data.define(...)+ subclass form (the
33
- # Steep-friendly shape — see +.rubocop.yml+ for the rationale).
34
- class Run < Data.define(:entrypoint, :args, :kwargs)
35
- # Ruby constant-name pattern enforced on the +entrypoint+ Symbol.
36
- # Parallel to
37
- # +Kobako::Catalog::Snippets::NAME_PATTERN+; the two constants name the
38
- # same regex but cover distinct surfaces (snippet identity vs.
39
- # entrypoint resolution) so a future divergence stays local.
40
- NAME_PATTERN = /\A[A-Z]\w*\z/
41
-
10
+ # A Handle already in the arguments is refused, since a caller never
11
+ # legitimately holds one.
12
+ class Run < Data.define(:entrypoint, :args, :kwargs) # :nodoc:
42
13
  def initialize(entrypoint:, args: [], kwargs: {})
43
14
  entrypoint = normalize_entrypoint(entrypoint)
44
15
  validate_args!(args)
@@ -46,61 +17,42 @@ module Kobako
46
17
  super
47
18
  end
48
19
 
49
- # Encode this Run's arguments as the codec payload the Run
50
- # envelope carries — +Runtime#run+ frames it with the entrypoint.
51
- # Walks +args+ / +kwargs+ through Codec::HandleWalk.deep_wrap so
52
- # any non-wire-representable leaf is allocated into +handler+ and
53
- # replaced with a +Kobako::Handle+; the +handler+ argument is the
54
- # invocation's table, sharing the same allocator the guest→host
55
- # return path uses. A wrapped leaf rides as ext 0x01 in its
56
- # original position (docs/wire/payload-msgpack.md § ext 0x01).
57
- # The walk starts one level down, since +args+ and +kwargs+ ride
58
- # inside the payload document the wire's nesting bound is counted
59
- # from.
60
- def payload(handler)
20
+ # A leaf with no wire form becomes a Capability Handle in the
21
+ # invocation's table. The walk starts one level down, since +args+ and
22
+ # +kwargs+ ride inside the payload document the wire's nesting bound
23
+ # is counted from.
24
+ def payload(handles)
61
25
  Payload::Arguments.new(
62
- args: Codec::HandleWalk.deep_wrap(args, handler, 1),
63
- kwargs: Codec::HandleWalk.deep_wrap(kwargs, handler, 1)
26
+ args: Codec::HandleWalk.deep_wrap(args, handles, 1),
27
+ kwargs: Codec::HandleWalk.deep_wrap(kwargs, handles, 1)
64
28
  ).encode
65
29
  end
66
30
 
67
31
  private
68
32
 
69
- # The target must be a Symbol or String (TypeError, not
70
- # ArgumentError — the wrong-type case is a Host App programming
71
- # error before the run reaches the guest). After +.to_s+
72
- # the value must match NAME_PATTERN (ArgumentError), rejecting
73
- # +::+-segmented names and any non-constant form.
33
+ # A target of the wrong type raises TypeError, a Host App programming
34
+ # error caught before the run reaches the guest.
74
35
  def normalize_entrypoint(target)
75
36
  unless target.is_a?(Symbol) || target.is_a?(String)
76
37
  raise TypeError, "entrypoint must be a Symbol or String, got #{target.class}"
77
38
  end
78
39
 
79
40
  target_str = target.to_s
80
- unless NAME_PATTERN.match?(target_str)
41
+ unless CONSTANT_NAME.match?(target_str)
81
42
  raise ArgumentError,
82
- "entrypoint must match #{NAME_PATTERN.inspect} (got #{target.inspect})"
43
+ "entrypoint must match #{CONSTANT_NAME.inspect} (got #{target.inspect})"
83
44
  end
84
45
 
85
46
  target_str.to_sym
86
47
  end
87
48
 
88
- # +args+ must not contain a +Kobako::Handle+. The Handle
89
- # allocator lives inside the Host Gem; legitimate paths surface
90
- # Handle objects only through raised error fields, so a Handle
91
- # reaching +args+ is a forged or smuggled token. Non-wire-
92
- # representable arguments that are not Handles are handled by
93
- # auto-wrap inside +#payload+ — the reject path is reserved
94
- # for Handle objects specifically.
95
49
  def validate_args!(args)
96
50
  raise ArgumentError, "arguments must be an Array" unless args.is_a?(Array)
97
51
  raise ArgumentError, forged_handle_message("arguments") if args.any?(Kobako::Handle)
98
52
  end
99
53
 
100
- # Reject a non-Symbol kwargs key, and a +Kobako::Handle+ arriving
101
- # as a kwargs value (same forged-token principle as the +args+
102
- # branch). Both checks live here so the Host App sees the
103
- # host-side error message before any encode / decode boundary.
54
+ # Checked here so the Host App sees the host-side message before any
55
+ # encode boundary.
104
56
  def validate_kwargs!(kwargs)
105
57
  raise ArgumentError, "keyword arguments must be a Hash" unless kwargs.is_a?(Hash)
106
58
 
@@ -112,12 +64,6 @@ module Kobako
112
64
  raise ArgumentError, forged_handle_message("keyword argument values") if kwargs.each_value.any?(Kobako::Handle)
113
65
  end
114
66
 
115
- # Single source of truth for the forged-Handle reject message so the
116
- # args and kwargs branches stay phrased identically. Message stays in
117
- # caller vocabulary: it names the affected slot and the reason
118
- # without leaking internal SPEC identifiers or self-referential
119
- # architecture terms — the error is raised BY kobako, so saying
120
- # "allocated by the Host Gem" reads as third-person about self.
121
67
  def forged_handle_message(slot)
122
68
  "#{slot} must not contain a Kobako::Handle — " \
123
69
  "Handles are created internally by the Sandbox and cannot be passed in"
@@ -4,101 +4,43 @@ require_relative "../codec"
4
4
  require_relative "../errors"
5
5
 
6
6
  module Kobako
7
- # See lib/kobako/transport.rb for the umbrella module doc; this file
8
- # owns the host-side object that materialises a guest-supplied block as
9
- # a Ruby callable the Service method can yield into.
10
7
  module Transport
11
- # Host-side stand-in for a guest-supplied block.
12
- #
13
- # Each guest call that carries +block_given: true+ gets a Yielder
14
- # that the Dispatcher hands to the Service method as +&block+. The
15
- # Service method observes it as an ordinary Ruby Proc through
16
- # #to_proc; +yield val+ / +block.call(val)+ invokes #yield, which
17
- # serialises the positional args, re-enters the guest via the injected
18
- # +yield_to_guest+ lambda, and reifies the Yield Reply into Ruby
19
- # control flow:
20
- #
21
- # * ok — return the decoded value to +yield+'s caller
22
- # * break — +throw break_tag, value+ so the Dispatcher's +catch+
23
- # frame unwinds the Service method
24
- # * error — raise a BlockError at the Service's yield site,
25
- # carrying the guest's class on +#klass+
26
- #
27
- # The Dispatcher calls #invalidate! from its +ensure+ block once
28
- # dispatch completes; any later call to a stashed Yielder then raises
29
- # +LocalJumpError+ — the observable shape of an escaped Yielder.
30
- class Yielder
31
- # +yield_to_guest+ is the ext's per-dispatch
32
- # +Kobako::Runtime::GuestYielder+, which #yield invokes to re-enter
33
- # the guest: it takes the argument payload and answers the reply
34
- # already split into +[arm, body, class]+. +break_tag+ is the
35
- # +catch+ throw tag the Dispatcher matches against to unwind the
36
- # Service on a break. +handler+ is the invocation's +Kobako::Catalog::Handles+,
37
- # used to restore a Capability Handle in the block's ok value back to
38
- # its host object before it reaches the Service +yield+ site.
39
- def initialize(yield_to_guest, break_tag, handler)
40
- @yield_to_guest = yield_to_guest
8
+ class Yielder # :nodoc:
9
+ def initialize(guest_yielder, break_tag, handles, failure)
10
+ @guest_yielder = guest_yielder
41
11
  @break_tag = break_tag
42
- @handler = handler
12
+ @handles = handles
13
+ @failure = failure
43
14
  @active = true
44
- @raised = nil
45
15
  end
46
16
 
47
- # The text the guest's own block failure crosses back as, or +nil+
48
- # when +error+ is not the BlockError this Yielder raised. Identity
49
- # rather than class: a Service that rescued the block's failure and
50
- # raised its own has reported something else, and the guest must
51
- # hear about that instead.
52
- #
53
- # The class travels with the message because not every block failure
54
- # is an exception the guest holds — a block value the guest refused
55
- # has a class to raise under and no object to continue.
56
- def fault_text(error)
57
- raised = @raised
58
- return if raised.nil? || !error.equal?(raised)
59
-
60
- "#{raised.klass}: #{raised.message}"
61
- end
62
-
63
- # Re-enter the guest with +args+ and reify the Yield Reply into
64
- # Ruby control flow. Raises +LocalJumpError+ if called after
65
- # #invalidate!. The ok value is consumed by the host Service
66
- # method, so a Capability Handle in it is restored to its host object.
67
- # The break value unwinds past the Service back to the guest
68
- # bound-constant call, so it passes through verbatim — a Handle stays a
69
- # Handle and rides back on the same id rather than churning a new one.
17
+ # The ok value is consumed by the host Service, so a Capability Handle
18
+ # in it is restored. A block exit stays in the guest, so the Service
19
+ # is only unwound.
70
20
  def yield(*args)
71
21
  raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
72
22
 
73
- arm, body, klass = @yield_to_guest.call(encode_args(args))
74
- raise remember(BlockError.new(body, klass: klass)) if arm == :error
23
+ arm, body, klass = @guest_yielder.call(encode_args(args))
24
+ raise UnsupportedError, "the Sandbox runtime does not support yielding to a block" if arm == :unsupported
25
+ raise @failure.remember(BlockError.new(body, klass: klass)) if arm == :error
75
26
 
76
- value, carried_handle = decode_body(body)
77
- throw @break_tag, value if arm == :break
27
+ throw @break_tag if arm == :break
78
28
 
79
- restore(value, carried_handle)
29
+ restore(*decode_body(body))
80
30
  end
81
31
 
82
- # The Proc the Dispatcher passes as +&block+, binding #yield so a
83
- # Service method's +yield+ / +block.call+ drives the round-trip.
84
32
  def to_proc
85
33
  method(:yield).to_proc
86
34
  end
87
35
 
88
- # Mark this Yielder dead. Called by the Dispatcher's +ensure+ block
89
- # when the originating dispatch frame returns; any later #yield
90
- # call then raises +LocalJumpError+.
91
- def invalidate!
36
+ def invalidate
92
37
  @active = false
93
38
  end
94
39
 
95
40
  private
96
41
 
97
- # Encode what the Service is yielding. Arguments outside the wire
98
- # type set, or nesting past the wire's bound as one list, fail here,
99
- # before the guest is re-entered, and are restated so the Service
100
- # reads a refusal of its own argument rather than a codec class it
101
- # never named.
42
+ # Restated so the Service reads a refusal of its own argument rather
43
+ # than a codec class it never named.
102
44
  def encode_args(args)
103
45
  Kobako::Codec::Nesting.assert_within_bound!(args)
104
46
  Kobako::Codec::Encoder.encode(args)
@@ -106,33 +48,17 @@ module Kobako
106
48
  raise YieldValueError, "Service yielded a value the block cannot receive: #{e.message}"
107
49
  end
108
50
 
109
- # Hold onto the BlockError being raised so #fault_text can recognise
110
- # it if it comes back unrescued, and return it so the raise site reads
111
- # as one statement. Only the newest is kept: a Service that rescued
112
- # an earlier one and yielded again has already handled it.
113
- def remember(error)
114
- @raised = error
115
- end
116
-
117
- # Decode a value-carrying arm's payload, answering the value and
118
- # whether the decode carried a Capability Handle. The tracking
119
- # bracket opens only around this decode: the guest re-entry may run
120
- # nested dispatches whose own brackets would otherwise pollute the
121
- # signal.
51
+ # The tracking bracket opens only around this decode: the guest
52
+ # re-entry may run nested dispatches whose own brackets would
53
+ # otherwise pollute the signal.
122
54
  def decode_body(body)
123
55
  Kobako::Codec.track_handles { Kobako::Codec::Decoder.decode(body) }
124
56
  end
125
57
 
126
- # Restore any Capability Handle in a block's ok value to its host
127
- # object via the injected +Catalog::Handles+. Only the
128
- # ok path calls this — host code consumes the ok value, whereas a
129
- # break value returns to the guest and stays a Handle. A response
130
- # whose decode carried no Handle resolves to itself, so the walk is
131
- # skipped entirely.
132
58
  def restore(value, carried_handle)
133
59
  return value unless carried_handle
134
60
 
135
- Kobako::Codec::HandleWalk.deep_restore(value, @handler)
61
+ Kobako::Codec::HandleWalk.deep_restore(value, @handles)
136
62
  end
137
63
  end
138
64
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "transport/call"
4
4
  require_relative "transport/run"
5
+ require_relative "transport/block_failure"
5
6
  require_relative "transport/yielder"
6
7
  require_relative "transport/error"
7
8
  require_relative "transport/exposure"
@@ -9,20 +10,8 @@ require_relative "transport/reflection"
9
10
  require_relative "transport/dispatcher"
10
11
 
11
12
  module Kobako
12
- # Kobako::Transport — host↔guest message transport namespace. Houses the
13
- # host side of one Call/Reply exchange: the call value objects +Call+
14
- # (guest→host, as the native side decoded it) and +Run+ (host→guest), the
15
- # +Dispatcher+ that answers a routed Call, the +Yielder+ that re-enters
16
- # the guest for a block, the +Reflection+ floor a dispatch must clear, the
17
- # +Exposure+ each reference authorizes a call against, and +Error+ for a
18
- # wire violation the host detects. Each invocation's
19
- # +Context+ composes them into the dispatch +Proc+ it passes +Runtime+ for
20
- # that run.
21
- #
22
- # "RPC" was deliberately not chosen — it implies a cross-process boundary that
23
- # kobako does not have, since host and guest share one OS thread and
24
- # one wasm linear memory. See
25
- # {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md].
26
- module Transport
13
+ # The host side of the guest↔host exchange. Only Transport::Error is
14
+ # public; the rest serves one invocation's dispatch.
15
+ module Transport # :nodoc:
27
16
  end
28
17
  end
@@ -1,17 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # Kobako::Unresolved — the sentinel backing a fillable Service path: one
5
- # declared with +Sandbox#bind(path)+ carrying no object. It reserves the
6
- # path's Frame 1 slot so the guest sees the bound constant while the host
7
- # defers the object it stands for. A guest dispatch to an unfilled fillable
8
- # is refused as an unresolved target and surfaces as +Kobako::ServiceError+
9
- # when the guest leaves it unrescued — the same capability-failure channel
10
- # as an idiom with no backend bound.
11
- #
12
- # A single shared value; only its identity distinguishes it, so the dispatch
13
- # layer recognises it with +equal?+. A Host App may name it at a +bind+ site
14
- # to declare a fillable explicitly.
4
+ # Stands for the object of a fillable Service, one declared with
5
+ # Sandbox#bind and no object. Context#bind supplies the object for an
6
+ # invocation; a call to one left unfilled fails as a ServiceError. Passing
7
+ # it to Sandbox#bind declares a fillable explicitly.
15
8
  module Unresolved
16
9
  end
17
10
  end
data/lib/kobako/usage.rb CHANGED
@@ -12,14 +12,10 @@ module Kobako
12
12
  # cap, even when the cap was hit.
13
13
  #
14
14
  # Filled on every outcome, traps included, so a Host App that rescues a
15
- # trap can read from the error's +#execution+ how much of the budget the
15
+ # trap reads from the error's +execution+ how much of the budget the
16
16
  # invocation used.
17
- #
18
- # Built on the +class X < Data.define(...)+ subclass form (the
19
- # Steep-friendly shape — see +.rubocop.yml+ for the rationale).
20
17
  class Usage < Data.define(:wall_time, :memory_peak)
21
- # Pre-run sentinel. A fresh +Kobako::Context+ holds it until its guest
22
- # runs, so an Execution's +#usage+ is never +nil+.
18
+ # The usage of a run that has not started: no time, no memory.
23
19
  EMPTY = new(wall_time: 0.0, memory_peak: 0)
24
20
  end
25
21
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.27.0"
4
+ VERSION = "0.29.0"
5
5
  end
data/lib/kobako.rb CHANGED
@@ -9,6 +9,7 @@ rescue LoadError
9
9
  require "kobako/kobako"
10
10
  end
11
11
 
12
+ require_relative "kobako/filesystem"
12
13
  require_relative "kobako/errors"
13
14
  require_relative "kobako/unresolved"
14
15
  require_relative "kobako/extension"
@@ -5,7 +5,7 @@ module Kobako
5
5
 
6
6
  def install: (untyped extension, snippets: Kobako::Catalog::Snippets, services: Kobako::Catalog::Services) -> self
7
7
 
8
- def seal!: () -> self
8
+ def assert_dependencies!: () -> self
9
9
 
10
10
  def resolve: () -> Hash[String, untyped]
11
11
 
@@ -17,7 +17,7 @@ module Kobako
17
17
 
18
18
  def validate!: (untyped extension) -> void
19
19
 
20
- def assert_dependencies!: () -> void
20
+ def unmet_dependency: () -> [untyped, untyped]?
21
21
 
22
22
  def symbolize: (untyped name) -> untyped
23
23
  end
@@ -0,0 +1,13 @@
1
+ module Kobako
2
+ module Catalog
3
+ class Flags
4
+ type tuple = [String, String, bool]
5
+
6
+ def initialize: () -> void
7
+
8
+ def entries: () -> Array[tuple]
9
+
10
+ def register: (Symbol | String name, String value, advisory: bool) -> nil
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Kobako
2
+ module Catalog
3
+ class Grants
4
+ def initialize: () -> void
5
+
6
+ def register: (Mount grant) -> nil
7
+
8
+ def include?: (Mount grant) -> bool
9
+
10
+ def entries: (?Hash[String, Mount] overrides) -> Array[Mount::tuple]
11
+ end
12
+ end
13
+ end