feat(runtime): introduce the venue-adapter component kind#249
Conversation
37f8439 to
c4e57ac
Compare
c4e57ac to
b13657b
Compare
b13657b to
440b73a
Compare
440b73a to
194e8f1
Compare
194e8f1 to
6fb3e4d
Compare
6fb3e4d to
f547ca7
Compare
f547ca7 to
caab1dd
Compare
caab1dd to
b4cdd00
Compare
b4cdd00 to
2d5c02a
Compare
2d5c02a to
a2fe188
Compare
a2fe188 to
378311c
Compare
378311c to
c3f32b2
Compare
c3f32b2 to
e551d21
Compare
e551d21 to
2f23d71
Compare
2f23d71 to
a2b95dd
Compare
b61ed02 to
083a41e
Compare
083a41e to
0eee5c3
Compare
0eee5c3 to
e5c2d0c
Compare
e5c2d0c to
67a033b
Compare
67a033b to
058c3b6
Compare
058c3b6 to
9b5e208
Compare
9b5e208 to
5b0c585
Compare
5b0c585 to
e4c6020
Compare
e4c6020 to
76faaec
Compare
lgahdl
left a comment
There was a problem hiding this comment.
Solid scaffold for the adapter kind. Three items below.
| @@ -21,3 +51,39 @@ impl<T: RuntimeTypes> nexum::host::messaging::Host for HostState<T> { | |||
| Ok(vec![]) | |||
There was a problem hiding this comment.
query is not scope-checked. publish enforces topic_in_scope, but query takes _content_topic and returns Ok(vec![]) unconditionally. An adapter with messaging_topics = ["/nexum/1/cow-orders/proto"] can call query on /nexum/99/anything without refusal. This is inert while the backend is deferred, but when the Waku backend lands in 0.3 the scope hole will be live. The enforcement machinery already exists — a one-liner mirrors publish.
| Ok(vec![]) | |
| Ok(vec![]) | |
| // TODO(0.3): enforce topic_in_scope here before the Waku backend lands. |
| /// way as a module's `[capabilities.http].allow`. | ||
| #[serde(default)] | ||
| pub http_allow: Vec<String>, | ||
| /// Messaging content topics this adapter may reach. |
There was a problem hiding this comment.
"May reach" is inaccurate — only publish is gated; query is unscoped. Mirrors the more precise phrasing on HostState::messaging_topics.
| /// Messaging content topics this adapter may reach. | |
| /// Messaging content topics this adapter may publish to. |
| if kind != ModuleKind::VenueAdapter { | ||
| return Err(anyhow!( | ||
| "adapter {} declares module kind {kind:?}; an [[adapters]] entry requires \ | ||
| a module.toml with [module] kind = \"venue-adapter\"", | ||
| entry.path.display(), |
There was a problem hiding this comment.
When no module.toml exists, fallback_manifest() supplies ModuleKind::EventModule (the #[default]) and this error fires saying the adapter "declares" EventModule — but the adapter declared nothing; it has no manifest. The warn! two lines up says "no module.toml", which correctly diagnoses the situation, but the error that propagates to the caller implies the operator should fix a kind field that doesn't exist yet. Two-branch message:
- No manifest found → "no
module.tomlfound; an[[adapters]]entry requires one with[module] kind = \"venue-adapter\"" - Manifest present but wrong kind → current text ("declares module kind…")
| /// Project an opaque intent body onto the stable header guard policy | ||
| /// runs on. A pure derivation: no transport, no side effects, so the | ||
| /// host can derive and inspect a header before deciding to submit. |
There was a problem hiding this comment.
The purity claim is convention only. The nexum:adapter/venue-adapter world imports chain and messaging at the world level, making both available to derive-header — WIT has no per-function import restriction. An adapter can call chain.get_block() or messaging.publish() inside derive-header, and those side effects happen before the host runs guard policy (the router calls derive-header first). Before the router lands, document this as a known gap, or restrict derive-header to a sub-world that withholds chain+messaging.
| /// Project an opaque intent body onto the stable header guard policy | |
| /// runs on. A pure derivation: no transport, no side effects, so the | |
| /// host can derive and inspect a header before deciding to submit. | |
| /// Project an opaque intent body onto the stable header guard policy | |
| /// runs on. Intended as a pure derivation (no transport, no side effects) | |
| /// so the host can inspect a header before deciding to submit — but the | |
| /// WIT interface cannot enforce this; enforcement is by adapter-author | |
| /// convention. The router must assume `derive-header` may call chain or | |
| /// messaging and account for those side effects in its policy timing. | |
| derive-header: func(body: list<u8>) -> result<intent-header, venue-error>; |
24bf6f3 to
892cd0c
Compare
8fab2d5 to
b3bdd6f
Compare
892cd0c to
1f3c3f5
Compare
1f3c3f5 to
a0d9541
Compare
Add the second component kind alongside the event-module. A venue adapter speaks one venue's protocol over scoped transport only and exports the intent adapter face. - WIT: a nexum:intent/adapter interface (derive-header, submit, status, cancel) and a nexum:adapter/venue-adapter world that imports scoped chain and messaging, exports init plus the adapter face, and withholds the core-only primitives. - Bindings: a second bindgen path binding VenueAdapter beside EventModule, reusing the shared nexum:host interfaces via with. - Manifest: a module-kind discriminator defaulting to event-module, plus an adapter capability registry that recognises only the scoped transport. - Engine config: an [[adapters]] table carrying path, manifest, a per-adapter HTTP allowlist, and messaging-topic scopes. - Supervisor: adapters instantiate into supervised stores against a dedicated scoped linker, reusing the store, fuel, and restart machinery; the kind discriminator gates the load path. No routing yet. - Messaging: per-store content-topic scope enforced ahead of the deferred backend.
The bindgen module rustdoc split the `nexum:adapter/venue-adapter` code span across a line break after the slash, so rustdoc rendered it with a stray space. Rewrap so the token stays intact.
What
Introduces the second component kind, the venue adapter, alongside the existing event-module. A venue adapter speaks one venue's protocol only: it imports the scoped
nexum:host/chainandnexum:host/messaginginterfaces and exportsinitplus the newnexum:intent/adapterface, and deliberately withholds the core-only primitives (local-store, remote-store, identity, logging) an event-module gets.nexum:intent/adapterinterface (derive-header/submit/status/cancel), host-independent and using intent types only.nexum:adapterpackage with avenue-adapterworld that imports scopednexum:host/chain/messaging, exportsinitplus the adapter face, and withholds local-store/remote-store/identity/logging.@0.2.0/@0.1.0) so the new packages resolve.VenueAdapterbesideEventModuleinbindings.rs, reusingnexum:host/{types,chain,messaging}viawithso the chain/messagingHostimpls and the fault type are shared with event-modules.ModuleKinddiscriminator on[module]in the manifest, defaulting toevent-module.[[adapters]]engine config with per-adapter scopedhttp_allowandmessaging_topicsgrants, enforced ahead of the messaging backend stub.Why
Event-modules and venue adapters have different trust boundaries: an adapter should only ever be able to reach the venue it speaks to, never the local-store, remote-store, identity, or logging primitives an event-module gets. Modelling the adapter as its own component kind, with its own capability namespace and its own scoped transport grants in
engine.toml, lets the operator (not the adapter author) decide what a venue adapter may reach, instead of relying on the adapter's own manifest to self-limit.Testing
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace(217 tests pass)nexum:adapter/nexum:intent/adapterpackages (the crate compiling is the bindgen smoke test)Scope of change is limited to the
nexum-runtimecrate plus two WIT additions (wit/nexum-adapter/venue-adapter.wit,wit/nexum-intent/adapter.wit); no wasm modules touched, so module-level wasm build/clippy gates are not applicable.AI Assistance
Claude (Opus) used for the implementation.
Closes #228