Class: Kobako::Sandbox

Inherits:
Object
  • Object
show all
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 reusable configuration — the Kobako::Runtime and the Kobako::Catalog::Services / Catalog::Snippets / Catalog::Extensions registries. Each #eval / #run seals that config on the first call and drives one guest invocation through a fresh Kobako::Context — its own Handle table, dispatch Proc, captures, and usage — so the reusable Sandbox holds no per-invocation state. 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.

A run's observables — the captured #stdout / #stderr (bounded by stdout_limit / stderr_limit, enforced inside the WASI pipe) with their #stdout_truncated? / #stderr_truncated? predicates, and #usage — live on the Kobako::Execution each #eval / #run returns, or on the one its raised error carries. The Sandbox itself keeps none of them, so nothing a run observes carries into the next one.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(wasm_path: nil) ⇒ Object

Every keyword beyond wasm_path forwards verbatim to SandboxOptions.new (the four caps + the profile floor).



55
56
57
58
59
60
61
62
# File 'lib/kobako/sandbox.rb', line 55

def initialize(wasm_path: nil, **)
  @wasm_path = wasm_path || Kobako::Runtime.default_path
  @options = SandboxOptions.new(**)
  @services = Kobako::Catalog::Services.new
  @snippets = Catalog::Snippets.new
  @extensions = Catalog::Extensions.new
  @runtime = build_runtime!
end

Instance Attribute Details

#optionsKobako::SandboxOptions (readonly)

Returns the value of attribute options.



35
36
37
# File 'lib/kobako/sandbox.rb', line 35

def options
  @options
end

#wasm_pathString (readonly)

Returns the value of attribute wasm_path.

Returns:

  • (String)


35
36
37
# File 'lib/kobako/sandbox.rb', line 35

def wasm_path
  @wasm_path
end

Instance Method Details

#begin_invocation!void

This method returns an undefined value.

Per-invocation prologue on the config tier: seals the Service / snippet / Extension registries on the first call (idempotent — asserting Extension dependencies then). Per-invocation provider resolution and observable state live on the Context, not here.



204
205
206
207
208
# File 'lib/kobako/sandbox.rb', line 204

def begin_invocation!
  @services.seal!
  @snippets.seal!
  @extensions.seal!
end

#bind(path, object = Unresolved) ⇒ Kobako::Sandbox

Bind object as the Service reachable at path — a Symbol or String of one or more +::+-separated constant-form segments (+"MyService::KV"+ or a top-level "File"). Returns self for chaining.

Called with only a path, it declares a fillable Service: bind(path) reserves the path for Kobako::Unresolved, so the guest sees the constant while the host defers the object it stands for. A guest dispatch to an unfilled fillable surfaces as Kobako::ServiceError when left unrescued.

Raises ArgumentError when a segment is malformed, when path collides with an existing binding (a name is a bound Service or a grouping prefix, never both), or when called after the first invocation has sealed Service registration.

Parameters:

  • path (Symbol, String)
  • object (Object) (defaults to: Unresolved)

Returns:



79
80
81
82
# File 'lib/kobako/sandbox.rb', line 79

def bind(path, object = Unresolved)
  @services.bind(path, object)
  self
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.

Returns:



183
184
185
186
187
188
189
# File 'lib/kobako/sandbox.rb', line 183

def build_runtime!
  runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
                                      @options.stdout_limit, @options.stderr_limit, @options.profile,
                                      @options.gvl)
  @options.enforce_floor!(runtime.profile)
  runtime
end

#eval(code, &block) ⇒ void

This method returns an undefined value.

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 (Service 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 #bind / #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.

Parameters:

  • code (String)

Raises:



171
172
173
174
175
# File 'lib/kobako/sandbox.rb', line 171

def eval(code, &block)
  raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)

  new_invocation.eval(code, &block)
end

#gvlSymbol

Returns:

  • (Symbol)


14
# File 'sig/kobako/sandbox.rbs', line 14

def gvl: () -> Symbol

#install(*extensions) ⇒ Kobako::Sandbox

Install one or more Extensions — each a guest idiom (+source+) paired with an optional host backend, composed onto the Sandbox through #preload and #bind. An Extension is any object exposing name / source / backend / depends_on; Kobako::Extension is the bundled value type. Returns self.

Raises ArgumentError for a malformed Extension, a call after the first invocation seals registration, or — at that first invocation — an unmet depends_on.

Parameters:

  • extensions (Object)

Returns:

Raises:

  • (ArgumentError)


93
94
95
96
97
98
# File 'lib/kobako/sandbox.rb', line 93

def install(*extensions)
  raise ArgumentError, "cannot install after first Sandbox invocation" if @services.sealed?

  extensions.each { |extension| @extensions.install(extension, snippets: @snippets, services: @services) }
  self
end

#memory_limitInteger?

Returns:

  • (Integer, nil)


10
# File 'sig/kobako/sandbox.rbs', line 10

def memory_limit: () -> Integer?

#new_invocationKobako::Context

Seal the config on the first invocation and return a fresh per-invocation Context for the verb to drive. The Context owns this run's Handle table, resolved Extension backends, captures, and usage, so no per-invocation state is written back onto the shared config.

Returns:



195
196
197
198
# File 'lib/kobako/sandbox.rb', line 195

def new_invocation
  begin_invocation!
  Context.new(runtime: @runtime, services: @services, snippets: @snippets, extensions: @extensions)
end

#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/+. Compile
failures surface as +Kobako::SandboxError+ on the first
invocation's replay. 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.

Overloads:

Raises:

  • (ArgumentError)


128
129
130
131
132
133
# File 'lib/kobako/sandbox.rb', line 128

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

#profileSymbol

Returns:

  • (Symbol)


13
# File 'sig/kobako/sandbox.rbs', line 13

def profile: () -> Symbol

#run(target, *args, **kwargs, &block) ⇒ void

This method returns an undefined value.

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.

Parameters:

  • target (Symbol, String)
  • args (Object)
  • kwargs (Object)


145
146
147
148
# File 'lib/kobako/sandbox.rb', line 145

def run(target, *args, **kwargs, &block)
  run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
  new_invocation.run(run_envelope, &block)
end

#stderr_limitInteger?

Returns:

  • (Integer, nil)


12
# File 'sig/kobako/sandbox.rbs', line 12

def stderr_limit: () -> Integer?

#stdout_limitInteger?

Returns:

  • (Integer, nil)


11
# File 'sig/kobako/sandbox.rbs', line 11

def stdout_limit: () -> Integer?

#timeoutFloat?

Forwarded to @options via Forwardable#def_delegators.

Returns:

  • (Float, nil)


9
# File 'sig/kobako/sandbox.rbs', line 9

def timeout: () -> Float?