kobako 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +32 -0
- data/Cargo.lock +3 -1
- data/README.md +47 -19
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +12 -2
- data/ext/kobako/src/runtime/ambient.rs +1 -1
- data/ext/kobako/src/runtime/cache.rs +170 -6
- data/ext/kobako/src/runtime/capture.rs +1 -1
- data/ext/kobako/src/runtime/config.rs +3 -4
- data/ext/kobako/src/runtime/dispatch.rs +8 -8
- data/ext/kobako/src/runtime/exports.rs +32 -21
- data/ext/kobako/src/runtime/instance_pre.rs +97 -0
- data/ext/kobako/src/runtime/invocation.rs +36 -93
- data/ext/kobako/src/runtime/trap.rs +5 -5
- data/ext/kobako/src/runtime.rs +389 -403
- data/ext/kobako/src/snapshot.rs +2 -2
- data/lib/kobako/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +28 -39
- data/lib/kobako/catalog/namespaces.rb +31 -20
- data/lib/kobako/catalog/snippets.rb +18 -16
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +40 -36
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +17 -6
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +176 -0
- data/lib/kobako/sandbox.rb +68 -88
- data/lib/kobako/sandbox_options.rb +5 -9
- data/lib/kobako/snapshot.rb +2 -4
- data/lib/kobako/snippet/binary.rb +1 -3
- data/lib/kobako/snippet/source.rb +1 -2
- data/lib/kobako/snippet.rb +1 -2
- data/lib/kobako/transport/dispatcher.rb +39 -38
- data/lib/kobako/transport/request.rb +1 -1
- data/lib/kobako/transport/run.rb +23 -28
- data/lib/kobako/transport/yielder.rb +11 -17
- data/lib/kobako/transport.rb +2 -3
- data/lib/kobako/usage.rb +10 -13
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +16 -1
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/namespace.rbs +2 -0
- data/sig/kobako/pool.rbs +44 -0
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +4 -1
data/lib/kobako/outcome.rb
CHANGED
|
@@ -7,8 +7,7 @@ module Kobako
|
|
|
7
7
|
# Host-facing boundary for the OUTCOME_BUFFER produced by
|
|
8
8
|
# +__kobako_eval+. Takes raw outcome bytes — a one-byte tag followed by
|
|
9
9
|
# the msgpack-encoded body — and maps them to either the unwrapped
|
|
10
|
-
# mruby return value or a raised three-layer
|
|
11
|
-
# ({docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]) exception.
|
|
10
|
+
# mruby return value or a raised three-layer exception.
|
|
12
11
|
#
|
|
13
12
|
# Self-contained: this module owns the wire framing (tag bytes,
|
|
14
13
|
# body decoding), and the +Panic+ wire record lives at
|
|
@@ -17,11 +16,11 @@ module Kobako
|
|
|
17
16
|
# nothing in +Transport+ participates.
|
|
18
17
|
#
|
|
19
18
|
# * tag 0x01, decode OK → return decoded value
|
|
20
|
-
# * tag 0x01, decode fails → SandboxError
|
|
21
|
-
# * tag 0x02, origin="service" → ServiceError
|
|
22
|
-
# * tag 0x02, origin="sandbox"/missing → SandboxError
|
|
23
|
-
# * tag 0x02, decode fails → SandboxError
|
|
24
|
-
# * unknown tag → TrapError
|
|
19
|
+
# * tag 0x01, decode fails → SandboxError
|
|
20
|
+
# * tag 0x02, origin="service" → ServiceError
|
|
21
|
+
# * tag 0x02, origin="sandbox"/missing → SandboxError
|
|
22
|
+
# * tag 0x02, decode fails → SandboxError
|
|
23
|
+
# * unknown tag → TrapError
|
|
25
24
|
module Outcome
|
|
26
25
|
# First byte of the OUTCOME_BUFFER for the success branch — body is
|
|
27
26
|
# the bare msgpack encoding of the returned value
|
|
@@ -71,7 +70,7 @@ module Kobako
|
|
|
71
70
|
[tag, body]
|
|
72
71
|
end
|
|
73
72
|
|
|
74
|
-
# Decode failure on the success tag is a SandboxError
|
|
73
|
+
# Decode failure on the success tag is a SandboxError: the
|
|
75
74
|
# framing was fine, but the carried value is unrepresentable. The
|
|
76
75
|
# specific codec fault is stashed in +details+ rather
|
|
77
76
|
# than spliced into the message — callers cannot act on the inner
|
|
@@ -86,7 +85,7 @@ module Kobako
|
|
|
86
85
|
)
|
|
87
86
|
end
|
|
88
87
|
|
|
89
|
-
# Decode failure on the panic tag is a SandboxError
|
|
88
|
+
# Decode failure on the panic tag is a SandboxError. Either
|
|
90
89
|
# path raises — on success the decoded Panic is mapped to its three-
|
|
91
90
|
# layer exception via +build_panic_error+ and raised; on wire-decode
|
|
92
91
|
# failure the rescue path raises the wire-violation +SandboxError+.
|
|
@@ -130,13 +129,12 @@ module Kobako
|
|
|
130
129
|
)
|
|
131
130
|
end
|
|
132
131
|
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# callers can rescue specific failure paths. +origin="service"+ →
|
|
132
|
+
# Map the panic +class+ field to the matching Ruby exception subclass
|
|
133
|
+
# so callers can rescue specific failure paths. +origin="service"+ →
|
|
136
134
|
# +ServiceError+; +origin="sandbox"+ plus
|
|
137
135
|
# +class="Kobako::BytecodeError"+ selects the +BytecodeError+
|
|
138
|
-
# subclass
|
|
139
|
-
#
|
|
136
|
+
# subclass. Everything else falls back to the base class for the
|
|
137
|
+
# origin.
|
|
140
138
|
def panic_target_class(panic)
|
|
141
139
|
case panic.origin
|
|
142
140
|
when Panic::ORIGIN_SERVICE
|
data/lib/kobako/pool.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "sandbox"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# Kobako::Pool — a bounded set of warm, identically set-up Sandboxes
|
|
8
|
+
# handed out one exclusive holder at a time.
|
|
9
|
+
#
|
|
10
|
+
# Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
|
|
11
|
+
# and holds the optional block as the per-Sandbox setup hook; a
|
|
12
|
+
# checkout prefers an idle Sandbox and constructs a new one only when
|
|
13
|
+
# none is idle and fewer than +slots+ exist. +#with+ blocks up
|
|
14
|
+
# to +checkout_timeout+ seconds when every slot is held, applies
|
|
15
|
+
# the +TrapError+ discard-and-recreate contract at checkin, and
|
|
16
|
+
# the Pool releases everything with its own reachability — there is no
|
|
17
|
+
# teardown verb.
|
|
18
|
+
class Pool
|
|
19
|
+
# The +#with+ wait bound applied when +checkout_timeout+ is not given.
|
|
20
|
+
DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
|
|
21
|
+
|
|
22
|
+
# Build a Pool of up to +slots+ Sandboxes. +slots+ is
|
|
23
|
+
# a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
|
|
24
|
+
# seconds (+nil+ waits indefinitely); every other keyword is
|
|
25
|
+
# forwarded verbatim to +Kobako::Sandbox.new+. The optional block
|
|
26
|
+
# runs exactly once per constructed Sandbox — it is the setup window
|
|
27
|
+
# for +#define+ / +#preload+ before that Sandbox's first checkout.
|
|
28
|
+
# No Sandbox is constructed here. Raises +ArgumentError+ for an
|
|
29
|
+
# invalid +slots+ / +checkout_timeout+.
|
|
30
|
+
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
|
31
|
+
validate_slots!(slots)
|
|
32
|
+
@slots = slots
|
|
33
|
+
@checkout_timeout = normalize_checkout_timeout(checkout_timeout)
|
|
34
|
+
@sandbox_options = sandbox_options
|
|
35
|
+
@setup = setup
|
|
36
|
+
@idle = [] # : Array[Kobako::Sandbox]
|
|
37
|
+
@constructed = 0
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
@slot_freed = ConditionVariable.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Yield one exclusively-held Sandbox to the block and return the
|
|
43
|
+
# block's value. Blocks while every slot is held; raises
|
|
44
|
+
# +Kobako::PoolTimeoutError+ once the wait exceeds +checkout_timeout+.
|
|
45
|
+
# The Sandbox returns to the pool at block exit — unless the block raised
|
|
46
|
+
# +Kobako::TrapError+, in which case the unrecoverable Sandbox is
|
|
47
|
+
# discarded and its slot refills by a fresh construction on next
|
|
48
|
+
# demand.
|
|
49
|
+
def with
|
|
50
|
+
sandbox = checkout
|
|
51
|
+
begin
|
|
52
|
+
yield sandbox
|
|
53
|
+
rescue TrapError
|
|
54
|
+
release_capacity!
|
|
55
|
+
sandbox = nil
|
|
56
|
+
raise
|
|
57
|
+
ensure
|
|
58
|
+
checkin(sandbox) if sandbox
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Acquire a Sandbox and hand it over in pre-invocation state — empty
|
|
65
|
+
# output buffers and truncation predicates false.
|
|
66
|
+
def checkout
|
|
67
|
+
acquire.tap(&:reset_invocation_state!)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The idle-first claim loop: an idle Sandbox wins, unclaimed
|
|
71
|
+
# capacity constructs, and a full pool waits for a checkin.
|
|
72
|
+
def acquire
|
|
73
|
+
timeout = @checkout_timeout
|
|
74
|
+
deadline = timeout && (monotonic_now + timeout)
|
|
75
|
+
loop do
|
|
76
|
+
action, sandbox = claim_or_wait(deadline)
|
|
77
|
+
return sandbox if action == :idle && sandbox
|
|
78
|
+
return construct_slot if action == :build
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Single locked decision point for one claim attempt. Waiting
|
|
83
|
+
# happens inside the lock (so a checkin can wake it); construction
|
|
84
|
+
# happens outside (so a slow setup block never holds the lock) —
|
|
85
|
+
# capacity is reserved here and released by +construct_slot+ on
|
|
86
|
+
# failure.
|
|
87
|
+
def claim_or_wait(deadline)
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
return [:idle, @idle.pop] unless @idle.empty?
|
|
90
|
+
|
|
91
|
+
if @constructed < @slots
|
|
92
|
+
@constructed += 1
|
|
93
|
+
return [:build, nil]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
await_slot!(deadline)
|
|
97
|
+
[:retry, nil]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Wait for a checkin or freed capacity; raises
|
|
102
|
+
# +Kobako::PoolTimeoutError+ once +deadline+ has passed. Must
|
|
103
|
+
# run while holding +@mutex+.
|
|
104
|
+
def await_slot!(deadline)
|
|
105
|
+
remaining = deadline && (deadline - monotonic_now)
|
|
106
|
+
if remaining && remaining <= 0
|
|
107
|
+
raise PoolTimeoutError,
|
|
108
|
+
"no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@slot_freed.wait(@mutex, remaining)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Construct and set up one pooled Sandbox against the capacity
|
|
115
|
+
# reserved by +claim_or_wait+. Construction and setup-block errors
|
|
116
|
+
# propagate to the checkout caller unchanged; the reserved
|
|
117
|
+
# capacity is released so a later checkout can retry.
|
|
118
|
+
def construct_slot
|
|
119
|
+
done = false
|
|
120
|
+
sandbox = Sandbox.new(**@sandbox_options)
|
|
121
|
+
@setup&.call(sandbox)
|
|
122
|
+
done = true
|
|
123
|
+
sandbox
|
|
124
|
+
ensure
|
|
125
|
+
release_capacity! unless done
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Return a Sandbox to the idle list and wake one waiting checkout.
|
|
129
|
+
def checkin(sandbox)
|
|
130
|
+
@mutex.synchronize do
|
|
131
|
+
@idle.push(sandbox)
|
|
132
|
+
@slot_freed.signal
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Give back reserved-but-unfilled capacity — a failed construction or
|
|
137
|
+
# a discarded Sandbox — and wake one waiting checkout to claim it.
|
|
138
|
+
def release_capacity!
|
|
139
|
+
@mutex.synchronize do
|
|
140
|
+
@constructed -= 1
|
|
141
|
+
@slot_freed.signal
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The wait deadline runs on the monotonic clock so a wall-clock jump
|
|
146
|
+
# cannot stretch or cut the checkout wait.
|
|
147
|
+
def monotonic_now
|
|
148
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Pre-flight for +slots+ — no coercion, a positive Integer is
|
|
152
|
+
# the only accepted shape.
|
|
153
|
+
def validate_slots!(slots)
|
|
154
|
+
return if slots.is_a?(Integer) && slots.positive?
|
|
155
|
+
|
|
156
|
+
raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Coerce +checkout_timeout+ into the Float seconds the wait loop
|
|
160
|
+
# consumes, or +nil+ to wait indefinitely — the same normalisation
|
|
161
|
+
# idiom +SandboxOptions+ applies to +timeout+.
|
|
162
|
+
def normalize_checkout_timeout(checkout_timeout)
|
|
163
|
+
return nil if checkout_timeout.nil?
|
|
164
|
+
unless checkout_timeout.is_a?(Numeric)
|
|
165
|
+
raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
seconds = checkout_timeout.to_f
|
|
169
|
+
unless seconds.positive? && seconds.finite?
|
|
170
|
+
raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
seconds
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -13,21 +13,19 @@ require_relative "catalog"
|
|
|
13
13
|
|
|
14
14
|
module Kobako
|
|
15
15
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
16
|
-
# scripts inside a wasmtime-hosted Wasm module
|
|
17
|
-
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
16
|
+
# scripts inside a wasmtime-hosted Wasm module.
|
|
18
17
|
#
|
|
19
18
|
# The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
|
|
20
|
-
# +Kobako::Catalog::Handles
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# Sandboxes amortises both costs automatically.
|
|
19
|
+
# +Kobako::Catalog::Handles+, the per-instance
|
|
20
|
+
# +Kobako::Catalog::Namespaces+ (which receives the +Catalog::Handles+ by
|
|
21
|
+
# injection so guest→host dispatch and host→guest auto-wrap share one
|
|
22
|
+
# allocator), and the dispatch +Proc+ / +yield_to_guest+ lambda installed
|
|
23
|
+
# on the Runtime via +Runtime#on_dispatch=+. The underlying wasmtime Engine
|
|
24
|
+
# and compiled Module are cached at process scope by the native ext and
|
|
25
|
+
# never surface to Ruby — constructing many Sandboxes amortises both costs
|
|
26
|
+
# automatically.
|
|
29
27
|
#
|
|
30
|
-
# Output capture policy
|
|
28
|
+
# Output capture policy: the
|
|
31
29
|
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
32
30
|
# WASI pipe — the host buffer stops growing at the cap, subsequent guest
|
|
33
31
|
# writes on that channel fail or are dropped, and +#run+ still returns
|
|
@@ -46,9 +44,8 @@ module Kobako
|
|
|
46
44
|
|
|
47
45
|
# Returns the bytes the guest wrote to stdout during the most recent
|
|
48
46
|
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
49
|
-
# any invocation
|
|
50
|
-
#
|
|
51
|
-
# observe overflow.
|
|
47
|
+
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
+
# so use +#stdout_truncated?+ to observe overflow.
|
|
52
49
|
def stdout
|
|
53
50
|
@stdout_capture.bytes
|
|
54
51
|
end
|
|
@@ -61,9 +58,8 @@ module Kobako
|
|
|
61
58
|
end
|
|
62
59
|
|
|
63
60
|
# Returns +true+ iff stdout capture during the most recent invocation
|
|
64
|
-
# exceeded +stdout_limit+
|
|
65
|
-
#
|
|
66
|
-
# ({docs/behavior.md B-03}[link:../../docs/behavior.md]).
|
|
61
|
+
# exceeded +stdout_limit+. Resets to +false+ at the start of the next
|
|
62
|
+
# invocation.
|
|
67
63
|
def stdout_truncated?
|
|
68
64
|
@stdout_capture.truncated?
|
|
69
65
|
end
|
|
@@ -75,8 +71,7 @@ module Kobako
|
|
|
75
71
|
end
|
|
76
72
|
|
|
77
73
|
# Returns the +Kobako::Usage+ value object for the most recent
|
|
78
|
-
# invocation (
|
|
79
|
-
# Carries +wall_time+ (Float seconds the guest export call spent
|
|
74
|
+
# invocation. Carries +wall_time+ (Float seconds the guest export call spent
|
|
80
75
|
# inside wasmtime) and +memory_peak+ (Integer bytes, high-water of
|
|
81
76
|
# the per-invocation +memory.grow+ delta past the entry-time
|
|
82
77
|
# baseline). Returns +Kobako::Usage::EMPTY+ before any invocation;
|
|
@@ -109,9 +104,8 @@ module Kobako
|
|
|
109
104
|
reset_invocation_state!
|
|
110
105
|
end
|
|
111
106
|
|
|
112
|
-
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
113
|
-
#
|
|
114
|
-
# Symbol or String in constant form. Returns the
|
|
107
|
+
# Declare or retrieve the Namespace named +name+ on this Sandbox. +name+
|
|
108
|
+
# must be a Symbol or String in constant form. Returns the
|
|
115
109
|
# +Kobako::Namespace+.
|
|
116
110
|
#
|
|
117
111
|
# Raises +ArgumentError+ when called after the first invocation, or
|
|
@@ -120,20 +114,18 @@ module Kobako
|
|
|
120
114
|
@services.define(name)
|
|
121
115
|
end
|
|
122
116
|
|
|
123
|
-
# Register a snippet on this Sandbox in one of two forms
|
|
124
|
-
# ({docs/behavior.md B-32}[link:../../docs/behavior.md]):
|
|
117
|
+
# Register a snippet on this Sandbox in one of two forms:
|
|
125
118
|
#
|
|
126
119
|
# * +preload(code: source, name: Name)+ — +source+ is mruby source
|
|
127
120
|
# as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. The +name+
|
|
128
121
|
# becomes the snippet's +(snippet:Name)+ backtrace filename and
|
|
129
|
-
# is the dedupe key
|
|
122
|
+
# is the dedupe key that rejects a duplicate +code:+ snippet.
|
|
130
123
|
# * +preload(binary: bytes)+ — +bytes+ is precompiled RITE
|
|
131
124
|
# bytecode as a +String+. The canonical name, when present,
|
|
132
125
|
# lives in the bytecode's embedded +debug_info+ and is resolved
|
|
133
126
|
# by the guest at load time; the host treats the bytes as
|
|
134
|
-
# opaque. Structural failures
|
|
135
|
-
#
|
|
136
|
-
# surface as +Kobako::BytecodeError+ on the first invocation.
|
|
127
|
+
# opaque. Structural failures surface as +Kobako::BytecodeError+
|
|
128
|
+
# on the first invocation.
|
|
137
129
|
#
|
|
138
130
|
# Subsequent invocations (+#eval+ or +#run+) replay every registered
|
|
139
131
|
# snippet — in insertion order — against the fresh +mrb_state+
|
|
@@ -145,11 +137,9 @@ module Kobako
|
|
|
145
137
|
# supplied, when both forms are mixed (e.g., +code:+ and +binary:+
|
|
146
138
|
# together, or +binary:+ paired with +name:+), when +code+ / +bytes+
|
|
147
139
|
# is not a +String+, when +name+ does not match the constant
|
|
148
|
-
# pattern
|
|
149
|
-
# when
|
|
150
|
-
#
|
|
151
|
-
# called after the first invocation
|
|
152
|
-
# ({docs/behavior.md E-35, B-33}[link:../../docs/behavior.md]).
|
|
140
|
+
# pattern, when +name+ duplicates an already-registered +code:+ form
|
|
141
|
+
# snippet, or when called after the first invocation has sealed the
|
|
142
|
+
# snippet table.
|
|
153
143
|
def preload(code: nil, name: nil, binary: nil)
|
|
154
144
|
raise ArgumentError, "cannot preload after first Sandbox invocation" if @services.sealed?
|
|
155
145
|
|
|
@@ -157,17 +147,16 @@ module Kobako
|
|
|
157
147
|
self
|
|
158
148
|
end
|
|
159
149
|
|
|
160
|
-
# Dispatch into a preloaded entrypoint constant
|
|
161
|
-
# ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
|
|
150
|
+
# Dispatch into a preloaded entrypoint constant. Delegates host
|
|
162
151
|
# pre-flight and wire encoding to +Kobako::Transport::Run+ /
|
|
163
152
|
# +Kobako::Transport::Run#encode+: a non-Symbol/String +target+ raises
|
|
164
|
-
# +TypeError
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
153
|
+
# +TypeError+, while a +target+ failing the constant pattern, a forged
|
|
154
|
+
# +Kobako::Handle+ in +args+ / +kwargs+, or a non-Symbol +kwargs+ key
|
|
155
|
+
# raise +ArgumentError+. The guest resolves +target+ as a top-level
|
|
156
|
+
# constant, calls +#call+ on it with +args+ / +kwargs+, and returns the
|
|
157
|
+
# deserialized result. The first invocation seals the Service registry
|
|
158
|
+
# and snippet table. Runtime errors follow the same three-class
|
|
159
|
+
# taxonomy as +#eval+.
|
|
171
160
|
def run(target, *args, **kwargs)
|
|
172
161
|
run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
173
162
|
invoke!(:run) do
|
|
@@ -175,29 +164,27 @@ module Kobako
|
|
|
175
164
|
end
|
|
176
165
|
end
|
|
177
166
|
|
|
178
|
-
# Execute a guest mruby source string in a fresh +mrb_state+
|
|
179
|
-
#
|
|
180
|
-
# mruby source as a UTF-8 String. Returns the deserialized last
|
|
167
|
+
# Execute a guest mruby source string in a fresh +mrb_state+. +code+ is
|
|
168
|
+
# the mruby source as a UTF-8 String. Returns the deserialized last
|
|
181
169
|
# expression of the source.
|
|
182
170
|
#
|
|
183
171
|
# Source delivery uses the WASI stdin three-frame protocol
|
|
184
172
|
# ({docs/wire-codec.md Invocation channels}[link:../../docs/wire-codec.md]):
|
|
185
173
|
# Frame 1 carries the msgpack-encoded preamble (Namespace / Member
|
|
186
174
|
# registry snapshot), Frame 2 carries the user source UTF-8 bytes, and
|
|
187
|
-
# Frame 3 carries the snippet table registered via +#preload
|
|
175
|
+
# Frame 3 carries the snippet table registered via +#preload+.
|
|
188
176
|
# Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
|
|
189
177
|
# mandatory-presence — an empty snippet table sends an empty msgpack
|
|
190
178
|
# array, never an absent frame.
|
|
191
179
|
#
|
|
192
|
-
# The first invocation seals the Service registry and snippet table
|
|
193
|
-
#
|
|
194
|
-
# +#define+ / +#preload+ calls raise +ArgumentError+.
|
|
180
|
+
# The first invocation seals the Service registry and snippet table;
|
|
181
|
+
# subsequent +#define+ / +#preload+ calls raise +ArgumentError+.
|
|
195
182
|
#
|
|
196
183
|
# Raises +Kobako::TrapError+ on a Wasm trap or wire-violation fallback;
|
|
197
184
|
# +Kobako::SandboxError+ when the guest ran to completion but failed
|
|
198
185
|
# (including when +code+ is +nil+ or not a String, or when a preloaded
|
|
199
|
-
# snippet's replay raises
|
|
200
|
-
#
|
|
186
|
+
# snippet's replay raises); +Kobako::ServiceError+ on an unrescued
|
|
187
|
+
# Service capability failure.
|
|
201
188
|
def eval(code)
|
|
202
189
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
203
190
|
|
|
@@ -206,16 +193,27 @@ module Kobako
|
|
|
206
193
|
end
|
|
207
194
|
end
|
|
208
195
|
|
|
196
|
+
# Reset all per-invocation observable state to its pre-invocation
|
|
197
|
+
# sentinels — both per-channel captures and the per-last-invocation
|
|
198
|
+
# usage record. Shared by +#initialize+ (first-time setup) and
|
|
199
|
+
# +#begin_invocation!+ (between-invocation reset) so both paths agree on
|
|
200
|
+
# what "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
|
|
201
|
+
# so a pooled Sandbox hands over empty output buffers.
|
|
202
|
+
def reset_invocation_state!
|
|
203
|
+
@stdout_capture = Capture::EMPTY
|
|
204
|
+
@stderr_capture = Capture::EMPTY
|
|
205
|
+
@usage = Usage::EMPTY
|
|
206
|
+
end
|
|
207
|
+
|
|
209
208
|
private
|
|
210
209
|
|
|
211
|
-
# Configure the +Runtime+'s host↔guest dispatch wiring
|
|
212
|
-
# ({docs/behavior.md B-12}[link:../../docs/behavior.md]). Builds a
|
|
210
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring. Builds a
|
|
213
211
|
# lambda that re-enters the guest via
|
|
214
|
-
# +Runtime#yield_to_active_invocation+
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
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.
|
|
219
217
|
def install_dispatch_proc!
|
|
220
218
|
yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
|
|
221
219
|
@runtime.on_dispatch = lambda do |request_bytes|
|
|
@@ -223,37 +221,21 @@ module Kobako
|
|
|
223
221
|
end
|
|
224
222
|
end
|
|
225
223
|
|
|
226
|
-
# Per-invocation prologue
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
# capability state — capture buffers, truncation predicates, and the
|
|
224
|
+
# Per-invocation prologue. Seals the Service / snippet registries on
|
|
225
|
+
# first call (idempotent) and zeros the per-invocation capability
|
|
226
|
+
# state — capture buffers, truncation predicates, and the
|
|
230
227
|
# +Catalog::Handles+ counter — before the guest runs. The
|
|
231
|
-
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
232
|
-
# this class: SPEC.md Terminology pins it as "Not exposed to the
|
|
233
|
-
# Host App"
|
|
228
|
+
# +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".
|
|
234
231
|
def begin_invocation!
|
|
235
232
|
@services.seal!
|
|
236
233
|
@handler.reset!
|
|
237
234
|
reset_invocation_state!
|
|
238
235
|
end
|
|
239
236
|
|
|
240
|
-
# Reset all per-invocation observable state to its pre-invocation
|
|
241
|
-
# sentinels — both per-channel captures
|
|
242
|
-
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
243
|
-
# per-last-invocation usage record
|
|
244
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
245
|
-
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
246
|
-
# (between-invocation reset) so both paths agree on what
|
|
247
|
-
# "pre-invocation state" means.
|
|
248
|
-
def reset_invocation_state!
|
|
249
|
-
@stdout_capture = Capture::EMPTY
|
|
250
|
-
@stderr_capture = Capture::EMPTY
|
|
251
|
-
@usage = Usage::EMPTY
|
|
252
|
-
end
|
|
253
|
-
|
|
254
237
|
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
255
|
-
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
256
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
|
238
|
+
# the ext and wrap them as a +Kobako::Usage+ value object. Runs in
|
|
257
239
|
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
258
240
|
# every outcome — value return, +Kobako::TrapError+ (including
|
|
259
241
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
@@ -271,8 +253,7 @@ module Kobako
|
|
|
271
253
|
end
|
|
272
254
|
|
|
273
255
|
# Pick the +TrapError+ subclass to re-raise based on +err+'s actual
|
|
274
|
-
# class. Cap-trap subclasses
|
|
275
|
-
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
256
|
+
# class. Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+)
|
|
276
257
|
# preserve their named identity; everything else collapses to the
|
|
277
258
|
# base +Kobako::TrapError+. The ext already raises the right subclass
|
|
278
259
|
# directly, so this is a pure re-attribution that lets +#invoke!+
|
|
@@ -296,9 +277,8 @@ module Kobako
|
|
|
296
277
|
# +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
|
|
297
278
|
# populated by the +ensure+ readout ({#read_usage!}) on every outcome.
|
|
298
279
|
# The rescue chain is the single trap-translation boundary —
|
|
299
|
-
# configured-cap paths
|
|
300
|
-
# (
|
|
301
|
-
# surface as named TrapError subclasses; everything else surfaces as
|
|
280
|
+
# configured-cap paths surface as named TrapError subclasses
|
|
281
|
+
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
302
282
|
# the base +TrapError+.
|
|
303
283
|
def invoke!(verb)
|
|
304
284
|
begin_invocation!
|
|
@@ -307,7 +287,7 @@ module Kobako
|
|
|
307
287
|
@stderr_capture = snapshot.stderr
|
|
308
288
|
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
309
289
|
# token; restore it to the host object the guest referenced before
|
|
310
|
-
# handing the value to the Host App
|
|
290
|
+
# handing the value to the Host App. @handler still holds this
|
|
311
291
|
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
312
292
|
Codec::Utils.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
|
|
313
293
|
rescue Kobako::TrapError => e
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
# Kobako::SandboxOptions — immutable Value Object holding the four
|
|
5
|
-
# per-Sandbox configuration caps
|
|
6
|
-
# E-20}[link:../../docs/behavior.md]). Built on the +class X <
|
|
5
|
+
# per-Sandbox configuration caps. Built on the +class X <
|
|
7
6
|
# Data.define(...)+ subclass form (the Steep-friendly shape — see
|
|
8
7
|
# +lib/kobako/outcome/panic.rb+).
|
|
9
8
|
#
|
|
@@ -13,18 +12,15 @@ module Kobako
|
|
|
13
12
|
# +super+. Anything that survives +SandboxOptions.new+ is a wire-ready
|
|
14
13
|
# cap bundle the +Kobako::Runtime+ constructor consumes as-is.
|
|
15
14
|
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
|
|
16
|
-
# Default wall-clock timeout for a single invocation: 60 seconds
|
|
17
|
-
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
15
|
+
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
18
16
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
19
17
|
|
|
20
18
|
# Default cap on the per-invocation guest linear-memory delta:
|
|
21
|
-
# 1 MiB
|
|
22
|
-
#
|
|
23
|
-
# watermark sit outside this budget — see B-01 Notes.
|
|
19
|
+
# 1 MiB. The mruby image's initial allocation and prior invocations'
|
|
20
|
+
# watermark sit outside this budget.
|
|
24
21
|
DEFAULT_MEMORY_LIMIT = 1 << 20
|
|
25
22
|
|
|
26
|
-
# Default per-channel capture ceiling: 1 MiB
|
|
27
|
-
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
23
|
+
# Default per-channel capture ceiling: 1 MiB.
|
|
28
24
|
DEFAULT_OUTPUT_LIMIT = 1 << 20
|
|
29
25
|
|
|
30
26
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
data/lib/kobako/snapshot.rb
CHANGED
|
@@ -16,8 +16,7 @@ module Kobako
|
|
|
16
16
|
# Host App.
|
|
17
17
|
class Snapshot
|
|
18
18
|
# Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
|
|
19
|
-
# as a +Kobako::Capture+ value object.
|
|
20
|
-
# B-04}[link:../../docs/behavior.md] — the byte content never carries
|
|
19
|
+
# as a +Kobako::Capture+ value object. The byte content never carries
|
|
21
20
|
# a truncation sentinel; +#truncated?+ is the only way to observe
|
|
22
21
|
# that the cap was hit.
|
|
23
22
|
def stdout
|
|
@@ -31,8 +30,7 @@ module Kobako
|
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
# Wrap the per-last-invocation usage pair (+wall_time+,
|
|
34
|
-
# +memory_peak+) as a +Kobako::Usage+ value object
|
|
35
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
|
|
33
|
+
# +memory_peak+) as a +Kobako::Usage+ value object.
|
|
36
34
|
def usage
|
|
37
35
|
Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
38
36
|
end
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
5
|
# Kobako::Snippet::Binary — value object representing a single
|
|
6
|
-
# +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets
|
|
7
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
6
|
+
# +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets+.
|
|
8
7
|
#
|
|
9
8
|
# The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
|
|
10
9
|
# +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
|
|
@@ -12,7 +11,6 @@ module Kobako
|
|
|
12
11
|
# The host treats the bytes as opaque — the snippet's canonical
|
|
13
12
|
# name, when present, lives in the bytecode's embedded +debug_info+
|
|
14
13
|
# and is resolved by the guest at load time; structural validation
|
|
15
|
-
# ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
|
|
16
14
|
# is deferred to the first invocation's guest replay.
|
|
17
15
|
#
|
|
18
16
|
# The class is a +Data.define+ subclass — frozen and value-equal.
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Snippet
|
|
5
5
|
# Kobako::Snippet::Source — value object representing a single
|
|
6
|
-
# +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets
|
|
7
|
-
# ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
|
|
6
|
+
# +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets+.
|
|
8
7
|
#
|
|
9
8
|
# +name+ is the canonical +Symbol+ identity baked into the loaded
|
|
10
9
|
# IREP's +debug_info+; backtrace frames originating in this snippet
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -5,8 +5,7 @@ require_relative "snippet/source"
|
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
7
|
# Kobako::Snippet — value-object family for preloaded snippet entries
|
|
8
|
-
# held by +Kobako::Catalog::Snippets
|
|
9
|
-
# ({docs/behavior.md B-32 / B-33}[link:../../docs/behavior.md]).
|
|
8
|
+
# held by +Kobako::Catalog::Snippets+.
|
|
10
9
|
#
|
|
11
10
|
# +Source+ represents a single +#preload(code:, name:)+ entry; +Binary+
|
|
12
11
|
# represents a single +#preload(binary:)+ entry. Both are plain value
|