2026-08-15 08:17:48 +00:00
|
|
|
//! Iroh/QUIC transport adapter for telemetry subscriptions.
|
2026-06-25 12:30:18 +00:00
|
|
|
|
|
|
|
|
use std::error::Error;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2026-07-21 06:25:52 +00:00
|
|
|
use crossbeam_channel::TryRecvError;
|
2026-06-25 12:30:18 +00:00
|
|
|
use iroh::endpoint::{Connection, RecvStream, SendStream};
|
|
|
|
|
use iroh::{Endpoint, EndpointAddr};
|
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::EngineHandle;
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
use telemetry::frame::{
|
|
|
|
|
ChannelDescriptor, ChannelId, ChannelRef, FrameDelivery, Position, StreamDescriptor,
|
|
|
|
|
TelemetryEvent,
|
|
|
|
|
};
|
|
|
|
|
use telemetry::{TelemetrySnapshot, TelemetrySubscription};
|
2026-06-25 12:30:18 +00:00
|
|
|
|
2026-08-15 08:17:48 +00:00
|
|
|
pub const TELEMETRY_ALPN: &[u8] = b"swactor/telemetry/0";
|
2026-06-25 12:30:18 +00:00
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
const MAGIC: &[u8; 4] = b"DSQ1";
|
|
|
|
|
const TAG_CHANNEL_DECLARED: u8 = 0x01;
|
|
|
|
|
const TAG_FRAME: u8 = 0x02;
|
|
|
|
|
const TAG_STREAM_ENDED: u8 = 0x03;
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
|
|
|
|
|
// ─── Pull model: collector-initiated subscriptions ───────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// A supervising node dials a freshly-bootstrapped node on `TELEMETRY_ALPN`,
|
|
|
|
|
// sends one subscription request on the first uni stream, and the node answers
|
|
|
|
|
// by writing the existing header+events stream shape on a uni stream of the
|
|
|
|
|
// same connection. Subscription lifetime = connection lifetime.
|
|
|
|
|
|
|
|
|
|
/// Write a pull request (magic + flow id + token + `SubscriptionRequest`) and
|
|
|
|
|
/// finish the stream so the serving side's read completes.
|
|
|
|
|
const REQUEST_MAGIC: &[u8; 4] = b"DSQR";
|
|
|
|
|
pub async fn write_pull_request(
|
|
|
|
|
send: &mut SendStream,
|
|
|
|
|
flow_id: [u8; 16],
|
|
|
|
|
token: &[u8],
|
|
|
|
|
request: &telemetry::SubscriptionRequest,
|
|
|
|
|
) -> Result<(), BoxError> {
|
|
|
|
|
if token.len() > u16::MAX as usize {
|
|
|
|
|
return Err("telemetry pull token exceeds u16 length prefix".into());
|
|
|
|
|
}
|
|
|
|
|
send.write_all(REQUEST_MAGIC).await?;
|
|
|
|
|
send.write_all(&flow_id).await?;
|
|
|
|
|
send.write_all(&(token.len() as u16).to_le_bytes()).await?;
|
|
|
|
|
send.write_all(token).await?;
|
|
|
|
|
write_json(send, request).await?;
|
|
|
|
|
send.finish()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a pull request written by [`write_pull_request`].
|
|
|
|
|
pub async fn read_pull_request(
|
|
|
|
|
recv: &mut RecvStream,
|
|
|
|
|
) -> Result<([u8; 16], Vec<u8>, telemetry::SubscriptionRequest), BoxError> {
|
|
|
|
|
let mut magic = [0u8; 4];
|
|
|
|
|
recv.read_exact(&mut magic).await?;
|
|
|
|
|
if &magic != REQUEST_MAGIC {
|
|
|
|
|
return Err("invalid telemetry pull request magic".into());
|
|
|
|
|
}
|
|
|
|
|
let mut flow_id = [0u8; 16];
|
|
|
|
|
recv.read_exact(&mut flow_id).await?;
|
|
|
|
|
let mut token_len = [0u8; 2];
|
|
|
|
|
recv.read_exact(&mut token_len).await?;
|
|
|
|
|
let token_len = u16::from_le_bytes(token_len) as usize;
|
|
|
|
|
let mut token = vec![0u8; token_len];
|
|
|
|
|
recv.read_exact(&mut token).await?;
|
|
|
|
|
let request = read_json(recv).await?;
|
|
|
|
|
Ok((flow_id, token, request))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Node side: serve one accepted `TELEMETRY_ALPN` connection. Reads the pull
|
|
|
|
|
/// request from the first uni stream, subscribes the local endpoint, and
|
|
|
|
|
/// writes the answering subscription stream (header + events, until the
|
|
|
|
|
/// subscription ends or the connection drops) on a uni stream of the same
|
|
|
|
|
/// connection. One request per connection; the task exits when the writer
|
|
|
|
|
/// ends or the connection fails.
|
|
|
|
|
pub fn spawn_pull_server(
|
|
|
|
|
engine: &EngineHandle,
|
|
|
|
|
conn: Connection,
|
|
|
|
|
endpoint: std::sync::Arc<telemetry::TelemetryEndpoint>,
|
|
|
|
|
idle_sleep: Duration,
|
|
|
|
|
) {
|
|
|
|
|
let engine_handle = engine.clone();
|
|
|
|
|
engine.spawn(async move {
|
|
|
|
|
let Ok(mut recv) = conn.accept_uni().await else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Ok((_flow_id, token, request)) = read_pull_request(&mut recv).await else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let subscription = endpoint.subscribe("supervisor-pull", request);
|
|
|
|
|
let header =
|
|
|
|
|
match TelemetryQuicHeader::from_snapshot(_flow_id, token, subscription.snapshot()) {
|
|
|
|
|
Ok(header) => header,
|
|
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
|
|
|
|
let Ok(send) = conn.open_uni().await else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let _ =
|
|
|
|
|
write_subscription_until_closed(&engine_handle, send, header, subscription, idle_sleep)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 10:17:04 +00:00
|
|
|
/// Supervisor side: retain a pull subscription to a node on `TELEMETRY_ALPN`.
|
|
|
|
|
///
|
|
|
|
|
/// A transport interruption reconnects with bounded backoff. Returning after
|
|
|
|
|
/// the first EOF leaves a healthy node permanently stale, which is especially
|
|
|
|
|
/// easy to trigger while several freshly-bootstrapped nodes answer at once.
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
pub fn spawn_pull_collector(
|
|
|
|
|
engine: &EngineHandle,
|
|
|
|
|
endpoint: Endpoint,
|
|
|
|
|
peer: EndpointAddr,
|
|
|
|
|
flow_id: [u8; 16],
|
|
|
|
|
token: Vec<u8>,
|
|
|
|
|
request: telemetry::SubscriptionRequest,
|
|
|
|
|
fanout: std::sync::Arc<telemetry::DeliveryFanout>,
|
|
|
|
|
on_header: std::sync::mpsc::Sender<TelemetryQuicHeader>,
|
|
|
|
|
) {
|
2026-08-18 10:17:04 +00:00
|
|
|
let engine_handle = engine.clone();
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
engine.spawn(async move {
|
|
|
|
|
let peer_id = peer.id.to_string();
|
2026-08-18 10:17:04 +00:00
|
|
|
let mut retry_delay = Duration::from_millis(250);
|
|
|
|
|
loop {
|
|
|
|
|
match collect_pull_once(
|
|
|
|
|
&endpoint, &peer, flow_id, &token, &request, &fanout, &on_header,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(()) => return,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
eprintln!(
|
|
|
|
|
"telemetry-pull: {peer_id}: {error}; retrying in {} ms",
|
|
|
|
|
retry_delay.as_millis()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
engine_handle.timer(retry_delay).await;
|
|
|
|
|
retry_delay = retry_delay
|
|
|
|
|
.checked_mul(2)
|
|
|
|
|
.unwrap_or(Duration::from_secs(5))
|
|
|
|
|
.min(Duration::from_secs(5));
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-08-18 10:17:04 +00:00
|
|
|
|
|
|
|
|
async fn collect_pull_once(
|
|
|
|
|
endpoint: &Endpoint,
|
|
|
|
|
peer: &EndpointAddr,
|
|
|
|
|
flow_id: [u8; 16],
|
|
|
|
|
token: &[u8],
|
|
|
|
|
request: &telemetry::SubscriptionRequest,
|
|
|
|
|
fanout: &telemetry::DeliveryFanout,
|
|
|
|
|
on_header: &std::sync::mpsc::Sender<TelemetryQuicHeader>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let conn = endpoint
|
|
|
|
|
.connect(peer.clone(), TELEMETRY_ALPN)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|error| format!("connect failed: {error}"))?;
|
|
|
|
|
let mut req = conn
|
|
|
|
|
.open_uni()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|error| format!("open request stream failed: {error}"))?;
|
|
|
|
|
write_pull_request(&mut req, flow_id, token, request)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|error| format!("write request failed: {error}"))?;
|
|
|
|
|
let mut recv = conn
|
|
|
|
|
.accept_uni()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|error| format!("no answer stream: {error}"))?;
|
|
|
|
|
let header = read_header(&mut recv)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|error| format!("answer header unreadable: {error}"))?;
|
|
|
|
|
if on_header.send(header.clone()).is_err() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
let stream = header.stream;
|
|
|
|
|
loop {
|
|
|
|
|
match read_next_event(&mut recv, &stream).await {
|
|
|
|
|
Ok(Some(event)) => {
|
|
|
|
|
fanout.publish(event);
|
|
|
|
|
}
|
|
|
|
|
Ok(None) => return Err("answer stream closed".to_owned()),
|
|
|
|
|
Err(error) => return Err(format!("read answer stream failed: {error}")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-25 12:30:18 +00:00
|
|
|
const MAX_RECORD_BYTES: usize = 16 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
type BoxError = Box<dyn Error + Send + Sync + 'static>;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
2026-08-15 08:17:48 +00:00
|
|
|
pub struct TelemetryQuicHeader {
|
2026-06-25 12:30:18 +00:00
|
|
|
pub flow_id: [u8; 16],
|
|
|
|
|
pub token: Vec<u8>,
|
2026-07-12 06:14:34 +00:00
|
|
|
pub stream: StreamDescriptor,
|
|
|
|
|
pub channels: Vec<ChannelDescriptor>,
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-15 08:17:48 +00:00
|
|
|
impl TelemetryQuicHeader {
|
2026-07-12 06:14:34 +00:00
|
|
|
pub fn new(
|
|
|
|
|
flow_id: [u8; 16],
|
|
|
|
|
token: impl Into<Vec<u8>>,
|
|
|
|
|
stream: StreamDescriptor,
|
|
|
|
|
channels: Vec<ChannelDescriptor>,
|
|
|
|
|
) -> Self {
|
2026-06-25 12:30:18 +00:00
|
|
|
Self {
|
|
|
|
|
flow_id,
|
|
|
|
|
token: token.into(),
|
2026-07-12 06:14:34 +00:00
|
|
|
stream,
|
|
|
|
|
channels,
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
pub fn from_snapshot(
|
|
|
|
|
flow_id: [u8; 16],
|
|
|
|
|
token: impl Into<Vec<u8>>,
|
2026-08-15 08:17:48 +00:00
|
|
|
snapshot: &TelemetrySnapshot,
|
2026-07-12 06:14:34 +00:00
|
|
|
) -> Result<Self, BoxError> {
|
|
|
|
|
let stream = snapshot
|
|
|
|
|
.streams
|
|
|
|
|
.first()
|
|
|
|
|
.cloned()
|
2026-08-15 08:17:48 +00:00
|
|
|
.ok_or("telemetry subscription snapshot has no stream")?;
|
2026-07-12 06:14:34 +00:00
|
|
|
Ok(Self::new(flow_id, token, stream, snapshot.channels.clone()))
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
2026-08-15 08:17:48 +00:00
|
|
|
pub struct TelemetryQuicWriteStats {
|
2026-07-12 06:14:34 +00:00
|
|
|
pub events: usize,
|
2026-06-25 12:30:18 +00:00
|
|
|
pub bytes: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
2026-08-15 08:17:48 +00:00
|
|
|
pub struct TelemetryQuicRead {
|
|
|
|
|
pub header: TelemetryQuicHeader,
|
|
|
|
|
pub events: Vec<TelemetryEvent>,
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn spawn_subscription_writer(
|
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
|
|
|
engine: &EngineHandle,
|
2026-06-25 12:30:18 +00:00
|
|
|
endpoint: Endpoint,
|
|
|
|
|
peer: EndpointAddr,
|
2026-08-15 08:17:48 +00:00
|
|
|
header: TelemetryQuicHeader,
|
|
|
|
|
subscription: TelemetrySubscription,
|
2026-06-25 12:30:18 +00:00
|
|
|
idle_sleep: Duration,
|
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 engine_handle = engine.clone();
|
|
|
|
|
engine.spawn(async move {
|
2026-08-15 08:17:48 +00:00
|
|
|
let Ok(conn) = endpoint.connect(peer, TELEMETRY_ALPN).await else {
|
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
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Ok(send) = conn.open_uni().await else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
let _ =
|
|
|
|
|
write_subscription_until_closed(&engine_handle, send, header, subscription, idle_sleep)
|
|
|
|
|
.await;
|
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
|
|
|
});
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn write_available_subscription(
|
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
|
|
|
engine: &EngineHandle,
|
2026-06-25 12:30:18 +00:00
|
|
|
send: SendStream,
|
2026-08-15 08:17:48 +00:00
|
|
|
header: &TelemetryQuicHeader,
|
|
|
|
|
subscription: &TelemetrySubscription,
|
|
|
|
|
) -> Result<TelemetryQuicWriteStats, BoxError> {
|
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
|
|
|
write_subscription_inner(engine, send, header, subscription, None).await
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn write_subscription_until_closed(
|
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
|
|
|
engine: &EngineHandle,
|
2026-06-25 12:30:18 +00:00
|
|
|
mut send: SendStream,
|
2026-08-15 08:17:48 +00:00
|
|
|
header: TelemetryQuicHeader,
|
|
|
|
|
subscription: TelemetrySubscription,
|
2026-06-25 12:30:18 +00:00
|
|
|
idle_sleep: Duration,
|
2026-08-15 08:17:48 +00:00
|
|
|
) -> Result<TelemetryQuicWriteStats, BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
write_header(&mut send, &header).await?;
|
2026-08-15 08:17:48 +00:00
|
|
|
let mut stats = TelemetryQuicWriteStats::default();
|
2026-06-25 12:30:18 +00:00
|
|
|
loop {
|
|
|
|
|
match subscription.try_recv() {
|
2026-07-12 06:14:34 +00:00
|
|
|
Ok(event) => {
|
|
|
|
|
let bytes = write_event(&mut send, &event).await?;
|
|
|
|
|
if bytes > 0 {
|
|
|
|
|
stats.bytes += bytes;
|
|
|
|
|
stats.events += 1;
|
|
|
|
|
}
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
2026-07-21 06:25:52 +00:00
|
|
|
Err(TryRecvError::Empty) => {
|
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
|
|
|
engine.timer(idle_sleep).await;
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
2026-07-21 06:25:52 +00:00
|
|
|
Err(TryRecvError::Disconnected) => break,
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
send.finish()?;
|
|
|
|
|
Ok(stats)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn write_subscription_inner(
|
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
|
|
|
engine: &EngineHandle,
|
2026-06-25 12:30:18 +00:00
|
|
|
mut send: SendStream,
|
2026-08-15 08:17:48 +00:00
|
|
|
header: &TelemetryQuicHeader,
|
|
|
|
|
subscription: &TelemetrySubscription,
|
2026-06-25 12:30:18 +00:00
|
|
|
idle_sleep: Option<Duration>,
|
2026-08-15 08:17:48 +00:00
|
|
|
) -> Result<TelemetryQuicWriteStats, BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
write_header(&mut send, header).await?;
|
2026-08-15 08:17:48 +00:00
|
|
|
let mut stats = TelemetryQuicWriteStats::default();
|
2026-06-25 12:30:18 +00:00
|
|
|
loop {
|
|
|
|
|
match subscription.try_recv() {
|
2026-07-12 06:14:34 +00:00
|
|
|
Ok(event) => {
|
|
|
|
|
let bytes = write_event(&mut send, &event).await?;
|
|
|
|
|
if bytes > 0 {
|
|
|
|
|
stats.bytes += bytes;
|
|
|
|
|
stats.events += 1;
|
|
|
|
|
}
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
2026-07-21 06:25:52 +00:00
|
|
|
Err(TryRecvError::Empty) => match idle_sleep {
|
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
|
|
|
Some(delay) => engine.timer(delay).await,
|
2026-06-25 12:30:18 +00:00
|
|
|
None => break,
|
|
|
|
|
},
|
2026-07-21 06:25:52 +00:00
|
|
|
Err(TryRecvError::Disconnected) => break,
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
send.finish()?;
|
|
|
|
|
Ok(stats)
|
|
|
|
|
}
|
|
|
|
|
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
pub async fn write_event(send: &mut SendStream, event: &TelemetryEvent) -> Result<usize, BoxError> {
|
2026-07-12 06:14:34 +00:00
|
|
|
let mut bytes = Vec::new();
|
|
|
|
|
match event {
|
2026-08-15 08:17:48 +00:00
|
|
|
TelemetryEvent::StreamDeclared(_) => return Ok(0),
|
|
|
|
|
TelemetryEvent::ChannelDeclared(descriptor) => {
|
2026-07-12 06:14:34 +00:00
|
|
|
bytes.push(TAG_CHANNEL_DECLARED);
|
|
|
|
|
put_json(&mut bytes, descriptor)?;
|
|
|
|
|
}
|
2026-08-15 08:17:48 +00:00
|
|
|
TelemetryEvent::Frame(delivery) => {
|
2026-07-12 06:14:34 +00:00
|
|
|
bytes.push(TAG_FRAME);
|
|
|
|
|
bytes.extend_from_slice(&delivery.channel.channel.0.to_le_bytes());
|
|
|
|
|
bytes.extend_from_slice(&delivery.position.0.to_le_bytes());
|
|
|
|
|
put_bytes(&mut bytes, &delivery.payload)?;
|
|
|
|
|
}
|
2026-08-15 08:17:48 +00:00
|
|
|
TelemetryEvent::StreamEnded(_) => {
|
2026-07-12 06:14:34 +00:00
|
|
|
bytes.push(TAG_STREAM_ENDED);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if bytes.len() > MAX_RECORD_BYTES {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry QUIC record exceeds max size".into());
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
send.write_all(&(bytes.len() as u32).to_le_bytes()).await?;
|
|
|
|
|
send.write_all(&bytes).await?;
|
|
|
|
|
Ok(4 + bytes.len())
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 08:17:48 +00:00
|
|
|
pub async fn read_stream_header(recv: &mut RecvStream) -> Result<TelemetryQuicHeader, BoxError> {
|
2026-07-12 06:14:34 +00:00
|
|
|
read_header(recv).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 08:17:48 +00:00
|
|
|
pub async fn read_events_from_stream(mut recv: RecvStream) -> Result<TelemetryQuicRead, BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
let header = read_header(&mut recv).await?;
|
2026-07-12 06:14:34 +00:00
|
|
|
let mut events = Vec::new();
|
2026-06-25 12:30:18 +00:00
|
|
|
loop {
|
2026-07-12 06:14:34 +00:00
|
|
|
match read_next_event(&mut recv, &header.stream).await? {
|
|
|
|
|
Some(event) => events.push(event),
|
2026-06-25 12:30:18 +00:00
|
|
|
None => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-15 08:17:48 +00:00
|
|
|
Ok(TelemetryQuicRead { header, events })
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn read_next_uni_from_connection(
|
|
|
|
|
conn: &Connection,
|
2026-08-15 08:17:48 +00:00
|
|
|
) -> Result<TelemetryQuicRead, BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
let recv = conn.accept_uni().await?;
|
2026-07-12 06:14:34 +00:00
|
|
|
read_events_from_stream(recv).await
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn spawn_connection_reader(
|
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
|
|
|
engine: &EngineHandle,
|
2026-06-25 12:30:18 +00:00
|
|
|
conn: Connection,
|
2026-08-15 08:17:48 +00:00
|
|
|
sink: std::sync::mpsc::Sender<TelemetryEvent>,
|
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
|
|
|
) {
|
|
|
|
|
engine.spawn(async move {
|
2026-06-25 12:30:18 +00:00
|
|
|
loop {
|
|
|
|
|
let recv = match conn.accept_uni().await {
|
|
|
|
|
Ok(recv) => recv,
|
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
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
|
|
|
|
let Ok(read) = read_events_from_stream(recv).await else {
|
|
|
|
|
continue;
|
2026-06-25 12:30:18 +00:00
|
|
|
};
|
2026-07-12 06:14:34 +00:00
|
|
|
for event in read.events {
|
|
|
|
|
if sink.send(event).is_err() {
|
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
|
|
|
return;
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
});
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
provisioning-reconciler-demo: wire-announce readiness + --docker node kind
Replace the per-attempt key-file side channel (DEMO_NODE_KEY_FILE,
read_key_report, JoinCheck) with a control-plane announce: node roles
send a tagged gossip frame {attempt, logical_node, key_hex,
endpoint_addr_json} after joining and every heartbeat thereafter.
- iroh-driver: IrohDriver::send_tagged_gossip — fire-and-forget
tag-routed gossip egress for bridge-less clients (reuses cached/join
connections, dials with backoff).
- provisioning: BootstrapMsg::Announce — first delivery while
bootstrapping completes the attempt (collector + exactly-once
Bootstrapped report); duplicates, misrouted attempts, and
terminal-phase announces drop. Unit-tested.
- xtask demo: AnnounceActor decodes the tag-routed frame and forwards
by attempt to the owning bootstrap actor; last_announce_ms is the
wire heartbeat. LocalProcessLogic keeps only process lifecycle.
- --docker: DockerProcessLogic (kind "docker") — attached
"docker run --rm" child on a per-run labeled bridge network
(foreign-node masking: per-container IPs, gateway-dialed
supervisor). Standalone scratch image from the static-musl xtask
binary (37MB), staged one-file build context. The container is
force-removed on every terminal path so a SIGKILLed docker CLI
cannot orphan a running container.
- Cleanup: no volumes/mounts; label-filtered exit sweep (SIGINT and
SIGTERM both drain first) + startup sweep of stale demo resources;
images persist per run token.
Verified live: process kind (kill -> replacement in 3.4s, provision/
remove/kill waves, zero orphans) and docker kind (8-node abuse waves
across docker kill, mid-provision control kills, CLI SIGKILL orphans
force-removed, SIGKILL-crash leftovers swept on restart, clean exits
leave zero containers/networks/CLIs). provisioning 22 + iroh-driver
13 tests pass.
2026-08-16 16:30:52 +00:00
|
|
|
async fn write_header(send: &mut SendStream, header: &TelemetryQuicHeader) -> Result<(), BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
if header.token.len() > u16::MAX as usize {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry token exceeds u16 length prefix".into());
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
send.write_all(MAGIC).await?;
|
|
|
|
|
send.write_all(&header.flow_id).await?;
|
|
|
|
|
send.write_all(&(header.token.len() as u16).to_le_bytes())
|
|
|
|
|
.await?;
|
|
|
|
|
send.write_all(&header.token).await?;
|
2026-07-12 06:14:34 +00:00
|
|
|
write_json(send, &header.stream).await?;
|
|
|
|
|
write_json(send, &header.channels).await?;
|
2026-06-25 12:30:18 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 08:17:48 +00:00
|
|
|
async fn read_header(recv: &mut RecvStream) -> Result<TelemetryQuicHeader, BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
let mut magic = [0u8; 4];
|
|
|
|
|
recv.read_exact(&mut magic).await?;
|
|
|
|
|
if &magic != MAGIC {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("invalid telemetry QUIC magic".into());
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
let mut flow_id = [0u8; 16];
|
|
|
|
|
recv.read_exact(&mut flow_id).await?;
|
|
|
|
|
let mut token_len = [0u8; 2];
|
|
|
|
|
recv.read_exact(&mut token_len).await?;
|
|
|
|
|
let token_len = u16::from_le_bytes(token_len) as usize;
|
|
|
|
|
let mut token = vec![0u8; token_len];
|
|
|
|
|
recv.read_exact(&mut token).await?;
|
2026-07-12 06:14:34 +00:00
|
|
|
let stream = read_json(recv).await?;
|
|
|
|
|
let channels = read_json(recv).await?;
|
2026-08-15 08:17:48 +00:00
|
|
|
Ok(TelemetryQuicHeader {
|
2026-07-12 06:14:34 +00:00
|
|
|
flow_id,
|
|
|
|
|
token,
|
|
|
|
|
stream,
|
|
|
|
|
channels,
|
|
|
|
|
})
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-12 06:14:34 +00:00
|
|
|
pub async fn read_next_event(
|
|
|
|
|
recv: &mut RecvStream,
|
|
|
|
|
stream: &StreamDescriptor,
|
2026-08-15 08:17:48 +00:00
|
|
|
) -> Result<Option<TelemetryEvent>, BoxError> {
|
2026-06-25 12:30:18 +00:00
|
|
|
let mut len = [0u8; 4];
|
|
|
|
|
if recv.read_exact(&mut len).await.is_err() {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
let len = u32::from_le_bytes(len) as usize;
|
|
|
|
|
if len > MAX_RECORD_BYTES {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry QUIC record exceeds max size".into());
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
let mut buf = vec![0u8; len];
|
|
|
|
|
recv.read_exact(&mut buf).await?;
|
2026-07-12 06:14:34 +00:00
|
|
|
decode_record(&buf, stream).map(Some)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 08:17:48 +00:00
|
|
|
fn decode_record(buf: &[u8], stream: &StreamDescriptor) -> Result<TelemetryEvent, BoxError> {
|
2026-07-12 06:14:34 +00:00
|
|
|
if buf.is_empty() {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("empty telemetry QUIC record".into());
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
|
|
|
|
match buf[0] {
|
|
|
|
|
TAG_CHANNEL_DECLARED => {
|
|
|
|
|
let descriptor: ChannelDescriptor = serde_json::from_slice(&buf[1..])?;
|
2026-08-15 08:17:48 +00:00
|
|
|
Ok(TelemetryEvent::ChannelDeclared(descriptor))
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
|
|
|
|
TAG_FRAME => {
|
|
|
|
|
if buf.len() < 1 + 4 + 8 + 4 {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry QUIC frame record truncated".into());
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
|
|
|
|
let channel = ChannelId(u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]));
|
|
|
|
|
let position = Position(u64::from_le_bytes([
|
|
|
|
|
buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11], buf[12],
|
|
|
|
|
]));
|
|
|
|
|
let mut len = [0u8; 4];
|
|
|
|
|
len.copy_from_slice(&buf[13..17]);
|
|
|
|
|
let payload_len = u32::from_le_bytes(len) as usize;
|
|
|
|
|
let payload = buf
|
|
|
|
|
.get(17..17 + payload_len)
|
2026-08-15 08:17:48 +00:00
|
|
|
.ok_or("telemetry QUIC frame payload truncated")?;
|
2026-07-12 06:14:34 +00:00
|
|
|
if 17 + payload_len != buf.len() {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("bytes remain after telemetry QUIC frame record".into());
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
2026-08-15 08:17:48 +00:00
|
|
|
Ok(TelemetryEvent::Frame(FrameDelivery {
|
2026-07-12 06:14:34 +00:00
|
|
|
channel: ChannelRef {
|
|
|
|
|
stream: stream.stream.clone(),
|
|
|
|
|
channel,
|
|
|
|
|
},
|
|
|
|
|
position,
|
|
|
|
|
payload: payload.to_vec(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
2026-08-15 08:17:48 +00:00
|
|
|
TAG_STREAM_ENDED => Ok(TelemetryEvent::StreamEnded(stream.stream.clone())),
|
|
|
|
|
_ => Err("unknown telemetry QUIC record tag".into()),
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
2026-06-25 12:30:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn read_stream_into_fanout(
|
|
|
|
|
recv: RecvStream,
|
2026-08-15 08:17:48 +00:00
|
|
|
fanout: Arc<telemetry::DeliveryFanout>,
|
|
|
|
|
) -> Result<TelemetryQuicHeader, BoxError> {
|
2026-07-12 06:14:34 +00:00
|
|
|
let read = read_events_from_stream(recv).await?;
|
2026-07-21 06:25:52 +00:00
|
|
|
fanout.publish_batch(read.events);
|
2026-06-25 12:30:18 +00:00
|
|
|
Ok(read.header)
|
|
|
|
|
}
|
2026-07-12 06:14:34 +00:00
|
|
|
|
|
|
|
|
fn put_json<T: serde::Serialize>(out: &mut Vec<u8>, value: &T) -> Result<(), BoxError> {
|
|
|
|
|
out.extend_from_slice(&serde_json::to_vec(value)?);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn put_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), BoxError> {
|
|
|
|
|
if bytes.len() > u32::MAX as usize {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry delivery exceeds u32 length prefix".into());
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
|
|
|
|
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
|
|
|
|
out.extend_from_slice(bytes);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn write_json<T: serde::Serialize>(send: &mut SendStream, value: &T) -> Result<(), BoxError> {
|
|
|
|
|
let bytes = serde_json::to_vec(value)?;
|
|
|
|
|
if bytes.len() > u32::MAX as usize {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry header JSON exceeds u32 length prefix".into());
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
|
|
|
|
send.write_all(&(bytes.len() as u32).to_le_bytes()).await?;
|
|
|
|
|
send.write_all(&bytes).await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn read_json<T: serde::de::DeserializeOwned>(recv: &mut RecvStream) -> Result<T, BoxError> {
|
|
|
|
|
let mut len = [0u8; 4];
|
|
|
|
|
recv.read_exact(&mut len).await?;
|
|
|
|
|
let len = u32::from_le_bytes(len) as usize;
|
|
|
|
|
if len > MAX_RECORD_BYTES {
|
2026-08-15 08:17:48 +00:00
|
|
|
return Err("telemetry QUIC record exceeds max size".into());
|
2026-07-12 06:14:34 +00:00
|
|
|
}
|
|
|
|
|
let mut bytes = vec![0u8; len];
|
|
|
|
|
recv.read_exact(&mut bytes).await?;
|
|
|
|
|
Ok(serde_json::from_slice(&bytes)?)
|
|
|
|
|
}
|