Skip to content

runtime: make the module event type extensible - extract intent-status behind a generic custom(kind, payload) variant #518

Description

@mfw78

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:

  1. 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).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions