Design question raised while reviewing #447 (the extension event seam). Needs a decision (WIT/ABI-breaking). Target: L1 nexum-runtime WIT + L2 videre. Related: #517 (kind-collision), #385 (zero-leak gate).
Problem
The event payload delivered to modules is a closed variant in the venue-agnostic core (wit/nexum-host/types.wit, nexum:host@0.1.0), and it hardcodes a venue concept:
variant event {
block(block),
chain-logs(chain-logs),
tick(tick),
message(message),
intent-status(intent-status-update), // venue/receipt vocabulary, in L1
}
record intent-status-update { venue: string, receipt: list<u8>, status: list<u8> }
So:
- L1's venue-agnostic claim leaks at the WIT level (
venue/receipt live in nexum:host), and check-venue-agnostic.sh does not catch it (no intent-status allowlist or flag - it slips the scan).
- The seam is not actually extensible: a new extension cannot add its own event kind without editing the core
event enum. The host side already routes by a generic ExtensionEvent.kind: &str, but the WIT payload is a closed core enum - the generality stops at the ABI boundary.
Proposed change
Component-model has no traits across the WASM ABI, so the idiom is native primitives + one opaque escape hatch:
variant event {
block(block), chain-logs(chain-logs), tick(tick), message(message),
custom(custom-event),
}
record custom-event { kind: string, payload: list<u8> } // kind already exists as ExtensionEvent.kind
- Move
intent-status / intent-status-update out of nexum:host into videre's WIT layer (videre:types already exists).
- videre emits
event::custom({ kind: "intent-status", payload: <borsh IntentStatusUpdate> }); videre-sdk decodes custom payloads by kind into a typed venue-module event.
- Core keeps only genuine runtime primitives plus the one generic channel.
Counter-argument (record, then decide)
nexum is an intent runtime, so "intent lifecycle status" could be argued a legitimate core primitive like block/tick, and the status body is already opaque. But the envelope carries venue/receipt, and the L1/L2 split exists precisely to keep venue vocabulary in L2, so extraction is the more consistent call.
Acceptance criteria (if extraction is chosen)
nexum:host event carries only block/chain-logs/tick/message + a generic custom(kind, payload); no venue/receipt/intent-status in L1 WIT.
- videre defines the intent-status payload in its own WIT/SDK and rides
custom.
check-venue-agnostic.sh gains a WIT-vocabulary check so a future core leak of this class fails the gate.
- A second extension can introduce a distinct event kind with no core WIT change.
Timing - do this before the M5 split
Far cheaper as one atomic monorepo change than after the three-repo split (M5, milestone #4), and it is on-theme for M2 (the venue-agnostic host), so it should land in M2 rather than slip:
- Now (pre-M5, monorepo): one PR touches
wit/nexum-host/types.wit (drop intent-status, add custom), the L1 event dispatch, wit/videre-* + videre-host + videre-sdk (the custom decode), and any modules. One repo, one CI, one review.
- After M5 (split into
nullislabs/nexum-runtime L1 + nullislabs/videre-nexum-module L2 + shepherd L3): a coordinated rev-pinned rollout - (1) change the L1 WIT + dispatch in nexum-runtime, cut a rev; (2) bump the rev pin in videre-nexum-module, move intent-status into videre's WIT/SDK, ride custom, cut a rev; (3) bump both pins in shepherd. Three repos, three CIs, staged rev-pinning for a single ABI change.
Milestoned to M2 so the venue-agnostic-host milestone is not closed while its own WIT still carries venue/receipt. If M2 cannot absorb the work, it must at minimum land before M5.
Blast radius if done pre-split (measured 2026-07-21)
Smaller than the foundational-WIT-change framing suggests:
- Semantic edit sites for the core
Event::IntentStatus variant: 2 - the emit site videre-host/src/lib.rs and the E2E test helper videre-host/tests/platform.rs. That is the entire hand-written surface that moves to custom.
- Module consumers: 0. No shipped module matches
Event::IntentStatus in on-event today (twap-monitor self-tracks); the only consumer is the E2E test. So the risky module-side decode work can be minimal or deferred until a module actually subscribes.
- The large reference counts are a red herring: the ~39
IntentStatus refs in registry.rs (and the venue-SDK/cow-venue sites) are the venue-side status enum (Open/Fulfilled/…), which stays in videre - not the core event variant.
- Bindings regenerate (wit-bindgen); they are not hand-edited, so they never conflict.
- Downstream conflict surface is bounded: ~3-4 later cars touch
wit/nexum-host/types.wit and ~3-4 touch videre-host/src/lib.rs between here and the tip - localized regions (the event variant block and the emit block), mergiraf-friendly, not a sprawling every-file conflict.
Net: shallow-to-moderate rebase impact. The real sequencing consideration is not conflict depth but landing it alongside M3 (Videre SDK), where the typed custom-payload decode naturally lives - and comfortably before the M6 vocabulary freeze.
Problem
The event payload delivered to modules is a closed variant in the venue-agnostic core (
wit/nexum-host/types.wit,nexum:host@0.1.0), and it hardcodes a venue concept:variant event { block(block), chain-logs(chain-logs), tick(tick), message(message), intent-status(intent-status-update), // venue/receipt vocabulary, in L1 } record intent-status-update { venue: string, receipt: list<u8>, status: list<u8> }So:
venue/receiptlive innexum:host), andcheck-venue-agnostic.shdoes not catch it (nointent-statusallowlist or flag - it slips the scan).eventenum. The host side already routes by a genericExtensionEvent.kind: &str, but the WIT payload is a closed core enum - the generality stops at the ABI boundary.Proposed change
Component-model has no traits across the WASM ABI, so the idiom is native primitives + one opaque escape hatch:
variant event { block(block), chain-logs(chain-logs), tick(tick), message(message), custom(custom-event), } record custom-event { kind: string, payload: list<u8> } // kind already exists as ExtensionEvent.kindintent-status/intent-status-updateout ofnexum:hostinto videre's WIT layer (videre:typesalready exists).event::custom({ kind: "intent-status", payload: <borsh IntentStatusUpdate> });videre-sdkdecodescustompayloads by kind into a typed venue-module event.Counter-argument (record, then decide)
nexum is an intent runtime, so "intent lifecycle status" could be argued a legitimate core primitive like
block/tick, and the status body is already opaque. But the envelope carriesvenue/receipt, and the L1/L2 split exists precisely to keep venue vocabulary in L2, so extraction is the more consistent call.Acceptance criteria (if extraction is chosen)
nexum:hosteventcarries onlyblock/chain-logs/tick/message+ a genericcustom(kind, payload); novenue/receipt/intent-statusin L1 WIT.custom.check-venue-agnostic.shgains a WIT-vocabulary check so a future core leak of this class fails the gate.Timing - do this before the M5 split
Far cheaper as one atomic monorepo change than after the three-repo split (M5, milestone #4), and it is on-theme for M2 (the venue-agnostic host), so it should land in M2 rather than slip:
wit/nexum-host/types.wit(dropintent-status, addcustom), the L1 event dispatch,wit/videre-*+ videre-host + videre-sdk (the custom decode), and any modules. One repo, one CI, one review.nullislabs/nexum-runtimeL1 +nullislabs/videre-nexum-moduleL2 + shepherd L3): a coordinated rev-pinned rollout - (1) change the L1 WIT + dispatch in nexum-runtime, cut a rev; (2) bump the rev pin in videre-nexum-module, moveintent-statusinto videre's WIT/SDK, ridecustom, cut a rev; (3) bump both pins in shepherd. Three repos, three CIs, staged rev-pinning for a single ABI change.Milestoned to M2 so the venue-agnostic-host milestone is not closed while its own WIT still carries
venue/receipt. If M2 cannot absorb the work, it must at minimum land before M5.Blast radius if done pre-split (measured 2026-07-21)
Smaller than the foundational-WIT-change framing suggests:
Event::IntentStatusvariant: 2 - the emit sitevidere-host/src/lib.rsand the E2E test helpervidere-host/tests/platform.rs. That is the entire hand-written surface that moves tocustom.Event::IntentStatusinon-eventtoday (twap-monitor self-tracks); the only consumer is the E2E test. So the risky module-side decode work can be minimal or deferred until a module actually subscribes.IntentStatusrefs inregistry.rs(and the venue-SDK/cow-venue sites) are the venue-side status enum (Open/Fulfilled/…), which stays in videre - not the core event variant.wit/nexum-host/types.witand ~3-4 touchvidere-host/src/lib.rsbetween here and the tip - localized regions (theeventvariant block and the emit block), mergiraf-friendly, not a sprawling every-file conflict.Net: shallow-to-moderate rebase impact. The real sequencing consideration is not conflict depth but landing it alongside M3 (Videre SDK), where the typed
custom-payload decode naturally lives - and comfortably before the M6 vocabulary freeze.