Kernels (validated sandbox logic)
A Kernel is client/plugin-supplied logic the host runs safely under policy. It is restricted IR (intermediate representation), authored as JSON, never C#, IL, reflection, CLR member names, or arbitrary host calls.
Kernels are the engine behind two of DotBoxD’s three modes:
event pipelines (called Query (RunLocal) in some API surfaces) and
Pushdown both compile the author’s C# down to kernels. You rarely hand-write
one - you write Where/Select chains or batch methods and the generator lowers them - but
everything about why that is safe lives on this page.
Why Kernels (the sandbox behind event filters and Pushdown batches)?
Section titled “Why Kernels (the sandbox behind event filters and Pushdown batches)?”The problem. Two of DotBoxD’s three modes run code the plugin author wrote inside the host
process: an event pipeline lowers Where/Select to
server-side logic, and Pushdown lowers a batch method that loops the host’s bindings
server-side. Running that logic as C#/IL would hand it full CLR access. The alternative - filtering
client-side or bloating a frozen host with every batch op - either wastes the wire or is impossible.
The kernel is the shared substrate that makes accepting untrusted author logic safe.
The payoff. A kernel is validated restricted IR, capability-gated, and metered: fuel (an abstract instruction budget - each operation costs fuel, and the kernel is stopped when the budget runs out), loop iterations, call depth, list length, output bytes, and per-capability quotas. A buggy or hostile kernel cannot exhaust host resources or reach disallowed effects, and it can touch only the host bindings the host explicitly exposes - a method reachable via normal RPC is not automatically reachable from a kernel. This is what makes both server-side lowerings safe to accept from less-trusted plugin authors.
Grounded specifics:
- One boundary, two lowerings. An event pipeline’s
Where/Selectand Pushdown’s batch run as the same validated, fuel-metered IR. For plugin-side terminals such asRunLocalandRegisterLocal, the filter runs server-side so non-matching events never cross, andSelectships only the projected scalar (fewer bytes, fewer wake-ups, one-way push, no round-trips). Server-sideRunandRegisteruse the same lowerings without transferring a value to the plugin. Pushdown keeps it in round-trips - N fine-grained calls collapse into one server-side batch next to the host’s data. - Capabilities are derived, not self-asserted, and fail closed. The manifest’s required
capabilities are the union of what the IR actually touches; install is rejected unless the host
policy grants them, so bad code never runs. Async is itself a gated capability
(
dotboxd.runtime.async) that fails closed without a grant. - No lock-in. The IR plus manifest are public artifacts; you can delete the
[ServerExtension]attribute and hand-author the same IR through the sameSandboxHostpipeline. The generator emits nothing a hand-written kernel could not also request.
When to use it - and when not. Reach for kernels (directly, or via event-pipeline/Pushdown lowering) when you must run author-supplied logic in-process safely. Prefer a Service when the logic is a host capability you implement and trust - it is a plain request/response RPC that needs no sandbox. And note the trust ceiling below: safe-mode kernels are the real boundary; trusted-plugin assembly loading is not a sandbox, and hard multi-tenant isolation against arbitrary compiled .NET still needs an OS/process boundary.
Lifecycle (via SandboxHost in DotBoxD.Hosting):
- Import - parse JSON IR into a
SandboxModule; reject anything outside the allowed shape. - Validate - structural, type, effect, capability, and binding checks (
DotBoxD.Kernels.Validation). - Prepare - produce a sealed
ExecutionPlan. - Execute - run on one of two backends:
- Interpreter (
DotBoxD.Kernels.Interpreter) - flexible, async-friendly. - Compiler (
DotBoxD.Kernels.Compiler) - emits verified IL; the generated assembly is checked byDotBoxD.Kernels.Verifierbefore it runs. Compiled async bindings run through a trusted runtime trampoline; generated kernel IL stays synchronous.
- Interpreter (
The smallest end-to-end host - import, prepare, execute under a hard budget (given kernelJson,
the module’s JSON IR, and subtotals, a list of ints):
using DotBoxD.Hosting;using DotBoxD.Kernels;using DotBoxD.Kernels.Sandbox;
// A sandbox host with only the safe, pure bindings enabled.var host = SandboxHost.Create(builder =>{ builder.AddDefaultPureBindings(); builder.UseInterpreter();});
// A policy is a hard budget: fuel, loop iterations, list length, capability grants.var policy = SandboxPolicyBuilder.Create() .WithFuel(1_000_000) .WithMaxLoopIterations(10_000) .WithMaxListLength(10_000) .Build();
var module = await host.ImportJsonAsync(kernelJson);var plan = await host.PrepareAsync(module, policy);
var input = SandboxValue.FromList( [.. subtotals.Select(SandboxValue.FromInt32)], SandboxType.I32);
var result = await host.ExecuteAsync(plan, "main", input);
if (result.Succeeded && result.Value is I32Value total){ // A buggy or hostile kernel cannot run away with host resources: Console.WriteLine($"total={total.Value}, fuel burned={result.ResourceUsage.FuelUsed}");}Beyond that metering, host capabilities (files, time,
random, logging, HTTP via DotBoxD.Hosting.Http) are exposed
only through explicit host bindings and capability grants - see
capabilities-and-bindings
and the full spec under docs/Specs/.
Async binding tails are also capability-gated. Policies must grant dotboxd.runtime.async via
AllowRuntimeAsync() before kernels can call bindings that may complete asynchronously.
Important: the kernel sandbox is the real boundary. It is not the same as loading a .NET plugin assembly - see security/sandbox-caveats.md.
Diagnostics use the DBXK### prefix. See also: the GameServer walkthrough
for a beginner-friendly tour, the GameServer sample under
samples/GameServer, runtime, and
docs/examples/coverage-gaps.md for kernel scenarios no longer shown
by maintained samples.