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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +170 -0
- data/README.md +154 -36
- data/ROADMAP.md +5 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +86 -0
- data/crates/kobako-runtime/README.md +13 -8
- data/crates/kobako-transport/CHANGELOG.md +17 -0
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
- data/crates/kobako-wasmtime/README.md +5 -3
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +11 -0
- data/lib/kobako/catalog/snippets.rb +5 -9
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +1 -0
- data/lib/kobako/codec/ext_types.rb +1 -1
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +38 -11
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/pool.rb +2 -2
- data/lib/kobako/sandbox.rb +40 -12
- data/lib/kobako/sandbox_options.rb +39 -12
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +30 -26
- data/lib/kobako/transport/exposure.rb +5 -7
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/yielder.rb +8 -30
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +4 -0
- data/sig/kobako/catalog/snippets.rbs +1 -1
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +2 -0
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/sandbox_options.rbs +24 -2
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +2 -2
- metadata +26 -8
- data/sig/kobako/snippet/binary.rbs +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f193ae13f99d944ee55b7bba450fb46bd9699d247c0f0fc51cb663742c0e4468
|
|
4
|
+
data.tar.gz: e72ee476dc6bcb959db4044f53303f8e94b76cec8f68920aa168d0985c852572
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd73101376fec2732e4e86ddbd7e52765001aea584a19f87ab10302094676c64c8c13093ea086945f9e6c91695118a631ff51097b6b93f40571d1185195946f7
|
|
7
|
+
data.tar.gz: 5b481242f339abffca1391de74ee24836ffc35b9a1535462d1fccd3f834de7413961b5cdc418976efd6b40ee73f1e884ca30a07b873389d006527ef888611a72
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"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
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
| `timeout`
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
465
|
-
class
|
|
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: :
|
|
475
|
-
source:
|
|
592
|
+
name: :Vfs,
|
|
593
|
+
source: VFS,
|
|
476
594
|
backend: Kobako::Extension::Backend.new(
|
|
477
|
-
path: "
|
|
478
|
-
provider: -> {
|
|
595
|
+
path: "Vfs",
|
|
596
|
+
provider: -> { CopyOnWriteFiles.new(root) } # invoked once per invocation
|
|
479
597
|
)
|
|
480
598
|
)
|
|
481
599
|
)
|
|
482
600
|
|
|
483
|
-
sandbox.eval('
|
|
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
|
|
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
|
|
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) | ~
|
|
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 | ~
|
|
530
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~
|
|
531
|
-
| Service call amortized inside one invocation | ~
|
|
532
|
-
| Snippet replay per invocation | ~7
|
|
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
|
|
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) | ~
|
|
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
|
|
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
|
|