Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion crates/nexum-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ const VENUE_EXPORTS: [&str; 5] = ["derive_header", "quote", "submit", "status",
/// Apply to an inherent `impl` block whose associated functions are the
/// adapter face: `derive_header`, `quote`, `submit`, `status`, `cancel`
/// (all required, from `videre:venue/adapter`), plus an optional `init`
/// (absent means a no-op). Each takes and returns the per-cdylib
/// (absent means a no-op) and an optional `body_versions` (absent
/// declares none). Each takes and returns the per-cdylib
/// wit-bindgen payloads for its signature. The macro reads the crate's
/// `module.toml`, synthesizes a per-component world exporting the
/// adapter face and importing exactly the manifest's declared scoped
Expand Down Expand Up @@ -382,6 +383,23 @@ pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream {
};
let inline_world = &venue_world.wit;

// `body-versions` is a required adapter export; when the adapter
// omits it, declare none. Install asserts the export equals the
// manifest `[venue] body_versions` set.
let body_versions_impl = if defines("body_versions") {
quote! {
fn body_versions() -> ::std::vec::Vec<u32> {
<#self_ty>::body_versions()
}
}
} else {
quote! {
fn body_versions() -> ::std::vec::Vec<u32> {
::std::vec::Vec::new()
}
}
};

// `init` is a required world export; when the adapter omits it the
// config is bound but unused, so drop it to stay warning-clean.
let init_impl = if defines("init") {
Expand Down Expand Up @@ -424,6 +442,8 @@ pub fn venue(attr: TokenStream, item: TokenStream) -> TokenStream {
}

impl exports::videre::venue::adapter::Guest for __NexumVenueAdapterExport {
#body_versions_impl

fn derive_header(
body: ::std::vec::Vec<u8>,
) -> ::core::result::Result<
Expand Down
50 changes: 47 additions & 3 deletions crates/nexum-runtime/src/host/extension.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! The extension seam: what one extension contributes to the host - a
//! namespace, a capability namespace, a linker hook, an optional host
//! service, an optional provider kind, and optional event sources.
//! service, an optional provider kind, optional event sources, and
//! optional install predicates over the manifest sections it claims.
//! Assembled at the composition root and threaded into every module
//! linker.

Expand All @@ -20,7 +21,7 @@ use crate::engine_config::EngineConfig;
use crate::host::actor::Liveness;
use crate::host::component::RuntimeTypes;
use crate::host::state::HostState;
use crate::manifest::NamespaceCaps;
use crate::manifest::{ExtensionSections, NamespaceCaps};

/// One runtime extension. A module that imports an extension interface
/// boots only if the linker entry AND the capability namespace are both
Expand Down Expand Up @@ -50,6 +51,32 @@ pub trait Extension<T: RuntimeTypes>: Send + Sync + 'static {
None
}

/// Manifest section names this extension claims. A non-core section
/// no wired extension claims is refused at boot.
fn manifest_sections(&self) -> &'static [&'static str] {
&[]
}

/// Admit one provider at install, over its opaque manifest sections.
/// Runs before compilation; an `Err` refuses the install fail-fast.
fn admit_provider(&self, provider: &str, sections: &ExtensionSections) -> anyhow::Result<()> {
let _ = (provider, sections);
Ok(())
}

/// Admit one worker at install, over its own and the loaded
/// providers' opaque manifest sections. Runs before compilation; an
/// `Err` refuses the install fail-fast.
fn admit_worker(
&self,
worker: &str,
sections: &ExtensionSections,
providers: &[ProviderManifest],
) -> anyhow::Result<()> {
let _ = (worker, sections, providers);
Ok(())
}

/// Manifest subscription kinds this extension's event sources emit.
/// A `[[subscription]]` entry of any other non-core kind is refused
/// at boot.
Expand Down Expand Up @@ -151,7 +178,8 @@ pub trait ProviderKind<T: RuntimeTypes>: Send + Sync + 'static {

/// One provider instance ready to install: the compiled component, the
/// linker the kind's [`ProviderKind::link`] populated, the supervised
/// store, the manifest `[config]`, and the per-call fuel budget.
/// store, the manifest `[config]` and extension sections, and the
/// per-call fuel budget.
pub struct ProviderInstance<'a, T: RuntimeTypes> {
/// Compiled provider component.
pub component: &'a Component,
Expand All @@ -161,13 +189,29 @@ pub struct ProviderInstance<'a, T: RuntimeTypes> {
pub store: Store<HostState<T>>,
/// Manifest `[config]` handed to the guest `init`.
pub config: Vec<(String, String)>,
/// The provider's extension-owned manifest sections, so a kind can
/// hold the instance to its manifest claims at install.
pub sections: &'a ExtensionSections,
/// Fuel budget applied before each routed guest call.
pub fuel_per_call: u64,
/// Shared liveness the installed instance reports traps on and the
/// supervisor's restart sweep reads.
pub liveness: Liveness,
}

/// One loaded provider as [`Extension::admit_worker`] sees it: its
/// namespace, registered kind, and opaque manifest sections. Manifest
/// data only, so the predicate is static and liveness-independent.
#[derive(Clone, Debug)]
pub struct ProviderManifest {
/// The provider's namespace: its manifest name.
pub name: String,
/// Registered kind spelling.
pub kind: &'static str,
/// The provider's extension-owned manifest sections.
pub sections: ExtensionSections,
}

/// Outcome of one provider install.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Installed {
Expand Down
37 changes: 37 additions & 0 deletions crates/nexum-runtime/src/manifest/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,43 @@ scope = 7
assert!(err.to_string().contains("must be a string"), "{err}");
}

/// A non-core top-level section parses into the opaque extension
/// map: the runtime carries it verbatim and ascribes it no meaning.
#[test]
fn load_parses_extension_sections_opaquely() {
let toml = r#"
[module]
name = "keeper"

[venue]
body_version = 2

[[subscription]]
kind = "block"
chain_id = 1
"#;
let manifest: Manifest = toml::from_str(toml).expect("parse");
assert_eq!(manifest.module.name, "keeper");
assert_eq!(manifest.subscriptions.len(), 1);
assert_eq!(manifest.extensions.len(), 1);
let venue = manifest.extensions.get("venue").expect("venue section");
assert_eq!(
venue.get("body_version").and_then(toml::Value::as_integer),
Some(2),
);
}

/// A manifest without extension sections carries an empty map.
#[test]
fn load_defaults_to_no_extension_sections() {
let toml = r#"
[module]
name = "plain"
"#;
let manifest: Manifest = toml::from_str(toml).expect("parse");
assert!(manifest.extensions.is_empty());
}

#[test]
fn load_parses_cron_subscription() {
let toml = r#"
Expand Down
1 change: 1 addition & 0 deletions crates/nexum-runtime/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod types;
pub(crate) use capabilities::enforce_capabilities;
pub use capabilities::{CapabilityRegistry, NamespaceCaps};
pub(crate) use load::{fallback_manifest, host_allowed, load};
pub use types::ExtensionSections;
pub(crate) use types::{ComponentKind, LoadedManifest, ResourceSection, Subscription};
// CapabilityViolation, ParseError, and the *Section structs are
// reachable through these functions' return / argument types;
Expand Down
10 changes: 10 additions & 0 deletions crates/nexum-runtime/src/manifest/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,18 @@ pub struct Manifest {
/// parsed and ignored (deferred to 0.3).
#[serde(default, rename = "subscription")]
pub subscriptions: Vec<Subscription>,
/// Extension-owned sections: every non-core top-level key, parsed
/// opaquely. The runtime ascribes them no meaning; it routes them
/// to the wired extensions' install predicates, and a section no
/// extension claims is refused at boot.
#[serde(flatten)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[serde(flatten)] into a BTreeMap<String, toml::Value> has no uniqueness check across wired extensions' claimed section names — enforce_extension_sections only verifies a section is claimed by at least one extension, not by at most one. If a second extension later picks the same section name (e.g. another one also wants "venue" for something unrelated), nothing detects the collision; whichever extension's parser runs first silently wins. Worth asserting uniqueness of manifest_sections() across the wired extension set at boot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed still valid at HEAD. enforce_extension_sections (supervisor.rs:358) is still .any(|ext| ext.manifest_sections().contains(...)) - at-least-one, never at-most-one. Same collision class as the event-seam dup-kind gap, so I folded it into #517: one boot-time uniqueness pass covers subscription kinds and manifest section names, mirroring the service-namespace bail HostServices::from_extensions already does.

pub extensions: ExtensionSections,
}

/// Extension-owned manifest sections, keyed by top-level name. Opaque
/// to the runtime; each claiming extension parses its own.
pub type ExtensionSections = BTreeMap<String, toml::Value>;

/// One `[[subscription]]` table in `module.toml`.
///
/// The discriminator is the `kind` field; remaining fields are
Expand Down
Loading
Loading