diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index debe6ad1..6207c455 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,14 +144,13 @@ jobs: AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar run: cargo check --workspace --all-features --locked --target aarch64-unknown-linux-gnu - # Advisory zero-leak guard that nexum-runtime stays venue-agnostic: crate - # graph, charter symbol scan, and nexum:host WIT leaf-ness - # (scripts/check-venue-agnostic.sh). `continue-on-error` keeps this a - # signal, not a gate; it flips to a required check at the physical repo cut. + # Blocking zero-leak gate: host-layer crate graphs stay venue-free, the + # runtime Rust sources carry no charter symbol and no privileged router + # field, and nexum:host names no foreign WIT package and resolves as a + # leaf (scripts/check-venue-agnostic.sh). venue-agnostic: - name: venue-agnostic (advisory) + name: venue-agnostic runs-on: ubuntu-latest - continue-on-error: true steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: ./.github/actions/rust-setup diff --git a/crates/videre-host/tests/zero_leak.rs b/crates/videre-host/tests/zero_leak.rs new file mode 100644 index 00000000..43f32ea5 --- /dev/null +++ b/crates/videre-host/tests/zero_leak.rs @@ -0,0 +1,155 @@ +//! Zero-leak oracle: the host boots the echo venue and routes a worker's +//! submission purely through the generic extension seam, while the +//! `nexum-runtime` crate graph reaches no venue-shaped crate. + +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; + +use nexum_runtime::bindings::nexum; +use nexum_runtime::engine_config::{AdapterEntry, EngineConfig, ModuleEntry}; +use nexum_runtime::host::component::ChainMethod; +use nexum_runtime::host::extension::Extension; +use nexum_runtime::supervisor::{Supervisor, build_linker}; +use nexum_runtime::test_utils::{ + MockChainProvider, MockStateStore, MockTypes, mock_components_from, +}; +use videre_host::{VenueRegistry, platform}; + +/// Workspace-root-relative path. `CARGO_MANIFEST_DIR` is +/// `crates/videre-host`; two parents up is the workspace root. +fn workspace_path(relative: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("crates dir") + .parent() + .expect("workspace root") + .join(relative) +} + +/// Path to a module's `.wasm` artefact under the workspace target dir. +/// A missing artefact is a hard failure under CI (the gate may not skip +/// itself) and a soft skip locally. +fn module_wasm_or_skip(module_name: &str) -> Option { + let artifact = module_name.replace('-', "_"); + let p = workspace_path(&format!("target/wasm32-wasip2/release/{artifact}.wasm")); + if p.exists() { + return Some(p); + } + assert!( + std::env::var_os("CI").is_none(), + "{} must be prebuilt in CI", + p.display() + ); + eprintln!( + "SKIP: {} not found - build with `cargo build -p {module_name} --target wasm32-wasip2 --release`", + p.display() + ); + None +} + +/// The boot oracle: the venue adapter installs and a worker's submission +/// reaches it, with the platform supplied only as a generic extension. +#[tokio::test] +async fn e2e_echo_venue_boots_and_submits_through_the_generic_seam() { + let (Some(adapter_wasm), Some(module_wasm)) = ( + module_wasm_or_skip("echo-venue"), + module_wasm_or_skip("echo-client"), + ) else { + return; + }; + + // The adapter reads eth_blockNumber on submit to justify its `chain` + // grant; program the mock so that read succeeds. + let chain = MockChainProvider::new(); + chain.on_method(ChainMethod::EthBlockNumber, "\"0x1\""); + let components = mock_components_from(chain, MockStateStore::new()); + + let mut engine_config = wasmtime::Config::new(); + engine_config.wasm_component_model(true); + engine_config.consume_fuel(true); + let engine = wasmtime::Engine::new(&engine_config).expect("wasmtime engine"); + + let config = EngineConfig { + adapters: vec![AdapterEntry { + path: adapter_wasm, + manifest: Some(workspace_path("modules/examples/echo-venue/module.toml")), + http_allow: Vec::new(), + messaging_topics: Vec::new(), + }], + modules: vec![ModuleEntry { + path: module_wasm, + manifest: Some(workspace_path("modules/examples/echo-client/module.toml")), + }], + ..Default::default() + }; + let extensions: Vec>> = vec![Arc::new(platform(&config))]; + let linker = build_linker::(&engine, &extensions).expect("build_linker"); + + let mut supervisor = + Supervisor::boot(&engine, &linker, &config, &components, &extensions, None) + .await + .expect("boot"); + assert_eq!(supervisor.adapter_alive_count(), 1, "echo-venue installed"); + assert_eq!(supervisor.alive_count(), 1, "echo-client alive"); + + // One block drives the worker's on_block submission; the registry the + // extension published on the service map observes the accepted receipt. + let block = nexum::host::types::Block { + chain_id: 1, + number: 19_000_000, + hash: vec![0xab; 32], + timestamp: 1_700_000_000_000, + }; + assert_eq!(supervisor.dispatch_block(block).await, 1); + let registry = supervisor + .services() + .get::(VenueRegistry::NAMESPACE) + .expect("registry service"); + let updates = registry.poll_status_transitions().await; + assert!( + updates.iter().any(|u| u.venue == "echo-venue"), + "the submission reached the venue; updates were: {updates:?}" + ); +} + +/// The graph oracle: `cargo tree` for the host crate (normal + build +/// edges) names no videre, intent, venue, or cow crate. +#[test] +fn host_crate_graph_reaches_no_venue_shaped_crate() { + let output = Command::new(env!("CARGO")) + .args([ + "tree", + "-p", + "nexum-runtime", + "-e", + "normal,build", + "--all-features", + "--prefix", + "none", + "--locked", + ]) + .current_dir(workspace_path("")) + .output() + .expect("cargo tree runs"); + assert!( + output.status.success(), + "cargo tree failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let tree = String::from_utf8_lossy(&output.stdout); + let reached: Vec<&str> = tree + .lines() + .filter_map(|line| line.split_whitespace().next()) + .filter(|name| { + let name = name.to_lowercase(); + ["videre", "intent", "venue", "cow"] + .iter() + .any(|word| name.contains(word)) + }) + .collect(); + assert!( + reached.is_empty(), + "venue-shaped crates reached: {reached:?}" + ); +} diff --git a/justfile b/justfile index de542480..270a8248 100644 --- a/justfile +++ b/justfile @@ -72,8 +72,9 @@ build-e2e: build-m2 build-m3 run-e2e: build-e2e build-engine cargo run -p shepherd -- --engine-config engine.e2e.toml -# Assert nexum-runtime is venue-agnostic: crate graph, symbol scan, and -# the nexum:host WIT leaf. Advisory in CI until the physical cut lands. +# Zero-leak gate: host-layer crate graphs, runtime charter-symbol and +# router-field scans, and the nexum:host WIT leaf and foreign-namespace +# scans. Blocking in CI. check-venue-agnostic: ./scripts/check-venue-agnostic.sh diff --git a/scripts/check-venue-agnostic.sh b/scripts/check-venue-agnostic.sh index 3c262410..cf1df1b7 100755 --- a/scripts/check-venue-agnostic.sh +++ b/scripts/check-venue-agnostic.sh @@ -1,10 +1,13 @@ #!/usr/bin/env bash -# Zero-leak check for the host layer: no host-layer crate graph -# (runtime, launcher, bare engine) reaches a videre/intent/venue/cow -# crate, the runtime sources carry no charter symbol -# (videre:|videre_host|Venue[A-Z]|EgressGuard|synthesize_venue|value-flow), -# and nexum:host resolves as a leaf WIT package. Advisory in CI until -# the physical cut lands; run locally via `just check-venue-agnostic`. +# Zero-leak check for the host layer, scoped precisely: no host-layer +# crate graph (runtime, launcher, bare engine) reaches a +# videre/intent/venue/cow crate; the runtime Rust sources carry no +# charter symbol +# (videre:|videre_host|Venue[A-Z]|EgressGuard|synthesize_venue|value-flow) +# and no privileged router field; and nexum:host names no foreign WIT +# package and resolves as a leaf. The opaque-status envelope +# (intent-status-update, its venue id string) is ratified host surface, +# not a leak. Blocking in CI; run locally via `just check-venue-agnostic`. set -uo pipefail @@ -47,8 +50,27 @@ case $? in *) fail "symbol scan errored (crates/nexum-runtime/src missing?)" ;; esac -# 3. WIT DAG: nexum:host is a leaf. No cross-package use/import, and the -# package resolves standalone. +# 3. Privileged-field scan: the venue registry rides the extension +# service map; no `VenueRegistry` router field may return to the +# runtime. (The charter scan above also catches the type; this stays +# as the named guard for that specific invariant.) +rg -n --no-heading -e 'VenueRegistry' crates/nexum-runtime/src +case $? in + 0) fail "a privileged router field returned to nexum-runtime" ;; + 1) pass "no privileged router field" ;; + *) fail "field scan errored (crates/nexum-runtime/src missing?)" ;; +esac + +# 4. WIT surface: nexum:host is a leaf. No foreign package named +# anywhere in its sources, no cross-package use/import, and the +# package resolves standalone. The opaque-status envelope stays. +wit_charter='nexum:intent|nexum:adapter|value-flow|videre:|shepherd:cow' +rg -n --no-heading -e "$wit_charter" wit/nexum-host +case $? in + 0) fail "a foreign WIT namespace leaks into wit/nexum-host" ;; + 1) pass "no foreign WIT namespace named" ;; + *) fail "WIT namespace scan errored (wit/nexum-host missing?)" ;; +esac rg -n --no-heading -e '^\s*(use|import)\s+[a-z0-9-]+:' wit/nexum-host case $? in 0) fail "nexum:host references another WIT package" ;;