{"id":50850798,"url":"https://github.com/elct9620/kobako","last_synced_at":"2026-07-11T18:00:29.531Z","repository":{"id":357760024,"uuid":"1237782053","full_name":"elct9620/kobako","owner":"elct9620","description":"Embeddable Wasm sandbox for running untrusted mruby code from Ruby applications.","archived":false,"fork":false,"pushed_at":"2026-07-06T14:37:20.000Z","size":4057,"stargazers_count":9,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-06T15:07:37.178Z","etag":null,"topics":["mruby","ruby","ruby-gem","sandbox","wasm"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elct9620.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-13T13:59:36.000Z","updated_at":"2026-07-06T14:35:14.000Z","dependencies_parsed_at":"2026-06-14T14:01:46.242Z","dependency_job_id":null,"html_url":"https://github.com/elct9620/kobako","commit_stats":null,"previous_names":["elct9620/kobako"],"tags_count":74,"template":false,"template_full_name":null,"purl":"pkg:github/elct9620/kobako","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elct9620%2Fkobako","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elct9620%2Fkobako/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elct9620%2Fkobako/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elct9620%2Fkobako/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elct9620","download_url":"https://codeload.github.com/elct9620/kobako/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elct9620%2Fkobako/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35370428,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mruby","ruby","ruby-gem","sandbox","wasm"],"created_at":"2026-06-14T14:00:28.461Z","updated_at":"2026-07-11T18:00:29.522Z","avatar_url":"https://github.com/elct9620.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kobako\n\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/elct9620/kobako)\n\nKobako is a Ruby gem that embeds a Wasm-isolated mruby interpreter inside your application, so you can execute untrusted Ruby scripts (LLM-generated code, user formulas, student submissions, third-party plugins) in-process without giving them access to host memory, files, network, or credentials. Its home is the Ruby ecosystem; a Rust SDK offers the same sandbox to hosts written in other languages.\n\nThe host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby and a Transport proxy. The only way a guest script can reach the outside world is through Host App-declared **Services** — named host objects you explicitly inject into the sandbox; the guest sees each one as a proxy that forwards calls back to the host over the Transport wire.\n\n```\n        Host process                       Wasm guest\n   ┌──────────────────────┐         ┌──────────────────────┐\n   │  Kobako::Sandbox     │ ─eval─▶ │  mruby interpreter   │\n   │                      │ ─run──▶ │                      │\n   │  Services            │ ◀─call─ │  KV::Lookup.call(k)  │\n   │   KV::Lookup         │ ─resp─▶ │                      │\n   │                      │         │                      │\n   │  stdout / stderr buf │ ◀─pipe─ │  puts / warn         │\n   │                      │         │                      │\n   │  return value        │ ◀─last─ │  last expression     │\n   └──────────────────────┘         └──────────────────────┘\n            trusted                       untrusted\n```\n\n## Requirements\n\n- **Ruby ≥ 3.3.0**\n- **Rust / Cargo** at install time — the native extension compiles from source via `rb_sys`\n- **Linux** or **macOS** — Windows is not supported\n\nThe precompiled `kobako.wasm` Guest Binary ships inside the gem, so end users do **not** need a WASI toolchain. (The toolchain is only required if you build the gem from a source checkout — see [Development](#development).)\n\n## Installation\n\n```bash\nbundle add kobako\n# or\ngem install kobako\n```\n\n## Quick Start\n\nThe same untrusted script runs identically from either host frontend. It executes inside the Wasm guest and cannot read your filesystem, open sockets, or touch your environment.\n\n```ruby\nrequire \"kobako\"\n\nsandbox = Kobako::Sandbox.new\nsandbox.eval(\"1 + 2\")  # =\u003e 3\n```\n\n```rust\nuse kobako::{Options, Sandbox};\n\n// Options::default() is secure by default: no caps, hermetic isolation.\nlet mut sandbox = Sandbox::new(\"kobako.wasm\", Options::default())?;\nsandbox.eval(\"1 + 2\")?;  // =\u003e Value::Int(3)\n```\n\nThe gem bundles its Guest Binary; a Rust host loads one explicitly — see [Frontends \u0026 Guest Binaries](#frontends--guest-binaries) for the packages and the Guest Binary variants.\n\n## Frontends \u0026 Guest Binaries\n\nEmbedding kobako is two independent choices: the **host frontend** you build against, and the **Guest Binary** it runs. They compose freely — any frontend loads any Guest Binary, so a Ruby host can run a JSON-enabled guest and a Rust host can run the pure default.\n\n### Host frontends\n\nThe host embeds the sandbox and owns the SPEC wire codec. Choose by your host language and how much of the contract you want pre-assembled.\n\n| Frontend | Package | Add it | Best for |\n|----------|---------|--------|----------|\n| Ruby gem | `kobako` (RubyGems) | `gem install kobako` | A Ruby host — Services, Handles, snippets, and pooling out of the box |\n| Rust SDK | `kobako` (crates.io) | `kobako = \"0.9\"` | A Rust host — the same behavior contract behind an idiomatic Rust API |\n| Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-codec` | Cargo deps | A custom host, or driving the wire directly in another language |\n\nThe Rust crates are documented on [crates.io](https://crates.io/crates/kobako); the Ruby gem is this README. Two runnable Rust hosts show the choice: [`plugin-rs`](examples/plugin-rs) builds on the SDK, and [`wire-rs`](examples/wire-rs) assembles a host by hand on the low-level crates.\n\n### Pre-built Guest Binaries\n\nThe gem bundles the pure `kobako.wasm`. Regexp and JSON are opt-in capabilities compiled into separate variants, each attached to every [GitHub Release](https://github.com/elct9620/kobako/releases). Download a variant and point your host at it — `Sandbox.new(wasm_path:)` in Ruby, `Sandbox::new(path, ...)` in Rust.\n\n| Variant | File | Adds | Distribution |\n|---------|------|------|--------------|\n| Pure (default) | `kobako.wasm` | mruby + IO | Bundled in the gem, and a Release asset |\n| +regexp | `kobako+regexp.wasm` | Regexp / MatchData (ASCII) | Release asset |\n| +regexp-unicode | `kobako+regexp-unicode.wasm` | Regexp / MatchData (Unicode) | Release asset |\n| +json | `kobako+json.wasm` | JSON | Release asset |\n| +full | `kobako+full.wasm` | JSON + Regexp (ASCII) | Release asset |\n\n```ruby\nsandbox = Kobako::Sandbox.new(wasm_path: \"kobako+full.wasm\")\nsandbox.eval('JSON.generate({ n: \"42\".to_i })')  # =\u003e \"{\\\"n\\\":42}\"\n```\n\n### Custom Guest Binaries\n\nWhen no pre-built variant matches your capability set, assemble a guest in Rust. `kobako-mruby` is the harness; its `init_gems` hook is where you install exactly the capability gems you want — the shipped `kobako-io` / `kobako-regexp` / `kobako-json`, or your own `beni::Gem`. `kobako-core`'s `export_guest!` emits the wasm ABI. `wasm/kobako-wasm/src/guest.rs` is the worked example.\n\n| Guest crate | Role |\n|-------------|------|\n| `kobako-mruby` | mruby guest harness — the `MrbGuest` trait and provided flows |\n| `kobako-core` | Guest ABI contract — the `Guest` trait and the `export_guest!` macro |\n| `kobako-io` | IO / Kernel capability gem |\n| `kobako-regexp` | Regexp / MatchData capability gem |\n| `kobako-json` | JSON capability gem |\n\n```rust\nuse beni::{Error, Mrb};\n\nstruct MyGuest;\n\n// Pick the capability gems the guest exposes.\nimpl kobako_mruby::MrbGuest for MyGuest {\n    fn init_gems(mrb: \u0026Mrb) -\u003e Result\u003c(), Error\u003e {\n        mrb.init_gem::\u003ckobako_io::KobakoIo\u003e()?;\n        mrb.init_gem::\u003ckobako_json::KobakoJson\u003e()?;\n        Ok(())\n    }\n}\n\n// Forward the ABI contract to the harness flows.\nimpl kobako_core::Guest for MyGuest {\n    fn eval() { \u003cMyGuest as kobako_mruby::MrbGuest\u003e::eval() }\n    fn run(env: \u0026[u8]) { \u003cMyGuest as kobako_mruby::MrbGuest\u003e::run(env) }\n    fn yield_to_block(req: \u0026[u8]) -\u003e u64 { \u003cMyGuest as kobako_mruby::MrbGuest\u003e::yield_to_block(req) }\n}\n\nkobako_core::export_guest!(MyGuest);\n```\n\nBuild the crate as a `cdylib` for `wasm32-wasip1`, then bake the canonical boot state into the artifact (see [`CLAUDE.md`](CLAUDE.md) § Build Pipeline).\n\n## Glossary\n\n| Term | Meaning |\n|------|---------|\n| Sandbox | The runtime unit (`Kobako::Sandbox`) that runs guest code and returns a result or raises a typed error. |\n| Service | A host object bound at a constant-path name (`MyService::KV`) — the guest's only path to host resources. |\n| Invocation | One `#eval` or `#run`; capability state resets between invocations. |\n| Snippet | Named mruby code (source or bytecode) replayed into a fresh state before every invocation. |\n| Handle | An opaque token the guest holds for a host object the wire cannot transmit directly. |\n| Block | A guest mruby block passed to a Service; each `yield` is a synchronous round-trip into the guest. |\n\n## Usage\n\n### Services\n\n`bind` any Ruby object as a Service at a constant-path name; the guest reaches it as a `MyService::KV` (or top-level `File`) proxy and invokes its public methods through the Transport wire. See [`docs/behavior/registration.md`](docs/behavior/registration.md) B-08..B-12.\n\n```ruby\nclass User\n  attr_reader :name\n\n  def initialize(name:)\n    @name = name\n  end\nend\n\nsandbox.bind(\"Project::User\", User.new(name: \"alice\"))\nsandbox.bind(\"KV::Lookup\",     -\u003e(key) { redis.get(key) })\n\nsandbox.eval(\u003c\u003c~RUBY)\n  Project::User.name         # =\u003e \"alice\"\n  KV::Lookup.call(\"user_42\") # =\u003e \"...\"\nRUBY\n```\n\nEach `::`-separated path segment must match `/\\A[A-Z]\\w*\\z/`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation (see [Invocation Lifecycle](#invocation-lifecycle)); later `#bind` raises `ArgumentError`.\n\n### Output Capture\n\nGuest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and exposed independently of the return value ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-04). Buffers clear at the start of each invocation; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.\n\n```ruby\nresult = sandbox.eval(\u003c\u003c~RUBY)\n  puts \"hello\"\n  warn \"be careful\"\n  42\nRUBY\n\nresult          # =\u003e 42\nsandbox.stdout  # =\u003e \"hello\\n\"\nsandbox.stderr  # =\u003e \"be careful\\n\"\n```\n\n### Error Handling\n\nEvery invocation either returns a value or raises exactly one of three classes, so you can route faults without inspecting messages. The full taxonomy lives in [`lib/kobako/errors.rb`](lib/kobako/errors.rb).\n\n```ruby\nbegin\n  sandbox.eval(script)\nrescue Kobako::TrapError\n  # Wasm engine fault or cap exhaustion. Discard the Sandbox.\nrescue Kobako::ServiceError\n  # A host Service call failed and the script did not rescue it.\nrescue Kobako::SandboxError\n  # The script raised, failed to compile, or returned an unrepresentable value.\nend\n```\n\n| Class                           | Parent         | Trigger                                              |\n|---------------------------------|----------------|------------------------------------------------------|\n| `Kobako::TimeoutError`          | `TrapError`    | Per-invocation `timeout` exhausted                   |\n| `Kobako::MemoryLimitError`      | `TrapError`    | Per-invocation `memory_limit` exhausted              |\n| `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap               |\n| `Kobako::BytecodeError`         | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |\n\n`SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` / `details` fields when the guest produced a panic envelope.\n\n### Resource Limits\n\nEach invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Sandbox#usage`](lib/kobako/sandbox.rb) after the call — populated on every outcome including traps — for actual consumption ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-35).\n\n```ruby\nsandbox = Kobako::Sandbox.new(\n  timeout:      5.0,              # seconds, default 60.0\n  memory_limit: 10 * 1024 * 1024, # bytes,   default 1 MiB\n  stdout_limit: 64 * 1024,        # bytes,   default 1 MiB\n  stderr_limit: 64 * 1024\n)\n```\n\n| Cap            | Raises                     | Default |\n|----------------|----------------------------|---------|\n| `timeout`      | `Kobako::TimeoutError`     | 60.0 s  |\n| `memory_limit` | `Kobako::MemoryLimitError` | 1 MiB   |\n| `stdout_limit` | output clipped (no raise)  | 1 MiB   |\n| `stderr_limit` | output clipped (no raise)  | 1 MiB   |\n\n`memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget.\n\nA fifth option, `profile:`, requests the Sandbox's isolation posture on the `:permissive` \u003c `:hermetic` ladder (default `:hermetic`). `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources — an explicit trade of reproducibility, with filesystem, environment, and network still unreachable. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/security-model.md`](docs/security-model.md) § Isolation profiles.\n\n### Invocation Lifecycle\n\nOne Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; capability state (Handles, stdout, stderr, memory delta) resets between them.\n\n```\n   ───────────── setup phase (mutable) ─────────────\n\n     sandbox = Kobako::Sandbox.new\n     sandbox.bind(\"KV::Lookup\", ...)\n     sandbox.preload(code: ..., name: :Adder)\n     sandbox.preload(code: ..., name: :Greeter)\n\n                          │\n                          ▼\n\n   ═════════════════ seal point ═════════════════\n   First #eval or #run freezes the Service registry\n   and snippet table. Further bind / preload now\n   raise ArgumentError.\n\n                          │\n                          ▼\n\n   ──────────────── invocation N ───────────────────\n\n     1. start from the canonical boot state\n        (mruby pre-initialized into the artifact at build time)\n\n     2. replay snippets (in insertion order):\n          :Adder     → defines Adder\n          :Greeter   → defines Greeter\n\n     3. dispatch:  eval(source)  or  run(:Target, *args, **kwargs)\n\n     4. return value to host\n\n     5. discard the instance; reset per-invocation state:\n          · Handles invalidated\n          · stdout / stderr buffers cleared\n          · memory delta zeroed\n\n     Services + snippets persist; invocation N+1 repeats.\n```\n\nFor workloads that must be isolated from each other (one Sandbox per tenant, per student submission, per agent session), construct a fresh `Kobako::Sandbox` per scope — wasmtime's Engine and the compiled Module are cached at process scope, so additional Sandboxes amortize cold-start cost automatically.\n\n### Pooling\n\nFor hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`docs/behavior/runtime.md`](docs/behavior/runtime.md) B-46..B-48). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.\n\n`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. B-49 bakes the shared boot state into the artifact and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the ~28 µs host-side `Sandbox.new`. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a significant gain (~4-5% in the [serverless example](examples/serverless/README.md), and proportionally less once the script itself does real work).\n\n```ruby\npool = Kobako::Pool.new(slots: 4) do |sandbox|\n  sandbox.bind(\"KV::Lookup\", -\u003e(key) { redis.get(key) })\nend\n\npool.with { |sandbox| sandbox.eval(%(KV::Lookup.call(\"user_42\"))) }\n```\n\n| Option | Meaning | Default |\n|--------|---------|---------|\n| `slots:` | Upper bound on constructed Sandboxes | required |\n| `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |\n\nSandboxes construct lazily on first demand. `#with` yields a Sandbox with empty output buffers and returns the block's value; at block exit the Sandbox returns to the pool, except a block that raises `Kobako::TrapError` discards its Sandbox and the slot refills by a fresh construction on next demand. A checkout that waits past `checkout_timeout` raises `Kobako::PoolTimeoutError`. There is no teardown verb — a Pool releases everything with its own reachability.\n\n### Service Blocks\n\nA Service method can accept a guest-supplied block via `\u0026blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch. See [`docs/behavior/yield.md`](docs/behavior/yield.md) B-23..B-30.\n\n```ruby\nsandbox.bind(\"Seq::Map\", -\u003e(items, \u0026blk) { items.map(\u0026blk) })\n\nsandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }')\n# =\u003e [2, 4, 6]\n```\n\n### Handle Management\n\nA non-wire-representable host object — returned from a Service (B-14), passed to `#run` (B-34), or handed back from the guest (B-37) — crosses the boundary as an opaque `Kobako::Handle` proxy and is restored to the original object before host code sees it; any other unrepresentable value raises `Kobako::SandboxError`. Handles are scoped to a single invocation ([`docs/behavior/dispatch.md`](docs/behavior/dispatch.md) B-13..B-21, B-34, B-37).\n\n```ruby\nclass Greeter\n  def initialize(name) = @name = name\n  def greet            = \"hi, #{@name}\"\nend\n\nsandbox.bind(\"Factory::Make\", -\u003e(name) { Greeter.new(name) })\n\nsandbox.eval('Factory::Make.call(\"Bob\").greet')  # =\u003e \"hi, Bob\"  (Handle round-trip inside guest)\nsandbox.eval('Factory::Make.call(\"Bob\")')        # =\u003e #\u003cGreeter @name=\"Bob\"\u003e  (B-37 restoration)\n```\n\nA `break` value from a guest block is the one exception: it unwinds back to the guest Service call rather than to host code, so a Handle in it stays a Handle — restoring would just re-wrap the same object into a new id on the return trip.\n\nEach dispatch that hands back a non-wire-representable object allocates a *new* Handle — kobako never deduplicates by object identity (B-15, B-17). This is most visible with fluent / builder APIs. An `ActiveRecord::Relation` chain `spawn`s a fresh relation at each step, so every hop is an independent dispatch that binds its own Handle:\n\n```\n   guest chain                        host  (Catalog::Handles, one invocation)\n   ───────────                        ─────────────────────────────────────────\n   User.where(active: true)  ─call──▶ Relation #1 (fresh clone)  bound ▶ Handle 1\n                             ◀─Handle 1\n       .order(:created_at)   ─call──▶ Relation #2 (fresh clone)  bound ▶ Handle 2\n                             ◀─Handle 2\n       .limit(10)            ─call──▶ Relation #3 (fresh clone)  bound ▶ Handle 3\n                             ◀─Handle 3\n\n   3 hops ─▶ 3 dispatches ─▶ 3 distinct relations ─▶ 3 Handles\n   all stay live until the invocation ends, then reset together\n```\n\nThis is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and reset between invocations, so even deep chains stay far inside the range. Two consequences are worth keeping in mind: the same host object handed back twice yields two *different* Handles — the guest cannot tell they alias — and every intermediate Handle stays live until the invocation ends, since there is no per-Handle release (B-19).\n\n### Snippets \u0026 Entrypoints\n\n`Sandbox#preload` registers named mruby snippets that replay into every invocation's canonical boot state; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets ([`docs/behavior/invocation.md`](docs/behavior/invocation.md) B-31..B-33).\n\n```ruby\nsandbox = Kobako::Sandbox.new\nsandbox.preload(code: \"Adder   = -\u003e(a, b)  { a + b }\",          name: :Adder)\nsandbox.preload(code: 'Greeter = -\u003e(name:) { \"hello, #{name}\" }', name: :Greeter)\n\nsandbox.run(:Adder, 2, 3)            # =\u003e 5\nsandbox.run(:Greeter, name: \"world\") # =\u003e \"hello, world\"\n```\n\n```\n   per-invocation replay (every #eval / #run, snippets in insertion order):\n\n      canonical boot state\n            │\n            ├──▶ replay :Adder            (defines Adder)\n            │\n            ├──▶ replay :Greeter          (defines Greeter)\n            │\n            └──▶ eval(source)  -or-  run(:Target, *args, **kwargs)\n                       │\n                       ▼\n                  return value, then instance discarded\n```\n\n`#preload` accepts two payload forms:\n\n| Form     | Signature                            | Snippet name source                   | Validation timing                                                          |\n|----------|--------------------------------------|---------------------------------------|----------------------------------------------------------------------------|\n| Source   | `preload(code: \"...\", name: :Const)` | The `name:` keyword                   | First invocation's replay; compile errors raise `Kobako::SandboxError`     |\n| Bytecode | `preload(binary: bytes)`             | Read from the bytecode's `debug_info` | First invocation's replay; structural failure raises `Kobako::BytecodeError` |\n\nUse the source form for snippets authored in your repo; use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Both replay through the same per-invocation path, so no snippet content failure surfaces at `#preload` — force the first replay with a no-op invocation (e.g. `sandbox.eval(\"nil\")`) when you want validation before real traffic.\n\n## Security\n\nkobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`\nexposes *every* public method of the object. So bind a purpose-built object scoped to the\ntask, not a capable one whose other methods leak more than you intend.\n\n```ruby\nclass ThemeReader          # only #color is reachable; AppConfig.secret_key is not\n  def color = AppConfig.theme.color\nend\n\nsandbox = Kobako::Sandbox.new\nsandbox.bind(\"Cfg::Settings\", ThemeReader.new)  # not: bind(\"Cfg::Settings\", AppConfig)\n\nsandbox.eval('Cfg::Settings.color')  # =\u003e \"#3366ff\"  — every other method raises NoMethodError\n```\n\nWhen a purpose-built wrapper is more than you need, an object can gate its own surface in\nplace: a private `respond_to_guest?(name)` answers, per method, whether the guest may call\nit. Returning `false` for every name makes the object opaque — a credential the guest\nforwards to another Service but never reads — while a named subset becomes an allow-list.\n\nGuest code can name any `MyService::KV` path, but a forged name only resolves to\nsomething you bound — the real authorization gate is this host-side allowlist. Give each\ntrust context its own Sandbox, and see [`docs/security-model.md`](docs/security-model.md) for the rest\nas security-design concerns: validating untrusted input, default-deny external effects,\nand controlling the return surface.\n\n## Performance\n\nOrder-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values vary by hardware but ratios are stable across machines. Full numbers, methodology, and the +10%-regression gate live in [`benchmark/README.md`](benchmark/README.md).\n\n| Phase                                                        | Cost                  |\n|--------------------------------------------------------------|-----------------------|\n| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |\n| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time        |\n| Subsequent `Sandbox.new` (caches warm)                       | ~28 µs                |\n| Warm `#eval(\"nil\")` on a reused Sandbox                      | ~71 µs                |\n| Warm `#run(:Entrypoint, ...)` dispatch                       | ~97 µs                |\n| Service call amortized inside one invocation                 | ~6.8 µs               |\n| Snippet replay per invocation                                | ~7.6 µs each          |\n| Per additional idle Sandbox (RSS)                            | ~1 KB                 |\n\nThe Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~33 MB total. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 17k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. A +10% regression on any of the six SPEC-mandated benchmarks blocks release.\n\nRegexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#10` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.\n\n```bash\nbundle exec rake bench  # six gated regression benchmarks (~5-8 min)\n```\n\n## Development\n\nAfter checking out the repo:\n\n```bash\nbin/setup         # install dependencies\nbundle exec rake  # default: compile + test + rubocop + steep\n```\n\nBuilding from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain; the first compile walks the full chain — the [beni](https://github.com/elct9620/beni) gem vendors wasi-sdk + mruby and builds `libmruby.a` (`rake beni:build`), then `rake wasm:build` produces the Guest Binary. See [`CLAUDE.md`](CLAUDE.md) for the rake task map and pipeline layout. `bin/console` opens an IRB session with the gem preloaded; `bundle exec rake install` installs the local checkout as a gem.\n\n## Contributing\n\nBug reports and pull requests are welcome at \u003chttps://github.com/elct9620/kobako\u003e. Please open an issue before starting on non-trivial changes so we can align on scope.\n\n## License\n\nKobako is released under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felct9620%2Fkobako","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felct9620%2Fkobako","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felct9620%2Fkobako/lists"}