kobako 0.28.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. data/sig/kobako/snippet/binary.rbs +0 -12
data/README.md CHANGED
@@ -45,7 +45,7 @@ gem install kobako
45
45
 
46
46
  ## Quick Start
47
47
 
48
- The same untrusted script runs identically from either host frontend. It executes inside the Wasm guest and cannot read your filesystem, open sockets, or touch your environment.
48
+ The same untrusted script runs identically from either host frontend. It executes inside the Wasm guest and reaches no file you have not mounted for it, no socket, and none of your environment.
49
49
 
50
50
  ```ruby
51
51
  require "kobako"
@@ -129,12 +129,13 @@ These terms name the concepts the rest of this README builds on.
129
129
  | Invocation | — | One run of guest code, by `#eval` or `#run`. |
130
130
  | Execution | `Kobako::Execution` | The frozen record an Invocation leaves. |
131
131
  | Service | — | A host object the guest reaches by name, such as `MyService::KV`. |
132
+ | Mount | — | A `Kobako::Filesystem` placed at a guest path by `Sandbox#mount`, for the guest's `File`. |
132
133
  | Context | `Kobako::Context` | The per-Invocation object the optional block receives. |
133
134
  | Snippet | — | Guest source or bytecode replayed at the start of every Invocation. |
134
135
  | Handle | `Kobako::Handle` | An opaque reference to a host object the wire cannot carry by value. |
135
136
  | Block | — | Guest code passed alongside a Service call. |
136
137
 
137
- A Sandbox keeps nothing from one Invocation to the next. An Execution holds what the run produced, wrote, and consumed: `#value`, output captures, and `#usage`. A failed run raises, carrying its Execution on the error's `#execution`. A Service is the guest's only route to a host resource. A Context's `ctx.bind` supplies a Service object for that one run. A Handle names its object only within the Invocation that issued it. A Block stays in the guest; each `yield` to it is a synchronous round-trip.
138
+ A Sandbox keeps nothing from one Invocation to the next. An Execution holds what the run produced, wrote, and consumed: `#value`, output captures, and `#usage`. A failed run raises, carrying its Execution on the error's `#execution`. A Service or a Mount is the guest's only route to a host resource. A Context's `ctx.bind` supplies a Service object for that one run. A Handle names its object only within the Invocation that issued it. A Block stays in the guest; each `yield` to it is a synchronous round-trip.
138
139
 
139
140
  ## Usage
140
141
 
@@ -143,6 +144,7 @@ Each section below stands on its own; this is the order they build in.
143
144
  | If you want to | Read |
144
145
  |---|---|
145
146
  | let guest code call into your host | [Services](#services), [Per-Invocation Bindings](#per-invocation-bindings), [Service Blocks](#service-blocks) |
147
+ | let guest code read and write files | [Mounts](#mounts) |
146
148
  | see what a run produced or why it failed | [Output Capture](#output-capture), [Error Handling](#error-handling), [Invocation Lifecycle](#invocation-lifecycle) |
147
149
  | bound what a run may consume | [Resource Limits](#resource-limits), [Concurrency](#concurrency), [Pooling](#pooling) |
148
150
  | hand host objects across the boundary | [Handle Management](#handle-management) |
@@ -150,7 +152,7 @@ Each section below stands on its own; this is the order they build in.
150
152
 
151
153
  ### Services
152
154
 
153
- `bind` any Ruby object as a Service at a constant-path name. The guest reaches it as a `MyService::KV` (or top-level `File`) proxy. Through the Transport wire, the guest invokes the public methods the object's own class defines.
155
+ `bind` any Ruby object as a Service at a constant-path name. The guest reaches it as a `MyService::KV` (or top-level `Clock`) proxy. Through the Transport wire, the guest invokes the public methods the object's own class defines.
154
156
 
155
157
  ```ruby
156
158
  class User
@@ -170,7 +172,7 @@ sandbox.eval(<<~RUBY)
170
172
  RUBY
171
173
  ```
172
174
 
173
- Each `::`-separated path segment must match `/\A[A-Z]\w{0,65533}\z/`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation (see [Invocation Lifecycle](#invocation-lifecycle)); later `#bind` raises `ArgumentError`.
175
+ Each `::`-separated path segment must match `/\A[A-Z]\w{0,65533}\z/`. A path under `Kobako`, which the Guest Binary keeps for itself, raises `ArgumentError`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation (see [Invocation Lifecycle](#invocation-lifecycle)); later `#bind` raises `ArgumentError`.
174
176
 
175
177
  ### Per-Invocation Bindings
176
178
 
@@ -184,6 +186,84 @@ sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
184
186
 
185
187
  `ctx.bind` also shadows an already-bound path for that one run. The Context is spent once the block returns, so a `ctx` captured out of it raises `ArgumentError`. Choosing the object per invocation lets concurrent Threads invoke a single Sandbox and still keep their identities apart. The [multi-tenant example](examples/multi-tenant/) runs that shape end to end.
186
188
 
189
+ ### Mounts
190
+
191
+ `mount` places a `Kobako::Filesystem` at an absolute guest path, where the guest's `File` reads it. The default access is `:read`, so a write beneath it raises `Errno::EROFS` in the guest.
192
+
193
+ ```ruby
194
+ sandbox.mount("/config", Kobako::Filesystem::Memory.new("app.yml" => yaml))
195
+ sandbox.mount("/tmp", Kobako::Filesystem::Temporary.new, access: :read_write)
196
+ sandbox.mount("/data", Kobako::Filesystem::Directory.new("var/data")) { |path, _access| path.end_with?(".csv") }
197
+
198
+ sandbox.eval('File.read("/config/app.yml")')
199
+ ```
200
+
201
+ | Kind | Holds |
202
+ |---|---|
203
+ | `Memory.new(files = {}, limit:, inodes:)` | files in memory, shared by every invocation |
204
+ | `Temporary.new(limit:, inodes:)` | files in memory, empty at every invocation |
205
+ | `Directory.new(path)` | a host directory; nothing outside it is reachable |
206
+ | `Overlay.new.layer(fs, access:)` | Layers stacked into one view, the first added on top |
207
+
208
+ A Memory or Temporary is bounded as a tmpfs mount is. Its files hold 64 MiB unless `limit:` gives another size in bytes. It holds one file or directory for each 4 KiB of that limit, and its root, unless `inodes:` gives another count. `nil` lifts either bound, and the guest raises `Errno::ENOSPC` past one.
209
+
210
+ A block decides each open and each change that reaches the Mount in the access's place, asked at most once for each. A falsy answer or a `StandardError` fails that call as `Errno::EACCES`. Mounts seal with the Sandbox like a `bind`, and `ctx.mount` replaces one for a single run.
211
+
212
+ #### Guest File
213
+
214
+ The guest's `File.open` takes a mode String such as `"r"`, `"w+"`, or `"wx"`, or an Integer of `File::Constants` flags, valued as CRuby's on Linux. An exclusive create lets Sandboxes sharing a directory agree on a lock file. The critical flag `disable_file` removes `File` from the guest altogether.
215
+
216
+ Beneath a mount, `File.open` and `IO.new` answer as CRuby does over a Linux directory. Where they differ, they only refuse more:
217
+
218
+ | When | The guest sees |
219
+ |---|---|
220
+ | nothing is mounted | `NotImplementedError` |
221
+ | a block refuses a change | `Errno::EACCES` where a read-only mount says `Errno::EROFS` |
222
+ | a Ruby Filesystem's file without `append` is asked to append | `Errno::ENOTSUP` on the write |
223
+ | an Integer mode holds a flag kobako does not honor, or `WRONLY` beside `RDWR` | `Errno::EINVAL` |
224
+ | a mode names an encoding, such as `"r:utf-8"` | `ArgumentError` |
225
+ | `IO.new` wraps stdout or stderr in a mode other than `"w"` | `ArgumentError` |
226
+ | `disable_file` is set | no `File` constant |
227
+
228
+ #### Overlays
229
+
230
+ `layer` adds a Filesystem beneath the Layers already there and answers a new Overlay. Each Layer is `:read`, the default, or `:read_write` on its own.
231
+
232
+ ```ruby
233
+ overlay = Kobako::Filesystem::Overlay.new
234
+ .layer(Kobako::Filesystem::Temporary.new, access: :read_write) # empty every run
235
+ .layer(Kobako::Filesystem::Directory.new("app")) # never written
236
+ sandbox.mount("/app", overlay, access: :read_write)
237
+ ```
238
+
239
+ A change to a file in a writable Layer stays there. A read-only Layer's file is first copied up to the nearest writable Layer above it, and a new or renamed file lands in the topmost one. A lower writable Layer's file does not move, raising `Errno::EXDEV`, as across devices. A removal hides what a lower Layer keeps with an [OCI whiteout](https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts), `.wh.<name>`, kept in the topmost writable Layer. Names starting with `.wh.`, in any case, are therefore reserved.
240
+
241
+ #### Filesystems in Ruby
242
+
243
+ Any object answering `open` and `stat` mounts as a Filesystem, directly or as a Layer. Its methods follow Ruby's `File`, `Dir`, and `IO`, so a real `::File` can answer as an open file.
244
+
245
+ | Method | Answers |
246
+ |---|---|
247
+ | `open(path, flags)` | an open file, below |
248
+ | `stat(path)` | an object answering `directory?`, `size`, and `ino`, as `File::Stat` does |
249
+ | `children(path)` | entry names, as `Dir.children` gives them |
250
+ | `mkdir` · `rmdir` · `unlink` · `rename(from, to, directory:)` | optional; without one, that change raises `Errno::EROFS` |
251
+
252
+ A `stat` may also answer `dev`; without one, the Filesystem stands for a single device. An `ino` names one directory while the guest holds it open, and names match exactly, as a block judges them. A path is relative to the mount, the empty path being its root, and `flags` are `File::Constants`. A `rename` told `directory: false` moves only a file and `true` only a directory; `nil` moves either. An `Errno` the guest has a class for reaches the script as itself, and any other `StandardError` as `Errno::EIO`. Every call re-enters Ruby, and its time counts toward `timeout:` without being interrupted.
253
+
254
+ ##### Open Files
255
+
256
+ The file `open` answers takes the guest's reads and writes, each method named as `IO` names it.
257
+
258
+ | Method | Answers | Without it |
259
+ |---|---|---|
260
+ | `pread(length, offset)` · `size` | bytes, `nil` or `EOFError` at the end; the length | required |
261
+ | `pwrite(data, offset)` · `truncate(length)` | bytes written; nothing | that change raises `Errno::EROFS` |
262
+ | `append(data)` | where the file then ends, as `IO#pos` does | an append raises `Errno::ENOTSUP` |
263
+ | `fsync` · `close` | nothing | nothing to flush or release |
264
+
265
+ An `append` writes all of `data` at the end in one step, so another writer's append never lands over it.
266
+
187
267
  ### Output Capture
188
268
 
189
269
  Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value`. Each invocation captures its own; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
@@ -225,7 +305,7 @@ Each of these carries the failed run's Execution on `#execution`. A rescue reads
225
305
  | `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
226
306
  | `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
227
307
  | `Kobako::NoServiceError` | `ServiceError` | Nothing bound there, a dead Handle, or a method the guest may not call |
228
- | `Kobako::ServiceArgumentError` | `ServiceError` | The arguments did not fit the Service method |
308
+ | `Kobako::ServiceArgumentError` | `ServiceError` | The call would not bind, or the Service raised `ArgumentError` |
229
309
  | `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
230
310
  | `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
231
311
  | `Kobako::UnsupportedFlagError` | `SandboxError` | The Guest Binary does not recognise a critical `#flag`; no script ran |
@@ -233,6 +313,15 @@ Each of these carries the failed run's Execution on `#execution`. A rescue reads
233
313
 
234
314
  `SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` fields when the guest produced a panic envelope.
235
315
 
316
+ #### Host Exceptions
317
+
318
+ A Service, a mount's block, or a Filesystem written in Ruby is your code running inside the run. What it raises splits by class.
319
+
320
+ | Raised by your code | Result |
321
+ |-----------------------------------------|-----------------------------------------------|
322
+ | a `StandardError` | the guest sees a failure it may rescue |
323
+ | `Interrupt`, `exit`, `throw`, `Timeout` | the run ends; the invocation raises it as is |
324
+
236
325
  ### Resource Limits
237
326
 
238
327
  Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does.
@@ -240,22 +329,36 @@ Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memo
240
329
  ```ruby
241
330
  sandbox = Kobako::Sandbox.new(
242
331
  timeout: 5.0, # seconds, default 60.0
243
- memory_limit: 10 * 1024 * 1024, # bytes, default 1 MiB
332
+ setup_timeout: 2.0, # seconds, default 10.0
333
+ memory_limit: 10 * 1024 * 1024, # bytes, default 4 MiB
244
334
  stdout_limit: 64 * 1024, # bytes, default 1 MiB
245
- stderr_limit: 64 * 1024
335
+ stderr_limit: 64 * 1024,
336
+ open_file_limit: 16 # descriptors, default 1024
246
337
  )
247
338
  ```
248
339
 
249
- | Cap | Raises | Default |
250
- |----------------|----------------------------|---------|
251
- | `timeout` | `Kobako::TimeoutError` | 60.0 s |
252
- | `memory_limit` | `Kobako::MemoryLimitError` | 1 MiB |
253
- | `stdout_limit` | output clipped (no raise) | 1 MiB |
254
- | `stderr_limit` | output clipped (no raise) | 1 MiB |
340
+ | Cap | Raises | Default |
341
+ |-------------------|--------------------------------|---------|
342
+ | `timeout` | `Kobako::TimeoutError` | 60.0 s |
343
+ | `setup_timeout` | `Kobako::SetupError` at `new` | 10.0 s |
344
+ | `memory_limit` | `Kobako::MemoryLimitError` | 4 MiB |
345
+ | `stdout_limit` | output clipped (no raise) | 1 MiB |
346
+ | `stderr_limit` | output clipped (no raise) | 1 MiB |
347
+ | `open_file_limit` | `Errno::EMFILE` in the guest | 1024 |
255
348
 
256
- `memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget.
349
+ `setup_timeout` bounds instantiating a Guest Binary and reading its ABI version, not compiling it. It applies the first time a path is loaded; later Sandboxes on that path skip the check. A Pool builds each Sandbox at its first checkout, so its `SetupError` raises from `with`. `memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget. `open_file_limit` counts every descriptor one invocation holds, the three standard ones and each Mount's included, so a guest cannot exhaust the host process's own.
257
350
 
258
- Beyond the four caps, `profile:` requests the Sandbox's isolation posture on the `:permissive` < `:hermetic` ladder (default `:hermetic`). `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources, an explicit trade of reproducibility. Filesystem, environment, and network stay unreachable under either. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/guides/security.md`](docs/guides/security.md) § Isolation Profiles.
351
+ #### Isolation Profile
352
+
353
+ Beyond the caps, `profile:` requests the Sandbox's isolation posture on a two-rung ladder.
354
+
355
+ ```
356
+ :permissive < :hermetic (default)
357
+ live clock frozen clock
358
+ live entropy constant entropy
359
+ ```
360
+
361
+ `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources, an explicit trade of reproducibility. The guest reaches no ambient filesystem, environment, or network under either. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/guides/security.md`](docs/guides/security.md) § Isolation Profiles.
259
362
 
260
363
  ### Concurrency
261
364
 
@@ -358,7 +461,7 @@ A Sandbox holds no state from any run, so Threads sharing one are equally safe.
358
461
 
359
462
  ### Service Blocks
360
463
 
361
- A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch.
464
+ A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest. Its `break` / `next` / `return` / exceptions follow normal Ruby semantics: an exit lands where it would through a plain method.
362
465
 
363
466
  ```ruby
364
467
  sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
@@ -367,13 +470,28 @@ sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
367
470
  # => [2, 4, 6]
368
471
  ```
369
472
 
370
- A `yield` that cannot complete raises at the Service's call site, so the Service may rescue it. Left unrescued, a `BlockError` hands the guest its own exception back; the other two fail the Service call.
473
+ A `yield` that cannot complete raises at the Service's call site, so the Service may rescue it. Left unrescued, a `BlockError` hands the guest its own exception back; the others fail the Service call.
371
474
 
372
475
  | Class | Trigger at `yield` |
373
476
  |----------------------------|---------------------------------------|
374
477
  | `Kobako::BlockError` | The guest block raised |
375
478
  | `Kobako::YieldValueError` | An argument cannot cross to the guest |
376
479
  | `Kobako::UnsupportedError` | The Guest Binary serves no block |
480
+ | `ThreadError` | The block runs off the calling Fiber |
481
+
482
+ #### Block Fiber
483
+
484
+ A block runs only on the Fiber that called the Service. The guest's Wasm stack lives on that Fiber's native stack, so entering it from another raises `ThreadError` before the block runs. CRuby itself lets a block run anywhere; kobako narrows that to keep the guest's stack checks sound.
485
+
486
+ ```
487
+ Service on the calling Fiber
488
+ ├─ blk.call / yield ─> runs in the guest
489
+ ├─ Thread.new { blk.call } ─> ThreadError
490
+ └─ Enumerator.new { |y| y << blk.call }
491
+ .next (runs its body on a Fiber) ─> ThreadError
492
+ ```
493
+
494
+ `Enumerator#next` counts as another Fiber, because it runs the enumerator's body on one. Internal iteration such as `each` or `map` stays on the calling Fiber and runs the block normally.
377
495
 
378
496
  ### Handle Management
379
497
 
@@ -391,7 +509,7 @@ sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle r
391
509
  sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (Handle restoration)
392
510
  ```
393
511
 
394
- A `break` value from a guest block is the one exception. It unwinds back to the guest Service call rather than to host code, so a Handle in it stays a Handle. Restoring would just re-wrap the same object into a new id on the return trip.
512
+ A `break` value from a guest block never reaches host code. It stays in the guest while the Service unwinds, so a Handle in it stays a Handle.
395
513
 
396
514
  #### Per-dispatch Handles
397
515
 
@@ -461,8 +579,8 @@ Use the source form for snippets authored in your repo; use the bytecode form wh
461
579
  An Extension teaches the guest a native-style constant by pairing a guest idiom (`source`) with an optional host `backend`. `Sandbox#install` composes the two through the existing `#preload` and `#bind` verbs, adding no wire or Guest Binary surface. Pure operations run in-guest with no round-trip. The rest dispatch to the backend under the same isolation and reflection guarantees as any bound Service ([`docs/guides/extensions.md`](docs/guides/extensions.md)).
462
580
 
463
581
  ```ruby
464
- FILE = <<~'MRUBY'
465
- class File
582
+ VFS = <<~'MRUBY'
583
+ class Vfs
466
584
  extend Kobako::Proxy
467
585
  def self.basename(path) = path.split("/").last || "" # pure: runs in-guest
468
586
  # read / write are undefined here, so they dispatch to the backend
@@ -471,16 +589,16 @@ MRUBY
471
589
 
472
590
  sandbox.install(
473
591
  Kobako::Extension.new(
474
- name: :File,
475
- source: FILE,
592
+ name: :Vfs,
593
+ source: VFS,
476
594
  backend: Kobako::Extension::Backend.new(
477
- path: "File",
478
- provider: -> { OverlayFileSystem.new(root) } # invoked once per invocation
595
+ path: "Vfs",
596
+ provider: -> { CopyOnWriteFiles.new(root) } # invoked once per invocation
479
597
  )
480
598
  )
481
599
  )
482
600
 
483
- sandbox.eval('File.read("sample.txt")') # dispatches to the backend's #read
601
+ sandbox.eval('Vfs.read("sample.txt")') # dispatches to the backend's #read
484
602
  ```
485
603
 
486
604
  A backend declares the bound object's lifetime by keyword, never by inference — so a static object that happens to be callable stays unambiguous.
@@ -491,12 +609,12 @@ A backend declares the bound object's lifetime by keyword, never by inference
491
609
  | `provider:` | A no-argument callable invoked per invocation |
492
610
  | neither | A fillable, standing for `Kobako::Unresolved` until `ctx.bind` supplies the run's object |
493
611
 
494
- A writable backend takes `provider:`, so its state cannot leak across runs. kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked `File` that reads through to disk while protecting it from guest writes.
612
+ A writable backend takes `provider:`, so its state cannot leak across runs. kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked virtual file that reads through to disk while protecting it from guest writes.
495
613
 
496
614
  ## Security
497
615
 
498
- kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
499
- exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
616
+ kobako isolates the guest, but **what it may reach is whatever you `bind` or `mount`**.
617
+ `bind` exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
500
618
  methods stay out of reach, but the class's own surface does not. Bind a purpose-built object
501
619
  scoped to the task, not a capable one whose other methods leak more than you intend.
502
620
 
@@ -524,23 +642,23 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
524
642
  | Phase | Cost |
525
643
  |--------------------------------------------------------------|-----------------------|
526
644
  | First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
527
- | First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time |
645
+ | First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~1.4 ms one-time |
528
646
  | Subsequent `Sandbox.new` (caches warm) | ~3 µs |
529
- | Warm `#eval` with one Service call on a reused Sandbox | ~70 µs |
530
- | Warm `#run(:Entrypoint, ...)` dispatch | ~79 µs |
531
- | Service call amortized inside one invocation | ~5.8 µs |
532
- | Snippet replay per invocation | ~7.0 µs each |
647
+ | Warm `#eval` with one Service call on a reused Sandbox | ~62 µs |
648
+ | Warm `#run(:Entrypoint, ...)` dispatch | ~57 µs |
649
+ | Service call amortized inside one invocation | ~4.9 µs |
650
+ | Snippet replay per invocation | ~2.7 µs each |
533
651
  | Per additional idle Sandbox (RSS) | < 1 KB |
534
652
 
535
- The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance, which is why a thousand idle tenants cost ~34 MB total. The canonical boot state is baked into the artifact and instantiated per invocation. A +10% regression on any gated benchmark blocks release.
653
+ The Cranelift JIT runs once per machine and wasmtime release — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance, which is why a thousand idle tenants cost ~37 MB total. The canonical boot state is baked into the artifact and instantiated per invocation. A +10% regression on any gated benchmark blocks release.
536
654
 
537
655
  #### Throughput Levers
538
656
 
539
657
  | Choice | What it changes |
540
658
  |---|---|
541
- | `gvl: :hold` (default) | ~16k `#eval`/s in aggregate, at any Thread count |
659
+ | `gvl: :hold` (default) | ~15–20k `#eval`/s in aggregate, at any Thread count |
542
660
  | `gvl: :release` | lifts that ceiling for compute-bound scripts (see [Concurrency](#concurrency)) |
543
- | `match?` over `=~` | `=~` (~5 µs/match) costs about 5× `match?` (~1.0 µs) |
661
+ | `match?` over `=~` | `=~` (~5.6 µs/match) costs about 6× `match?` (~0.9 µs) |
544
662
 
545
663
  Under `:hold` the GVL serializes wasm work, though Ruby-side `#eval` setup still overlaps. `=~` eagerly builds the `MatchData` and match globals, so prefer `match?` for boolean tests. Regexp is an opt-in capability gem, excluded from the default binary and the gated set. Its throughput is tracked in a separate non-gated characterization (`#11` in [`benchmark/README.md`](benchmark/README.md)).
546
664
 
data/ROADMAP.md CHANGED
@@ -24,3 +24,8 @@ row below links a feature to its scenarios and to where its code starts.
24
24
  | ✅ [Regexp](docs/spec/behavior/regexp.md) | [wasm/kobako-regexp/src/regexp.rs](wasm/kobako-regexp/src/regexp.rs) |
25
25
  | ✅ [Regexp over String](docs/spec/behavior/regexp-string.md) | [wasm/kobako-regexp/src/string_ext.rs](wasm/kobako-regexp/src/string_ext.rs) |
26
26
  | ✅ [MatchData](docs/spec/behavior/regexp-matchdata.md) | [wasm/kobako-regexp/src/matchdata.rs](wasm/kobako-regexp/src/matchdata.rs) |
27
+ | ✅ [Guest File](docs/spec/behavior/file.md) | [wasm/kobako-io/src/file.rs](wasm/kobako-io/src/file.rs) |
28
+ | ✅ [Filesystem](docs/spec/behavior/filesystem.md) | [crates/kobako-runtime/src/filesystem.rs](crates/kobako-runtime/src/filesystem.rs) |
29
+ | ✅ [Mount](docs/spec/behavior/mount.md) | [lib/kobako/catalog/mount.rb](lib/kobako/catalog/mount.rb) |
30
+ | ✅ [Ruby Filesystem](docs/spec/behavior/ruby-filesystem.md) | [ext/kobako/src/runtime/filesystem/ruby.rs](ext/kobako/src/runtime/filesystem/ruby.rs) |
31
+ | ✅ [Journeys](docs/spec/behavior/journeys.md) | [test/e2e/test_journeys.rb](test/e2e/test_journeys.rb) |
data/SECURITY.md CHANGED
@@ -24,7 +24,7 @@ ask to stay anonymous.
24
24
  ## Scope
25
25
 
26
26
  In scope is anything that lets guest code cross the isolation boundary it should not:
27
- reaching host memory, the filesystem, the network, or `ENV`; obtaining ambient time or
27
+ reaching host memory, the filesystem beyond what you mounted, the network, or `ENV`; obtaining ambient time or
28
28
  entropy the host froze; reaching a Service you never bound; or a
29
29
  memory-safety fault in the host codec or wasmtime driver.
30
30
 
@@ -1,5 +1,91 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.18.0...kobako-runtime-v0.19.0) (2026-10-26)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **runtime:** a struct literal no longer builds OpenOptions, Metadata, DirEntry, Snapshot, Capture, or Usage outside kobako-runtime; use their `new` or `Default` and assign each field.
9
+ * the Guest ABI is version 5, so a Guest Binary built against version 4 is refused at Sandbox construction until rebuilt. The Yielder trait of kobako-runtime gains frame and takes the frame in yield_to_block; the Guest trait of kobako-core and the MrbGuest trait of kobako-mruby take the frame in yield_to_block; Downcall::Yield carries frame and args.
10
+ * **runtime:** the Runtime trait's invoke takes the invocation's Grants as a fourth argument, so every engine implementing it must accept them.
11
+
12
+ ### Features
13
+
14
+ * check what a rename moves as it moves, refusing a directory beneath a Policy ([23794bf](https://github.com/elct9620/kobako/commit/23794bfcb54e148126e29873fa9b52d9e3de8d8c))
15
+ * **runtime:** bound the files and directories a Memory or Temporary holds ([8d0be0a](https://github.com/elct9620/kobako/commit/8d0be0ab3dc8c11bcb96865b0bf1a0bbb8dd3b16))
16
+ * **runtime:** bound what a Memory or Temporary holds, 64 MiB by default ([2557d1b](https://github.com/elct9620/kobako/commit/2557d1ba7530aacfddf908aeddac561c4fbc5c8a))
17
+ * **runtime:** draw every device a host does not name from one source ([210d55b](https://github.com/elct9620/kobako/commit/210d55b1b4e0699dc5ef56b4b6930be01658a3c6))
18
+ * **runtime:** hand each invocation its Grants and let an engine declare the kinds it supports ([cba90ee](https://github.com/elct9620/kobako/commit/cba90eed09c8afcb23080cef1c9cbb3650648472)), closes [#78](https://github.com/elct9620/kobako/issues/78)
19
+ * **runtime:** keep a directory descriptor's identity its own while open ([6267642](https://github.com/elct9620/kobako/commit/6267642ea831917be76a0101e8d206ccfa8834c1))
20
+ * **runtime:** let a dispatch handler abandon its invocation ([179da9e](https://github.com/elct9620/kobako/commit/179da9ee1f31fed9e6d0c149faee0ad55f8db487))
21
+ * **runtime:** let a Filesystem's file refuse its close ([af16007](https://github.com/elct9620/kobako/commit/af1600797b098796dd6f329672c7d04e2e1cd2fa)), closes [#78](https://github.com/elct9620/kobako/issues/78)
22
+ * **runtime:** let a Mount decide its own access as the one kind of Grant ([5c9189e](https://github.com/elct9620/kobako/commit/5c9189eea8ac61cf165dca1b38e459c4f14acdd0)), closes [#78](https://github.com/elct9620/kobako/issues/78)
23
+ * **runtime:** let a Mount's Policy judge the spelling the host stores ([5281057](https://github.com/elct9620/kobako/commit/52810573f22d2a3edfae9b34b7767a5f440fba15))
24
+ * **runtime:** let the records a Filesystem or an engine builds gain fields ([c0ef88d](https://github.com/elct9620/kobako/commit/c0ef88d0b6df16d651e4c49226e41667307d772f))
25
+ * **runtime:** name a file grown past what a Filesystem keeps ([7bac3e6](https://github.com/elct9620/kobako/commit/7bac3e65534097b85d93f5967b6c73a979869a78))
26
+ * **runtime:** name each code a Filesystem may fail with ([1d980fa](https://github.com/elct9620/kobako/commit/1d980fa4328bad42e8a8665adfec9fc844690e97))
27
+ * **runtime:** name the POSIX error each FilesystemError stands for ([01c301b](https://github.com/elct9620/kobako/commit/01c301b4af9f489594eb7a98ab48c508e5b61ca4))
28
+ * **runtime:** refuse a Memory seed path through `..` ([b31434d](https://github.com/elct9620/kobako/commit/b31434d386ebde838c15755619c67da95311ecf6))
29
+ * **runtime:** refuse a Mount at a path that is no absolute guest path ([a525758](https://github.com/elct9620/kobako/commit/a5257589f9d831b9a3b6d1952df6e5c8469630e1))
30
+ * **runtime:** report a link a lookup does not follow as a link ([8c1e0b3](https://github.com/elct9620/kobako/commit/8c1e0b3e63fbc91b10f11cd91817ce94de24c7e6))
31
+ * **runtime:** report the identity of what a path holds ([392bc0e](https://github.com/elct9620/kobako/commit/392bc0e9f2fb935934a0031bf7d64dec0b27df6a))
32
+ * **runtime:** say whether a path beneath a Mount may pass a link ([7706a25](https://github.com/elct9620/kobako/commit/7706a2537bf8e5975b2b227bef0c05cb48c09f4b))
33
+ * **runtime:** seed a Memory from a list that may not hold, answering why ([8ac2dfd](https://github.com/elct9620/kobako/commit/8ac2dfdf73b84fe2e53405a653d861ddca9ea98c))
34
+ * **runtime:** ship the Memory, Temporary, Directory, and Overlay filesystems ([c60c453](https://github.com/elct9620/kobako/commit/c60c4535e966ffd501e03d7be7fef0a44d860b0b)), closes [#78](https://github.com/elct9620/kobako/issues/78)
35
+ * **runtime:** stack an Overlay from Layers, each read-only or writable ([6c88188](https://github.com/elct9620/kobako/commit/6c88188cd7992c218f8c5a3be30981e685804078)), closes [#78](https://github.com/elct9620/kobako/issues/78)
36
+
37
+
38
+ ### Bug Fixes
39
+
40
+ * append through a Filesystem's file only in one step ([0509ca7](https://github.com/elct9620/kobako/commit/0509ca70d2d5a832303973ba0f7280be0731fd3d))
41
+ * ask a mount's block once for a read ([f89bd16](https://github.com/elct9620/kobako/commit/f89bd16c1a71ed1d385ab9d3ac5974ac3f0a9103))
42
+ * follow no link swapped in after a Policy judges a path ([ed93213](https://github.com/elct9620/kobako/commit/ed9321303dd45e113fc413e99e1240118cc4bbe4))
43
+ * hold an Overlay's lock across each call, waiting without the GVL ([b8e7342](https://github.com/elct9620/kobako/commit/b8e734273fdc577a40be97c43c46d9548d66c625))
44
+ * keep a link within the directory a descriptor opened ([952dd05](https://github.com/elct9620/kobako/commit/952dd0556969683f44798da94f9fc7a17c1f6d55))
45
+ * run a stored block as the block of the Call that carried it ([bf33dc1](https://github.com/elct9620/kobako/commit/bf33dc1fd40c83498c6c12b0be52b3a835a144cf))
46
+ * **runtime:** answer a host failure as the error naming it ([effd43a](https://github.com/elct9620/kobako/commit/effd43a85a3ba69aaa068d2c384c3b5cb466161c))
47
+ * **runtime:** append to a Directory file in one host write ([4a5450a](https://github.com/elct9620/kobako/commit/4a5450a67e202e45299175c4fe7528cf50489f0e))
48
+ * **runtime:** copy a file up whole when its Layer writes in parts ([89d87f7](https://github.com/elct9620/kobako/commit/89d87f7225611786467968ec34eba5a8223b5fc5))
49
+ * **runtime:** fail an Overlay call a Layer cannot answer instead of guessing ([5ba0db3](https://github.com/elct9620/kobako/commit/5ba0db33c56ed837401bcb62345f81b4b50c317a))
50
+ * **runtime:** hold each Memory entry under its own name ([8f1ba8a](https://github.com/elct9620/kobako/commit/8f1ba8a50d3d8ca3a695115d51b605be1f18e29e))
51
+ * **runtime:** judge a link an Overlay's Layer holds as the link itself ([959c1f3](https://github.com/elct9620/kobako/commit/959c1f3dab6f6948baabbd914bbb86dfff090f7b))
52
+ * **runtime:** keep an Overlay rename to one Layer and answer a landed change as done ([e3405b7](https://github.com/elct9620/kobako/commit/e3405b7139809ada202ffdb0d6bf1eef3dcbf056))
53
+ * **runtime:** leave a Memory file as it was on a write of no bytes ([9f8fd23](https://github.com/elct9620/kobako/commit/9f8fd237b9b0042590b149f9b2051e0773d8b9af))
54
+ * **runtime:** leave an Overlay unchanged when a removal or rename is refused ([a7bee4b](https://github.com/elct9620/kobako/commit/a7bee4b2a465a0ef0ad2e376c281dca82a0df0e1))
55
+ * **runtime:** let a file or link in a Layer hide what lies below it ([fd579f6](https://github.com/elct9620/kobako/commit/fd579f679cfa8a83ec7d139066bc1a4570af398b))
56
+ * **runtime:** make every Filesystem answer the methods that keep a guarantee ([00f5af6](https://github.com/elct9620/kobako/commit/00f5af63f34ac7722249746d0b0eb566da618803))
57
+ * **runtime:** move a directory made again as the writable Layer's own ([ac3e864](https://github.com/elct9620/kobako/commit/ac3e864bbb0ce5d61def45e0403239be405481d3))
58
+ * **runtime:** move a directory onto one that holds only markers ([934f0c3](https://github.com/elct9620/kobako/commit/934f0c39e0ad1189bdb1cb0461671245dea32f93))
59
+ * **runtime:** open a Directory file the runtime neither reads nor writes ([6b30858](https://github.com/elct9620/kobako/commit/6b308583383d6803894b2c7af1a9ef05d72b0e7d)), closes [#78](https://github.com/elct9620/kobako/issues/78)
60
+ * **runtime:** open a file already there without writing to create it ([7f76a1a](https://github.com/elct9620/kobako/commit/7f76a1af575739ae16c5daeb1df8268af44cea63))
61
+ * **runtime:** panic on files a Memory cannot be seeded with ([cf5c835](https://github.com/elct9620/kobako/commit/cf5c835d7df8bf82972487280c6a6c79be955a68))
62
+ * **runtime:** put an Overlay's markers back when the change fails ([78ddeed](https://github.com/elct9620/kobako/commit/78ddeeddda8b9f797a80ec3796b28e04cf99ffc9))
63
+ * **runtime:** refuse a backslash in a Directory path on Windows ([88b5719](https://github.com/elct9620/kobako/commit/88b57195325b168395248a82b94a5ebf7e5f4142))
64
+ * **runtime:** refuse a Directory append on Windows rather than take two steps ([a528cc9](https://github.com/elct9620/kobako/commit/a528cc9577f1b2e1359699e2400be93c1f3a9be7))
65
+ * **runtime:** refuse a file moved beneath itself as no directory ([bfabb52](https://github.com/elct9620/kobako/commit/bfabb52b27080ee9e5000bd5524ea201a7e33d80))
66
+ * **runtime:** refuse a host name that is not UTF-8 as an illegal sequence ([c4ff030](https://github.com/elct9620/kobako/commit/c4ff030ffa8e29fa1b10df4aaf6af65162c76353))
67
+ * **runtime:** refuse a Memory file grown past what can be allocated ([302e2d4](https://github.com/elct9620/kobako/commit/302e2d4fcd5b31fee4e953e8993bb47d9a2a673a))
68
+ * **runtime:** refuse an Overlay change landing beneath a read-only Layer ([e7b2f19](https://github.com/elct9620/kobako/commit/e7b2f19156df9773f669cbe8a7aaefc17591cca3))
69
+ * **runtime:** refuse to open a FIFO, socket, or device beneath a Directory ([bdcaf78](https://github.com/elct9620/kobako/commit/bdcaf78bf131998648c65ac51ced4a588a79211f))
70
+ * **runtime:** remove no directory as a file on any host ([c9c0820](https://github.com/elct9620/kobako/commit/c9c0820fd39c0744b9d9c367b977f6587d1f9589))
71
+ * **runtime:** rename and copy up through an Overlay as overlayfs does ([ada5a86](https://github.com/elct9620/kobako/commit/ada5a86a8647d89666e9de808eb13ba638882e51))
72
+ * **runtime:** replace neither a FIFO nor a directory with the other through an Overlay ([8fed062](https://github.com/elct9620/kobako/commit/8fed062438e105c6a784bf4bde1582fda9e3c9c2))
73
+ * **runtime:** report a merged directory's identity alike through an Overlay's lstat ([ded3b4a](https://github.com/elct9620/kobako/commit/ded3b4aba78efa678318c38b139b5c9c76b5d3fd))
74
+ * **runtime:** require cap-std 4.0.3 for the trailing-slash lookup fix ([549cfe0](https://github.com/elct9620/kobako/commit/549cfe07fc8b9affdc8a1765a9e4bc5963ef546e))
75
+ * **runtime:** reserve an Overlay's marker name in any case ([ef39923](https://github.com/elct9620/kobako/commit/ef399230eabe01ad6eed632b2753880ae1fc322c))
76
+ * **runtime:** reserve exactly what a Memory file grows by ([96f8141](https://github.com/elct9620/kobako/commit/96f8141442cac2170a8bf8a76055d4f4659b1100))
77
+ * **runtime:** spell a Directory path's last name without following a link there ([c5e5ae7](https://github.com/elct9620/kobako/commit/c5e5ae74ec1f6ee07e24da08d7b097fb6a882961))
78
+ * **runtime:** spell a path on XFS by listing rather than trusting exact names ([f89495d](https://github.com/elct9620/kobako/commit/f89495dcfd7c6d05d46ebc5dbae0039856cc15c6))
79
+ * **wasmtime:** hold the whole invocation to its deadline ([231e325](https://github.com/elct9620/kobako/commit/231e32592699008adf3105050bee8b5528ea27a8))
80
+ * **wasmtime:** place an append at the end as the file stands when it lands ([d7291c4](https://github.com/elct9620/kobako/commit/d7291c4f32fb1f5f2494c69c76796476bbcf4d43))
81
+ * **wasmtime:** refuse a link an open asks not to follow ([123a01b](https://github.com/elct9620/kobako/commit/123a01b01f59cc69c0e3a23f9aeb01aa89132c07))
82
+
83
+
84
+ ### Performance Improvements
85
+
86
+ * **runtime:** list an Overlay directory in time linear to its entries ([be3f446](https://github.com/elct9620/kobako/commit/be3f446f782cbd72b114f7b6072e3b50c391f093))
87
+ * **runtime:** stop an Overlay's marker lookup at a directory a Layer lacks ([494fa09](https://github.com/elct9620/kobako/commit/494fa090e90806c50e4e8baaa4f1001efc388b45))
88
+
3
89
  ## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.17.0...kobako-runtime-v0.18.0) (2026-10-13)
4
90
 
5
91
 
@@ -2,10 +2,12 @@
2
2
  #
3
3
  # The contract a wasm engine must satisfy to drive a kobako guest,
4
4
  # free of any engine or frontend type: the `Runtime` trait plus the
5
- # neutral value types (`Snapshot`, `Trap`, `SetupError`, ...) and the
6
- # dispatch / yield re-entry traits a frontend supplies. Engine
7
- # implementations (`kobako-wasmtime`) and host frontends (the Ruby
8
- # ext) meet on this surface, so the engine stays swappable.
5
+ # neutral value types (`Snapshot`, `Trap`, `SetupError`, ...), the
6
+ # dispatch / yield re-entry traits a frontend supplies, and the Grants
7
+ # a frontend hands each invocation, the Mount and the Filesystems it
8
+ # places. Engine implementations (`kobako-wasmtime`) and host frontends
9
+ # (the Ruby ext) meet on this surface, so the engine stays swappable.
10
+ # `cap-std` confines the Directory Filesystem to its host directory.
9
11
  #
10
12
  # Self-contained by design: the Ruby gem ships this crate without the
11
13
  # `crates/` workspace manifest, so no `workspace = true` inheritance
@@ -13,7 +15,7 @@
13
15
 
14
16
  [package]
15
17
  name = "kobako-runtime"
16
- version = "0.18.0"
18
+ version = "0.19.0"
17
19
  edition = "2021"
18
20
  description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
19
21
  license = "Apache-2.0"
@@ -27,4 +29,23 @@ categories = ["wasm", "virtualization"]
27
29
  # in-tree builds (and the Ruby gem, which ships both crates) resolving
28
30
  # locally.
29
31
  [dependencies]
30
- kobako-transport = { version = "0.18.0", path = "../kobako-transport" }
32
+ kobako-transport = { version = "0.19.0", path = "../kobako-transport" }
33
+ # cap-std resolves every path of the Directory Filesystem beneath its host
34
+ # directory, refusing a `..` or a symbolic link that would lead out of it.
35
+ # 4.0.3 is the first release whose trailing-slash lookup keeps that
36
+ # refusal (GHSA-hp8f-xmx4-4qrg).
37
+ cap-std = "4.0.3"
38
+ # cap-fs-ext opens a Directory's path one directory at a time without
39
+ # following a link, so a Mount's Policy judges the path its call reaches.
40
+ cap-fs-ext = "4.0.3"
41
+
42
+ # rustix switches a Directory file to append mode for one write, so the
43
+ # host places it at the file's end in one step; std cannot set file flags.
44
+ [target.'cfg(unix)'.dependencies]
45
+ rustix = { version = "1.1.4", features = ["fs"] }
46
+
47
+ # libc asks macOS for the name a directory stores, which a Mount's Policy
48
+ # judges in place of a spelling the host folds; rustix has no
49
+ # getattrlist.
50
+ [target.'cfg(target_os = "macos")'.dependencies]
51
+ libc = "0.2.191"
@@ -4,9 +4,9 @@ Engine-neutral host runtime contract for
4
4
  [kobako](https://github.com/elct9620/kobako), an in-process Wasm
5
5
  sandbox for running untrusted mruby scripts.
6
6
 
7
- A kobako host drives a Guest Binary through a wasm engine; this crate
8
- is the surface where the two meet, free of any engine or frontend
9
- type, so the engine stays swappable.
7
+ A kobako host drives a Guest Binary through a wasm engine. This crate
8
+ is where the two meet, free of any engine or frontend type, so the
9
+ engine stays swappable.
10
10
 
11
11
  | Module | What it declares |
12
12
  |---|---|
@@ -14,19 +14,24 @@ type, so the engine stays swappable.
14
14
  | `snapshot` | each invocation's `Completion` (outcome or trap), both output `Capture`s, and `Usage` |
15
15
  | `error` | the neutral failure types: `Trap`, `SetupError`, and `InvokeError` over both |
16
16
  | `profile` | the isolation `Profile` a runtime declares |
17
+ | `grant` | the `Grant` a frontend hands each invocation, and the `GrantKind`s a runtime supports |
18
+ | `mount` | the `Mount` at a guest path, and the `Policy` judging its paths |
19
+ | `filesystem` | the `Filesystem` trait, its `Access`, and the four kinds kobako ships |
17
20
  | `dispatch` / `yielder` | the `DispatchHandler` and `Yielder` traits a frontend supplies for guest→host dispatch and block-yield re-entry |
18
21
 
19
- Engine implementations (such as `kobako-wasmtime`) implement
20
- `Runtime`; host frontends (such as the kobako Ruby gem's native ext)
21
- map the neutral types onto their own language surface.
22
+ Engine implementations such as `kobako-wasmtime` implement `Runtime`.
23
+ Host frontends such as the kobako Ruby gem's native ext map the neutral
24
+ types onto their own language surface.
22
25
 
23
26
  ## Usage
24
27
 
28
+ Add the crate to an engine or a frontend that speaks the contract.
29
+
25
30
  ```toml
26
31
  [dependencies]
27
- kobako-runtime = "0.18.0" # x-release-please-version
32
+ kobako-runtime = "0.19.0" # x-release-please-version
28
33
  ```
29
34
 
30
35
  ## License
31
36
 
32
- Apache-2.0
37
+ Licensed under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
@@ -25,4 +25,12 @@ use kobako_transport::envelope::{Call, Reply};
25
25
  /// the payload onward without copying it first.
26
26
  pub trait DispatchHandler: Send + Sync {
27
27
  fn dispatch(&self, call: Call<'_>, yielder: &mut dyn Yielder) -> Option<Reply>;
28
+
29
+ /// Whether the host gave up on this invocation, as when one of its
30
+ /// callbacks raised something meant for the host rather than the
31
+ /// guest. The engine ends the invocation at the next host call it
32
+ /// answers, so the guest cannot go on as if it had been answered.
33
+ fn abandoned(&self) -> bool {
34
+ false
35
+ }
28
36
  }