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
28 changes: 14 additions & 14 deletions crates/nexum-macros/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,37 @@ struct Capability {
const KNOWN: &[Capability] = &[
Capability {
name: "chain",
import: Some("nexum:host/chain@0.2.0"),
import: Some("nexum:host/chain@0.1.0"),
packages: &[],
adapter: Some("chain"),
},
Capability {
name: "identity",
import: Some("nexum:host/identity@0.2.0"),
import: Some("nexum:host/identity@0.1.0"),
packages: &[],
adapter: None,
},
Capability {
name: "local-store",
import: Some("nexum:host/local-store@0.2.0"),
import: Some("nexum:host/local-store@0.1.0"),
packages: &[],
adapter: Some("local_store"),
},
Capability {
name: "remote-store",
import: Some("nexum:host/remote-store@0.2.0"),
import: Some("nexum:host/remote-store@0.1.0"),
packages: &[],
adapter: None,
},
Capability {
name: "messaging",
import: Some("nexum:host/messaging@0.2.0"),
import: Some("nexum:host/messaging@0.1.0"),
packages: &[],
adapter: None,
},
Capability {
name: "logging",
import: Some("nexum:host/logging@0.2.0"),
import: Some("nexum:host/logging@0.1.0"),
packages: &[],
adapter: Some("logging"),
},
Expand All @@ -78,7 +78,7 @@ const KNOWN: &[Capability] = &[
},
Capability {
name: "cow-api",
import: Some("shepherd:cow/cow-api@0.2.0"),
import: Some("shepherd:cow/cow-api@0.1.0"),
packages: &["shepherd-cow"],
adapter: None,
},
Expand Down Expand Up @@ -198,7 +198,7 @@ pub fn synthesize_venue(declared: &[String]) -> Result<ModuleWorld, String> {

let mut wit = String::from(
"package nexum:venue-world;\n\nworld venue-adapter {\n \
use nexum:host/types@0.2.0.{config, fault};\n\n",
use nexum:host/types@0.1.0.{config, fault};\n\n",
);
wit.push_str(&imports);
wit.push_str(
Expand Down Expand Up @@ -259,7 +259,7 @@ pub fn synthesize(declared: &[String]) -> Result<ModuleWorld, String> {

let mut wit = String::from(
"package nexum:module-world;\n\nworld module {\n \
use nexum:host/types@0.2.0.{config, event, fault};\n\n",
use nexum:host/types@0.1.0.{config, event, fault};\n\n",
);
wit.push_str(&imports);
wit.push_str(
Expand Down Expand Up @@ -294,7 +294,7 @@ mod tests {
#[test]
fn logging_only_world_imports_logging_alone() {
let world = synthesize(&["logging".to_string()]).unwrap();
assert!(world.wit.contains("import nexum:host/logging@0.2.0;"));
assert!(world.wit.contains("import nexum:host/logging@0.1.0;"));
assert!(!world.wit.contains("import nexum:host/chain"));
assert!(!world.wit.contains("shepherd:cow"));
assert_eq!(world.packages, MODULE_PACKAGES);
Expand All @@ -304,7 +304,7 @@ mod tests {
#[test]
fn cow_api_pulls_the_shepherd_cow_package() {
let world = synthesize(&["logging".to_string(), "cow-api".to_string()]).unwrap();
assert!(world.wit.contains("import shepherd:cow/cow-api@0.2.0;"));
assert!(world.wit.contains("import shepherd:cow/cow-api@0.1.0;"));
assert_eq!(world.packages, vec!["nexum-host", "shepherd-cow"]);
}

Expand Down Expand Up @@ -363,12 +363,12 @@ mod tests {
#[test]
fn venue_world_imports_only_declared_transport() {
let world = synthesize_venue(&["chain".to_string()]).unwrap();
assert!(world.wit.contains("import nexum:host/chain@0.2.0;"));
assert!(world.wit.contains("import nexum:host/chain@0.1.0;"));
assert!(!world.wit.contains("import nexum:host/messaging"));

let both = synthesize_venue(&["chain".to_string(), "messaging".to_string()]).unwrap();
assert!(both.wit.contains("import nexum:host/chain@0.2.0;"));
assert!(both.wit.contains("import nexum:host/messaging@0.2.0;"));
assert!(both.wit.contains("import nexum:host/chain@0.1.0;"));
assert!(both.wit.contains("import nexum:host/messaging@0.1.0;"));
}

#[test]
Expand Down
34 changes: 17 additions & 17 deletions crates/nexum-runtime/src/manifest/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,11 @@ impl CapabilityRegistry {
/// manifest declaration.
///
/// Examples:
/// - `"nexum:host/chain@0.2.0"` -> `Some("chain")`
/// - `"shepherd:cow/cow-api@0.2.0"` -> `Some("cow-api")` once the cow
/// - `"nexum:host/chain@0.1.0"` -> `Some("chain")`
/// - `"shepherd:cow/cow-api@0.1.0"` -> `Some("cow-api")` once the cow
/// namespace is registered
/// - `"wasi:http/outgoing-handler@0.2.12"` -> `Some("http")`
/// - `"nexum:host/types@0.2.0"` -> `None` (type-only, not a capability)
/// - `"nexum:host/types@0.1.0"` -> `None` (type-only, not a capability)
/// - `"wasi:io/streams@0.2.0"` -> `None`
pub fn wit_import_to_cap<'a>(&self, import_name: &'a str) -> Option<&'a str> {
let without_version = import_name.split('@').next().unwrap_or(import_name);
Expand Down Expand Up @@ -284,9 +284,9 @@ mod tests {
#[test]
fn wit_import_to_cap_nexum_host() {
let r = CapabilityRegistry::core();
assert_eq!(r.wit_import_to_cap("nexum:host/chain@0.2.0"), Some("chain"));
assert_eq!(r.wit_import_to_cap("nexum:host/chain@0.1.0"), Some("chain"));
assert_eq!(
r.wit_import_to_cap("nexum:host/local-store@0.2.0"),
r.wit_import_to_cap("nexum:host/local-store@0.1.0"),
Some("local-store")
);
}
Expand Down Expand Up @@ -318,11 +318,11 @@ mod tests {
fn wit_import_to_cap_shepherd_cow_needs_registration() {
// Core registry does not recognise the cow namespace.
let core = CapabilityRegistry::core();
assert_eq!(core.wit_import_to_cap("shepherd:cow/cow-api@0.2.0"), None);
assert_eq!(core.wit_import_to_cap("shepherd:cow/cow-api@0.1.0"), None);
// Once registered, it resolves.
let r = registry_with_cow();
assert_eq!(
r.wit_import_to_cap("shepherd:cow/cow-api@0.2.0"),
r.wit_import_to_cap("shepherd:cow/cow-api@0.1.0"),
Some("cow-api")
);
}
Expand Down Expand Up @@ -376,7 +376,7 @@ mod tests {
fn enforce_passes_when_caps_absent() {
// 0.1-fallback: no capabilities section -> all imports allowed
let loaded = manifest_no_caps();
let imports = ["nexum:host/chain@0.2.0", "nexum:host/remote-store@0.2.0"];
let imports = ["nexum:host/chain@0.1.0", "nexum:host/remote-store@0.1.0"];
let r = registry_with_cow();
assert!(enforce_capabilities(&loaded, imports.into_iter(), &r).is_ok());
}
Expand All @@ -385,8 +385,8 @@ mod tests {
fn enforce_passes_when_all_imports_declared() {
let loaded = manifest_with_caps(&["chain", "cow-api"], &["http"]);
let imports = [
"nexum:host/chain@0.2.0",
"shepherd:cow/cow-api@0.2.0",
"nexum:host/chain@0.1.0",
"shepherd:cow/cow-api@0.1.0",
"wasi:http/outgoing-handler@0.2.12",
"wasi:io/streams@0.2.0", // non-http wasi is always skipped
];
Expand All @@ -398,7 +398,7 @@ mod tests {
fn enforce_rejects_wasi_http_import_without_declaration() {
let loaded = manifest_with_caps(&["chain"], &[]);
let imports = [
"nexum:host/chain@0.2.0",
"nexum:host/chain@0.1.0",
"wasi:http/outgoing-handler@0.2.12",
];
let r = registry_with_cow();
Expand Down Expand Up @@ -428,7 +428,7 @@ mod tests {
fn enforce_rejects_undeclared_import() {
let loaded = manifest_with_caps(&["chain"], &[]);
// module imports remote-store but didn't declare it
let imports = ["nexum:host/chain@0.2.0", "nexum:host/remote-store@0.2.0"];
let imports = ["nexum:host/chain@0.1.0", "nexum:host/remote-store@0.1.0"];
let r = registry_with_cow();
let err = enforce_capabilities(&loaded, imports.into_iter(), &r).unwrap_err();
let CapabilityError::Undeclared(v) = err else {
Expand All @@ -440,7 +440,7 @@ mod tests {
#[test]
fn enforce_optional_caps_are_also_allowed() {
let loaded = manifest_with_caps(&["chain"], &["remote-store"]);
let imports = ["nexum:host/chain@0.2.0", "nexum:host/remote-store@0.2.0"];
let imports = ["nexum:host/chain@0.1.0", "nexum:host/remote-store@0.1.0"];
let r = registry_with_cow();
assert!(enforce_capabilities(&loaded, imports.into_iter(), &r).is_ok());
}
Expand All @@ -463,17 +463,17 @@ mod tests {
#[test]
fn adapter_registry_maps_transport_imports_but_not_core_only() {
let r = CapabilityRegistry::adapter();
assert_eq!(r.wit_import_to_cap("nexum:host/chain@0.2.0"), Some("chain"));
assert_eq!(r.wit_import_to_cap("nexum:host/chain@0.1.0"), Some("chain"));
assert_eq!(
r.wit_import_to_cap("nexum:host/messaging@0.2.0"),
r.wit_import_to_cap("nexum:host/messaging@0.1.0"),
Some("messaging")
);
assert_eq!(
r.wit_import_to_cap("wasi:http/outgoing-handler@0.2.12"),
Some("http")
);
// A core-only interface is not a recognised adapter capability.
assert_eq!(r.wit_import_to_cap("nexum:host/local-store@0.2.0"), None);
assert_eq!(r.wit_import_to_cap("nexum:host/local-store@0.1.0"), None);
}

#[test]
Expand Down Expand Up @@ -575,7 +575,7 @@ mod tests {
let loaded = manifest_no_caps();
let r = registry_with_cow();
assert!(
enforce_capabilities(&loaded, ["nexum:host/remote-store@0.2.0"].into_iter(), &r)
enforce_capabilities(&loaded, ["nexum:host/remote-store@0.1.0"].into_iter(), &r)
.is_ok()
);
assert!(enforce_capabilities(&loaded, ["wasi:io/streams@0.2.6"].into_iter(), &r).is_ok());
Expand Down
2 changes: 1 addition & 1 deletion crates/nexum-runtime/src/manifest/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct CapabilityViolation {
/// Capability name (e.g. `"remote-store"`).
pub capability: String,
/// Full WIT import name as it appeared in the component (e.g.
/// `"nexum:host/remote-store@0.2.0"`).
/// `"nexum:host/remote-store@0.1.0"`).
pub wit_import: String,
}

Expand Down
17 changes: 7 additions & 10 deletions docs/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ Two project names look similar but mean different things - keeping them straight
| Term | What it is | Where you find it |
|---|---|---|
| **engine** (`nexum`) | A concrete *implementation* that loads and runs WASM components. The 0.2 reference engine is a wasmtime-based server daemon. Mobile / browser / embedded engines could exist later - each is a separate engine. | `crates/nexum-runtime/`, the `nexum` binary, `cargo run -p nexum-cli` |
| **host** (`nexum:host`) | The WIT *contract* - the set of host-imported interfaces (chain, identity, local-store, etc.), types, and worlds that every engine must implement and every module imports. The contract is one; engines are many. | `wit/nexum-host/`, `package nexum:host@0.2.0`, Rust path `nexum::host::*` |
| **host** (`nexum:host`) | The WIT *contract* - the set of host-imported interfaces (chain, identity, local-store, etc.), types, and worlds that every engine must implement and every module imports. The contract is one; engines are many. | `wit/nexum-host/`, `package nexum:host@0.1.0`, Rust path `nexum::host::*` |

The relationship: an engine *implements* `nexum:host` so that modules *built against* `nexum:host` can run on it. The `nexum:host` package itself does not run anything - it's a specification. When this doc says "the host", it means whichever engine the module currently runs on, as seen through the `nexum:host` contract.

The reference engine ships as two crates: the `nexum-runtime` library (embeddable, no CLI surface) and the `nexum` binary in `crates/nexum-cli`, a thin consumer of it. A Rust embedder skips the binary entirely, constructs an `EngineConfig` in code, and calls `nexum_runtime::bootstrap::run_from_config`. See `crates/nexum-runtime/examples/embed.rs` for a minimal end-to-end example.

> **Upgrading from 0.1?** See the [Migration Guide](migration/0.1-to-0.2.md) for the full rename table (`web3:runtime` → `nexum:host`, `csn` → `chain`, `msg` → `messaging`, `headless-module` → `event-module`, etc.), the per-interface typed error model over the shared `fault` vocabulary, and the manifest-driven capability negotiation introduced in 0.2.

## Architecture

```mermaid
Expand Down Expand Up @@ -96,7 +94,7 @@ In addition to the six core primitives, 0.2 introduces one optional capability t

Time and secure randomness are WASI concerns rather than Nexum capabilities: `wasi:clocks` and `wasi:random` are linked into every module store ambiently.

0.2 also publishes (but does not yet host) the experimental **`query-module`** world for request/response modules (wallet rule evaluators, signature validators, pricing oracles). The WIT is stable enough to target with `MockHost` tests; production host support lands in 0.3. See the migration guide for the full WIT.
0.2 also publishes (but does not yet host) the experimental **`query-module`** world for request/response modules (wallet rule evaluators, signature validators, pricing oracles). The WIT is stable enough to target with `MockHost` tests; production host support lands in 0.3.

## WIT Worlds

Expand All @@ -121,7 +119,7 @@ graph TB

```
// Universal layer - any platform, any blockchain app
package nexum:host@0.2.0
package nexum:host@0.1.0

world event-module {
import chain - consensus access (JSON-RPC passthrough)
Expand All @@ -136,7 +134,7 @@ world event-module {
}

// CoW Protocol extension
package shepherd:cow@0.2.0
package shepherd:cow@0.1.0

world shepherd {
include event-module
Expand All @@ -158,7 +156,7 @@ The world imports no WASI interfaces. `wasi:clocks` and `wasi:random` are linked
|---------|--------|---------|
| Language | Rust | 1.90+ |
| WASM runtime | wasmtime (Component Model) | 45.x |
| API contract | WIT (`nexum:host@0.2.0`, `shepherd:cow@0.2.0`) | - |
| API contract | WIT (`nexum:host@0.1.0`, `shepherd:cow@0.1.0`) | - |
| Guest bindings | wit-bindgen | 0.57.x |
| Async | Tokio | - |
| Ethereum RPC | alloy | 1.5.x |
Expand Down Expand Up @@ -311,7 +309,7 @@ Tower layer stack per chain: timeout -> retry (exponential + jitter) -> rate lim

### Error Model

In 0.2 each interface declares its own typed error and they share one payload-bearing `fault` vocabulary for the cross-domain cases. `fault` has seven cases: `unsupported(string)`, `unavailable(string)`, `denied(string)`, `rate-limited(rate-limit)`, `timeout`, `invalid-input(string)`, and `internal(string)`. Interfaces with nothing to add report `fault` directly (identity, local-store, remote-store, messaging, and the module exports); a richer interface embeds `fault` as one case of its own variant and adds the cases only it needs (`chain-error` adds an `rpc` case carrying the node code and decoded revert bytes). Modules match on the typed variant for retry/backoff decisions; the per-protocol error types from 0.1 (`json-rpc-error`, `msg-error`, `store-error`, `api-error`) are gone. See [ADR-0011](adr/0011-per-interface-typed-errors.md) for the model and the [migration guide](migration/0.1-to-0.2.md#2-error-model-unification-both) for the embedder mapping.
In 0.2 each interface declares its own typed error and they share one payload-bearing `fault` vocabulary for the cross-domain cases. `fault` has seven cases: `unsupported(string)`, `unavailable(string)`, `denied(string)`, `rate-limited(rate-limit)`, `timeout`, `invalid-input(string)`, and `internal(string)`. Interfaces with nothing to add report `fault` directly (identity, local-store, remote-store, messaging, and the module exports); a richer interface embeds `fault` as one case of its own variant and adds the cases only it needs (`chain-error` adds an `rpc` case carrying the node code and decoded revert bytes). Modules match on the typed variant for retry/backoff decisions; the per-protocol error types from 0.1 (`json-rpc-error`, `msg-error`, `store-error`, `api-error`) are gone. See [ADR-0011](adr/0011-per-interface-typed-errors.md) for the model.

### Observability

Expand Down Expand Up @@ -379,8 +377,7 @@ shepherd/
├── operations/ Runbooks, E2E reports, load reports, baselines
├── production.md Operator handbook
├── sdk.md Module-author entry point (shipped SDK reference)
├── tutorial-first-module.md
└── migration/0.1-to-0.2.md
└── tutorial-first-module.md
```

The SDK split is in place: `nexum-sdk` carries the universal surface and `shepherd-sdk` layers the CoW domain on top, with no re-export between them. Shipping a `cargo-nexum` subcommand for module authors remains future direction.
10 changes: 5 additions & 5 deletions docs/01-runtime-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ let bindings = EventModule::instantiate_pre(&mut store, &pre)?;

Nexum uses a two-layer WIT architecture. The **universal** package `nexum:host` defines platform-agnostic interfaces and the `event-module` world. The **CoW-specific** package `shepherd:cow` extends it with CoW Protocol interfaces and the `shepherd` world.

### Universal Package: `nexum:host@0.2.0`
### Universal Package: `nexum:host@0.1.0`

The `nexum:host` package is the single source of truth for the universal host-guest contract. It defines a custom world with **no WASI imports**:

```wit
package nexum:host@0.2.0;
package nexum:host@0.1.0;

interface types {
type chain-id = u64;
Expand Down Expand Up @@ -301,14 +301,14 @@ world event-module {
}
```

In addition to the six core imports, 0.2 publishes one additive optional capability - `http` (allowlisted outbound HTTP) - which modules declare in their `module.toml` `[capabilities]` section. The declaration is a manifest concern only: the capability is serviced by the standard `wasi:http/outgoing-handler` interface, not a `nexum:host` one. The migration guide carries the details. 0.2 also publishes the experimental **`query-module`** world for request/response modules; the WIT is stable but no host implementation ships in 0.2, so it's a target for `MockHost` testing only.
In addition to the six core imports, 0.2 publishes one additive optional capability - `http` (allowlisted outbound HTTP) - which modules declare in their `module.toml` `[capabilities]` section. The declaration is a manifest concern only: the capability is serviced by the standard `wasi:http/outgoing-handler` interface, not a `nexum:host` one. 0.2 also publishes the experimental **`query-module`** world for request/response modules; the WIT is stable but no host implementation ships in 0.2, so it's a target for `MockHost` testing only.

### CoW-Specific Package: `shepherd:cow@0.2.0`
### CoW-Specific Package: `shepherd:cow@0.1.0`

The `shepherd:cow` package extends the universal world with CoW Protocol interfaces. In 0.2 the two 0.1 interfaces (`cow` + `order`) merge into a single `cow-api` interface to eliminate the `cow::cow::request` triple-stutter:

```wit
package shepherd:cow@0.2.0;
package shepherd:cow@0.1.0;

interface cow-api {
use nexum:host/types.{chain-id, fault};
Expand Down
Loading
Loading