Skip to content

Single file, or a typed project

Two authoring shapes carry the same function contract:

  • wasm/handlers.js — the zero-toolchain single file. Plain JavaScript, no package.json, no npm. Compiled to dist/handlers.wasm on load. The whole realm is realm.yml + this file + a hand-authored dist/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 .ts files under src/, ordinary import semantics between them, a package.json, and a typed view of the gateway. The build type-checks, bundles the reachable module graph into one module, extracts dist/manifest.json from the typed exports (function names, inputSchema/outputSchema from the handlers’ parameter and return types — the same schema-from-types generation TS handlers already use), and compiles the bundle to dist/handlers.wasm. The author runs the build; the shipped artifacts are the bundle and the generated manifest. Extraction sees exports only: defineHandler is a single-file form, so a defineHandler call in a src/ 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 project
import { 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 under ctx.gateway (as shown throughout). The docker src/ handler today is (ctx, args) with the gateway passed directly as the first argument. The invocation convention is selected by a top-level manifest field handlerAbi — 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 one src/ serves both hosts is the convergence; the (ctx, args) form persists only under an explicit handlerAbi: "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 docker src/ build already points at.