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-instance RPC Server (which itself owns the per-run HandleTable), 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.



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/kobako/sandbox.rb', line 84

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)
  @services = Kobako::RPC::Server.new
  @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
  clear_captures!
end

Instance Attribute Details

#instanceObject (readonly)

Returns the value of attribute instance.



37
38
39
# File 'lib/kobako/sandbox.rb', line 37

def instance
  @instance
end

#optionsObject (readonly)

Returns the value of attribute options.



37
38
39
# File 'lib/kobako/sandbox.rb', line 37

def options
  @options
end

#servicesObject (readonly)

Returns the value of attribute services.



37
38
39
# File 'lib/kobako/sandbox.rb', line 37

def services
  @services
end

#snippetsObject (readonly)

Returns the value of attribute snippets.



37
38
39
# File 'lib/kobako/sandbox.rb', line 37

def snippets
  @snippets
end

#wasm_pathObject (readonly)

Returns the value of attribute wasm_path.



37
38
39
# File 'lib/kobako/sandbox.rb', line 37

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.



104
105
106
# File 'lib/kobako/sandbox.rb', line 104

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:



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

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)


138
139
140
141
142
143
# File 'lib/kobako/sandbox.rb', line 138

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.



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

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)
  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.



57
58
59
# File 'lib/kobako/sandbox.rb', line 57

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)


71
72
73
# File 'lib/kobako/sandbox.rb', line 71

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.



50
51
52
# File 'lib/kobako/sandbox.rb', line 50

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)


65
66
67
# File 'lib/kobako/sandbox.rb', line 65

def stdout_truncated?
  @stdout_capture.truncated?
end