2026-02-15 09:47:05 +00:00
|
|
|
//! Integration tests for the iroh-based P2P driver.
|
|
|
|
|
//!
|
|
|
|
|
//! These tests verify that IrohDriver can:
|
|
|
|
|
//! - Create endpoints with matching identities
|
|
|
|
|
//! - Form clusters via join
|
|
|
|
|
//! - Detect membership changes through SWIM
|
2026-02-19 14:39:33 +00:00
|
|
|
//! - Reject unauthorized peers
|
2026-02-15 09:47:05 +00:00
|
|
|
//!
|
2026-06-23 20:10:41 +00:00
|
|
|
//! Runs in the `iroh-driver` crate, where iroh support is always available.
|
2026-02-15 09:47:05 +00:00
|
|
|
|
2026-08-22 17:16:56 +00:00
|
|
|
pub mod common;
|
2026-02-15 09:47:05 +00:00
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
use std::sync::Arc;
|
2026-07-09 08:53:53 +00:00
|
|
|
use std::time::{Duration, Instant};
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
use common::iroh::*;
|
|
|
|
|
use distribution::peer_auth::PeerAllowList;
|
|
|
|
|
use iroh::PublicKey;
|
2026-08-29 07:28:05 +00:00
|
|
|
use iroh_driver::{EndpointAddrMask, advertised_endpoint};
|
2026-07-12 06:14:34 +00:00
|
|
|
use parking_lot::Mutex;
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
// ─── Identity tests ─────────────────────────────────────────────────────
|
2026-02-15 09:47:05 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn iroh_driver_creates_with_unique_identity() {
|
2026-02-19 14:39:33 +00:00
|
|
|
let mut d1 = make_driver();
|
|
|
|
|
let mut d2 = make_driver();
|
2026-02-15 09:47:05 +00:00
|
|
|
assert_ne!(d1.node_id(), d2.node_id());
|
|
|
|
|
d1.shutdown();
|
|
|
|
|
d2.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-06-09 09:29:07 +00:00
|
|
|
fn iroh_driver_reports_listen_addr_and_no_routes() {
|
|
|
|
|
// A freshly created driver knows where it listens and, having learned no
|
|
|
|
|
// peers, has converged on an empty directory route view.
|
2026-02-19 14:39:33 +00:00
|
|
|
let mut driver = make_driver();
|
2026-06-09 09:29:07 +00:00
|
|
|
assert!(!driver.listen_addr().is_empty());
|
|
|
|
|
assert_eq!(driver.directory_route_count(), 0);
|
2026-02-15 09:47:05 +00:00
|
|
|
driver.shutdown();
|
|
|
|
|
}
|
2026-02-19 14:39:33 +00:00
|
|
|
|
2026-07-09 08:53:53 +00:00
|
|
|
#[test]
|
|
|
|
|
fn endpoint_addr_includes_home_relay() {
|
|
|
|
|
let (relay_url, _relay_guard) = spawn_test_relay();
|
|
|
|
|
let expected_relay_url = relay_url.to_string();
|
|
|
|
|
let mut node = make_driver_with_relay(relay_url.clone());
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
let (relay_advertised, observed_relay_url) = loop {
|
|
|
|
|
let endpoint = node.endpoint_addr();
|
|
|
|
|
let current_relay_url = endpoint.relay_urls().next().map(|url| url.to_string());
|
|
|
|
|
if current_relay_url.as_deref() == Some(expected_relay_url.as_str()) {
|
|
|
|
|
break (true, current_relay_url);
|
|
|
|
|
}
|
|
|
|
|
if start.elapsed() >= Duration::from_secs(5) {
|
|
|
|
|
break (false, current_relay_url);
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
node.shutdown();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
relay_advertised,
|
|
|
|
|
"advertised endpoint did not include home relay {expected_relay_url} within timeout; last relay URL: {observed_relay_url:?}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 07:28:05 +00:00
|
|
|
#[test]
|
|
|
|
|
fn relay_masked_local_nodes_join_through_only_the_relay() {
|
|
|
|
|
let (relay_url, _relay_guard) = spawn_test_relay();
|
|
|
|
|
let mut node_a = make_driver_with_relay(relay_url.clone());
|
|
|
|
|
let mut node_b = make_driver_with_relay(relay_url);
|
|
|
|
|
|
|
|
|
|
node_a
|
|
|
|
|
.driver
|
|
|
|
|
.wait_for_relay_endpoint()
|
|
|
|
|
.expect("node A relay ready");
|
|
|
|
|
node_b
|
|
|
|
|
.driver
|
|
|
|
|
.wait_for_relay_endpoint()
|
|
|
|
|
.expect("node B relay ready");
|
|
|
|
|
let a_addr = advertised_endpoint(node_a.endpoint_addr(), EndpointAddrMask::RelayOnly)
|
|
|
|
|
.expect("mask node A endpoint");
|
|
|
|
|
let b_addr = advertised_endpoint(node_b.endpoint_addr(), EndpointAddrMask::RelayOnly)
|
|
|
|
|
.expect("mask node B endpoint");
|
|
|
|
|
assert_eq!(a_addr.ip_addrs().count(), 0);
|
|
|
|
|
assert_eq!(b_addr.ip_addrs().count(), 0);
|
|
|
|
|
assert_eq!(a_addr.relay_urls().count(), 1);
|
|
|
|
|
assert_eq!(b_addr.relay_urls().count(), 1);
|
|
|
|
|
|
|
|
|
|
node_a.join(std::slice::from_ref(&b_addr));
|
|
|
|
|
node_b.join(std::slice::from_ref(&a_addr));
|
|
|
|
|
|
|
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
converged,
|
|
|
|
|
"relay-only masked nodes did not converge through the relay"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:39:33 +00:00
|
|
|
// ─── Join integration tests ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_nodes_form_cluster_via_join() {
|
|
|
|
|
let mut node_a = make_driver();
|
|
|
|
|
let mut node_b = make_driver();
|
|
|
|
|
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
assert!(converged, "nodes did not converge within timeout");
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_a.alive_count(), 1, "node_a should see 1 alive peer");
|
|
|
|
|
assert_eq!(node_b.alive_count(), 1, "node_b should see 1 alive peer");
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_nodes_form_cluster_via_mutual_join() {
|
|
|
|
|
let mut node_a = make_driver();
|
|
|
|
|
let mut node_b = make_driver();
|
|
|
|
|
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
let a_addr = node_a.endpoint_addr();
|
|
|
|
|
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
node_b.join(&[a_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
assert!(
|
|
|
|
|
converged,
|
|
|
|
|
"nodes did not converge within timeout (mutual join)"
|
|
|
|
|
);
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_a.alive_count(), 1);
|
|
|
|
|
assert_eq!(node_b.alive_count(), 1);
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_nodes_form_cluster_with_peer_auth() {
|
|
|
|
|
let auth_a = Arc::new(Mutex::new(PeerAllowList::open()));
|
|
|
|
|
let auth_b = Arc::new(Mutex::new(PeerAllowList::open()));
|
|
|
|
|
|
|
|
|
|
let mut node_a = make_driver_with_auth(auth_a.clone());
|
|
|
|
|
let mut node_b = make_driver_with_auth(auth_b.clone());
|
|
|
|
|
|
|
|
|
|
let a_id = node_a.node_id();
|
|
|
|
|
let b_id = node_b.node_id();
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
|
|
|
|
|
// Switch to restrictive mode by adding each other
|
2026-07-12 06:14:34 +00:00
|
|
|
auth_a.lock().add_peer(b_id, "node-b".into());
|
|
|
|
|
auth_b.lock().add_peer(a_id, "node-a".into());
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(5), |a, b| {
|
|
|
|
|
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
|
|
|
|
|
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
|
|
|
|
|
sees_alive(a, &b_key) && sees_alive(b, &a_key)
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
assert!(
|
|
|
|
|
converged,
|
|
|
|
|
"nodes with peer auth did not converge within timeout"
|
|
|
|
|
);
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_a.alive_count(), 1);
|
|
|
|
|
assert_eq!(node_b.alive_count(), 1);
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn peer_auth_prevents_unauthorized_join() {
|
|
|
|
|
let mut node_a = make_driver();
|
|
|
|
|
|
|
|
|
|
// Node B has auth with only a dummy peer — node_a is NOT authorized
|
|
|
|
|
let auth_b = Arc::new(Mutex::new(PeerAllowList::open()));
|
|
|
|
|
let dummy_id = distribution::types::NodeId([0xAA; 32]);
|
2026-07-12 06:14:34 +00:00
|
|
|
auth_b.lock().add_peer(dummy_id, "dummy".into());
|
2026-02-19 14:39:33 +00:00
|
|
|
let mut node_b = make_driver_with_auth(auth_b);
|
|
|
|
|
|
|
|
|
|
let b_addr = node_b.endpoint_addr();
|
|
|
|
|
node_a.join(&[b_addr]);
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
let converged = pump_until_pair(&mut node_a, &mut node_b, Duration::from_secs(3), |_a, b| {
|
|
|
|
|
b.alive_count() > 0
|
|
|
|
|
});
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
assert!(!converged, "unauthorized peer should NOT have joined");
|
2026-06-09 09:29:07 +00:00
|
|
|
assert_eq!(node_b.alive_count(), 0, "node_b should have no alive peers");
|
2026-02-19 14:39:33 +00:00
|
|
|
|
|
|
|
|
node_a.shutdown();
|
|
|
|
|
node_b.shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Multi-node tests ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn three_nodes_converge_via_star_join() {
|
|
|
|
|
let mut cluster = IrohTestCluster::star(3);
|
|
|
|
|
|
2026-06-09 09:29:07 +00:00
|
|
|
let converged = cluster.pump_until(Duration::from_secs(10), |nodes| {
|
|
|
|
|
nodes.iter().all(|d| d.alive_count() == 2)
|
2026-02-19 14:39:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert!(converged, "3-node star did not converge");
|
|
|
|
|
cluster.shutdown();
|
|
|
|
|
}
|
2026-06-09 09:29:07 +00:00
|
|
|
|
|
|
|
|
// ─── Goal 2 (real-QUIC) — genuine death is detected ──────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn goal2_shutdown_node_is_detected_dead_by_survivors() {
|
|
|
|
|
// BEHAVIORAL_TEST_SPEC Goal 2 over real QUIC: shut one node down for real and
|
|
|
|
|
// poll until the survivors converge on it being Dead — a genuine probe
|
|
|
|
|
// timeout, not an injected death. Observed only through the membership view.
|
|
|
|
|
let mut cluster = IrohTestCluster::star(3);
|
|
|
|
|
let converged = cluster.pump_until(Duration::from_secs(10), |nodes| {
|
|
|
|
|
nodes.iter().all(|d| d.alive_count() == 2)
|
|
|
|
|
});
|
|
|
|
|
assert!(converged, "precondition: 3-node star must converge");
|
|
|
|
|
|
|
|
|
|
let dead = 2usize;
|
|
|
|
|
let dead_key = cluster.key(dead);
|
|
|
|
|
cluster.shutdown_one(dead);
|
|
|
|
|
|
feat(engine): substrate-neutral execution engine abstraction
Introduce the swactor engine: a swactor-owned composite that retains a
selected execution substrate, drives the core runtime, and hosts the
async/blocking/timer work that backs actors. Integrations receive one
cloneable EngineHandle and never construct or borrow a raw Tokio
runtime/handle.
Engine crate (crates/engine):
- The contract: spawn / spawn_blocking / timer / interval / now, a
per-implementation capability model with construction-time binding
(require()), and engine-owned time. The engine owns all progression;
actor handlers stay synchronous and never .await.
- TokioBackend owns the Tokio runtime and schedules core ticks and
supporting futures on it; SteppingBackend is a single-threaded
deterministic scheduler with virtual time (the non-Tokio portability
proof). Core is driven through its existing tick() surface; a
self-rescheduling CoreDriver is installed at construction and is the
sole place permitted to call try_tick.
iroh-driver:
- Receives an EngineHandle instead of a raw Tokio Handle. Accepts,
reads, dials, writes, endpoint construction, and teardown schedule
through it; required capabilities (tasks/timers/io) are validated
before the endpoint binds. Engine-hosted interval pumps drive
actor-bridge, datastream, and edge ingress.
myelin:
- One node/orchestrator engine owns core, protocol tick injection, and
transport progression; the application loop only drains
integration-owned queues. Stage-shard process readers, delayed actor
messages, helper stdout/stderr, prompt RPC, and CPU sampling all
schedule through the engine (spawn_blocking / engine tasks / timers).
- Removed the split-engine APIs: install_actor_bridge_pump(period) and
spawn_protocol_ticker(period) use each component's stored engine;
deleted the no-op pump_network callback and its plumbing; deleted the
dashboard raw-Tokio/standalone-runtime conveniences.
Enforcement:
- A clippy disallowed-methods boundary forbids direct runtime/scheduling/
time/core-driving bypasses, denied in swactor-engine, iroh-driver, and
myelin. Retained excluded uses (VastAI provider, provider process
supervision/log capture, OS-signal/stdin/process-control sequencing)
carry narrow allowances with reasons.
Verification:
- Engine contract + unit tests (incl. the SteppingBackend portability
proof), iroh integration tests (capability rejection before binding,
multi-node actor behavior), and a production execution-composition
smoke test that observes engine-driven actor progress with no ambient
Tokio runtime and no manual tick/pump. Workspace all-target/all-feature
clippy and tests are green.
Specs co-located with their crates: ENGINE_SPEC.md in crates/engine,
IROH_DRIVER_SPEC.md in crates/iroh-driver. VastAI remains explicitly out
of scope pending its separate redesign.
2026-08-10 20:23:03 +00:00
|
|
|
// The survivors' probes to the dead node now truly fail; poll until both
|
|
|
|
|
// converge on it being Dead.
|
|
|
|
|
let detected = cluster.pump_until(Duration::from_secs(30), |drivers| {
|
2026-06-09 09:29:07 +00:00
|
|
|
drivers
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.all(|(i, d)| i == dead || sees_dead(d, &dead_key))
|
|
|
|
|
});
|
|
|
|
|
assert!(detected, "survivors must detect the shut-down node as Dead");
|
|
|
|
|
|
|
|
|
|
// Tear down the two survivors (the third is already shut down).
|
|
|
|
|
cluster[0].shutdown();
|
|
|
|
|
cluster[1].shutdown();
|
|
|
|
|
}
|
feat(engine): substrate-neutral execution engine abstraction
Introduce the swactor engine: a swactor-owned composite that retains a
selected execution substrate, drives the core runtime, and hosts the
async/blocking/timer work that backs actors. Integrations receive one
cloneable EngineHandle and never construct or borrow a raw Tokio
runtime/handle.
Engine crate (crates/engine):
- The contract: spawn / spawn_blocking / timer / interval / now, a
per-implementation capability model with construction-time binding
(require()), and engine-owned time. The engine owns all progression;
actor handlers stay synchronous and never .await.
- TokioBackend owns the Tokio runtime and schedules core ticks and
supporting futures on it; SteppingBackend is a single-threaded
deterministic scheduler with virtual time (the non-Tokio portability
proof). Core is driven through its existing tick() surface; a
self-rescheduling CoreDriver is installed at construction and is the
sole place permitted to call try_tick.
iroh-driver:
- Receives an EngineHandle instead of a raw Tokio Handle. Accepts,
reads, dials, writes, endpoint construction, and teardown schedule
through it; required capabilities (tasks/timers/io) are validated
before the endpoint binds. Engine-hosted interval pumps drive
actor-bridge, datastream, and edge ingress.
myelin:
- One node/orchestrator engine owns core, protocol tick injection, and
transport progression; the application loop only drains
integration-owned queues. Stage-shard process readers, delayed actor
messages, helper stdout/stderr, prompt RPC, and CPU sampling all
schedule through the engine (spawn_blocking / engine tasks / timers).
- Removed the split-engine APIs: install_actor_bridge_pump(period) and
spawn_protocol_ticker(period) use each component's stored engine;
deleted the no-op pump_network callback and its plumbing; deleted the
dashboard raw-Tokio/standalone-runtime conveniences.
Enforcement:
- A clippy disallowed-methods boundary forbids direct runtime/scheduling/
time/core-driving bypasses, denied in swactor-engine, iroh-driver, and
myelin. Retained excluded uses (VastAI provider, provider process
supervision/log capture, OS-signal/stdin/process-control sequencing)
carry narrow allowances with reasons.
Verification:
- Engine contract + unit tests (incl. the SteppingBackend portability
proof), iroh integration tests (capability rejection before binding,
multi-node actor behavior), and a production execution-composition
smoke test that observes engine-driven actor progress with no ambient
Tokio runtime and no manual tick/pump. Workspace all-target/all-feature
clippy and tests are green.
Specs co-located with their crates: ENGINE_SPEC.md in crates/engine,
IROH_DRIVER_SPEC.md in crates/iroh-driver. VastAI remains explicitly out
of scope pending its separate redesign.
2026-08-10 20:23:03 +00:00
|
|
|
|
|
|
|
|
// ─── Capability binding (ENGINE_SPEC.md) ──────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn driver_rejects_engine_without_io() {
|
|
|
|
|
// The SteppingBackend advertises tasks + timers + blocking but NOT io.
|
|
|
|
|
// The driver requires tasks + timers + io, so construction must fail
|
|
|
|
|
// before any endpoint is bound or background work starts.
|
|
|
|
|
use distribution::node::DistributedNodeConfig;
|
|
|
|
|
use iroh::RelayMode;
|
|
|
|
|
use iroh_driver::{IrohDriver, IrohDriverConfig};
|
|
|
|
|
use swactor::config::RuntimeConfig;
|
2026-08-11 12:08:06 +00:00
|
|
|
use swactor::runtime::RuntimeParts;
|
feat(engine): substrate-neutral execution engine abstraction
Introduce the swactor engine: a swactor-owned composite that retains a
selected execution substrate, drives the core runtime, and hosts the
async/blocking/timer work that backs actors. Integrations receive one
cloneable EngineHandle and never construct or borrow a raw Tokio
runtime/handle.
Engine crate (crates/engine):
- The contract: spawn / spawn_blocking / timer / interval / now, a
per-implementation capability model with construction-time binding
(require()), and engine-owned time. The engine owns all progression;
actor handlers stay synchronous and never .await.
- TokioBackend owns the Tokio runtime and schedules core ticks and
supporting futures on it; SteppingBackend is a single-threaded
deterministic scheduler with virtual time (the non-Tokio portability
proof). Core is driven through its existing tick() surface; a
self-rescheduling CoreDriver is installed at construction and is the
sole place permitted to call try_tick.
iroh-driver:
- Receives an EngineHandle instead of a raw Tokio Handle. Accepts,
reads, dials, writes, endpoint construction, and teardown schedule
through it; required capabilities (tasks/timers/io) are validated
before the endpoint binds. Engine-hosted interval pumps drive
actor-bridge, datastream, and edge ingress.
myelin:
- One node/orchestrator engine owns core, protocol tick injection, and
transport progression; the application loop only drains
integration-owned queues. Stage-shard process readers, delayed actor
messages, helper stdout/stderr, prompt RPC, and CPU sampling all
schedule through the engine (spawn_blocking / engine tasks / timers).
- Removed the split-engine APIs: install_actor_bridge_pump(period) and
spawn_protocol_ticker(period) use each component's stored engine;
deleted the no-op pump_network callback and its plumbing; deleted the
dashboard raw-Tokio/standalone-runtime conveniences.
Enforcement:
- A clippy disallowed-methods boundary forbids direct runtime/scheduling/
time/core-driving bypasses, denied in swactor-engine, iroh-driver, and
myelin. Retained excluded uses (VastAI provider, provider process
supervision/log capture, OS-signal/stdin/process-control sequencing)
carry narrow allowances with reasons.
Verification:
- Engine contract + unit tests (incl. the SteppingBackend portability
proof), iroh integration tests (capability rejection before binding,
multi-node actor behavior), and a production execution-composition
smoke test that observes engine-driven actor progress with no ambient
Tokio runtime and no manual tick/pump. Workspace all-target/all-feature
clippy and tests are green.
Specs co-located with their crates: ENGINE_SPEC.md in crates/engine,
IROH_DRIVER_SPEC.md in crates/iroh-driver. VastAI remains explicitly out
of scope pending its separate redesign.
2026-08-10 20:23:03 +00:00
|
|
|
use swactor_engine::{Engine, SteppingBackend};
|
|
|
|
|
|
2026-08-11 12:08:06 +00:00
|
|
|
let parts = RuntimeParts::new(RuntimeConfig::default());
|
|
|
|
|
let engine = Engine::new(parts, SteppingBackend::default()).expect("stepping engine");
|
feat(engine): substrate-neutral execution engine abstraction
Introduce the swactor engine: a swactor-owned composite that retains a
selected execution substrate, drives the core runtime, and hosts the
async/blocking/timer work that backs actors. Integrations receive one
cloneable EngineHandle and never construct or borrow a raw Tokio
runtime/handle.
Engine crate (crates/engine):
- The contract: spawn / spawn_blocking / timer / interval / now, a
per-implementation capability model with construction-time binding
(require()), and engine-owned time. The engine owns all progression;
actor handlers stay synchronous and never .await.
- TokioBackend owns the Tokio runtime and schedules core ticks and
supporting futures on it; SteppingBackend is a single-threaded
deterministic scheduler with virtual time (the non-Tokio portability
proof). Core is driven through its existing tick() surface; a
self-rescheduling CoreDriver is installed at construction and is the
sole place permitted to call try_tick.
iroh-driver:
- Receives an EngineHandle instead of a raw Tokio Handle. Accepts,
reads, dials, writes, endpoint construction, and teardown schedule
through it; required capabilities (tasks/timers/io) are validated
before the endpoint binds. Engine-hosted interval pumps drive
actor-bridge, datastream, and edge ingress.
myelin:
- One node/orchestrator engine owns core, protocol tick injection, and
transport progression; the application loop only drains
integration-owned queues. Stage-shard process readers, delayed actor
messages, helper stdout/stderr, prompt RPC, and CPU sampling all
schedule through the engine (spawn_blocking / engine tasks / timers).
- Removed the split-engine APIs: install_actor_bridge_pump(period) and
spawn_protocol_ticker(period) use each component's stored engine;
deleted the no-op pump_network callback and its plumbing; deleted the
dashboard raw-Tokio/standalone-runtime conveniences.
Enforcement:
- A clippy disallowed-methods boundary forbids direct runtime/scheduling/
time/core-driving bypasses, denied in swactor-engine, iroh-driver, and
myelin. Retained excluded uses (VastAI provider, provider process
supervision/log capture, OS-signal/stdin/process-control sequencing)
carry narrow allowances with reasons.
Verification:
- Engine contract + unit tests (incl. the SteppingBackend portability
proof), iroh integration tests (capability rejection before binding,
multi-node actor behavior), and a production execution-composition
smoke test that observes engine-driven actor progress with no ambient
Tokio runtime and no manual tick/pump. Workspace all-target/all-feature
clippy and tests are green.
Specs co-located with their crates: ENGINE_SPEC.md in crates/engine,
IROH_DRIVER_SPEC.md in crates/iroh-driver. VastAI remains explicitly out
of scope pending its separate redesign.
2026-08-10 20:23:03 +00:00
|
|
|
let result = IrohDriver::with_engine(
|
|
|
|
|
engine.handle(),
|
|
|
|
|
IrohDriverConfig {
|
|
|
|
|
secret_key: None,
|
|
|
|
|
relay_mode: RelayMode::Disabled,
|
runtime: checkpoint distributed execution and retained-node deployment
Integrate namespace and source-route lifecycle changes, contextual process cleanup, Python binding updates, and Myelin worker/orchestrator recovery. Keep the shared control contracts, deployment identity fencing, SSH bootstrap adapters, paid admission accounting, and VastAI cleanup implementation together with their consumers.
Migrate Iroh dependencies and telemetry transport/collection with dashboard and demo callsites, workspace build configuration, and actor-control-flow policy updates. This is an intermediate development checkpoint, not paid-provider qualification.
Review verification: contextual_process_guarantees (4 tests), telemetry_transport (4 tests), and shared control contracts (5 tests) passed. Historical five-node redeployment and campaign execution passed individually; complete ordered qualification remains pending.
2026-09-14 09:20:56 +00:00
|
|
|
bind_port: None,
|
feat(engine): substrate-neutral execution engine abstraction
Introduce the swactor engine: a swactor-owned composite that retains a
selected execution substrate, drives the core runtime, and hosts the
async/blocking/timer work that backs actors. Integrations receive one
cloneable EngineHandle and never construct or borrow a raw Tokio
runtime/handle.
Engine crate (crates/engine):
- The contract: spawn / spawn_blocking / timer / interval / now, a
per-implementation capability model with construction-time binding
(require()), and engine-owned time. The engine owns all progression;
actor handlers stay synchronous and never .await.
- TokioBackend owns the Tokio runtime and schedules core ticks and
supporting futures on it; SteppingBackend is a single-threaded
deterministic scheduler with virtual time (the non-Tokio portability
proof). Core is driven through its existing tick() surface; a
self-rescheduling CoreDriver is installed at construction and is the
sole place permitted to call try_tick.
iroh-driver:
- Receives an EngineHandle instead of a raw Tokio Handle. Accepts,
reads, dials, writes, endpoint construction, and teardown schedule
through it; required capabilities (tasks/timers/io) are validated
before the endpoint binds. Engine-hosted interval pumps drive
actor-bridge, datastream, and edge ingress.
myelin:
- One node/orchestrator engine owns core, protocol tick injection, and
transport progression; the application loop only drains
integration-owned queues. Stage-shard process readers, delayed actor
messages, helper stdout/stderr, prompt RPC, and CPU sampling all
schedule through the engine (spawn_blocking / engine tasks / timers).
- Removed the split-engine APIs: install_actor_bridge_pump(period) and
spawn_protocol_ticker(period) use each component's stored engine;
deleted the no-op pump_network callback and its plumbing; deleted the
dashboard raw-Tokio/standalone-runtime conveniences.
Enforcement:
- A clippy disallowed-methods boundary forbids direct runtime/scheduling/
time/core-driving bypasses, denied in swactor-engine, iroh-driver, and
myelin. Retained excluded uses (VastAI provider, provider process
supervision/log capture, OS-signal/stdin/process-control sequencing)
carry narrow allowances with reasons.
Verification:
- Engine contract + unit tests (incl. the SteppingBackend portability
proof), iroh integration tests (capability rejection before binding,
multi-node actor behavior), and a production execution-composition
smoke test that observes engine-driven actor progress with no ambient
Tokio runtime and no manual tick/pump. Workspace all-target/all-feature
clippy and tests are green.
Specs co-located with their crates: ENGINE_SPEC.md in crates/engine,
IROH_DRIVER_SPEC.md in crates/iroh-driver. VastAI remains explicitly out
of scope pending its separate redesign.
2026-08-10 20:23:03 +00:00
|
|
|
node: DistributedNodeConfig::default(),
|
|
|
|
|
peer_auth: None,
|
|
|
|
|
additional_alpns: vec![],
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
result.is_err(),
|
|
|
|
|
"driver must reject an engine that lacks the io capability"
|
|
|
|
|
);
|
|
|
|
|
}
|