| Authority | PROJECT |
|---|---|
| Date | 2026-03-16 |
| Decision-Owner | Sebastian (Architect) |
| Recorder | Claude Code (Implementation Assistant) |
| Status | DECIDED |
| Decides | GW-EI8-1 |
| Unblocks | feat/ergo-init (EI-8) |
The v1 mechanism for loading custom implementations is in-process
Rust crate compilation. The user implements traits from ergo-runtime
(SourcePrimitive, ComputePrimitive, TriggerPrimitive,
ActionPrimitive) in a Rust crate that compiles and links against the
same ergo-runtime dependency as the engine.
Custom implementations enter the runtime through CatalogBuilder,
which already exists and already enforces the full registration
validation pipeline identically for custom and stdlib primitives.
User code implements the same traits as stdlib primitives. AckAction,
NumberSource, Add, EmitIfTrue — all use the identical trait
interface. No adapter layer, no serialization boundary, no separate
API surface.
CatalogBuilder::build() runs the same registration pipeline for
custom code as for stdlib. ACT-1 through ACT-33, COMP-* rules,
manifest validation, duplicate ID rejection — all enforced. No bypass
path exists. This is already proven by existing tests:
catalog_builder_admits_external_implementations_by_kindcatalog_builder_rejects_invalid_manifest_via_existing_validationcatalog_builder_rejects_duplicate_core_id_even_with_new_version
Rust has no stable ABI. Dynamic libraries break across compiler
versions — vtable layout for dyn ComputePrimitive is not guaranteed
stable. In-process compilation means the user's crate compiles against
the same ergo-runtime dependency as the engine. Type safety is
compile-time, not runtime.
WASM adds a runtime layer (wasmtime/wasmer), limits what user code can
do (no filesystem, no network without host functions), and introduces a
value-marshaling boundary between WASM and native on every compute()
or execute() call. For v1, the user's code runs in the same process
as the engine.
ComputePrimitive::compute() must be deterministic given identical
inputs. ActionPrimitive::execute() returns outputs only — side
effects are manifest-declared, not implementation-emitted. The
execution model enforces correctness, not the loading mechanism.
Platform-specific (.so vs .dylib vs .dll). ABI-fragile across
Rust compiler versions. #[repr(C)] or vtable stability not
guaranteed. Would require a C FFI layer around the trait surface,
adding complexity without benefit.
Adds wasmtime/wasmer dependency. Value marshaling overhead on every
compute() / execute() call. User code cannot use Rust's full
ecosystem (no std::fs, no reqwest, no tokio without host function
plumbing). Sandboxing is a security feature Ergo does not need in v1 —
the user is running their own code on their own machine.
This decision selects the loading mechanism only. It does not decide:
- Build orchestration. How
ergo runtriggerscargo buildon the user's crate, finds the compiled artifact, and links it. That isfeat/ergo-initimplementation scope (EI-8). - Workspace layout for implementations. The
implementations/directory convention is defined inergo-init.md, not here. - Hot reloading. Not in scope. User rebuilds and reruns.
- Plugin versioning. The user's crate depends on a specific
ergo-runtimeversion. Compatibility is Cargo's job. - Future WASM support. This decision does not prevent adding WASM loading later as an alternative mechanism. It selects in-process Rust as the v1 mechanism.
User code runs in-process with the same permissions as the Ergo process. This is acceptable for v1 because:
- The user is running their own code on their own machine.
- Ergo is not a multi-tenant platform.
- The trait contracts enforce determinism and manifest-declared effects.
- If sandboxing is needed later (multi-tenant, untrusted code), WASM can be added as a v2 loading mechanism alongside in-process Rust.
Custom implementations register through the same validation pipeline as
stdlib. Tests in catalog.rs cover:
- Registration alongside core (
catalog_builder_admits_external_implementations_by_kind) - Manifest validation rejection (
catalog_builder_rejects_invalid_manifest_via_existing_validation) - Duplicate ID rejection (
catalog_builder_rejects_duplicate_core_id_even_with_new_version)
These prove the integration point works.
- Crate discovery from
implementations/ cargo buildinvocation- Compiled artifact linking into
CatalogBuilder
These are implementation-mechanism tests, not loading-decision tests.
- Move GW-EI8-1 into the closed gap lane and reference this decision.
- Proceed with EI-8 implementation in
feat/ergo-initusing in-process Rust crate registration againstCatalogBuilder.