WASM Engine ABI
The browser build of kapi (kapi/cmd/kapi-wasm-cli) registers a set of global
JS functions at boot. That set is a versioned contract — the ABI consumed by
the @neokapi/engine npm package (packages/engine), which wraps it in the
typed KapiRuntime facade.
Contract
- Single registration point. Every entry point is declared in the
engineExportstable inkapi/cmd/kapi-wasm-cli/main.goand installed onglobalThisfrom there. Nothing else registers engine globals. - Feature detection.
kapiEngineABI()returns{abi, version, functions}— the ABI major version, the kapi build version, and the list of registered global names. A build withoutkapiEngineABIpredates the descriptor and is treated as abi 0 (probe individual globals). - Additive within a version. For one
abivalue, changes are strictly additive: new functions may appear infunctions, but existing names, signatures, and payload shapes never change or disappear. Renaming or removing an entry point, or changing a signature incompatibly, bumps the version. - Boot handshake. The host installs its
fs/processshims (thewasm_exec.jsenvironment) before instantiation; after registering the globals, the engine invokes the host's__kapiCliReady()callback and then blocks forever so the globals stay callable. - Reverse bridges. Some features call back into optional host-provided
globals:
__kapiPdfium(PDF text + geometry),kapiLocalGenerate(on-device LLM),kapiLocalNER(on-device NER), andkapiBrowserTranslate(with the platformTranslatorAPI). Each degrades with an actionable error when its bridge is absent. The typed interfaces live inpackages/engine/src/capabilities.ts.
Command surface
kapiRun(argv) executes the ordinary kapi CLI, so the browser build has a
second contract alongside the global function set: which verbs it answers.
- One declaration.
cli.BrowserCommandSetis the browser build's command set, mirroringcli.KapiCommandSet(the native binary's) verb for verb.kapi/cmd/kapi-wasm-cliregisters it wholesale and declares nothing itself. - No missing verbs. A verb the browser cannot run — one needing a
subprocess, the OS keychain, the network, or a socket — is recorded in
cli.browserGapswith the facility it needs, and registers a command that reports it.unknown commandtherefore means the verb does not exist in kapi at all, never that the browser omitted it.--helpstill works on those verbs; their help text carries the limitation. - Drift is a test failure.
cli.TestBrowserCommandSurfacecompares the two sets and fails when a verb appears in one and not the other, or when a gap's help metadata drifts from the command it stands in for. Adding a verb toKapiCommandSetis a decision: wire it up for the browser, or record why it cannot run there. - Runtime guard.
make wasm-surface-smoke(scripts/verify-snippets/command-surface-smoke.ts) boots the real wasm in Node, sweeps every reachable verb forunknown command, asserts each gap's message, and replays the argv the lab explorers themselves use. It runs in the Docs — Verify Snippets workflow. - Capture strips ANSI. The engine boots with
CLICOLOR_FORCE=1so the playground terminal renders kapi's real styling;runCapturehands output to program code instead, and strips the escapes so a--jsonpayload parses.
The same derivation feeds the docs: scripts/gen-refs reads
cli.BrowserUnavailableReason for each command's runnable-in-browser badge, so
the Command Reference cannot claim a verb runs in the lab when it does not.
Where the pieces live
| Concern | Location |
|---|---|
Registration table + kapiEngineABI() | kapi/cmd/kapi-wasm-cli/main.go |
| Browser command set + recorded gaps | cli/browsercmds.go |
| Surface drift guard | cli/browsercmds_test.go |
| Runtime surface smoke | scripts/verify-snippets/command-surface-smoke.ts |
| Ambient TS typings for the globals | packages/engine/src/globals.ts |
Wire shapes + engineABI() helper | packages/engine/src/abi.ts |
KapiRuntime facade + boot | packages/engine/src/runtime.ts |
| Reverse-bridge capability types | packages/engine/src/capabilities.ts |
| Payload types (ContentTree, runs) | @neokapi/contract-types (generated) |
When adding an entry point: add it to engineExports, type it in
globals.ts, surface it on the facade if user-facing, and leave the abi
value alone (additions don't bump it).