kobako 0.12.1-aarch64-linux → 0.13.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/README.md +3 -1
  5. data/crates/kobako-runtime/CHANGELOG.md +16 -0
  6. data/crates/kobako-runtime/README.md +34 -0
  7. data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
  8. data/crates/kobako-wasmtime/README.md +32 -0
  9. data/data/kobako.wasm +0 -0
  10. data/lib/kobako/3.3/kobako.so +0 -0
  11. data/lib/kobako/3.4/kobako.so +0 -0
  12. data/lib/kobako/4.0/kobako.so +0 -0
  13. data/lib/kobako/catalog/handles.rb +3 -3
  14. data/lib/kobako/catalog/namespaces.rb +4 -0
  15. data/lib/kobako/catalog/snippets.rb +4 -0
  16. data/lib/kobako/codec/encoder.rb +5 -1
  17. data/lib/kobako/codec/factory.rb +41 -13
  18. data/lib/kobako/codec/handle_walk.rb +4 -0
  19. data/lib/kobako/codec.rb +1 -1
  20. data/lib/kobako/errors.rb +18 -16
  21. data/lib/kobako/sandbox.rb +71 -39
  22. data/lib/kobako/sandbox_options.rb +71 -13
  23. data/lib/kobako/transport/dispatcher.rb +2 -2
  24. data/lib/kobako/transport/response.rb +14 -14
  25. data/lib/kobako/transport/run.rb +2 -6
  26. data/lib/kobako/transport/yield.rb +1 -1
  27. data/lib/kobako/transport/yielder.rb +2 -2
  28. data/lib/kobako/version.rb +1 -1
  29. data/lib/kobako.rb +0 -1
  30. data/release-please-config.json +78 -3
  31. data/sig/kobako/codec/factory.rbs +3 -0
  32. data/sig/kobako/errors.rbs +7 -14
  33. data/sig/kobako/runtime.rbs +16 -6
  34. data/sig/kobako/sandbox.rbs +11 -7
  35. data/sig/kobako/sandbox_options.rbs +15 -4
  36. data/sig/kobako/transport/dispatcher.rbs +1 -1
  37. data/sig/kobako/transport/run.rbs +2 -2
  38. data/sig/kobako/transport/yielder.rbs +2 -2
  39. data/sig/kobako/transport.rbs +8 -0
  40. metadata +6 -4
  41. data/lib/kobako/snapshot.rb +0 -38
  42. data/sig/kobako/snapshot.rbs +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d480be9786ea4e836ec39bf80b704c07373a6a283ce3fd0e822799511430e28
4
- data.tar.gz: fec449faec3e9072fa222948509c86bc8e3e401180c56c5b801bcebe8b73f36a
3
+ metadata.gz: c0a1b4ccd9f4e1debe32912b925847390d00fbf09e798b41eb54ac0756ef4ab3
4
+ data.tar.gz: 4819002a95e4407ebacb91d1ae77cd8a7c52d80a1eb0511dcc01ac405d6b7459
5
5
  SHA512:
6
- metadata.gz: 56ea11910d7a393725f27ec696e271f71f69c0eb26d9fb20ee5001d3401e149a558ca248cdcc17cafb45ffa083df5a489e077906a126e2aa177ce570950eeb22
7
- data.tar.gz: 2bd8f82df6f33072a177226ec9f9b11926bbe374cba1258b60b4510f5be493a598fe2e68c46b3fc87a6f3519857fa7695c0a1675a1655add87233de207664488
6
+ metadata.gz: 902954ca6a47611ab670ea52dbe5edc2268428128ffaac0dd9d55e309af6d5b4a70fc47d07eed4f31261bdcdc7158cbdd91fbfad32d56098c8a2c84077db2855
7
+ data.tar.gz: 53174e7d4e921b558bca17b3683c0865954ff73219a0c07d86d11e18e6e812506aece5d5dc0626ce021fe9342a63d8d3b5ab612f513d7349e939e0e6e08ed063
@@ -1 +1 @@
1
- {".":"0.12.1","wasm/kobako-core":"0.6.0","wasm/kobako":"0.6.0","wasm/kobako-io":"0.6.0","wasm/kobako-json":"0.6.0","wasm/kobako-regexp":"0.6.0","wasm/kobako-baker":"0.6.0"}
1
+ {".":"0.13.0","wasm/kobako-core":"0.7.0","wasm/kobako":"0.7.0","wasm/kobako-io":"0.7.0","wasm/kobako-json":"0.7.0","wasm/kobako-regexp":"0.7.0","wasm/kobako-baker":"0.7.0","crates/kobako-codec":"0.7.0","crates/kobako-runtime":"0.7.0","crates/kobako-wasmtime":"0.7.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/elct9620/kobako/compare/v0.12.2...v0.13.0) (2026-07-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
9
+ * enforce the isolation-profile floor at Sandbox construction ([73f0dfe](https://github.com/elct9620/kobako/commit/73f0dfe34a144045a559a28caec31468351de64a))
10
+ * forward the Sandbox profile request through the ext to the driver ([f47f906](https://github.com/elct9620/kobako/commit/f47f9063aa56462be023f5c76cbc73db6e4315ad))
11
+ * keep guest output readable after a trap ([464454c](https://github.com/elct9620/kobako/commit/464454cd774db96ad72b3787007f2ce015673587))
12
+ * **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
13
+
14
+ ## [0.12.2](https://github.com/elct9620/kobako/compare/v0.12.1...v0.12.2) (2026-07-02)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **codec:** bound ext-envelope nesting to keep deep Fault chains off the native stack ([7bed2b2](https://github.com/elct9620/kobako/commit/7bed2b2f43a63538aa60610c82d2eb65bcce7b15))
20
+ * **guest:** size collection conversions by C array length, not #length ([90ecbd0](https://github.com/elct9620/kobako/commit/90ecbd0cb6a990b8c5a1e5deec3a10df4eaa37df))
21
+ * **io:** enforce the fd allowlist at the write syscall ([1b300df](https://github.com/elct9620/kobako/commit/1b300df7bee8f87b701f76b42300163a8899b93e))
22
+ * **release:** advance last-release-sha past the unparseable fork merge ([d06117d](https://github.com/elct9620/kobako/commit/d06117d462eea0ae5648e5bdd6886b735765f3b3))
23
+ * **sandbox:** honor nil to disable the output caps, and validate them ([51d1e90](https://github.com/elct9620/kobako/commit/51d1e900e3d7d659ffd432ff3d613786e9073b05))
24
+
3
25
  ## [0.12.1](https://github.com/elct9620/kobako/compare/v0.12.0...v0.12.1) (2026-06-27)
4
26
 
5
27
 
data/README.md CHANGED
@@ -127,7 +127,7 @@ end
127
127
  |---------------------------------|----------------|------------------------------------------------------|
128
128
  | `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
129
129
  | `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
130
- | `Kobako::HandlerExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
130
+ | `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
131
131
  | `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
132
132
 
133
133
  `SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` / `details` fields when the guest produced a panic envelope.
@@ -154,6 +154,8 @@ sandbox = Kobako::Sandbox.new(
154
154
 
155
155
  `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.
156
156
 
157
+ A fifth option, `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, with filesystem, environment, and network still unreachable. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/security-model.md`](docs/security-model.md) § Isolation profiles.
158
+
157
159
  ### Invocation Lifecycle
158
160
 
159
161
  One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; capability state (Handles, stdout, stderr, memory delta) resets between them.
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.1...kobako-runtime-v0.7.0) (2026-07-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
9
+ * **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
10
+
11
+ ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.0...kobako-runtime-v0.6.1) (2026-07-02)
12
+
13
+
14
+ ### Miscellaneous Chores
15
+
16
+ * **kobako-runtime:** Synchronize kobako crates versions
@@ -0,0 +1,34 @@
1
+ # kobako-runtime
2
+
3
+ Engine-neutral host runtime contract for
4
+ [kobako](https://github.com/elct9620/kobako), an in-process Wasm
5
+ sandbox for running untrusted mruby scripts.
6
+
7
+ A kobako host drives a Guest Binary through a wasm engine; this crate
8
+ is the surface where the two meet, free of any engine or frontend
9
+ type, so the engine stays swappable:
10
+
11
+ - `runtime` — the `Runtime` trait: one guest invocation on a fresh
12
+ instance in, its observable `Snapshot` out
13
+ - `snapshot` — the per-invocation observables: `Completion` (outcome
14
+ or trap), the two output `Capture`s, and resource `Usage`, uniform
15
+ across success and trap
16
+ - `error` — the neutral failure channels: `Trap` (engine fault) and
17
+ `SetupError` (the invocation never started)
18
+ - `dispatch` / `yielder` — the `DispatchHandler` and `Yielder` traits
19
+ a frontend supplies for guest→host dispatch and block-yield re-entry
20
+
21
+ Engine implementations (such as `kobako-wasmtime`) implement
22
+ `Runtime`; host frontends (such as the kobako Ruby gem's native ext)
23
+ map the neutral types onto their own language surface.
24
+
25
+ ## Usage
26
+
27
+ ```toml
28
+ [dependencies]
29
+ kobako-runtime = "0.7.0" # x-release-please-version
30
+ ```
31
+
32
+ ## License
33
+
34
+ Apache-2.0
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [0.7.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.1...kobako-wasmtime-v0.7.0) (2026-07-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **crates:** build the requested isolation profile into the WASI context ([63c25d8](https://github.com/elct9620/kobako/commit/63c25d835d4d03010c1658217cee412318e6b5d8))
9
+ * **runtime:** runtimes declare their isolation profile ([f89717a](https://github.com/elct9620/kobako/commit/f89717a6bc9809f0de0df78f97da33a49a1474ac))
10
+
11
+ ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.0...kobako-wasmtime-v0.6.1) (2026-07-02)
12
+
13
+
14
+ ### Miscellaneous Chores
15
+
16
+ * **kobako-wasmtime:** Synchronize kobako crates versions
@@ -0,0 +1,32 @@
1
+ # kobako-wasmtime
2
+
3
+ The [wasmtime](https://wasmtime.dev) implementation of the
4
+ [kobako](https://github.com/elct9620/kobako) host runtime contract
5
+ ([`kobako-runtime`](https://crates.io/crates/kobako-runtime)).
6
+
7
+ `Driver` implements the contract's `Runtime` trait over wasmtime and
8
+ owns every engine-bound mechanic, so frontends see only the neutral
9
+ contract surface:
10
+
11
+ - process-wide Engine and compiled-Module caches with an on-disk AOT
12
+ (`.cwasm`) artifact cache keyed by Guest Binary content
13
+ - a pre-linked `InstancePre` per guest path; every invocation runs on
14
+ a fresh instance and discards its Store afterwards
15
+ - the epoch-based wall-clock timeout and the per-invocation
16
+ linear-memory cap
17
+ - ambient denial: frozen WASI clocks and a constant RNG, so a guest
18
+ observes no real time and no real entropy
19
+
20
+ The kobako Ruby gem's native ext is the first frontend; a Rust host
21
+ SDK consumes the same surface.
22
+
23
+ ## Usage
24
+
25
+ ```toml
26
+ [dependencies]
27
+ kobako-wasmtime = "0.7.0" # x-release-please-version
28
+ ```
29
+
30
+ ## License
31
+
32
+ Apache-2.0
data/data/kobako.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
@@ -38,7 +38,7 @@ module Kobako
38
38
  # for it. +object+ is any host-side Ruby object to bind. Returns a
39
39
  # freshly-allocated +Kobako::Handle+ whose +#id+ falls in
40
40
  # +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
41
- # +Kobako::HandlerExhaustedError+ if the next ID would exceed the
41
+ # +Kobako::HandleExhaustedError+ if the next ID would exceed the
42
42
  # cap. The cap is anchored on +Kobako::Handle+ — the wire codec
43
43
  # and the allocator share the same invariant.
44
44
  #
@@ -96,12 +96,12 @@ module Kobako
96
96
  end
97
97
 
98
98
  # Guard {#alloc} against issuing an ID past the cap. Returns +nil+
99
- # on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
99
+ # on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
100
100
  def ensure_capacity!
101
101
  cap = Kobako::Handle::MAX_ID
102
102
  return unless @next_id > cap
103
103
 
104
- raise HandlerExhaustedError,
104
+ raise HandleExhaustedError,
105
105
  "Out of handle allocations: too many host objects were referenced " \
106
106
  "in a single invocation (limit #{cap})"
107
107
  end
@@ -82,6 +82,10 @@ module Kobako
82
82
  # first invocation, so the preamble is exactly the bindings that
83
83
  # existed at that moment — a bind reaching a +Kobako::Namespace+
84
84
  # after the seal raises +ArgumentError+ and never alters Frame 1.
85
+ # The memo is gated on the seal rather than dropped per mutation (the
86
+ # +Catalog::Snippets#encode+ approach) because a +Member+ bind lands
87
+ # on a child +Kobako::Namespace+, invisible to this collection; only
88
+ # the seal guarantees nothing further can change.
85
89
  def encode
86
90
  return @encoded if @encoded
87
91
 
@@ -44,6 +44,10 @@ module Kobako
44
44
  # The bytes are memoized — the table is replayed verbatim on every
45
45
  # invocation after sealing, so Frame 3 never changes between
46
46
  # encodes; {#register} drops the memo while the table is still open.
47
+ # Unlike +Catalog::Namespaces#encode+, which gates its memo on the
48
+ # seal, this one can fill eagerly and invalidate in +#register+
49
+ # because every mutation funnels through that single method — there is
50
+ # no out-of-sight child object to change the result behind its back.
47
51
  def encode
48
52
  return @encoded if @encoded
49
53
 
@@ -26,7 +26,11 @@ module Kobako
26
26
  # mapping is a closed set, and anything outside it is rejected by
27
27
  # the msgpack gem itself (arbitrary objects raise +NoMethodError+
28
28
  # from missing +to_msgpack+, integers outside i64..u64 raise
29
- # +RangeError+).
29
+ # +RangeError+). The +NoMethodError+ catch is deliberately broad:
30
+ # MessagePack signals "no wire representation" only through that error,
31
+ # so there is no narrower discriminator — a packer-internal
32
+ # +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
33
+ # propagating.
30
34
  def self.encode(value)
31
35
  Factory.dump(value)
32
36
  rescue ::RangeError, ::NoMethodError => e
@@ -48,6 +48,16 @@ module Kobako
48
48
  EXT_ERRENV = 0x02
49
49
  private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
50
50
 
51
+ # An ext 0x02 (Fault) envelope nests through its +details+ field, and
52
+ # each level re-enters the codec with a fresh +MessagePack+ unpacker
53
+ # whose built-in stack guard resets — so ext-envelope depth is tracked
54
+ # here instead. The cap matches the wire's overall nesting bound and
55
+ # keeps a nested chain from exhausting the native stack: an over-deep
56
+ # chain fails as a clean wire error, never a stack-level trap.
57
+ MAX_EXT_DEPTH = 128
58
+ EXT_DEPTH_KEY = :__kobako_codec_ext_depth__
59
+ private_constant :MAX_EXT_DEPTH, :EXT_DEPTH_KEY
60
+
51
61
  # Instance-level pass-through onto the wrapped +MessagePack::Factory+.
52
62
  # Spelled +def_instance_delegators+ rather than +def_delegators+ because
53
63
  # the class also extends +SingleForwardable+ (see the +extend+ block
@@ -129,9 +139,13 @@ module Kobako
129
139
  # Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
130
140
  # the embedded payload flows through the same boundary as a top-level
131
141
  # encode — nested kobako values (Handle, nested Fault) reach the
132
- # registered ext-type packers via the cached singleton.
142
+ # registered ext-type packers via the cached singleton. A +details+
143
+ # chain nested past {MAX_EXT_DEPTH} has no wire representation and
144
+ # surfaces as +UnsupportedType+.
133
145
  def pack_fault(fault)
134
- Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
146
+ within_ext_frame(UnsupportedType) do
147
+ Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
148
+ end
135
149
  end
136
150
 
137
151
  # Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
@@ -139,19 +153,33 @@ module Kobako
139
153
  # +ArgumentError+ invariants surface as +InvalidType+ through the
140
154
  # decoder boundary. Inner decode goes through {Decoder} (not
141
155
  # +factory.load+) so the embedded +str+ payloads flow through the
142
- # same UTF-8 validation as a top-level decode.
143
- #
144
- # This establishes a runtime cycle Factory Decoder Factory: the
145
- # singleton instance feeds +Decoder.decode+, which re-enters this
146
- # method when a nested ext 0x02 appears inside +details+. The recursion
147
- # is bounded by msgpack nesting depth — identical to nested Array /
148
- # Hash payloads — so no extra guard is needed. Do not switch back to
149
- # +factory.load+ to "simplify": that path bypasses UTF-8 validation.
156
+ # same UTF-8 validation as a top-level decode. A nested ext 0x02 in
157
+ # +details+ re-enters this method, so {#within_ext_frame} bounds the
158
+ # chain depth to keep it from exhausting the native stack.
150
159
  def unpack_fault(payload)
151
- Decoder.decode(payload) do |map|
152
- raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
160
+ within_ext_frame(InvalidType) do
161
+ Decoder.decode(payload) do |map|
162
+ raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
163
+
164
+ Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
165
+ end
166
+ end
167
+ end
153
168
 
154
- Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
169
+ # Track ext-envelope re-entry depth and refuse a chain past
170
+ # {MAX_EXT_DEPTH}, raising +over_limit+ so the failure lands in the
171
+ # caller's existing wire-error class. The counter is thread-scoped and
172
+ # balanced by the +ensure+, so a raise mid-chain still unwinds it to
173
+ # its entry value.
174
+ def within_ext_frame(over_limit)
175
+ depth = (Thread.current[EXT_DEPTH_KEY] || 0) + 1
176
+ raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
177
+
178
+ Thread.current[EXT_DEPTH_KEY] = depth
179
+ begin
180
+ yield
181
+ ensure
182
+ Thread.current[EXT_DEPTH_KEY] = depth - 1
155
183
  end
156
184
  end
157
185
  end
@@ -97,6 +97,10 @@ module Kobako
97
97
  case value
98
98
  when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
99
99
  when ::Hash
100
+ # Rebuilt with each key restored: two distinct Handle keys that
101
+ # resolve to equal host objects collapse to the later pair, as in
102
+ # any Ruby Hash. The guest authored this payload, so that collapse
103
+ # is its own concern, not a fidelity guarantee the host owes it.
100
104
  value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
101
105
  when Kobako::Handle then handler.fetch(value.id)
102
106
  else value
data/lib/kobako/codec.rb CHANGED
@@ -22,7 +22,7 @@ module Kobako
22
22
  # {Decoder} are thin wrappers that register the three kobako-specific
23
23
  # ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
24
24
  # envelope) on a single +MessagePack::Factory+ instance. The Rust side
25
- # mirrors this layer as the +codec+ module in the +kobako-core+ crate;
25
+ # mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
26
26
  # the ext-code constants live as module-private values on {Factory}
27
27
  # alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
28
28
  # +codec::EXT_ERRENV+ on that side.
data/lib/kobako/errors.rb CHANGED
@@ -35,7 +35,7 @@ module Kobako
35
35
  #
36
36
  # * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
37
37
  # at +wasm_path+.
38
- # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit.
38
+ # * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
39
39
 
40
40
  # Base for all kobako-raised errors so callers that want to ignore the
41
41
  # taxonomy can rescue a single class.
@@ -89,10 +89,13 @@ module Kobako
89
89
  # +ModuleNotBuiltError+ first.
90
90
  class ModuleNotBuiltError < SetupError; end
91
91
 
92
- # Sandbox / wire layer. Raised when the guest ran to completion but
93
- # execution failed due to a mruby script error, a protocol fault, or a
94
- # host-side wire decode failure on an otherwise valid outcome tag.
95
- class SandboxError < Error
92
+ # The structured attribution the two invocation-failure classes carry
93
+ # from a decoded guest exception its +origin+, original +klass+,
94
+ # +backtrace_lines+, and +details+ so a Host App can inspect a failure
95
+ # beyond its message. Mixed into both rather than promoted to a shared
96
+ # superclass because +SandboxError+ and +ServiceError+ sit in distinct
97
+ # branches of the invocation-outcome taxonomy under +Kobako::Error+.
98
+ module StructuredError
96
99
  attr_reader :origin, :klass, :backtrace_lines, :details
97
100
 
98
101
  def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
@@ -104,26 +107,25 @@ module Kobako
104
107
  end
105
108
  end
106
109
 
110
+ # Sandbox / wire layer. Raised when the guest ran to completion but
111
+ # execution failed due to a mruby script error, a protocol fault, or a
112
+ # host-side wire decode failure on an otherwise valid outcome tag.
113
+ class SandboxError < Error
114
+ include StructuredError
115
+ end
116
+
107
117
  # Service layer. Raised when a Service capability call inside a mruby
108
118
  # script reported an application-level failure that the script did not
109
119
  # rescue.
110
120
  class ServiceError < Error
111
- attr_reader :origin, :klass, :backtrace_lines, :details
112
-
113
- def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
114
- super(message)
115
- @origin = origin
116
- @klass = klass
117
- @backtrace_lines = backtrace_lines
118
- @details = details
119
- end
121
+ include StructuredError
120
122
  end
121
123
 
122
- # HandlerExhaustedError is the canonical SandboxError subclass for the
124
+ # HandleExhaustedError is the canonical SandboxError subclass for the
123
125
  # id-cap-hit path. Raised when the per-invocation Handle ID counter in
124
126
  # Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
125
127
  # allocation would exceed the cap.
126
- class HandlerExhaustedError < SandboxError; end
128
+ class HandleExhaustedError < SandboxError; end
127
129
 
128
130
  # BytecodeError is the SandboxError subclass raised when a
129
131
  # `#preload(binary:)` snippet fails structural validation during the
@@ -38,14 +38,16 @@ module Kobako
38
38
 
39
39
  attr_reader :wasm_path, :options
40
40
 
41
- # Per-cap accessors forward to the immutable +SandboxOptions+ Value
41
+ # Per-option accessors forward to the immutable +SandboxOptions+ Value
42
42
  # Object so the Host App still reads them off Sandbox directly.
43
- def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit
43
+ def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
44
44
 
45
45
  # Returns the bytes the guest wrote to stdout during the most recent
46
46
  # invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
47
47
  # any invocation; the byte content never contains a truncation sentinel,
48
- # so use +#stdout_truncated?+ to observe overflow.
48
+ # so use +#stdout_truncated?+ to observe overflow. Populated on every
49
+ # outcome — including a rescued +TrapError+, after which it holds the
50
+ # bytes written before the trap fired — mirroring +#usage+.
49
51
  def stdout
50
52
  @stdout_capture.bytes
51
53
  end
@@ -83,23 +85,24 @@ module Kobako
83
85
  # Build a fresh Sandbox.
84
86
  #
85
87
  # +wasm_path+ is the absolute path to the Guest Binary; defaults to the
86
- # gem-bundled +data/kobako.wasm+. The four caps (+stdout_limit+,
87
- # +stderr_limit+, +timeout+, +memory_limit+) are forwarded verbatim to
88
- # +Kobako::SandboxOptions+, which owns their DEFAULT fallback and
88
+ # gem-bundled +data/kobako.wasm+. Every other keyword — the four caps
89
+ # (+stdout_limit+, +stderr_limit+, +timeout+, +memory_limit+) and the
90
+ # requested isolation profile (+profile+) is forwarded verbatim to
91
+ # +Kobako::SandboxOptions+, which owns the DEFAULT fallbacks and
89
92
  # normalisation. The constructed +SandboxOptions+ is exposed as
90
- # +#options+ and the four caps remain readable directly on Sandbox via
91
- # +Forwardable+ delegation.
92
- def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
93
- timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
94
- memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
93
+ # +#options+ and every option remains readable directly on Sandbox via
94
+ # +Forwardable+ delegation. The runtime builds the requested profile —
95
+ # +:hermetic+ (the default) denies the guest ambient time and entropy,
96
+ # +:permissive+ leaves them live — and construction refuses a runtime
97
+ # whose declared profile falls below the request, raising
98
+ # +Kobako::SetupError+ before any invocation entry point runs.
99
+ def initialize(wasm_path: nil, **)
95
100
  @wasm_path = wasm_path || Kobako::Runtime.default_path
96
- @options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
97
- stderr_limit: stderr_limit)
101
+ @options = SandboxOptions.new(**)
98
102
  @handler = Catalog::Handles.new
99
103
  @services = Kobako::Catalog::Namespaces.new(handler: @handler)
100
104
  @snippets = Catalog::Snippets.new
101
- @runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
102
- @options.stdout_limit, @options.stderr_limit)
105
+ @runtime = build_runtime!
103
106
  install_dispatch_proc!
104
107
  reset_invocation_state!
105
108
  end
@@ -207,17 +210,28 @@ module Kobako
207
210
 
208
211
  private
209
212
 
210
- # Configure the +Runtime+'s host↔guest dispatch wiring. Builds a
211
- # lambda that re-enters the guest via
212
- # +Runtime#yield_to_active_invocation+ and a dispatch +Proc+ that routes
213
- # guest→host calls through the stateless +Transport::Dispatcher+,
214
- # capturing +@services+ / +@handler+ in the closure. Both are registered
215
- # on the +Runtime+ once at construction time so the wasm ext callback can
216
- # fire without further setup.
213
+ # Construct the +Runtime+ with the requested isolation profile and
214
+ # refuse one whose declared posture falls below the request
215
+ # +SandboxOptions#enforce_floor!+ owns the ladder comparison, so a
216
+ # runtime that cannot honor the request never runs guest code.
217
+ def build_runtime!
218
+ runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
219
+ @options.stdout_limit, @options.stderr_limit, profile)
220
+ @options.enforce_floor!(runtime.profile)
221
+ runtime
222
+ end
223
+
224
+ # Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
225
+ # dispatch +Proc+ that routes guest→host calls through the stateless
226
+ # +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
227
+ # closure. The ext hands the +Proc+ a per-dispatch +guest_yielder+ — a
228
+ # +String → String+ callable that re-enters the in-flight guest to run a
229
+ # yielded block — which the +Dispatcher+ forwards to the +Transport::Yielder+
230
+ # it builds for the call. Registered once at construction time so the
231
+ # wasm ext callback can fire without further setup.
217
232
  def install_dispatch_proc!
218
- yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
219
- @runtime.on_dispatch = lambda do |request_bytes|
220
- Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
233
+ @runtime.on_dispatch = lambda do |request_bytes, guest_yielder|
234
+ Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder)
221
235
  end
222
236
  end
223
237
 
@@ -226,8 +240,7 @@ module Kobako
226
240
  # state — capture buffers, truncation predicates, and the
227
241
  # +Catalog::Handles+ counter — before the guest runs. The
228
242
  # +Catalog::Handles+ itself is held as +@handler+ and never exposed
229
- # beyond this class: SPEC.md Terminology pins it as "Not exposed to the
230
- # Host App".
243
+ # beyond this class it is not part of the Host App's surface.
231
244
  def begin_invocation!
232
245
  @services.seal!
233
246
  @handler.reset!
@@ -239,9 +252,10 @@ module Kobako
239
252
  # the +invoke!+ +ensure+ block so the usage record is populated on
240
253
  # every outcome — value return, +Kobako::TrapError+ (including
241
254
  # +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
242
- # and +Kobako::ServiceError+. On the success path the same figures
243
- # already arrive via +Snapshot#usage+; on the trap path the Snapshot
244
- # never reaches Ruby so the ext readout here is the only source.
255
+ # and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
256
+ # both paths: the figures are stashed in the ext on every outcome, so
257
+ # the readout here also covers the trap path, where +Runtime#eval+ /
258
+ # +#run+ raise instead of returning outcome bytes.
245
259
  #
246
260
  # The ext returns a positional 2-tuple +[wall_time, memory_peak]+
247
261
  # whose order matches the +Kobako::Usage+ field order; the
@@ -267,33 +281,51 @@ module Kobako
267
281
  end
268
282
  end
269
283
 
284
+ # Read the per-last-invocation output captures from the ext and wrap
285
+ # them as +Kobako::Capture+ value objects. Runs in the +invoke!+
286
+ # +ensure+ block next to {#read_usage!} for the same reason: the ext
287
+ # stashes the captures on every outcome, so the readout also covers
288
+ # the trap path, where +Runtime#eval+ / +#run+ raise instead of
289
+ # returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
290
+ # partial output readable after a rescue.
291
+ #
292
+ # The ext returns a positional 4-tuple
293
+ # +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+;
294
+ # the destructure-then-kwargs handoff below is the explicit
295
+ # positional→keyword conversion point.
296
+ def read_captures!
297
+ stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
298
+ @stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
299
+ @stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
300
+ end
301
+
270
302
  # Shared prologue / epilogue + trap-class translator for both
271
303
  # invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
272
304
  # TrapError message so the failing export is identifiable.
273
305
  #
274
- # The yielded block must return a +Kobako::Snapshot+ i.e. the
275
- # value of +Runtime#eval+ / +#run+ (SPEC.md Internal Concepts
276
- # Snapshot). The success path unpacks +#stdout+ / +#stderr+ into
277
- # +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
278
- # populated by the +ensure+ readout ({#read_usage!}) on every outcome.
306
+ # The yielded block must return the invocation's raw outcome bytes —
307
+ # i.e. the value of +Runtime#eval+ / +#run+ which the success path
308
+ # feeds to +Outcome.decode+. Captures and usage are populated by the
309
+ # +ensure+ readouts ({#read_usage!} / {#read_captures!}) on every
310
+ # outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
311
+ # rescued trap.
279
312
  # The rescue chain is the single trap-translation boundary —
280
313
  # configured-cap paths surface as named TrapError subclasses
281
314
  # (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
282
315
  # the base +TrapError+.
283
316
  def invoke!(verb)
284
317
  begin_invocation!
285
- snapshot = yield
286
- @stdout_capture = snapshot.stdout
287
- @stderr_capture = snapshot.stderr
318
+ return_bytes = yield
288
319
  # A Capability Handle in the result is decoded as a Kobako::Handle
289
320
  # token; restore it to the host object the guest referenced before
290
321
  # handing the value to the Host App. @handler still holds this
291
322
  # invocation's table — reset only happens at the next #begin_invocation!.
292
- Codec::HandleWalk.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
323
+ Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler)
293
324
  rescue Kobako::TrapError => e
294
325
  raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
295
326
  ensure
296
327
  read_usage!
328
+ read_captures!
297
329
  end
298
330
  end
299
331
  end