You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Concurrent-invocation isolation, the Guest Binary ABI gate, the warm Kobako::Pool, and the canonical boot state every invocation begins from. The governing summary lives in SPEC.md
§ Behavior; this file is the per-anchor reference. B-xx anchors are global
and append-only across the corpus (N-8).
B-22 — Concurrent invocations execute independently on distinct or shared Sandboxes
Field
Value
Initial State
Two or more Ruby Threads exist within the same process and invoke Kobako concurrently — each Thread on its own Kobako::Sandbox, or several Threads sharing one.
Operation
Each Thread invokes #eval or #run. A single Thread runs at most one invocation at a time; distinct Threads may invoke distinct Sandboxes or the same Sandbox concurrently.
Result / Final State
Each invocation executes independently on its own per-invocation Context — capability state, Handle IDs, and capture buffers are scoped to the invocation and never observed by another Thread's invocation, whether or not the two share a Sandbox (B-03). When Threads share a Sandbox, each invocation supplies its own identity through the per-invocation ctx.bind override (B-63) while the sealed Service registry and static bindings stay read-only and shared safely. The wasmtime Engine and the compiled Module for data/kobako.wasm are shared at process scope: the first Sandbox in the process pays the Engine init and Module compile cost, subsequent Sandboxes in any Thread amortize against that shared state, and each invocation instantiates its own guest instance from that shared state (B-49). Under the default :hold GVL mode, aggregate throughput across Threads is bounded by Ruby's GVL — wasm-side work is serialized while Ruby-side setup (preamble pack, buffer init) can overlap; a Sandbox opted into gvl: :release lifts that serialization, running its guest span in parallel with other Threads' (B-64). Kobako needs no locking for its own state, since every invocation is isolated; a host Service object bound once and shared across concurrently-invoking Threads must itself be thread-safe, while a provider: or ctx.bind object is per-invocation and carries no such obligation.
B-40 — Host validates the Guest Binary ABI version at construction
Field
Value
Initial State
No Kobako::Sandbox instance exists. A Guest Binary artifact is present at the resolved wasm_path and exports __kobako_abi_version returning the ABI version the Host Gem implements (→ docs/wire-codec.md § ABI Version).
Operation
Kobako::Sandbox.new — with the default bundled Guest Binary or a custom wasm_path:.
Result / Final State
Construction validates the version the Guest Binary reports through __kobako_abi_version against the Host Gem's implemented version by equality, after the wasm runtime setup of B-01. On equality, construction completes per B-01. No invocation entry point runs. The reported version is a property of the artifact rather than of the Sandbox asking, so the probe may be amortized across Sandboxes built from the same wasm_path in one process, alongside the B-22 Engine and Module sharing; the amortization is unobservable — an artifact that failed the check is never retained, so every construction on it fails alike. The check exists for Guest Binaries that ship independently of the Host Gem: the bundled data/kobako.wasm matches by construction, while a custom guest built against a different ABI version — or one predating the version export, which is the same failure — fails loudly at Sandbox.new (E-42) instead of misbehaving mid-invocation.
B-46 — Construct a Kobako::Pool
Field
Value
Initial State
No Kobako::Pool instance exists. The Guest Binary is resolvable per B-01.
Operation
Kobako::Pool.new(slots: n) { |sandbox| ... } — slots: (positive Integer) is the number of pooled Sandboxes; checkout_timeout: (Numeric seconds, default 5.0, nil to wait indefinitely) bounds the B-47 checkout wait; every other keyword argument is forwarded verbatim to Kobako::Sandbox.new; the optional block is the per-Sandbox setup hook.
Result / Final State
A Pool managing up to slots Sandboxes is returned. No Sandbox is constructed yet: a checkout (B-47) receives an idle constructed Sandbox when one exists, and constructs a new one — with the forwarded keyword arguments — only when no idle Sandbox exists and fewer than slots have been constructed. The setup block runs exactly once per pooled Sandbox immediately after its construction, before that Sandbox is first handed to any checkout caller. No invocation entry point runs. The setup block is the pooled Sandbox's setup window (B-08 / B-32); its registrations seal at that Sandbox's first invocation (B-33) exactly as on a directly constructed Sandbox. Sandbox construction and setup-block errors surface unchanged — original class and message — at the #with call whose checkout triggered the creation (E-39..E-42, E-49, or the block's own exception). Invalid slots: / checkout_timeout: raise ArgumentError at Pool.new (E-47). Any per-slot resource provisioning is unobservable — a pooled Sandbox satisfies every behavior anchor identically (B-47).
B-47 — Check out a Sandbox via Pool#with
Field
Value
Initial State
A Pool (B-46). Any number of threads call #with concurrently.
Operation
pool.with { |sandbox| ... }
Result / Final State
The calling thread holds exclusive use of one pooled Sandbox for the duration of the block; #with returns the block's return value. When all slots Sandboxes are held, the call blocks until one is returned, or raises Kobako::PoolTimeoutError once the wait exceeds checkout_timeout (E-46). At block exit — normal return or raised exception — the Sandbox returns to the pool. A checked-out Sandbox carries no observables of its own — each invocation's captures and usage live on the Execution it returns (B-61); Service bindings and snippets registered by the setup block remain active. Every B-xx / E-xx behavior holds for a pooled Sandbox exactly as for a directly constructed one — in particular per-invocation isolation (B-03) guarantees that no guest-observable state crosses from one checkout holder to the next. Checkouts are independent: a nested #with on the same thread checks out a second Sandbox and counts against slots like any other holder, and the pool lends each Sandbox to one holder at a time — its own exclusivity, stronger than the per-invocation isolation B-22 already guarantees. A checkout whose block raises Kobako::TrapError has its Sandbox discarded at checkin — the pool applies the discard-and-recreate recovery contract itself, refilling the slot by a fresh construction and setup-block run on next demand.
B-48 — Pool teardown follows Pool reachability
Field
Value
Initial State
A Pool holding constructed Sandboxes; zero or more are checked out.
Operation
The Host App drops its last reference to the Pool.
Result / Final State
The Pool and the pooled Sandboxes it holds become unreachable and are reclaimed by Ruby garbage collection like ordinary objects, releasing every runtime resource they held. A Sandbox held by an in-flight #with block remains valid until that block exits. The Pool has no explicit teardown verb; reachability is the lifecycle, mirroring B-19's per-Sandbox release path.
B-49 — Every invocation begins from the canonical boot state
Field
Value
Initial State
A Sandbox with any invocation history — none, successful, or failed.
Operation
Any invocation entry — sandbox.eval(code) or sandbox.run(target, ...).
Result / Final State
The mruby interpreter the invocation observes starts in the canonical boot state: the deterministic post-boot interpreter state of the Guest Binary, identical for every invocation of the same artifact on either isolation profile (B-54) — the state is fixed before any ambient source is consulted — and carrying no artifact of any prior invocation (B-03). On top of that state, in order: the Frame 1 preamble installs the Sandbox's registrations, preloaded snippets replay (B-32), and then per-invocation source load (#eval) or entrypoint resolution (#run) proceeds. The canonical boot state may be computed at build time and embedded in the Guest Binary as a pre-initialized image; embedding is unobservable, and re-baking the same inputs yields a byte-identical artifact (the reproducible-build pipeline F-10 gates this). The engine may likewise provision per-invocation instance resources for reuse (B-46); provisioning is equally unobservable.
B-64 — Release Ruby's GVL for the guest span (gvl: mode)
Field
Value
Initial State
A Kobako::Sandbox constructed with a gvl: mode — :hold (the default) or :release. Distinct Threads may invoke concurrently, on distinct Sandboxes or a shared one (B-22).
Operation
#eval or #run on that Sandbox.
Result / Final State
Under :release, the invocation releases Ruby's GVL for the span of guest execution, so guest code on distinct Threads runs in parallel rather than serializing on the GVL. It re-acquires the GVL for each guest→host dispatch callback — ordinary Ruby that must hold the lock — including the nested dispatch of B-28. Under :hold the GVL is held for the whole invocation; :hold is the default. The mode is per-Sandbox and fixed at construction. Releasing changes scheduling only: every isolation (B-03), Handle-lifecycle (B-18), capture (B-04), and invocation-outcome guarantee holds identically under either mode, whether the Threads use distinct Sandboxes or share one (B-22). An unrecognized gvl: value raises ArgumentError at construction, failing closed like an unrecognized profile:.