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,11 +2,11 @@
2
2
  //!
3
3
  //! The Ruby-visible classes are
4
4
  //!
5
- //! Kobako::Runtime — wraps a `kobako_wasmtime::Driver`
6
- //! Kobako::Runtime::Snapshot — one invocation's completion + captures + usage
5
+ //! Kobako::Runtime — wraps a `kobako_wasmtime::Driver`
6
+ //! Kobako::Snapshot — one invocation's completion + captures + usage
7
7
  //!
8
8
  //! `Kobako::Runtime` is constructed via `Kobako::Runtime.from_path(path,
9
- //! timeout, memory_limit, stdout_limit, stderr_limit, profile)`. Every
9
+ //! **options)`, the options keyed by the `Kobako::SandboxOptions` members. Every
10
10
  //! invocation (`#eval` / `#run`) takes that run's dispatch handler as a call
11
11
  //! argument, instantiates a fresh instance, and returns a `Snapshot` — the
12
12
  //! whole per-invocation result — so the Runtime holds no per-invocation
@@ -15,21 +15,20 @@
15
15
  //! `kobako-wasmtime` crate behind the `kobako_runtime` contract; no wasm
16
16
  //! engine type reaches this crate or the Host App.
17
17
  //!
18
- //! Module layout — one responsibility per file:
19
- //!
20
- //! * `bridge` — the magnus dispatch bridge: `RubyDispatchHandler` plus the
21
- //! frame-scoped `GuestYielder` Ruby class.
22
- //! * `errors` — the single boundary mapping the neutral `Trap` /
23
- //! `SetupError` channels onto the `Kobako::*` classes for a failure that
24
- //! never produced a `Snapshot` (a could-not-start fault).
18
+ //! Each module holds one responsibility and opens with its own doc.
25
19
 
26
20
  mod bridge;
27
21
  mod errors;
22
+ mod escape;
23
+ mod filesystem;
28
24
  mod gvl;
25
+ mod pinned_value;
26
+ mod snapshot;
29
27
 
30
28
  use magnus::{
31
- function, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
32
- Error as MagnusError, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
29
+ function, method, prelude::*, scan_args::get_kwargs, typed_data::DataTypeFunctions,
30
+ value::Opaque, Error as MagnusError, RArray, RHash, RModule, RString, Ruby, Symbol, TypedData,
31
+ Value,
33
32
  };
34
33
 
35
34
  use std::path::Path;
@@ -37,53 +36,31 @@ use std::sync::Arc;
37
36
  use std::time::Duration;
38
37
 
39
38
  use kobako_runtime::dispatch::DispatchHandler;
40
- use kobako_runtime::error::Trap;
41
39
  use kobako_runtime::profile::Profile;
42
- use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
43
- use kobako_runtime::snapshot::{Capture, Completion, Snapshot as RuntimeSnapshot, Usage};
44
- use kobako_transport::envelope::{Bindings, Outcome, Run, Snippet, Snippets};
40
+ use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
41
+ use kobako_transport::envelope::{Bindings, Flag, Preamble, Snippet, Snippets};
45
42
  use kobako_wasmtime::{Config, Driver};
46
43
 
47
- /// A Panic's fields as they cross to Ruby: origin, class, message,
48
- /// backtrace, and the names the invocation could have used in place of the
49
- /// one it named. Named so the Ruby side reads them positionally against one
50
- /// shape, matching the `Kobako::Outcome::panic_fields` alias.
51
- type PanicFields = (String, String, String, Vec<String>, Vec<String>);
44
+ use snapshot::Snapshot;
52
45
 
53
- /// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
54
- /// what would otherwise be an inline `unsafe { rstring.as_slice() }
55
- /// .to_vec()` duplicated at every host-↔-guest boundary. The borrow
56
- /// does not outlive this call, so no Ruby allocation can move the
57
- /// underlying RString between the borrow and the copy — the safety
58
- /// invariant the inline form relied on is established once here.
46
+ /// The one place the boundary borrows an RString's bytes: the borrow does
47
+ /// not outlive this call, so no Ruby allocation can move the RString
48
+ /// between the borrow and the copy.
59
49
  fn rstring_to_vec(s: RString) -> Vec<u8> {
60
50
  // SAFETY: see item doc.
61
51
  unsafe { s.as_slice() }.to_vec()
62
52
  }
63
53
 
64
- /// Frame the Frame 1 preamble from the Service registry's bind paths.
65
- /// The core envelope's byte layout lives on this side of the boundary,
66
- /// so the registry stays a registry and never holds a wire image.
67
- fn frame_preamble(paths: RArray) -> Result<Vec<u8>, MagnusError> {
68
- Ok(Bindings {
69
- paths: paths.to_vec()?,
70
- }
71
- .encode())
72
- }
73
-
74
- /// Frame the Frame 3 snippet table from the registry's entries — one
75
- /// `[kind, name, body]` triple each, `kind` a Symbol naming the form so
76
- /// the wire's discriminant byte stays here. An off-ladder kind raises
77
- /// rather than defaulting, the same fail-closed posture `from_path`
78
- /// takes on its Symbol options.
79
- fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError> {
80
- let mut frame = Snippets {
54
+ /// `kind` arrives as a Symbol so the registry never names the wire's
55
+ /// discriminant; an off-ladder kind raises rather than defaulting.
56
+ fn to_snippets(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError> {
57
+ let mut snippets = Snippets {
81
58
  entries: Vec::with_capacity(entries.len()),
82
59
  };
83
60
  for index in 0..entries.len() as isize {
84
61
  let entry: RArray = entries.entry(index)?;
85
62
  let kind: Symbol = entry.entry(0)?;
86
- frame.entries.push(match kind.name()?.as_ref() {
63
+ snippets.entries.push(match kind.name()?.as_ref() {
87
64
  "source" => Snippet::Source {
88
65
  name: entry.entry(1)?,
89
66
  body: entry.entry(2)?,
@@ -99,7 +76,30 @@ fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError>
99
76
  }
100
77
  });
101
78
  }
102
- Ok(frame.encode())
79
+ Ok(snippets)
80
+ }
81
+
82
+ /// One owner for the Preamble every guest entry reads, so an invocation
83
+ /// and a compile cannot drift in what they send. An invocation's setup
84
+ /// and a compile's preamble both open with its paths, snippets, and flags.
85
+ fn to_preamble(ruby: &Ruby, parts: RArray) -> Result<Preamble, MagnusError> {
86
+ let paths: RArray = parts.entry(0)?;
87
+ let flags: RArray = parts.entry(2)?;
88
+ let mut preamble = Preamble::default();
89
+ preamble.bindings = Bindings {
90
+ paths: paths.to_vec()?,
91
+ };
92
+ preamble.snippets = to_snippets(ruby, parts.entry(1)?)?;
93
+ preamble.flags = flags
94
+ .to_vec::<(String, String, bool)>()?
95
+ .into_iter()
96
+ .map(|(name, value, critical)| Flag {
97
+ name,
98
+ value,
99
+ critical,
100
+ })
101
+ .collect();
102
+ Ok(preamble)
103
103
  }
104
104
 
105
105
  // ---------------------------------------------------------------------------
@@ -112,27 +112,27 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
112
112
  // in `runtime/errors.rs` — no intermediate hierarchy is registered.
113
113
 
114
114
  let runtime = kobako.define_class("Runtime", ruby.class_object())?;
115
- runtime.define_singleton_method("from_path", function!(Runtime::from_path, 7))?;
116
- runtime.define_method("eval", method!(Runtime::eval, 4))?;
117
- runtime.define_method("run", method!(Runtime::run, 5))?;
115
+ runtime.define_singleton_method("from_path", function!(Runtime::from_path, 2))?;
116
+ runtime.define_method("eval", method!(Runtime::eval, 3))?;
117
+ runtime.define_method("run", method!(Runtime::run, 4))?;
118
118
  runtime.define_method("profile", method!(Runtime::profile, 0))?;
119
+ runtime.define_method("compile", method!(Runtime::compile, 1))?;
120
+ runtime.define_singleton_method("validate_mount!", function!(filesystem::validate_mount, 3))?;
119
121
  // The guest re-enters for a block yield through a frame-scoped
120
- // `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
121
- // method on Runtime.
122
- bridge::register(runtime)?;
122
+ // `Kobako::GuestYielder` the dispatcher hands the Proc, not a method on
123
+ // Runtime.
124
+ bridge::register(kobako)?;
125
+ filesystem::register(ruby, kobako)?;
123
126
 
124
127
  // Snapshot — the per-invocation result object each entry point returns.
125
- let snapshot = runtime.define_class("Snapshot", ruby.class_object())?;
126
- snapshot.define_method("outcome", method!(Snapshot::outcome, 0))?;
127
- snapshot.define_method("trapped?", method!(Snapshot::trapped, 0))?;
128
- snapshot.define_method("trap_kind", method!(Snapshot::trap_kind, 0))?;
129
- snapshot.define_method("trap_message", method!(Snapshot::trap_message, 0))?;
130
- snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
131
- snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
132
- snapshot.define_method("stdout", method!(Snapshot::stdout, 0))?;
133
- snapshot.define_method("stdout_truncated?", method!(Snapshot::stdout_truncated, 0))?;
134
- snapshot.define_method("stderr", method!(Snapshot::stderr, 0))?;
135
- snapshot.define_method("stderr_truncated?", method!(Snapshot::stderr_truncated, 0))?;
128
+ snapshot::register(ruby, kobako)?;
129
+
130
+ // A wrapped class is otherwise looked up the first time an object is
131
+ // wrapped, through a Ruby call that a pending interrupt would turn into
132
+ // a panic; a signal arriving during a process's first run waits for
133
+ // exactly that moment.
134
+ Runtime::class(ruby);
135
+ Snapshot::class(ruby);
136
136
 
137
137
  Ok(())
138
138
  }
@@ -157,49 +157,62 @@ struct Runtime {
157
157
  impl DataTypeFunctions for Runtime {}
158
158
 
159
159
  impl Runtime {
160
- /// Construct a Runtime from a wasm file path, using the process-wide
161
- /// shared Engine and per-path Module / InstancePre caches. The single
162
- /// Ruby-facing constructor for `Kobako::Runtime` — Engine and Module
163
- /// are never visible to Ruby.
164
- ///
165
- /// `timeout_seconds` is the wall-clock cap in seconds
166
- /// (`None` disables); `memory_limit` is the linear-memory cap in
167
- /// bytes (`None` disables); `stdout_limit` / `stderr_limit`
168
- /// are the per-channel output caps (`None`
169
- /// disables); `profile` is the isolation rung the driver builds
170
- /// (`:permissive` / `:hermetic`); `gvl` is the scheduling mode
171
- /// (`:hold` / `:release`) deciding whether each invocation releases
172
- /// the GVL for its guest span. All six are validated by the caller
173
- /// (`Kobako::Sandbox`); this method only refuses non-finite or
174
- /// non-positive timeouts, off-ladder profiles, and unrecognized gvl
175
- /// modes as a defence in depth.
176
- fn from_path(
177
- path: String,
178
- timeout_seconds: Option<f64>,
179
- memory_limit: Option<usize>,
180
- stdout_limit: Option<usize>,
181
- stderr_limit: Option<usize>,
182
- profile: Symbol,
183
- gvl: Symbol,
184
- ) -> Result<Self, MagnusError> {
160
+ /// The only Ruby-facing constructor, so Engine and Module are never
161
+ /// visible to Ruby. Every option is a required keyword: the defaults
162
+ /// belong to `Kobako::SandboxOptions`, which names the keys.
163
+ fn from_path(path: String, options: RHash) -> Result<Self, MagnusError> {
185
164
  let ruby = Ruby::get().expect("Ruby thread");
186
- let timeout = match timeout_seconds {
187
- None => None,
188
- Some(secs) if secs.is_finite() && secs > 0.0 => Some(Duration::from_secs_f64(secs)),
189
- Some(secs) => {
190
- // An invalid cap argument is a Host App
191
- // programming error and raises `ArgumentError`, outside the
192
- // construction-failure `SetupError` branch. `SandboxOptions`
193
- // is the primary guard (it never lets a bad timeout reach
194
- // here); this is defence-in-depth for direct `from_path` calls.
195
- return Err(MagnusError::new(
196
- ruby.exception_arg_error(),
197
- format!("timeout must be > 0 and finite, got {secs} seconds"),
198
- ));
199
- }
165
+ type Options = (
166
+ Option<f64>,
167
+ Option<f64>,
168
+ Option<usize>,
169
+ Option<usize>,
170
+ Option<usize>,
171
+ Option<usize>,
172
+ Symbol,
173
+ Symbol,
174
+ );
175
+ let options = get_kwargs::<_, Options, (), ()>(
176
+ options,
177
+ &[
178
+ "timeout",
179
+ "setup_timeout",
180
+ "memory_limit",
181
+ "stdout_limit",
182
+ "stderr_limit",
183
+ "open_file_limit",
184
+ "profile",
185
+ "gvl",
186
+ ],
187
+ &[],
188
+ )?;
189
+ let (
190
+ timeout_seconds,
191
+ setup_timeout_seconds,
192
+ memory_limit,
193
+ stdout_limit,
194
+ stderr_limit,
195
+ open_file_limit,
196
+ profile,
197
+ gvl,
198
+ ) = options.required;
199
+ // An invalid cap argument is a Host App programming error and
200
+ // raises `ArgumentError`, outside the construction-failure
201
+ // `SetupError` branch. `SandboxOptions` is the primary guard (it
202
+ // never lets a bad timeout reach here); this is defence-in-depth
203
+ // for direct `from_path` calls.
204
+ let to_duration = |seconds: Option<f64>, name: &str| match seconds {
205
+ None => Ok(None),
206
+ Some(secs) if secs.is_finite() && secs > 0.0 => Ok(Some(Duration::from_secs_f64(secs))),
207
+ Some(secs) => Err(MagnusError::new(
208
+ ruby.exception_arg_error(),
209
+ format!("{name} must be > 0 and finite, got {secs} seconds"),
210
+ )),
200
211
  };
212
+ let timeout = to_duration(timeout_seconds, "timeout")?;
213
+ let setup_timeout = to_duration(setup_timeout_seconds, "setup_timeout")?;
201
214
  // Fail closed on an off-ladder rung: an unrecognized posture must
202
- // never fall back to a grant. Same defence-in-depth posture as the
215
+ // never fall back to a weaker posture. Same defence in depth as the
203
216
  // timeout guard above — `SandboxOptions` is the primary validator.
204
217
  let profile = match profile.name()?.as_ref() {
205
218
  "hermetic" => Profile::Hermetic,
@@ -225,17 +238,16 @@ impl Runtime {
225
238
  }
226
239
  };
227
240
 
228
- let driver = Driver::new(
229
- Path::new(&path),
230
- Config {
231
- timeout,
232
- memory_limit,
233
- stdout_limit,
234
- stderr_limit,
235
- profile,
236
- },
237
- )
238
- .map_err(|e| errors::setup_to_magnus(&ruby, e))?;
241
+ let mut config = Config::default();
242
+ config.timeout = timeout;
243
+ config.setup_timeout = setup_timeout;
244
+ config.memory_limit = memory_limit;
245
+ config.stdout_limit = stdout_limit;
246
+ config.stderr_limit = stderr_limit;
247
+ config.open_file_limit = open_file_limit;
248
+ config.profile = profile;
249
+ let driver =
250
+ Driver::new(Path::new(&path), config).map_err(|e| errors::setup_to_magnus(&ruby, e))?;
239
251
  Ok(Self {
240
252
  driver,
241
253
  release_gvl,
@@ -246,90 +258,90 @@ impl Runtime {
246
258
  // Run-path methods. Each takes the run's dispatch handler as its first
247
259
  // argument and returns a `Snapshot` for any completed invocation —
248
260
  // success or trap alike. Only a could-not-start fault (a missing export
249
- // or a fault before the export call) raises a `Kobako::TrapError`
250
- // directly, since it yields no `Snapshot`.
261
+ // or a fault before the export call) raises directly, as the class
262
+ // `errors::to_magnus` gives its cause, since it yields no `Snapshot`.
263
+ //
264
+ // `setup` is the invocation's `[paths, snippets, flags, mounts]`: the
265
+ // Preamble's three parts, then the Mounts as
266
+ // `filesystem::to_grants` reads them.
251
267
  // -----------------------------------------------------------------
252
268
 
253
- /// One-shot mruby source execution (`#eval`). Builds the dispatch
254
- /// handler from `dispatch` (the per-invocation Proc), frames the two
255
- /// stdin invocation frames from the registry state (`paths`,
256
- /// `snippets`), hands them and the source to the driver, and returns
257
- /// the run's `Snapshot`.
258
269
  fn eval(
259
270
  &self,
260
271
  dispatch: Value,
261
- paths: RArray,
272
+ setup: RArray,
262
273
  source: RString,
263
- snippets: RArray,
264
274
  ) -> Result<Snapshot, MagnusError> {
265
- let ruby = Ruby::get().expect("Ruby thread");
266
- let handler = build_handler(dispatch);
267
- let preamble = frame_preamble(paths)?;
268
275
  let source = rstring_to_vec(source);
269
- let snippets = frame_snippets(&ruby, snippets)?;
270
- // Release the GVL around the guest span iff this Sandbox asks for it;
271
- // the closure touches no Ruby VALUE (the driver is magnus-free, and a
272
- // guest→host dispatch re-acquires the GVL through the bridge).
273
- let result = gvl::region(self.release_gvl, || {
274
- self.driver.invoke(
275
- Entry::Eval { source: &source },
276
- Frames {
277
- preamble: &preamble,
278
- snippets: &snippets,
279
- },
280
- handler,
281
- )
282
- });
283
- let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
284
- Ok(Snapshot::from(snapshot))
276
+ self.invoke(dispatch, setup, Entry::Eval { source: &source })
285
277
  }
286
278
 
287
- /// Execute one entrypoint dispatch (`__kobako_run`) and return its
288
- /// `Snapshot`.
289
- ///
290
- /// The two-frame stdin protocol (preamble + snippets; no user source
291
- /// frame — docs/wire-codec.md § Invocation channels) plus the Run
292
- /// envelope copied into guest linear memory — `entrypoint` routes it,
293
- /// `payload` feeds it, and this side frames the two together. Cap
294
- /// semantics match `#eval`.
295
279
  fn run(
296
280
  &self,
297
281
  dispatch: Value,
298
- paths: RArray,
299
- snippets: RArray,
282
+ setup: RArray,
300
283
  entrypoint: String,
301
284
  payload: RString,
285
+ ) -> Result<Snapshot, MagnusError> {
286
+ self.invoke(
287
+ dispatch,
288
+ setup,
289
+ Entry::Run {
290
+ entrypoint: &entrypoint,
291
+ payload: rstring_to_vec(payload),
292
+ },
293
+ )
294
+ }
295
+
296
+ /// One owner for the wiring both verbs share, so a Preamble, a Grant,
297
+ /// or a handler change cannot drift between them.
298
+ fn invoke(
299
+ &self,
300
+ dispatch: Value,
301
+ setup: RArray,
302
+ entry: Entry<'_>,
302
303
  ) -> Result<Snapshot, MagnusError> {
303
304
  let ruby = Ruby::get().expect("Ruby thread");
305
+ let verb = match entry {
306
+ Entry::Eval { .. } => "eval",
307
+ Entry::Run { .. } => "run",
308
+ };
304
309
  let handler = build_handler(dispatch);
305
- let preamble = frame_preamble(paths)?;
306
- let snippets = frame_snippets(&ruby, snippets)?;
307
- let envelope = Run {
308
- entrypoint,
309
- payload: rstring_to_vec(payload),
310
- }
311
- .encode();
310
+ let preamble = to_preamble(&ruby, setup)?;
311
+ let (grants, _pins) = filesystem::to_grants(&ruby, setup.entry(3)?)?;
312
312
  // Release the GVL around the guest span iff this Sandbox asks for it;
313
- // see the note in `#eval`.
313
+ // the closure touches no Ruby VALUE (the driver is magnus-free, and a
314
+ // guest→host dispatch, or a mount's block, re-acquires the GVL).
314
315
  let result = gvl::region(self.release_gvl, || {
315
- self.driver.invoke(
316
- Entry::Run {
317
- envelope: &envelope,
318
- },
319
- Frames {
320
- preamble: &preamble,
321
- snippets: &snippets,
322
- },
323
- handler,
324
- )
316
+ self.driver.invoke(entry, &preamble, handler, &grants)
325
317
  });
326
- let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
327
- Ok(Snapshot::from(snapshot))
318
+ escape::resume()?;
319
+ result
320
+ .map(|snapshot| Snapshot::new(snapshot, verb))
321
+ .map_err(|e| errors::to_magnus(&ruby, e))
322
+ }
323
+
324
+ /// The bytecode of the source snippets, one binary String each in
325
+ /// order, or `nil` when the guest compiled nothing; no failure raises,
326
+ /// since the Sandbox keeps the source either way. Takes the Preamble an
327
+ /// invocation sends, `paths` included, so the driver can tell whether
328
+ /// the bytecode still fits it; no dispatch handler is bound, since
329
+ /// compiling runs no script.
330
+ fn compile(&self, preamble: RArray) -> Result<Option<RArray>, MagnusError> {
331
+ let ruby = Ruby::get().expect("Ruby thread");
332
+ let preamble = to_preamble(&ruby, preamble)?;
333
+ let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
334
+ Ok(bytecodes.map(|bytecodes| {
335
+ ruby.ary_from_iter(
336
+ bytecodes
337
+ .iter()
338
+ .map(|bytecode| ruby.str_from_slice(bytecode)),
339
+ )
340
+ }))
328
341
  }
329
342
 
330
- /// Return the isolation profile the driver built, as a Symbol
331
- /// (`:hermetic` / `:permissive`) — the declaration the Sandbox
332
- /// compares against the posture its `profile:` option requested.
343
+ /// The driver's declaration, which the Sandbox checks against the
344
+ /// posture its `profile:` option requested.
333
345
  fn profile(&self) -> Symbol {
334
346
  let ruby = Ruby::get().expect("Ruby thread");
335
347
  match self.driver.profile() {
@@ -339,11 +351,9 @@ impl Runtime {
339
351
  }
340
352
  }
341
353
 
342
- /// Build the dispatch handler for one invocation from the per-call `dispatch`
343
- /// Proc. A `nil` Proc yields no handler. The Proc stays GC-rooted for the
344
- /// duration of the synchronous `#eval` / `#run` call as a live method
345
- /// argument on the Ruby stack, so the driver only borrows it (the safety
346
- /// contract on `kobako_runtime::runtime::Runtime`).
354
+ /// The Proc stays GC-rooted for the synchronous `#eval` / `#run` call as a
355
+ /// live method argument on the Ruby stack, so the driver only borrows it
356
+ /// (the safety contract on `kobako_runtime::runtime::Runtime`).
347
357
  fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
348
358
  if dispatch.is_nil() {
349
359
  return None;
@@ -353,135 +363,3 @@ fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
353
363
  as Arc<dyn DispatchHandler>,
354
364
  )
355
365
  }
356
-
357
- /// One invocation's result at the Ruby boundary — the whole `Snapshot` the
358
- /// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
359
- /// two output captures are present on every outcome, so the trap path
360
- /// carries them just like the value path; the completion is read as either
361
- /// the outcome bytes (`#outcome`) or a trap (`#trapped?` / `#trap_kind` /
362
- /// `#trap_message`), and the Sandbox layer maps a trap onto its
363
- /// `Kobako::TrapError` family.
364
- #[derive(TypedData)]
365
- #[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
366
- struct Snapshot {
367
- completion: Completion,
368
- stdout: Capture,
369
- stderr: Capture,
370
- usage: Usage,
371
- }
372
-
373
- impl DataTypeFunctions for Snapshot {}
374
-
375
- impl From<RuntimeSnapshot> for Snapshot {
376
- fn from(snapshot: RuntimeSnapshot) -> Self {
377
- let RuntimeSnapshot {
378
- completion,
379
- stdout,
380
- stderr,
381
- usage,
382
- } = snapshot;
383
- Self {
384
- completion,
385
- stdout,
386
- stderr,
387
- usage,
388
- }
389
- }
390
- }
391
-
392
- impl Snapshot {
393
- /// One completed run's outcome, already split off the core envelope:
394
- /// `[kind, payload, panic]`. `kind` names the arm — `:ok`,
395
- /// `:panic`, `:absent` (nothing written), or `:malformed` (bytes the
396
- /// envelope cannot frame). `payload` is the invocation's
397
- /// codec-encoded value, carried only by `:ok`; every other arm
398
- /// answers empty. `panic` carries the Panic's own fields on `:panic`
399
- /// and is `nil` otherwise, so the Ruby side maps a failure onto its
400
- /// error taxonomy without decoding a payload byte.
401
- ///
402
- /// A trap answers `:absent` — `#trapped?` is the authoritative
403
- /// discriminator there and this is never read.
404
- fn outcome(&self) -> (Symbol, RString, Option<PanicFields>) {
405
- let ruby = Ruby::get().expect("Ruby thread");
406
- let empty = || ruby.str_from_slice(&[]);
407
- let Completion::Outcome(bytes) = &self.completion else {
408
- return (ruby.to_symbol("absent"), empty(), None);
409
- };
410
- match Outcome::decode(bytes) {
411
- Ok(Outcome::Ok(value)) => (ruby.to_symbol("ok"), ruby.str_from_slice(&value), None),
412
- Ok(Outcome::Panic(panic)) => (
413
- ruby.to_symbol("panic"),
414
- empty(),
415
- Some((
416
- panic.origin.name().to_owned(),
417
- panic.error.name,
418
- panic.error.message,
419
- panic.error.backtrace,
420
- panic.available,
421
- )),
422
- ),
423
- Err(_) if bytes.is_empty() => (ruby.to_symbol("absent"), empty(), None),
424
- Err(_) => (ruby.to_symbol("malformed"), empty(), None),
425
- }
426
- }
427
-
428
- /// `true` iff the invocation completed via an engine trap.
429
- fn trapped(&self) -> bool {
430
- matches!(self.completion, Completion::Trap(_))
431
- }
432
-
433
- /// The trap's neutral kind as a Symbol (`:timeout` / `:memory_limit` /
434
- /// `:trap`), or `nil` on a completed run. The Sandbox maps this onto the
435
- /// named `Kobako::TrapError` subclass.
436
- fn trap_kind(&self) -> Option<Symbol> {
437
- let ruby = Ruby::get().expect("Ruby thread");
438
- match &self.completion {
439
- Completion::Trap(Trap::Timeout(_)) => Some(ruby.to_symbol("timeout")),
440
- Completion::Trap(Trap::MemoryLimit(_)) => Some(ruby.to_symbol("memory_limit")),
441
- // A cap this frontend has no named subclass for is still a
442
- // trap; the base class is what the taxonomy owes it.
443
- Completion::Trap(_) => Some(ruby.to_symbol("trap")),
444
- Completion::Outcome(_) => None,
445
- }
446
- }
447
-
448
- /// The trap's message, or `nil` on a completed run.
449
- fn trap_message(&self) -> Option<String> {
450
- match &self.completion {
451
- Completion::Trap(trap) => Some(trap.to_string()),
452
- Completion::Outcome(_) => None,
453
- }
454
- }
455
-
456
- /// Wall-clock seconds the guest export call spent inside wasmtime.
457
- fn wall_time(&self) -> f64 {
458
- self.usage.wall_time
459
- }
460
-
461
- /// High-water `memory.grow` delta in bytes past the entry-time baseline.
462
- fn memory_peak(&self) -> usize {
463
- self.usage.memory_peak
464
- }
465
-
466
- /// Bytes captured on the guest's stdout channel, clipped to the cap.
467
- fn stdout(&self) -> RString {
468
- let ruby = Ruby::get().expect("Ruby thread");
469
- ruby.str_from_slice(&self.stdout.bytes)
470
- }
471
-
472
- /// `true` iff the stdout channel reached its cap during this run.
473
- fn stdout_truncated(&self) -> bool {
474
- self.stdout.truncated
475
- }
476
-
477
- /// Bytes captured on the guest's stderr channel, clipped to the cap.
478
- fn stderr(&self) -> RString {
479
- let ruby = Ruby::get().expect("Ruby thread");
480
- ruby.str_from_slice(&self.stderr.bytes)
481
- }
482
-
483
- /// `true` iff the stderr channel reached its cap during this run.
484
- fn stderr_truncated(&self) -> bool {
485
- self.stderr.truncated
486
- }
487
- }
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # What one invocation wrote to stdout or stderr, up to that channel's
5
- # cap, together with whether it wrote past the cap. Frozen, so the bytes
6
- # and the flag always travel together.
7
- class Capture
4
+ class Capture # :nodoc:
8
5
  attr_reader :bytes
9
6
 
10
7
  # The bytes read as UTF-8 when they are valid UTF-8, and as binary