@atheory-ai/wasm-plugin-toolkit
v0.0.4
Published
Shared TS->WASM build, sandbox, and validate pipeline for atheory-ai plugin SDKs (ce-plugin-sdk, argent-plugin-sdk).
Readme
@atheory-ai/wasm-plugin-toolkit
Shared TypeScript-to-WebAssembly build / sandbox / validate pipeline for the atheory-ai plugin SDK family.
Extracted from ce-plugin-sdk
during Stage 4 of the
harmonization plan,
when a second consumer (argent-plugin-sdk,
planned) appeared to validate the shape.
The toolkit is product-neutral. It knows how to:
- compile a TS plugin to WASM via esbuild + javy,
- run a built plugin against a fixture suite,
- validate that a built plugin's WASM exports match a declared ABI and that its manifest passes a declared schema.
It deliberately knows nothing about Context Engine plugins, argent plugins, or whichever product comes next. Consumers wire it to a particular product via three plug-in points described below.
The three plug-in points
Each consumer supplies three small modules. They evolve independently — the ABI is breaking and rare, the host-function module is additive and frequent, the manifest schema lives on a per-registry release cadence.
1. AbiModule — what the WASM must export
Declares the WIT world the build pipeline uses to bake exports into the binary, the literal entry-wrapper TypeScript the build pipeline writes ahead of esbuild, and the export names the validator will look for.
import type { AbiModule } from "@atheory-ai/wasm-plugin-toolkit"
export const ceAbiV1: AbiModule = {
name: "ce-plugin",
abiVersion: 1,
wit: `package atheory:ce;
world plugin {
export ce-plugin-manifest: func();
export ce-language-match: func();
export ce-language-extract: func();
// ...
}`,
entryWrapper: ({ userEntryRelative }) => `
import plugin from "${userEntryRelative}"
import { setPluginDefinition } from "@atheory-ai/ce-plugin-sdk"
import * as abi from "@atheory-ai/ce-plugin-sdk/abi"
setPluginDefinition(plugin)
export const {
cePluginManifest, ceLanguageMatch, ceLanguageExtract, ...
} = abi
`,
requiredExports: [
"ce-plugin-manifest",
"ce-language-match",
"ce-language-extract",
// ...
],
}2. HostFunctionModule — what __host_* imports look like
Declares the import surface a built plugin will need at runtime, plus Node-side stubs so the sandbox can run the plugin out-of-engine for tests.
import type { HostFunctionModule } from "@atheory-ai/wasm-plugin-toolkit"
export const ceHostFunctions: HostFunctionModule = {
name: "ce-host",
hostFunctions: [
{
name: "__ce_log",
signature: "(level: string, message: string) => void",
stub: () => undefined,
},
{
name: "__ce_substrate_query",
signature: "(queryJSON: string) => string",
stub: () => "[]",
},
// ...
],
}Today the sandbox uses these only when the consumer's PluginLoader runs
WASM in-process. For consumers that shell out to a product binary (CE's
current model: ce plugin extract) the engine wires its own host
functions; the descriptor list is then purely informational, used by the
validator to cross-check that manifest permissions enumerate every
import the WASM actually declares.
3. ManifestSchemaModule — what the text manifest must contain
Declares the manifest's filename plus how to parse and validate it. The toolkit doesn't pick a schema language — bring your own (JSON Schema + ajv, zod, hand-rolled, etc.) as long as it implements the small interface.
import type { ManifestSchemaModule } from "@atheory-ai/wasm-plugin-toolkit"
export const cePluginManifest: ManifestSchemaModule = {
name: "ce-plugin",
filename: "ce-plugin.json",
parse: (raw) => JSON.parse(raw),
validate: (m) => {
const errors: string[] = []
if (!m.id) errors.push("id is required")
if (!m.version) errors.push("version is required")
// ...
return { ok: errors.length === 0, errors, warnings: [], parsed: m }
},
}How ce-plugin-sdk will consume this
// packages/plugin-sdk/scripts/build-plugin.mjs
import { buildWasmPlugin } from "@atheory-ai/wasm-plugin-toolkit/build"
import { ceAbiV1 } from "../abi/v1.js"
await buildWasmPlugin({ pluginDir: process.argv[2], output: process.argv[3], abi: ceAbiV1 })// packages/plugin-sandbox/src/runner/loader.ts — wraps the toolkit's
// PluginLoader with CE-binary-backed validate/extract calls.How argent-plugin-sdk will consume this (stub)
import { buildWasmPlugin } from "@atheory-ai/wasm-plugin-toolkit/build"
import { argentAbiV1 } from "./abi/v1.js" // exports argent.process.*, argent.route.*, …
await buildWasmPlugin({ pluginDir, output, abi: argentAbiV1 })The only difference from CE is the ABI module — same pipeline, different WIT world and entry wrapper. That equivalence is the whole reason the toolkit was extracted.
Package layout
src/
build/ # esbuild -> javy WASM compilation
sandbox/ # fixture runner (consumer-injected loader)
validate/ # WASM well-formed + ABI + manifest schema
test-preset/ # vitest defineFixtureSuite preset
types.ts # AbiModule, HostFunctionModule, ManifestSchemaModule
bin/
install-javy.js # postinstall: download javy v8.0.0 binary
wasm-toolkit-validate.mjs # CLI for the validator core
tests/
validate.test.ts # end-to-end with hand-assembled WASM
build.test.ts # smoke test, javy shimmedInstall
pnpm add @atheory-ai/wasm-plugin-toolkit esbuildThe postinstall hook downloads javy v8.0.0 for your platform from
bytecodealliance/javy releases.
Set WASM_TOOLKIT_SKIP_JAVY=1 to skip (e.g., CI environments that
provision javy out of band).
Trust model
This package is the authoring-time half of the WASM-plugin story. Signing prevents substitution; sandboxing prevents misbehavior; the validator catches obvious shape errors before either matters. See the harmonization plan section on "signing prevents substitution, sandbox prevents misbehavior" for the full framing.
License
Apache-2.0.
