Single file, or a typed project
Two authoring shapes carry the same function contract:
-
wasm/handlers.js— the zero-toolchain single file. Plain JavaScript, nopackage.json, no npm. Compiled todist/handlers.wasmon load. The whole realm isrealm.yml+ this file + a hand-authoreddist/manifest.json. This is the floor. -
src/— a TypeScript project. When a realm outgrows one file — multiple handlers, shared helpers, real types — it authors a normal TypeScript project: many.tsfiles undersrc/, ordinaryimportsemantics between them, apackage.json, and a typed view of the gateway. The build type-checks, bundles the reachable module graph into one module, extractsdist/manifest.jsonfrom the typed exports (function names,inputSchema/outputSchemafrom the handlers’ parameter and return types — the same schema-from-types generation TS handlers already use), and compiles the bundle todist/handlers.wasm. The author runs the build; the shipped artifacts are the bundle and the generated manifest. Extraction sees exports only:defineHandleris a single-file form, so adefineHandlercall in asrc/project contributes no manifest entry and the build warns.
Types come from two places, both already in the ecosystem: @embabel/runtime-types supplies the Ctx, event, and envelope types, and a generated .embabel/gateway.d.ts (via embabel-realm sync) types ctx.gateway.<namespace>.<method> against the world’s actual tool surface. So ctx.gateway.signals.recent({ … }) autocompletes and type-checks, and a handler’s declared input/return types drive the manifest schemas rather than being hand-copied into JSON.
// src/api/chatops.ts — typed, multi-file projectimport { digestByChannel } from "../lib/aggregate";import type { Ctx } from "@embabel/runtime-types";
export async function digest(input: { limit?: number }, ctx: Ctx): Promise<Digest> { const messages = await ctx.gateway.signals.recent({ type: "chatops.message", hours: 168, limit: 200 }); return digestByChannel(messages, input.limit ?? 5);}The project layout and toolchain are the docker src/ project’s: source under src/api/<namespace>.ts (the filename is the namespace), each exported handler a function in that namespace, helpers under src/lib/ excluded from extraction. Functions resolve by (namespace, name), so a.get and b.get are distinct. The build bundles the reachable module graph from those entrypoints into one module and compiles it to the artifact its placement runs — docker JS modules, or a wasm bundle. wasm/handlers.js stays the no-build escape hatch; src/ is the path when you want types, imports, and tests.
A wasm-targeted src/ project ships its built dist/handlers.wasm and generated manifest as one
artifactDigest-bound set; the host does not build src/ on load (build-on-load covers
wasm/handlers.js only). A marketplace accepts that set only when the platform built it from the
reviewed source or can verify a reproducible-build attestation. A local or organization-reviewed
installation may choose a weaker provenance policy, but still verifies the artifact-set digest.
artifactDigest covers the executable and manifest correspondence. The broader canonical
realmDigest covers all author-controlled package content: the artifact set plus handlers,
events/mappings, channels, types, producers, APIs, MCP/webhook declarations, prompts, and other realm
surfaces. Adoption and dispatch pin realmDigest; changing a non-code declaration can change
behavior just as surely as changing code.
Two points of this are forward-looking convergence, not shipped today:
- One signature across hosts. The canonical handler signature is
(input, ctx)with the gateway underctx.gateway(as shown throughout). The dockersrc/handler today is(ctx, args)with the gateway passed directly as the first argument. The invocation convention is selected by a top-level manifest fieldhandlerAbi— one of"input-ctx"(the default when absent) or"ctx-args-legacy"— applying to the whole artifact, never inferred from parameter names or arity (both are arity-two). Unifying on(input, ctx)so onesrc/serves both hosts is the convergence; the(ctx, args)form persists only under an explicithandlerAbi: "ctx-args-legacy". - Manifest from types. The single-file JS path hand-authors
dist/manifest.json; the TS build extracting it from the typed exports is the same generation the dockersrc/build already points at.

