kobako 0.28.0-aarch64-linux → 0.29.0-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/README.md +154 -36
  5. data/ROADMAP.md +5 -0
  6. data/SECURITY.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  8. data/crates/kobako-runtime/README.md +13 -8
  9. data/crates/kobako-transport/CHANGELOG.md +17 -0
  10. data/crates/kobako-transport/README.md +1 -1
  11. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  12. data/crates/kobako-wasmtime/README.md +5 -3
  13. data/data/kobako.wasm +0 -0
  14. data/lib/kobako/3.3/kobako.so +0 -0
  15. data/lib/kobako/3.4/kobako.so +0 -0
  16. data/lib/kobako/4.0/kobako.so +0 -0
  17. data/lib/kobako/catalog/flags.rb +2 -14
  18. data/lib/kobako/catalog/grants.rb +30 -0
  19. data/lib/kobako/catalog/mount.rb +27 -0
  20. data/lib/kobako/catalog/services.rb +11 -0
  21. data/lib/kobako/catalog/snippets.rb +5 -9
  22. data/lib/kobako/catalog.rb +11 -0
  23. data/lib/kobako/codec/decoder.rb +1 -0
  24. data/lib/kobako/codec/ext_types.rb +1 -1
  25. data/lib/kobako/codec/state.rb +19 -3
  26. data/lib/kobako/codec/value_budget.rb +86 -0
  27. data/lib/kobako/codec.rb +7 -0
  28. data/lib/kobako/context.rb +38 -11
  29. data/lib/kobako/errors.rb +15 -14
  30. data/lib/kobako/filesystem.rb +38 -0
  31. data/lib/kobako/pool.rb +2 -2
  32. data/lib/kobako/sandbox.rb +40 -12
  33. data/lib/kobako/sandbox_options.rb +39 -12
  34. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  35. data/lib/kobako/snippet.rb +1 -1
  36. data/lib/kobako/transport/block_failure.rb +35 -0
  37. data/lib/kobako/transport/dispatcher.rb +30 -26
  38. data/lib/kobako/transport/exposure.rb +5 -7
  39. data/lib/kobako/transport/reflection.rb +79 -23
  40. data/lib/kobako/transport/yielder.rb +8 -30
  41. data/lib/kobako/transport.rb +1 -0
  42. data/lib/kobako/version.rb +1 -1
  43. data/lib/kobako.rb +1 -0
  44. data/sig/kobako/catalog/flags.rbs +0 -4
  45. data/sig/kobako/catalog/grants.rbs +13 -0
  46. data/sig/kobako/catalog/mount.rbs +28 -0
  47. data/sig/kobako/catalog/services.rbs +4 -0
  48. data/sig/kobako/catalog/snippets.rbs +1 -1
  49. data/sig/kobako/catalog.rbs +1 -0
  50. data/sig/kobako/codec/state.rbs +7 -1
  51. data/sig/kobako/codec/value_budget.rbs +23 -0
  52. data/sig/kobako/codec.rbs +2 -0
  53. data/sig/kobako/context.rbs +14 -6
  54. data/sig/kobako/errors.rbs +4 -4
  55. data/sig/kobako/filesystem.rbs +88 -0
  56. data/sig/kobako/guest_yielder.rbs +9 -0
  57. data/sig/kobako/pool.rbs +1 -1
  58. data/sig/kobako/runtime.rbs +21 -49
  59. data/sig/kobako/sandbox.rbs +7 -1
  60. data/sig/kobako/sandbox_options.rbs +24 -2
  61. data/sig/kobako/snapshot.rbs +20 -0
  62. data/sig/kobako/snippet/bytecode.rbs +12 -0
  63. data/sig/kobako/transport/block_failure.rbs +13 -0
  64. data/sig/kobako/transport/dispatcher.rbs +3 -3
  65. data/sig/kobako/transport/exposure.rbs +1 -1
  66. data/sig/kobako/transport/reflection.rbs +14 -1
  67. data/sig/kobako/transport/yielder.rbs +3 -7
  68. data/sig/kobako/transport.rbs +2 -2
  69. metadata +26 -8
  70. data/sig/kobako/snippet/binary.rbs +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39d1ced9e3eb9fe9ccb9294bbdc1f388cac128a64e9675799fbba9a47eb9927f
4
- data.tar.gz: b65042c33ac3d8f200737593f98507c122e36b18ef49e979441f6b8ea7d25726
3
+ metadata.gz: f193ae13f99d944ee55b7bba450fb46bd9699d247c0f0fc51cb663742c0e4468
4
+ data.tar.gz: e72ee476dc6bcb959db4044f53303f8e94b76cec8f68920aa168d0985c852572
5
5
  SHA512:
6
- metadata.gz: ea1ce60db52f531b02cc2c89386889519d180ef62ff5c7936daa4b13bd2d0fea51a61f9d92e2b5db43742a31d90658dcc115a3c01a7f1dc9d35ccc0d1639b51b
7
- data.tar.gz: 22b18b4883ec79c102fb50d6973283e3f3112f2c153d43d8c4d9b40e542ca162585a022535d04330bbaa882363d11303d9f29e3c135231af9bb2dcdc3855a42c
6
+ metadata.gz: fd73101376fec2732e4e86ddbd7e52765001aea584a19f87ab10302094676c64c8c13093ea086945f9e6c91695118a631ff51097b6b93f40571d1185195946f7
7
+ data.tar.gz: 5b481242f339abffca1391de74ee24836ffc35b9a1535462d1fccd3f834de7413961b5cdc418976efd6b40ee73f1e884ca30a07b873389d006527ef888611a72
@@ -1 +1 @@
1
- {".":"0.28.0","wasm/kobako-core":"0.18.0","wasm/kobako-mruby":"0.18.0","wasm/kobako-io":"0.18.0","wasm/kobako-json":"0.18.0","wasm/kobako-regexp":"0.18.0","wasm/kobako-baker":"0.18.0","crates/kobako-transport":"0.18.0","crates/kobako-codec":"0.18.0","crates/kobako-runtime":"0.18.0","crates/kobako-wasmtime":"0.18.0","crates/kobako":"0.18.0"}
1
+ {".":"0.29.0","wasm/kobako-core":"0.19.0","wasm/kobako-mruby":"0.19.0","wasm/kobako-io":"0.19.0","wasm/kobako-json":"0.19.0","wasm/kobako-regexp":"0.19.0","wasm/kobako-baker":"0.19.0","crates/kobako-transport":"0.19.0","crates/kobako-codec":"0.19.0","crates/kobako-runtime":"0.19.0","crates/kobako-wasmtime":"0.19.0","crates/kobako":"0.19.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,175 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.29.0](https://github.com/elct9620/kobako/compare/v0.28.0...v0.29.0) (2026-10-26)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * a class's alias or define_method copy of a method mixed into Object or Module is no longer reachable from the guest; define the method on the Service's own class to expose it.
9
+ * a narrowing predicate no longer reaches a method a library mixes into Object or Module; define the method on the Service's own class to expose it.
10
+ * **wasmtime:** a call through a directory descriptor answers ENOENT when the directory at its path is not the one it opened, where it used to reach that directory.
11
+ * **wasmtime:** a struct literal no longer builds the wasmtime driver's Config outside its crate; start from `Config::default()` and assign each field.
12
+ * **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.
13
+ * a narrowing predicate no longer reaches a built-in method an object copies, aliases or rebinds, nor a private method it forwards; write the method in Ruby to expose it.
14
+ * the wasmtime Config and the SDK Options gain a setup_timeout field, so a struct literal that names every field must add it; one built from Options::default() is unaffected.
15
+ * in kobako-transport, YieldReply::Break carries no payload and Reply gains a Break arm, so a Guest Binary built against the earlier version 5 wire must be rebuilt.
16
+ * Overlay#layer with an access that is not a Symbol raises ArgumentError, as Sandbox#mount does, instead of TypeError.
17
+ * kobako-codec renames `Decoder::at_end` to `is_at_end`, `Decoder::read_only_value` to `read_sole_value`, and `Decoder::with_value_limit` to `with_value_budget`; the kobako crate renames `handles::Detached` to `handles::DetachedHandles`.
18
+ * **spec:** `Decoder::at_end`, `Decoder::read_only_value`, `Decoder::with_value_limit`, and `kobako::handles::Detached` are registered as `is_at_end`, `read_sole_value`, `with_value_budget`, and `DetachedHandles`.
19
+ * a guest block called from another Thread or Fiber, including through Enumerator#next, raises ThreadError instead of running.
20
+ * a guest payload holding more than memory_limit / 16 values is refused where it used to decode.
21
+ * 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.
22
+ * **io:** putc, IO#read and SystemCallError.new raise RangeError for those numbers instead of writing, refusing or ignoring them.
23
+ * **guest:** kobako-mruby drops the narrow_int method on Kobako, the IntegerOutOfRange type and the Interpreter variant of CodecError; a codec builds a guest Integer from a wire integer through beni's IntoValue.
24
+ * **guest:** the kobako guest crates name beni 0.25's types, so a shell or codec built on them moves to beni 0.25 with them.
25
+ * **guest:** the default memory_limit is now 4 MiB. JSON.parse in the guest answers an Integer for every integer within 64 bits, where it used to answer a Float past 32 bits. An integer past 64 bits now raises
26
+ * **guest:** the kobako guest crates name beni 0.24's types, so a shell or codec built on them moves to beni 0.24 with them.
27
+ * **io:** reading `IO#sync` after assigning a non-boolean now answers true or false. It used to answer the assigned value, and nil used to read back as true.
28
+ * **io:** a script writing to a file through `IO#write` beneath a Mount now raises on a failed write where it used to go on.
29
+ * **io:** a script rescuing IOError for a failed read or write beneath a Mount must rescue the EIO error class, or SystemCallError, instead.
30
+ * **guest:** a Service or Extension backend bound at the path File fails its first invocation, naming the segment the Guest Binary now defines.
31
+ * **runtime:** the Runtime trait's invoke takes the invocation's Grants as a fourth argument, so every engine implementing it must accept them.
32
+ * Sandbox#bind and the Rust SDK's bind raise ArgumentError for a path whose first segment is Kobako, where a path such as Kobako::KV used to bind.
33
+
34
+ ### Features
35
+
36
+ * build a Kobako::Filesystem::Overlay one Layer at a time ([f776824](https://github.com/elct9620/kobako/commit/f776824e3be77eeea0874fd94dd89d42ce92de26)), closes [#78](https://github.com/elct9620/kobako/issues/78)
37
+ * cap the files a script holds open with open_file_limit ([e587abf](https://github.com/elct9620/kobako/commit/e587abf02dfbbc44792469dd0515780647849ac5))
38
+ * check what a rename moves as it moves, refusing a directory beneath a Policy ([23794bf](https://github.com/elct9620/kobako/commit/23794bfcb54e148126e29873fa9b52d9e3de8d8c))
39
+ * give a Memory or Temporary a count of inodes from Ruby ([8b390e3](https://github.com/elct9620/kobako/commit/8b390e3da88c59c7b0120c6f092a372da0c4ddee))
40
+ * give a Memory or Temporary a limit from Ruby ([14592af](https://github.com/elct9620/kobako/commit/14592af11b49c5e9e6e87aef514ad5061283a50f))
41
+ * **guest:** carry every integer msgpack encodes in both directions ([ec349f9](https://github.com/elct9620/kobako/commit/ec349f98fcb38443d9f9dfc45701e2ce87ff7710))
42
+ * **guest:** carry mruby-bigint so an Integer has no width of its own ([a74266d](https://github.com/elct9620/kobako/commit/a74266d3e7b1295b75770b62894b232195f9b3bb))
43
+ * **guest:** ship File in the default Guest Binary over the filesystem Grants ([535527b](https://github.com/elct9620/kobako/commit/535527b2c6beb82fe37758df62c3804ce5c7191a)), closes [#78](https://github.com/elct9620/kobako/issues/78)
44
+ * **guest:** widen the guest Integer to 64 bits ([7236a26](https://github.com/elct9620/kobako/commit/7236a26285dfdef175048f659c1094dd3bbb5b4d))
45
+ * **json:** read and write an Integer of any width exactly ([c839ad0](https://github.com/elct9620/kobako/commit/c839ad0401b1e2f6a6f8c0689d6cd7ed7c4885b3))
46
+ * let a block's break or return unwind a Service as a plain method ([4e9eca0](https://github.com/elct9620/kobako/commit/4e9eca05f1647c8b6409fc00d08317b431c0c648))
47
+ * let a Host App bound how long a Guest Binary takes to be set up ([2addab0](https://github.com/elct9620/kobako/commit/2addab002550539e3ebb24bdd599dd424c2dc3d9))
48
+ * mount a Filesystem written in Ruby ([891106c](https://github.com/elct9620/kobako/commit/891106c5cd91f4bd59986a75965b2f79ba97495e)), closes [#78](https://github.com/elct9620/kobako/issues/78)
49
+ * mount a Kobako::Filesystem on a Sandbox for the guest's File ([118e1ab](https://github.com/elct9620/kobako/commit/118e1ab99ddb22b205ef1580285cda04e55ebdf4)), closes [#78](https://github.com/elct9620/kobako/issues/78)
50
+ * name what a Ruby Filesystem's path holds by the ino its stat answers ([adb2363](https://github.com/elct9620/kobako/commit/adb2363ae56c281d2d443e9f0c8386ed7788eea6))
51
+ * **runtime:** bound the files and directories a Memory or Temporary holds ([8d0be0a](https://github.com/elct9620/kobako/commit/8d0be0ab3dc8c11bcb96865b0bf1a0bbb8dd3b16))
52
+ * **runtime:** bound what a Memory or Temporary holds, 64 MiB by default ([2557d1b](https://github.com/elct9620/kobako/commit/2557d1ba7530aacfddf908aeddac561c4fbc5c8a))
53
+ * **runtime:** draw every device a host does not name from one source ([210d55b](https://github.com/elct9620/kobako/commit/210d55b1b4e0699dc5ef56b4b6930be01658a3c6))
54
+ * **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)
55
+ * **runtime:** keep a directory descriptor's identity its own while open ([6267642](https://github.com/elct9620/kobako/commit/6267642ea831917be76a0101e8d206ccfa8834c1))
56
+ * **runtime:** let a dispatch handler abandon its invocation ([179da9e](https://github.com/elct9620/kobako/commit/179da9ee1f31fed9e6d0c149faee0ad55f8db487))
57
+ * **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)
58
+ * **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)
59
+ * **runtime:** let a Mount's Policy judge the spelling the host stores ([5281057](https://github.com/elct9620/kobako/commit/52810573f22d2a3edfae9b34b7767a5f440fba15))
60
+ * **runtime:** let the records a Filesystem or an engine builds gain fields ([c0ef88d](https://github.com/elct9620/kobako/commit/c0ef88d0b6df16d651e4c49226e41667307d772f))
61
+ * **runtime:** refuse a Mount at a path that is no absolute guest path ([a525758](https://github.com/elct9620/kobako/commit/a5257589f9d831b9a3b6d1952df6e5c8469630e1))
62
+ * **runtime:** report a link a lookup does not follow as a link ([8c1e0b3](https://github.com/elct9620/kobako/commit/8c1e0b3e63fbc91b10f11cd91817ce94de24c7e6))
63
+ * **runtime:** report the identity of what a path holds ([392bc0e](https://github.com/elct9620/kobako/commit/392bc0e9f2fb935934a0031bf7d64dec0b27df6a))
64
+ * **runtime:** say whether a path beneath a Mount may pass a link ([7706a25](https://github.com/elct9620/kobako/commit/7706a2537bf8e5975b2b227bef0c05cb48c09f4b))
65
+ * **runtime:** seed a Memory from a list that may not hold, answering why ([8ac2dfd](https://github.com/elct9620/kobako/commit/8ac2dfdf73b84fe2e53405a653d861ddca9ea98c))
66
+ * **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)
67
+ * **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)
68
+ * **sdk:** hand a Host App the whole filesystem vocabulary as kobako::filesystem ([3998574](https://github.com/elct9620/kobako/commit/3998574fcc3eb5c1b9a58d657c9d6ebda7aa4914))
69
+ * take a setup_timeout for building a Sandbox over a Guest Binary ([22d19a3](https://github.com/elct9620/kobako/commit/22d19a3ee61a7726ba9059ad2796e83e2ca2f98e))
70
+ * **wasmtime:** let Config gain a cap without breaking a frontend ([55d8093](https://github.com/elct9620/kobako/commit/55d80933b958bab67f1b94deb7633272dc56f972))
71
+
72
+
73
+ ### Bug Fixes
74
+
75
+ * append through a Filesystem's file only in one step ([0509ca7](https://github.com/elct9620/kobako/commit/0509ca70d2d5a832303973ba0f7280be0731fd3d))
76
+ * ask a mount's block once for a read ([f89bd16](https://github.com/elct9620/kobako/commit/f89bd16c1a71ed1d385ab9d3ac5974ac3f0a9103))
77
+ * **deps:** require a msgpack without the Buffer#clear use-after-free ([46638db](https://github.com/elct9620/kobako/commit/46638db4ca5b893843558aa4430ff31f966258aa))
78
+ * end the run when a host callback raises beyond StandardError ([bb96c87](https://github.com/elct9620/kobako/commit/bb96c87dbb1d7337d2478376e9b8366d5a298e1f))
79
+ * follow no link swapped in after a Policy judges a path ([ed93213](https://github.com/elct9620/kobako/commit/ed9321303dd45e113fc413e99e1240118cc4bbe4))
80
+ * **guest:** name the segment a bind path meets when the Guest Binary already defines it ([0dc1766](https://github.com/elct9620/kobako/commit/0dc1766ff60a1ef03a5b0c337dfd02041fb3fb8f))
81
+ * hold an Overlay's lock across each call, waiting without the GVL ([b8e7342](https://github.com/elct9620/kobako/commit/b8e734273fdc577a40be97c43c46d9548d66c625))
82
+ * **io:** build Errno and IOError as CRuby does ([da77c47](https://github.com/elct9620/kobako/commit/da77c4725d3701ba982f5425b1fcf2fb0038ac89))
83
+ * **io:** carry a stream's descriptor in its payload, not in hidden ivars ([76ca3c0](https://github.com/elct9620/kobako/commit/76ca3c03fdef537041a167bb58b1af2b176d772a))
84
+ * **io:** keep a stream's bookkeeping out of its instance variables ([5ecf833](https://github.com/elct9620/kobako/commit/5ecf8332c2ebff9a1d01c528fa3af31afdebdee3))
85
+ * **io:** keep the number a SystemCallError is built with ([5fd327a](https://github.com/elct9620/kobako/commit/5fd327add59173cd5756fecb41d97aebcba7b881))
86
+ * **io:** raise each code the engine answers as its own Errno ([eb6382e](https://github.com/elct9620/kobako/commit/eb6382e506a30e82576fa12ab9859e7d35be2b17))
87
+ * **io:** raise Errno::EIO for a file failure no other code names ([8a0447d](https://github.com/elct9620/kobako/commit/8a0447d2d09ea83e6f7771cf0411bfd6deec3d1a)), closes [#78](https://github.com/elct9620/kobako/issues/78)
88
+ * **io:** raise RangeError for a number past what putc, read and errno take ([6f87cc0](https://github.com/elct9620/kobako/commit/6f87cc08dff7146415f80ca25cd75ddbec4391c0))
89
+ * **io:** raise the error a failed file write names ([bcc8e00](https://github.com/elct9620/kobako/commit/bcc8e00662741fcac3b91f58c0b60f4a79d2ab9f))
90
+ * **io:** write a file to the end when its Filesystem takes it in parts ([029b439](https://github.com/elct9620/kobako/commit/029b43939a3a48bc2925057df69bfcb85b1a0548))
91
+ * keep a link within the directory a descriptor opened ([952dd05](https://github.com/elct9620/kobako/commit/952dd0556969683f44798da94f9fc7a17c1f6d55))
92
+ * keep a mount's block and Ruby Filesystem in place under GC compaction ([4a88c05](https://github.com/elct9620/kobako/commit/4a88c054ba22ea15cdf1ca5d691053a0f81d0661))
93
+ * pass an Errno a Ruby Filesystem raises through as itself ([b107a0f](https://github.com/elct9620/kobako/commit/b107a0f058da2a642317de4edd911fa7001e55bc))
94
+ * raise a signal during a process's first run as itself, not fatal ([7803e19](https://github.com/elct9620/kobako/commit/7803e19cb3812d9ff5184f50cc7d3836da7e3087))
95
+ * raise ArgumentError for files a Memory cannot be seeded with ([2b89c57](https://github.com/elct9620/kobako/commit/2b89c579519961529e3f3a618eda5d9b7d421d75))
96
+ * read a real File a Ruby Filesystem opens to its end ([019c523](https://github.com/elct9620/kobako/commit/019c523a365c4de9cee6b38b263b1ea621b8897f))
97
+ * refuse a copy of a method mixed into Object or Module ([4476af9](https://github.com/elct9620/kobako/commit/4476af9993da9e6dabc22ed8cc5dcd45b1ec6a3c))
98
+ * refuse a guest block run off the fiber driving its invocation ([3a5e144](https://github.com/elct9620/kobako/commit/3a5e144431ac316290b3136a7b22be05783b0d13))
99
+ * refuse a guest payload holding more values than the memory cap allows ([51999f2](https://github.com/elct9620/kobako/commit/51999f2833a9c37e8fe43004f6b6568665a4fa0b))
100
+ * refuse a keyword Memory.new does not know ([ecd4660](https://github.com/elct9620/kobako/commit/ecd4660a2d9d001286adb955c4aca4356685b5f4))
101
+ * refuse a Mount by the native rules alone, at mount time ([c371d6d](https://github.com/elct9620/kobako/commit/c371d6d9088cb6a9d924b847a30cec4f0348f8f6))
102
+ * refuse a mount of what is no Filesystem as the wrong type ([0f53e4b](https://github.com/elct9620/kobako/commit/0f53e4b3f2e497577019c5afbb4f430409e83f3f))
103
+ * refuse a Ruby Filesystem's write that claims more than it was handed ([3183b65](https://github.com/elct9620/kobako/commit/3183b65dad1a913c87b327a55c7c104fc2360adb))
104
+ * refuse a Service path under the Kobako namespace when it is bound ([5a83c14](https://github.com/elct9620/kobako/commit/5a83c145118abec8065d3846e2c73e51f9491757))
105
+ * refuse every method built into Ruby, whatever its owner ([963a316](https://github.com/elct9620/kobako/commit/963a3169a7607e83ab0bd2fa98296326efaacb02))
106
+ * refuse every method mixed into Object or Module ([70de6c1](https://github.com/elct9620/kobako/commit/70de6c19ddbcdb8038873d6e06a096c188169f8d))
107
+ * run a stored block as the block of the Call that carried it ([bf33dc1](https://github.com/elct9620/kobako/commit/bf33dc1fd40c83498c6c12b0be52b3a835a144cf))
108
+ * **runtime:** answer a host failure as the error naming it ([effd43a](https://github.com/elct9620/kobako/commit/effd43a85a3ba69aaa068d2c384c3b5cb466161c))
109
+ * **runtime:** append to a Directory file in one host write ([4a5450a](https://github.com/elct9620/kobako/commit/4a5450a67e202e45299175c4fe7528cf50489f0e))
110
+ * **runtime:** copy a file up whole when its Layer writes in parts ([89d87f7](https://github.com/elct9620/kobako/commit/89d87f7225611786467968ec34eba5a8223b5fc5))
111
+ * **runtime:** judge a link an Overlay's Layer holds as the link itself ([959c1f3](https://github.com/elct9620/kobako/commit/959c1f3dab6f6948baabbd914bbb86dfff090f7b))
112
+ * **runtime:** leave an Overlay unchanged when a removal or rename is refused ([a7bee4b](https://github.com/elct9620/kobako/commit/a7bee4b2a465a0ef0ad2e376c281dca82a0df0e1))
113
+ * **runtime:** let a file or link in a Layer hide what lies below it ([fd579f6](https://github.com/elct9620/kobako/commit/fd579f679cfa8a83ec7d139066bc1a4570af398b))
114
+ * **runtime:** make every Filesystem answer the methods that keep a guarantee ([00f5af6](https://github.com/elct9620/kobako/commit/00f5af63f34ac7722249746d0b0eb566da618803))
115
+ * **runtime:** move a directory made again as the writable Layer's own ([ac3e864](https://github.com/elct9620/kobako/commit/ac3e864bbb0ce5d61def45e0403239be405481d3))
116
+ * **runtime:** move a directory onto one that holds only markers ([934f0c3](https://github.com/elct9620/kobako/commit/934f0c39e0ad1189bdb1cb0461671245dea32f93))
117
+ * **runtime:** open a file already there without writing to create it ([7f76a1a](https://github.com/elct9620/kobako/commit/7f76a1af575739ae16c5daeb1df8268af44cea63))
118
+ * **runtime:** panic on files a Memory cannot be seeded with ([cf5c835](https://github.com/elct9620/kobako/commit/cf5c835d7df8bf82972487280c6a6c79be955a68))
119
+ * **runtime:** put an Overlay's markers back when the change fails ([78ddeed](https://github.com/elct9620/kobako/commit/78ddeeddda8b9f797a80ec3796b28e04cf99ffc9))
120
+ * **runtime:** refuse a backslash in a Directory path on Windows ([88b5719](https://github.com/elct9620/kobako/commit/88b57195325b168395248a82b94a5ebf7e5f4142))
121
+ * **runtime:** refuse a file moved beneath itself as no directory ([bfabb52](https://github.com/elct9620/kobako/commit/bfabb52b27080ee9e5000bd5524ea201a7e33d80))
122
+ * **runtime:** refuse a host name that is not UTF-8 as an illegal sequence ([c4ff030](https://github.com/elct9620/kobako/commit/c4ff030ffa8e29fa1b10df4aaf6af65162c76353))
123
+ * **runtime:** remove no directory as a file on any host ([c9c0820](https://github.com/elct9620/kobako/commit/c9c0820fd39c0744b9d9c367b977f6587d1f9589))
124
+ * **runtime:** rename and copy up through an Overlay as overlayfs does ([ada5a86](https://github.com/elct9620/kobako/commit/ada5a86a8647d89666e9de808eb13ba638882e51))
125
+ * **runtime:** reserve an Overlay's marker name in any case ([ef39923](https://github.com/elct9620/kobako/commit/ef399230eabe01ad6eed632b2753880ae1fc322c))
126
+ * stop a guest-chosen method name from becoming a permanent Symbol ([83b19dc](https://github.com/elct9620/kobako/commit/83b19dc9231a9e62e292eb71de295ba3cd5b3523))
127
+ * tell a Ruby Filesystem's refusal from a method it lacks ([670d0b6](https://github.com/elct9620/kobako/commit/670d0b659c8735a3b69169a7572a23d5f3623724))
128
+ * type a Ruby Filesystem's file as answering fsync and close apart ([bb1d6ff](https://github.com/elct9620/kobako/commit/bb1d6ff05bcbb2197d42e00ee1ed7c4dad3f231c))
129
+ * type a Ruby Filesystem's optional methods and a mount's access ([e64fdec](https://github.com/elct9620/kobako/commit/e64fdeca499d9d08fbc52d9daf31902aa77190fc))
130
+ * type a Ruby Filesystem's stat as answering ino ([54ead22](https://github.com/elct9620/kobako/commit/54ead222fc91761c51539a7985283cf54f436612))
131
+ * **wasmtime:** answer a path past Linux's limits as NAMETOOLONG ([af43917](https://github.com/elct9620/kobako/commit/af43917e30a634d6c1bd70813044967bd1057ba3))
132
+ * **wasmtime:** answer a stat that follows no link with the link itself ([ad0072e](https://github.com/elct9620/kobako/commit/ad0072e03ecc363487598a507de20a8f7aec2927))
133
+ * **wasmtime:** answer dot paths, creates, and far seeks as Linux does ([68c234e](https://github.com/elct9620/kobako/commit/68c234e06859b93fb986bdbab92cca944ce39700))
134
+ * **wasmtime:** answer every preview1 call that reads a descriptor from kobako's own table ([d0e39ae](https://github.com/elct9620/kobako/commit/d0e39aebd6d729d49111340af7f2af4e37aa7909)), closes [#78](https://github.com/elct9620/kobako/issues/78)
135
+ * **wasmtime:** answer fd_allocate beneath a Mount as unsupported ([e3d2071](https://github.com/elct9620/kobako/commit/e3d20713e4474cd637dacfece99e52b80d7ad2f6))
136
+ * **wasmtime:** bound what one preview1 call copies to wasmtime's hostcall fuel ([097c554](https://github.com/elct9620/kobako/commit/097c55426c1e71467bb1e372abc190331dd3e601))
137
+ * **wasmtime:** check path_open's flags as wasmtime-wasi does ([22eb494](https://github.com/elct9620/kobako/commit/22eb4944117260831e92fe616b02888078c02b0c))
138
+ * **wasmtime:** hold the whole invocation to its deadline ([231e325](https://github.com/elct9620/kobako/commit/231e32592699008adf3105050bee8b5528ea27a8))
139
+ * **wasmtime:** judge the cache directory itself, not where a link leads ([6baf6fd](https://github.com/elct9620/kobako/commit/6baf6fd2ad845cea83049c4b2bb51174aa205cd0))
140
+ * **wasmtime:** key the compiled-artifact cache by the engine that compiles it ([b43bad2](https://github.com/elct9620/kobako/commit/b43bad2fe176c153ed82a3a8825698414df78dfa))
141
+ * **wasmtime:** list a directory once for each listing a guest reads ([c333001](https://github.com/elct9620/kobako/commit/c3330011725c3dba01ea248ed7d1dfea31085af0))
142
+ * **wasmtime:** look up what stands before a dot path as Linux does ([0898eaf](https://github.com/elct9620/kobako/commit/0898eafb55397c84ca9007561ddb35dd007ed2b8))
143
+ * **wasmtime:** place an append at the end as the file stands when it lands ([d7291c4](https://github.com/elct9620/kobako/commit/d7291c4f32fb1f5f2494c69c76796476bbcf4d43))
144
+ * **wasmtime:** reach the artifact cache through the directory it checked ([462fe91](https://github.com/elct9620/kobako/commit/462fe918d516cd639e405b06a5e280a9e58624df))
145
+ * **wasmtime:** refuse a `..` that steps out of a host link ([4599386](https://github.com/elct9620/kobako/commit/45993868c7334376f5d1a455afbd15fe4a2146bc))
146
+ * **wasmtime:** refuse a directory made in an opened directory's place ([5e67d4c](https://github.com/elct9620/kobako/commit/5e67d4cd2a52e3aa37bdd615fe1ae101a618db88))
147
+ * **wasmtime:** refuse a link an open asks not to follow ([123a01b](https://github.com/elct9620/kobako/commit/123a01b01f59cc69c0e3a23f9aeb01aa89132c07))
148
+ * **wasmtime:** refuse a link to a directory an open asks not to follow ([eec218f](https://github.com/elct9620/kobako/commit/eec218fdd5584f641070032cf1110ef1683eaae5))
149
+ * **wasmtime:** refuse a path through a link beneath a Mount with a Policy ([3da1641](https://github.com/elct9620/kobako/commit/3da16416692ade01db2d9cf366d54d3ec5f50605))
150
+ * **wasmtime:** refuse a rename of a file onto a path ending in a slash ([486a0ab](https://github.com/elct9620/kobako/commit/486a0ab3584772b58e181d5de57ef505e610eb78))
151
+ * **wasmtime:** refuse to remove or move a Mount's root ([9f2e000](https://github.com/elct9620/kobako/commit/9f2e000115a4dd4aecfcc4b1cd39b23f6f20fe05))
152
+ * **wasmtime:** require wasmtime 49.0.4 for the aarch64 br_table fix ([390e30b](https://github.com/elct9620/kobako/commit/390e30ba51ed4175820f2bb230e3ad5b0ad1e9a5))
153
+ * **wasmtime:** resolve a `..` and a trailing slash as a host does ([d2d04c1](https://github.com/elct9620/kobako/commit/d2d04c1fff24d8829b2cf4fcbf628565f35eeadc))
154
+ * **wasmtime:** resolve a link a stat names with a trailing slash ([6da98fa](https://github.com/elct9620/kobako/commit/6da98facbccaa9bf6d8f8280a56841e19a7c879b))
155
+ * **wasmtime:** take wasmtime 49.0.3 for its sandbox advisories ([7672592](https://github.com/elct9620/kobako/commit/76725923cac50473c7e3a21cc5121e3cbba0cba5))
156
+
157
+
158
+ ### Documentation
159
+
160
+ * **spec:** register the decoder and detached-table names the conventions give ([469636b](https://github.com/elct9620/kobako/commit/469636b25fb9bd1768e6afe6604da3614ac18e95))
161
+
162
+
163
+ ### Code Refactoring
164
+
165
+ * name the value budget and decoder queries by the conventions ([918f748](https://github.com/elct9620/kobako/commit/918f7488dea13d27def92dca9e31128d90856f1c))
166
+
167
+
168
+ ### Build System
169
+
170
+ * **guest:** rebuild the guest crates on beni 0.24 ([4599337](https://github.com/elct9620/kobako/commit/4599337261d1d28ff8c36435062d34cbccf09abb))
171
+ * **guest:** rebuild the guest crates on beni 0.25 ([a06dfd2](https://github.com/elct9620/kobako/commit/a06dfd25958ded9c82072149d73e56372343ba31))
172
+
3
173
  ## [0.28.0](https://github.com/elct9620/kobako/compare/v0.27.0...v0.28.0) (2026-10-13)
4
174
 
5
175
 
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