Class: Kobako::Sandbox
- Inherits:
-
Object
- Object
- Kobako::Sandbox
- Extended by:
- Forwardable
- Defined in:
- lib/kobako/sandbox.rb,
sig/kobako/sandbox.rbs
Overview
Kobako::Sandbox — the user-facing entry point for executing guest mruby scripts inside a wasmtime-hosted Wasm module.
The Sandbox owns the Kobako::Runtime, the per-Sandbox
Kobako::Catalog::Handles, the per-instance
Kobako::Catalog::Namespaces (which receives the Catalog::Handles by
injection so guest→host dispatch and host→guest auto-wrap share one
allocator), and the dispatch Proc / yield_to_guest lambda installed
on the Runtime via Runtime#on_dispatch=. The underlying wasmtime Engine
and compiled Module are cached at process scope by the native ext and
never surface to Ruby — constructing many Sandboxes amortises both costs
automatically.
Output capture policy: the
per-channel cap (+stdout_limit+ / stderr_limit) is enforced inside the
WASI pipe — the host buffer stops growing at the cap, subsequent guest
writes on that channel fail or are dropped, and #run still returns
normally. #stdout / #stderr return the captured prefix as a UTF-8
String; the byte content never carries a truncation sentinel.
#stdout_truncated? / #stderr_truncated? are the only way to observe
that the cap was hit.
Instance Attribute Summary collapse
-
#options ⇒ Kobako::SandboxOptions
readonly
Returns the value of attribute options.
-
#usage ⇒ Kobako::Usage
readonly
Returns the
Kobako::Usagevalue object for the most recent invocation. -
#wasm_path ⇒ String
readonly
Returns the value of attribute wasm_path.
Instance Method Summary collapse
-
#begin_invocation! ⇒ void
Per-invocation prologue.
-
#build_runtime! ⇒ Kobako::Runtime
Construct the
Runtimewith the requested isolation profile and refuse one whose declared posture falls below the request —SandboxOptions#enforce_floor!owns the ladder comparison, so a runtime that cannot honor the request never runs guest code. -
#define(name) ⇒ Kobako::Namespace
Declare or retrieve the Namespace named
nameon this Sandbox. -
#eval(code) ⇒ Object
Execute a guest mruby source string in a fresh
mrb_state. -
#initialize(wasm_path: nil) ⇒ Object
constructor
Every keyword beyond wasm_path forwards verbatim to SandboxOptions.new (the four caps + the profile floor).
-
#install_dispatch_proc! ⇒ void
Configure the +Runtime+'s host↔guest dispatch wiring.
-
#invoke!(verb) { ... } ⇒ Object
Shared prologue / epilogue + trap-class translator for both invocation verbs.
- #memory_limit ⇒ Integer?
-
#preload(code: nil, name: nil, binary: nil) ⇒ Object
Register a snippet on this Sandbox in one of two forms:.
- #profile ⇒ Symbol
-
#read_captures! ⇒ void
Read the per-last-invocation output captures from the ext and wrap them as
Kobako::Capturevalue objects. -
#read_usage! ⇒ void
Read the per-last-invocation
wall_timeandmemory_peakfrom the ext and wrap them as aKobako::Usagevalue object. -
#reset_invocation_state! ⇒ void
Reset all per-invocation observable state to its pre-invocation sentinels — both per-channel captures and the per-last-invocation usage record.
-
#run(target, *args, **kwargs) ⇒ Object
Dispatch into a preloaded entrypoint constant.
-
#stderr ⇒ String
Returns the bytes the guest wrote to stderr during the most recent invocation as a UTF-8 String, clipped at
stderr_limit. - #stderr_limit ⇒ Integer?
-
#stderr_truncated? ⇒ Boolean
Returns
trueiff stderr capture during the most recent invocation exceededstderr_limit. -
#stdout ⇒ String
Returns the bytes the guest wrote to stdout during the most recent invocation as a UTF-8 String, clipped at
stdout_limit. - #stdout_limit ⇒ Integer?
-
#stdout_truncated? ⇒ Boolean
Returns
trueiff stdout capture during the most recent invocation exceededstdout_limit. -
#timeout ⇒ Float?
Forwarded to @options via Forwardable#def_delegators.
-
#trap_class_for(err) ⇒ singleton(Kobako::TrapError)
Pick the
TrapErrorsubclass to re-raise based on +err+'s actual class.
Constructor Details
#initialize(wasm_path: nil) ⇒ Object
Every keyword beyond wasm_path forwards verbatim to SandboxOptions.new (the four caps + the profile floor).
99 100 101 102 103 104 105 106 107 108 |
# File 'lib/kobako/sandbox.rb', line 99 def initialize(wasm_path: nil, **) @wasm_path = wasm_path || Kobako::Runtime.default_path @options = SandboxOptions.new(**) @handler = Catalog::Handles.new @services = Kobako::Catalog::Namespaces.new(handler: @handler) @snippets = Catalog::Snippets.new @runtime = build_runtime! install_dispatch_proc! reset_invocation_state! end |
Instance Attribute Details
#options ⇒ Kobako::SandboxOptions (readonly)
Returns the value of attribute options.
39 40 41 |
# File 'lib/kobako/sandbox.rb', line 39 def @options end |
#usage ⇒ Kobako::Usage (readonly)
Returns the Kobako::Usage value object for the most recent
invocation. Carries wall_time (Float seconds the guest export call spent
inside wasmtime) and memory_peak (Integer bytes, high-water of
the per-invocation memory.grow delta past the entry-time
baseline). Returns Kobako::Usage::EMPTY before any invocation;
populated on every outcome — including TrapError — so the Host
App can read it after rescuing a trap to diagnose budget
consumption.
83 84 85 |
# File 'lib/kobako/sandbox.rb', line 83 def usage @usage end |
#wasm_path ⇒ String (readonly)
Returns the value of attribute wasm_path.
39 40 41 |
# File 'lib/kobako/sandbox.rb', line 39 def wasm_path @wasm_path end |
Instance Method Details
#begin_invocation! ⇒ void
This method returns an undefined value.
Per-invocation prologue. Seals the Service / snippet registries on
first call (idempotent) and zeros the per-invocation capability
state — capture buffers, truncation predicates, and the
Catalog::Handles counter — before the guest runs. The
Catalog::Handles itself is held as @handler and never exposed
beyond this class — it is not part of the Host App's surface.
244 245 246 247 248 |
# File 'lib/kobako/sandbox.rb', line 244 def begin_invocation! @services.seal! @handler.reset! reset_invocation_state! end |
#build_runtime! ⇒ Kobako::Runtime
Construct the Runtime with the requested isolation profile and
refuse one whose declared posture falls below the request —
SandboxOptions#enforce_floor! owns the ladder comparison, so a
runtime that cannot honor the request never runs guest code.
217 218 219 220 221 222 |
# File 'lib/kobako/sandbox.rb', line 217 def build_runtime! runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit, @options.stdout_limit, @options.stderr_limit, profile) @options.enforce_floor!(runtime.profile) runtime end |
#define(name) ⇒ Kobako::Namespace
Declare or retrieve the Namespace named name on this Sandbox. name
must be a Symbol or String in constant form. Returns the
Kobako::Namespace.
Raises ArgumentError when called after the first invocation, or
when name does not match the constant-name pattern.
116 117 118 |
# File 'lib/kobako/sandbox.rb', line 116 def define(name) @services.define(name) end |
#eval(code) ⇒ Object
Execute a guest mruby source string in a fresh mrb_state. code is
the mruby source as a UTF-8 String. Returns the deserialized last
expression of the source.
Source delivery uses the WASI stdin three-frame protocol
(docs/wire-codec.md Invocation channels):
Frame 1 carries the msgpack-encoded preamble (Namespace / Member
registry snapshot), Frame 2 carries the user source UTF-8 bytes, and
Frame 3 carries the snippet table registered via #preload.
Each frame is prefixed by a 4-byte big-endian u32 length; Frame 3 is
mandatory-presence — an empty snippet table sends an empty msgpack
array, never an absent frame.
The first invocation seals the Service registry and snippet table;
subsequent #define / #preload calls raise ArgumentError.
Raises Kobako::TrapError on a Wasm trap or wire-violation fallback;
Kobako::SandboxError when the guest ran to completion but failed
(including when code is nil or not a String, or when a preloaded
snippet's replay raises); Kobako::ServiceError on an unrescued
Service capability failure.
191 192 193 194 195 196 197 |
# File 'lib/kobako/sandbox.rb', line 191 def eval(code) raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String) invoke!(:eval) do @runtime.eval(@services.encode, code.b, @snippets.encode) end end |
#install_dispatch_proc! ⇒ void
This method returns an undefined value.
Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
dispatch Proc that routes guest→host calls through the stateless
Transport::Dispatcher, capturing @services / @handler in the
closure. The ext hands the Proc a per-dispatch guest_yielder — a
String → String callable that re-enters the in-flight guest to run a
yielded block — which the Dispatcher forwards to the Transport::Yielder
it builds for the call. Registered once at construction time so the
wasm ext callback can fire without further setup.
232 233 234 235 236 |
# File 'lib/kobako/sandbox.rb', line 232 def install_dispatch_proc! @runtime.on_dispatch = lambda do |request_bytes, guest_yielder| Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder) end end |
#invoke!(verb) { ... } ⇒ Object
Shared prologue / epilogue + trap-class translator for both
invocation verbs. verb is :eval or :run; it tags the
TrapError message so the failing export is identifiable.
The yielded block must return the invocation's raw outcome bytes —
i.e. the value of Runtime#eval / #run — which the success path
feeds to Outcome.decode. Captures and usage are populated by the
ensure readouts (#read_usage! / #read_captures!) on every
outcome, so #stdout / #stderr / #usage stay readable after a
rescued trap.
The rescue chain is the single trap-translation boundary —
configured-cap paths surface as named TrapError subclasses
(+TimeoutError+ / MemoryLimitError); everything else surfaces as
the base TrapError.
316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/kobako/sandbox.rb', line 316 def invoke!(verb) begin_invocation! return_bytes = yield # A Capability Handle in the result is decoded as a Kobako::Handle # token; restore it to the host object the guest referenced before # handing the value to the Host App. @handler still holds this # invocation's table — reset only happens at the next #begin_invocation!. Codec::HandleWalk.deep_restore(Outcome.decode(return_bytes), @handler) rescue Kobako::TrapError => e raise trap_class_for(e), "Sandbox##{verb} failed: #{e.}" ensure read_usage! read_captures! end |
#memory_limit ⇒ Integer?
10 |
# File 'sig/kobako/sandbox.rbs', line 10
def memory_limit: () -> Integer?
|
#preload(code:, name:) ⇒ Kobako::Sandbox #preload(binary:) ⇒ Kobako::Sandbox
Register a snippet on this Sandbox in one of two forms:
* +preload(code: source, name: Name)+ — +source+ is mruby source
as a +String+ and +Name+ matches +/\A[A-Z]\w*\z/+. The +name+
becomes the snippet's +(snippet:Name)+ backtrace filename and
is the dedupe key that rejects a duplicate +code:+ snippet.
* +preload(binary: bytes)+ — +bytes+ is precompiled RITE
bytecode as a +String+. The canonical name, when present,
lives in the bytecode's embedded +debug_info+ and is resolved
by the guest at load time; the host treats the bytes as
opaque. Structural failures surface as +Kobako::BytecodeError+
on the first invocation.
Subsequent invocations (+#eval+ or #run) replay every registered
snippet — in insertion order — against the fresh mrb_state
before per-invocation source or entrypoint resolution.
Returns self to allow chaining.
Raises ArgumentError when neither form's keyword set is
supplied, when both forms are mixed (e.g., code: and binary:
together, or binary: paired with name:), when code / bytes
is not a String, when name does not match the constant
pattern, when name duplicates an already-registered code: form
snippet, or when called after the first invocation has sealed the
snippet table.
146 147 148 149 150 151 |
# File 'lib/kobako/sandbox.rb', line 146 def preload(code: nil, name: nil, binary: nil) raise ArgumentError, "cannot preload after first Sandbox invocation" if @services.sealed? @snippets.register(code: code, name: name, binary: binary) self end |
#profile ⇒ Symbol
13 |
# File 'sig/kobako/sandbox.rbs', line 13
def profile: () -> Symbol
|
#read_captures! ⇒ void
This method returns an undefined value.
Read the per-last-invocation output captures from the ext and wrap
them as Kobako::Capture value objects. Runs in the invoke!
ensure block next to #read_usage! for the same reason: the ext
stashes the captures on every outcome, so the readout also covers
the trap path, where Runtime#eval / #run raise instead of
returning outcome bytes — #stdout / #stderr keep the guest's
partial output readable after a rescue.
The ext returns a positional 4-tuple
[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated];
the destructure-then-kwargs handoff below is the explicit
positional→keyword conversion point.
296 297 298 299 300 |
# File 'lib/kobako/sandbox.rb', line 296 def read_captures! stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures @stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated) @stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated) end |
#read_usage! ⇒ void
This method returns an undefined value.
Read the per-last-invocation wall_time and memory_peak from
the ext and wrap them as a Kobako::Usage value object. Runs in
the invoke! ensure block so the usage record is populated on
every outcome — value return, Kobako::TrapError (including
TimeoutError / MemoryLimitError), Kobako::SandboxError,
and Kobako::ServiceError. Runtime#usage is the single source for
both paths: the figures are stashed in the ext on every outcome, so
the readout here also covers the trap path, where Runtime#eval /
#run raise instead of returning outcome bytes.
The ext returns a positional 2-tuple [wall_time, memory_peak]
whose order matches the Kobako::Usage field order; the
destructure-then-kwargs handoff below is the explicit
positional→keyword conversion point.
264 265 266 267 |
# File 'lib/kobako/sandbox.rb', line 264 def read_usage! wall_time, memory_peak = @runtime.usage @usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak) end |
#reset_invocation_state! ⇒ void
This method returns an undefined value.
Reset all per-invocation observable state to its pre-invocation
sentinels — both per-channel captures and the per-last-invocation
usage record. Shared by #initialize (first-time setup) and
#begin_invocation! (between-invocation reset) so both paths agree on
what "pre-invocation state" means; Kobako::Pool calls it at checkout
so a pooled Sandbox hands over empty output buffers.
205 206 207 208 209 |
# File 'lib/kobako/sandbox.rb', line 205 def reset_invocation_state! @stdout_capture = Capture::EMPTY @stderr_capture = Capture::EMPTY @usage = Usage::EMPTY end |
#run(target, *args, **kwargs) ⇒ Object
Dispatch into a preloaded entrypoint constant. Delegates host
pre-flight and wire encoding to Kobako::Transport::Run /
Kobako::Transport::Run#encode: a non-Symbol/String target raises
TypeError, while a target failing the constant pattern, a forged
Kobako::Handle in args / kwargs, or a non-Symbol kwargs key
raise ArgumentError. The guest resolves target as a top-level
constant, calls #call on it with args / kwargs, and returns the
deserialized result. The first invocation seals the Service registry
and snippet table. Runtime errors follow the same three-class
taxonomy as #eval.
163 164 165 166 167 168 |
# File 'lib/kobako/sandbox.rb', line 163 def run(target, *args, **kwargs) run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs) invoke!(:run) do @runtime.run(@services.encode, @snippets.encode, run_envelope.encode(@handler)) end end |
#stderr ⇒ String
Returns the bytes the guest wrote to stderr during the most recent
invocation as a UTF-8 String, clipped at stderr_limit. Empty before
any invocation. Mirror of #stdout.
58 59 60 |
# File 'lib/kobako/sandbox.rb', line 58 def stderr @stderr_capture.bytes end |
#stderr_limit ⇒ Integer?
12 |
# File 'sig/kobako/sandbox.rbs', line 12
def stderr_limit: () -> Integer?
|
#stderr_truncated? ⇒ Boolean
Returns true iff stderr capture during the most recent invocation
exceeded stderr_limit. Mirror of #stdout_truncated?.
71 72 73 |
# File 'lib/kobako/sandbox.rb', line 71 def stderr_truncated? @stderr_capture.truncated? end |
#stdout ⇒ String
Returns the bytes the guest wrote to stdout during the most recent
invocation as a UTF-8 String, clipped at stdout_limit. Empty before
any invocation; the byte content never contains a truncation sentinel,
so use #stdout_truncated? to observe overflow. Populated on every
outcome — including a rescued TrapError, after which it holds the
bytes written before the trap fired — mirroring #usage.
51 52 53 |
# File 'lib/kobako/sandbox.rb', line 51 def stdout @stdout_capture.bytes end |
#stdout_limit ⇒ Integer?
11 |
# File 'sig/kobako/sandbox.rbs', line 11
def stdout_limit: () -> Integer?
|
#stdout_truncated? ⇒ Boolean
Returns true iff stdout capture during the most recent invocation
exceeded stdout_limit. Resets to false at the start of the next
invocation.
65 66 67 |
# File 'lib/kobako/sandbox.rb', line 65 def stdout_truncated? @stdout_capture.truncated? end |
#timeout ⇒ Float?
Forwarded to @options via Forwardable#def_delegators.
9 |
# File 'sig/kobako/sandbox.rbs', line 9
def timeout: () -> Float?
|
#trap_class_for(err) ⇒ singleton(Kobako::TrapError)
Pick the TrapError subclass to re-raise based on +err+'s actual
class. Cap-trap subclasses (+TimeoutError+ / MemoryLimitError)
preserve their named identity; everything else collapses to the
base Kobako::TrapError. The ext already raises the right subclass
directly, so this is a pure re-attribution that lets #invoke!
add the verb prefix without erasing TimeoutError /
MemoryLimitError.
276 277 278 279 280 281 282 |
# File 'lib/kobako/sandbox.rb', line 276 def trap_class_for(err) case err when TimeoutError then TimeoutError when MemoryLimitError then MemoryLimitError else TrapError end end |