Building
Authoring extensions
Scellis draws the ownership line in an unusual place: the maintainer owns the grammar, the checker, and the reference oracle — and nothing else. Every instance is yours. Everything the maintainer can compute, you can author from the interface, with no engine patch and no approval queue. The credo behind the design is blunt: “if a user cannot author a Laplacian without me, the substrate is mine, not theirs.” This page is the practical walk through that claim: what is authorable, what an authored op actually looks like, how a kernel earns trust, and how your code builds on everything already in the Catalog.
The authorable surface#
The authorable surface is the platform's own vocabulary, end to end — not a plugin niche bolted to the side:
- Computation: blocks — composite blocks whose body is a graph-as-data, and sandboxed function blocks — plus ops with declared shape and typing behavior, and GPU kernels paired with CPU references.
- Training: gradient rules, optimizers, schedulers, losses, metrics, PEFT adapter kinds, and the aggregation strategies collaborative training runs on.
- Interface and data: viewers, connectors, format codecs, models, datasets — and the Packs that ship any of the above.
What the maintainer keeps is exactly three grammars: a shape/signature language that ops declare their contracts in; a buffer-binding ABI covering storage buffers, uniforms, and workgroup-shared scratch; and a backward (VJP) representation. Everything else is an instance written over those grammars — and because the engine interprets the grammars generically, it cannot tell a builtin from a user entity at dispatch time. The difference is a provenance chip in the catalog, never a separate code path.
Anatomy of an authored op#
You author in a form the Studio renders from the op schema — and the Copilot can draft the same record, since it writes through the same API. Underneath, the record has one shape:
- name: user/ada/mish # namespaced — never shadows a builtin
version: 1
summary: "Mish activation: out = a * tanh(softplus(a))"
taxonomy: webir.op.elementwise
inputs: [{ name: a, dtype: [f32], shape: ["..."] }]
outputs: [{ name: out, dtype: [f32], shape: ["..."] }]
tolerance: { class: tight } # justified against the op's semantics
determinism: { debug: deterministic, prod: deterministic_within_tolerance }
gradients: { kind: elementwise, vjp_rule: "grad_out * mish'(a)" }
kernels: { forward: [{ id: user/ada/mish_f32, when: { dtype: f32 } }] }Each field is a checked contract, not documentation. The id is namespaced (user/<owner>/<name>) so a user op stands beside a builtin, never in its place. The shape strings are an interpreted signature — an op whose output extent depends on data values declares that class explicitly, with a worst-case bound, so memory stays plannable. The tolerance class must be justified against the op's semantic category: a matmul cannot claim loose to dodge conformance. And determinism is declared per tier — debug, prod, fast — never silently defaulted.
The kernel and its reference#
Authored kernels are not a restricted dialect. The binding ABI covers read and read-write storage buffers, uniforms, and workgroup-shared scratch — so a tiled matmul, a reduction, or a stencil is authorable, not just an elementwise map. A minimal forward kernel for the op above:
@group(0) @binding(0) var<storage, read> in0 : array<f32>;
@group(0) @binding(1) var<storage, read_write> out : array<f32>;
@group(0) @binding(2) var<uniform> p : Params;
@compute @workgroup_size(256, 1, 1)
fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
let i = gid.x;
if (i >= p.outNumel) { return; }
let x = in0[i];
out[i] = x * tanh(log(1.0 + exp(x))); // must match the reference within "tight"
}The pairing rule is where trust begins. Alongside the WGSL you supply a reference implementation — in a closed scalar grammar, or as sandboxed JS at a lower trust ceiling. The conformance runner generates test cases, executes both, and requires agreement within the declared tolerance class. Passing mints a hash-bound ConformanceReceipt that travels with the kernel and is visible in the catalog — the same regime that gates every builtin on the correctness surface, and the same IR ladder underneath.
One honest detail: the receipt is profile-scoped. It proves the pairing on the GPU envelope it actually ran on; on a different device class the check re-runs locally. Receipts never travel as imported trust — that would claim more than was tested.
Note
Conformance gates trusted activation and sharing — never your own loop. You can always run your own unconformed kernel on your own device: write, run, inspect, fix, run again.
Gradients and reducers earn trust too#
A declared backward is an ordinary composed op-graph — the same expressiveness as a forward — and it is gradchecked with finite differences against your own forward, including a mandatory negative control. Stated plainly: gradcheck is a strong necessary condition, not a formal proof. A user-declared reducer goes further — its claimed algebra (associativity, commutativity) is property-tested before activation, because a wrongly declared combiner would tree-reduce into silently wrong numbers.
Reuse the catalog like a library#
An authored body starts from the whole catalog, not from zero. The design mandate reads “like importing pandas and using a DataFrame” — from inside any authored body you invoke other entities through one governed surface:
// Inside any authored body: the governed call surface.
const y = ctx.op("core/softmax", x, { axis: -1 }); // a builtin, resolved by name
const z = ctx.call("user/kim/zscore@3", y); // another author's block
const s = ctx.tensor.matmul(z, w); // the tensor-algebra surface
// Every reference resolves through YOUR pinned uses closure —
// never against "whatever happens to be newest".The bookkeeping is not yours to maintain. Dependency edges are derived automatically from your call sites and pinned to the exact versions resolved at author time; the lockfile records their hashes. Afterwards every runtime call resolves through that pin — the call surface and the declared closure are the same set, so what you call is exactly what you locked. That makes an authored entity reproducible by construction and safe to run offline; a dependency cycle or an unresolvable pin is refused loudly, never resolved to “newest wins”.
Two properties keep composition honest. Effects are transitive across the closure: a function declared pure cannot quietly call a network-effecting helper, because a callee's effects count against the whole chain and consent is asked on the union. And trust composes conservatively: a callee runs at the strictest trust tier in its call chain — untrusted code cannot launder itself through trusted callers.
One check, one bar — and honest names#
Every authored entity passes the author-check on its full record — at create, and again at every edit. It is the same check that gates every builtin: missing or partial information is rejected, and the rejection names what is missing. Builtins are immutable, so “editing” one forks a first-class copy you own, with the lineage recorded; your own entities edit in place through the same validated path.
Override and naming stay honest, too. Any op that declares an authorable binding contract is overridable — the override set is a catalog query, not a hardcoded list — and on your device your workspace's kernel wins over an installed Pack's, which wins over the built-in core. But a user op can never impersonate a builtin: authored ids live under user/<owner>/<name>, and a bare builtin name always resolves to the core op. Overriding is a local dispatch preference; impersonation is structurally unavailable.
Publishing is a separate step#
Trust levels — unverified, community, verified, official — are earned per artifact and are orthogonal to who authored it: user-authored entities can earn verified. When an entity is ready to leave your device, you pack it, sign it in the browser, and pass the publication gates: a resolved lockfile, conformance evidence where it applies, complete metadata, and an explicit license. That path — and what buyers and installers see on the other side — is the subject of Packs and publishing. If what you authored is a training component, it now shows up in the Model View pickers like any built-in one — see Training in the browser.