Class: Kobako::Sandbox

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/kobako/sandbox.rb

Overview

Kobako::Sandbox — the user-facing entry point for executing guest mruby scripts inside a wasmtime-hosted Wasm module (docs/behavior.md B-01).

The Sandbox owns the Kobako::Wasm::Instance, the per-Sandbox Kobako::HandleTable (docs/behavior.md B-19), the per-instance RPC Server (which receives the HandleTable by injection so guest→host dispatch and host→guest auto-wrap share one allocator), and the per-channel byte caches for guest stdout / stderr capture. 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 (docs/behavior.md B-04): 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

Instance Method Summary collapse

Constructor Details

#initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil, timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS, memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT) ⇒ Sandbox

Build a fresh Sandbox.

wasm_path is the absolute path to the Guest Binary; defaults to the gem-bundled data/kobako.wasm. The four caps (stdout_limit, stderr_limit, timeout, memory_limit) are forwarded verbatim to Kobako::SandboxOptions, which owns their DEFAULT fallback and normalisation. The constructed SandboxOptions is exposed as #options and the four caps remain readable directly on Sandbox via Forwardable delegation.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/kobako/sandbox.rb', line 99

def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
               timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
               memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
  @wasm_path = wasm_path || Kobako::Wasm.default_path
  @options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
                                stderr_limit: stderr_limit)
  @handle_table = HandleTable.new
  @services = Kobako::RPC::Server.new(handle_table: @handle_table)
  @snippets = Snippet::Table.new
  @instance = Kobako::Wasm::Instance.from_path(@wasm_path, @options.timeout, @options.memory_limit,
                                               @options.stdout_limit, @options.stderr_limit)
  @instance.server = @services
  reset_invocation_state!
end

Instance Attribute Details

#instanceObject (readonly)

Returns the value of attribute instance.



41
42
43
# File 'lib/kobako/sandbox.rb', line 41

def instance
  @instance
end

#optionsObject (readonly)

Returns the value of attribute options.



41
42
43
# File 'lib/kobako/sandbox.rb', line 41

def options
  @options
end

#servicesObject (readonly)

Returns the value of attribute services.



41
42
43
# File 'lib/kobako/sandbox.rb', line 41

def services
  @services
end

#snippetsObject (readonly)

Returns the value of attribute snippets.



41
42
43
# File 'lib/kobako/sandbox.rb', line 41

def snippets
  @snippets
end

#usageObject (readonly)

Returns the Kobako::Usage value object for the most recent invocation (docs/behavior.md B-35). 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.



88
89
90
# File 'lib/kobako/sandbox.rb', line 88

def usage
  @usage
end

#wasm_pathObject (readonly)

Returns the value of attribute wasm_path.



41
42
43
# File 'lib/kobako/sandbox.rb', line 41

def wasm_path
  @wasm_path
end

Instance Method Details

#define(name) ⇒ Object

Declare or retrieve the Namespace named name on this Sandbox (docs/behavior.md B-07, B-09, B-10). name must be a Symbol or String in constant form. Returns the Kobako::RPC::Namespace.

Raises ArgumentError when called after the first invocation, or when name does not match the constant-name pattern.



120
121
122
# File 'lib/kobako/sandbox.rb', line 120

def define(name)
  @services.define(name)
end

#eval(code) ⇒ Object

Execute a guest mruby source string in a fresh mrb_state (docs/behavior.md B-02 / B-03 / B-06). 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 (B-32). 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 (docs/behavior.md B-07 / B-33); 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 — E-36); Kobako::ServiceError on an unrescued Service capability failure.

Raises:



200
201
202
203
204
205
206
# File 'lib/kobako/sandbox.rb', line 200

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

  invoke!(:eval) do
    @instance.eval(@services.encoded_preamble, code.b, @snippets.encode)
  end
end

#preload(code: nil, name: nil, binary: nil) ⇒ Object

Register a snippet on this Sandbox in one of two forms (docs/behavior.md B-32):

* +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 for E-33.
* +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
  ({docs/behavior.md E-37 / E-38}[link:../../docs/behavior.md])
  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 (docs/behavior.md E-34), when name duplicates an already-registered code: form snippet (docs/behavior.md E-33), or when called after the first invocation (docs/behavior.md E-35, B-33).

Raises:

  • (ArgumentError)


154
155
156
157
158
159
# File 'lib/kobako/sandbox.rb', line 154

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

#run(target, *args, **kwargs) ⇒ Object

Dispatch into a preloaded entrypoint constant (docs/behavior.md B-31). Delegates host pre-flight (E-24 / E-25 / E-29 / E-30) and wire encoding to Kobako::Invocation / Kobako::Invocation#encode; 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 (B-07 / B-33). Runtime errors follow the same three-class taxonomy as #eval.



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

def run(target, *args, **kwargs)
  invocation = Invocation.new(entrypoint: target, args: args, kwargs: kwargs)
  invoke!(:run) do
    @instance.run(@services.encoded_preamble, @snippets.encode, invocation.encode(@handle_table))
  end
end

#stderrObject

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.



61
62
63
# File 'lib/kobako/sandbox.rb', line 61

def stderr
  @stderr_capture.bytes
end

#stderr_truncated?Boolean

Returns true iff stderr capture during the most recent invocation exceeded stderr_limit. Mirror of #stdout_truncated?.

Returns:

  • (Boolean)


75
76
77
# File 'lib/kobako/sandbox.rb', line 75

def stderr_truncated?
  @stderr_capture.truncated?
end

#stdoutObject

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. docs/behavior.md B-04 — the byte content never contains a truncation sentinel; use #stdout_truncated? to observe overflow.



54
55
56
# File 'lib/kobako/sandbox.rb', line 54

def stdout
  @stdout_capture.bytes
end

#stdout_truncated?Boolean

Returns true iff stdout capture during the most recent invocation exceeded stdout_limit (docs/behavior.md B-04). Resets to false at the start of the next invocation (docs/behavior.md B-03).

Returns:

  • (Boolean)


69
70
71
# File 'lib/kobako/sandbox.rb', line 69

def stdout_truncated?
  @stdout_capture.truncated?
end