diff --git a/Cargo.lock b/Cargo.lock index 2de3a1b..3079515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1327,13 +1327,12 @@ dependencies = [ name = "distribution" version = "0.1.0" dependencies = [ - "ed25519-dalek 2.2.0", "iroh", "iroh-relay", - "rand_core 0.6.4", "serde", "serde_json", "swactor", + "swactor-transport", "tokio", ] @@ -5337,16 +5336,15 @@ dependencies = [ "clap", "crossbeam-queue", "ctrlc", - "ed25519-dalek 2.2.0", "getrandom 0.2.17", "iroh", "proptest", "proptest-state-machine", - "rand_core 0.6.4", "serde", "serde_json", "stateright", "swactor", + "swactor-transport", "tempfile", "tiny_http", "tokio", diff --git a/crates/dashboard/src/actor_detail_html.rs b/crates/dashboard/src/actor_detail_html.rs deleted file mode 100644 index 6364cee..0000000 --- a/crates/dashboard/src/actor_detail_html.rs +++ /dev/null @@ -1,416 +0,0 @@ -pub const ACTOR_DETAIL_HTML: &str = r##" - - - - -Actor Detail — Swactor Dashboard - - - -
-
-

- Swactor Runtime Dashboard - -

- -
-
- -
- - -
- -
-
Address
—
-
Worker
—
-
Status
—
-
-
-
Last Message Type
—
-
-
- -
-
0
Messages
-
0
Mailbox
-
0
Msg/s
-
—
Worker Load
-
- -
-

Message Rate

- -
- -
-

Mailbox Depth

- -
- - - -
-

- Logs - (0) -
- - - - - -
-

-
-
-
- - - - -"##; diff --git a/crates/dashboard/src/actors_html.rs b/crates/dashboard/src/actors_html.rs deleted file mode 100644 index f4fe739..0000000 --- a/crates/dashboard/src/actors_html.rs +++ /dev/null @@ -1,807 +0,0 @@ -pub const ACTORS_HTML: &str = r##" - - - - -Swactor Runtime – Actors - - - -
-
-

- Swactor Runtime Dashboard - - REPLAY -

- -
-
- - - -
-
-
-
-
- -
- -
-

Actor Stats

-
-
0
Total Actors
-
0
Avg Mailbox
-
0
Max Mailbox
-
0
Active Workers
-
-
- - -
-

Mailbox Depth Distribution

- -
- - -
-

Actors per Worker

- -
- - -
-

All Actors

-
- - - - - -
-
- - - - - - - - - - - - -
Address Worker Mailbox Msgs Last MsgDepth
-
-
- - -
- - -
-
-

Actor

- -
-
-
Address
-
Worker
-
Mailbox
-
Messages
-
Last Type
-
Status
-
-
Mailbox History
- -
- - - - -"##; diff --git a/crates/dashboard/src/command/mod.rs b/crates/dashboard/src/command/mod.rs index ad1e2b3..0ec0cec 100644 --- a/crates/dashboard/src/command/mod.rs +++ b/crates/dashboard/src/command/mod.rs @@ -16,9 +16,6 @@ //! ``` pub mod builtins; -mod parse; - -pub use parse::{from_query_params, parse_line}; use std::collections::HashMap; use std::sync::Arc; @@ -246,3 +243,78 @@ impl CommandRouter { names } } + +// ─── Input Parsers ────────────────────────────────────────────────────────── + +/// Parse a REPL text line into a [`CommandRequest`]. +/// +/// Handles `--flag value` pairs and maps positional arguments to +/// command-specific named parameters. +pub fn parse_line(line: &str) -> CommandRequest { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.is_empty() { + return CommandRequest { + command: "help".to_string(), + args: HashMap::new(), + }; + } + let command = parts[0].to_string(); + let rest = &parts[1..]; + + let mut args = HashMap::new(); + let mut i = 0; + let mut positional = 0; + + while i < rest.len() { + if let Some(key) = rest[i].strip_prefix("--") { + if i + 1 < rest.len() && !rest[i + 1].starts_with("--") { + args.insert( + key.to_string(), + serde_json::Value::String(rest[i + 1].to_string()), + ); + i += 2; + } else { + args.insert(key.to_string(), serde_json::Value::Bool(true)); + i += 1; + } + } else { + let name = positional_arg_name(&command, positional); + if !name.is_empty() { + args.insert( + name.to_string(), + serde_json::Value::String(rest[i].to_string()), + ); + } + positional += 1; + i += 1; + } + } + + CommandRequest { command, args } +} + +/// Convert HTTP query parameters to a [`CommandRequest`]. +pub fn from_query_params(params: &HashMap) -> CommandRequest { + let command = params + .get("cmd") + .cloned() + .unwrap_or_else(|| "help".into()); + let args: HashMap = params + .iter() + .filter(|(k, _)| *k != "cmd") + .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone()))) + .collect(); + CommandRequest { command, args } +} + +/// Map positional argument index to the named parameter for each command. +fn positional_arg_name(command: &str, position: usize) -> &'static str { + match (command, position) { + ("worker", 0) => "id", + ("actor", 0) => "prefix", + ("hot", 0) => "n", + ("phases", 0) => "worker", + ("diff", 0) => "seconds", + _ => "", + } +} diff --git a/crates/dashboard/src/command/parse.rs b/crates/dashboard/src/command/parse.rs deleted file mode 100644 index 4809bde..0000000 --- a/crates/dashboard/src/command/parse.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Input parsers for REPL lines and HTTP query parameters. - -use std::collections::HashMap; - -use super::CommandRequest; - -/// Parse a REPL text line into a [`CommandRequest`]. -/// -/// Handles `--flag value` pairs and maps positional arguments to -/// command-specific named parameters. -/// -/// # Examples -/// -/// ```text -/// "overview" → { command: "overview", args: {} } -/// "worker 3" → { command: "worker", args: { "id": "3" } } -/// "actors --sort mailbox" → { command: "actors", args: { "sort": "mailbox" } } -/// "hot 5" → { command: "hot", args: { "n": "5" } } -/// ``` -pub fn parse_line(line: &str) -> CommandRequest { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.is_empty() { - return CommandRequest { - command: "help".to_string(), - args: HashMap::new(), - }; - } - let command = parts[0].to_string(); - let rest = &parts[1..]; - - let mut args = HashMap::new(); - let mut i = 0; - let mut positional = 0; - - while i < rest.len() { - if let Some(key) = rest[i].strip_prefix("--") { - if i + 1 < rest.len() && !rest[i + 1].starts_with("--") { - args.insert( - key.to_string(), - serde_json::Value::String(rest[i + 1].to_string()), - ); - i += 2; - } else { - args.insert(key.to_string(), serde_json::Value::Bool(true)); - i += 1; - } - } else { - let name = positional_arg_name(&command, positional); - if !name.is_empty() { - args.insert( - name.to_string(), - serde_json::Value::String(rest[i].to_string()), - ); - } - positional += 1; - i += 1; - } - } - - CommandRequest { command, args } -} - -/// Convert HTTP query parameters to a [`CommandRequest`]. -/// -/// The `cmd` parameter becomes the command name; all other parameters -/// become string-valued arguments. -pub fn from_query_params(params: &HashMap) -> CommandRequest { - let command = params - .get("cmd") - .cloned() - .unwrap_or_else(|| "help".into()); - let args: HashMap = params - .iter() - .filter(|(k, _)| *k != "cmd") - .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone()))) - .collect(); - CommandRequest { command, args } -} - -/// Map positional argument index to the named parameter for each command. -fn positional_arg_name(command: &str, position: usize) -> &'static str { - match (command, position) { - ("worker", 0) => "id", - ("actor", 0) => "prefix", - ("hot", 0) => "n", - ("phases", 0) => "worker", - ("diff", 0) => "seconds", - _ => "", - } -} diff --git a/crates/dashboard/src/dashboard_html.rs b/crates/dashboard/src/dashboard_html.rs deleted file mode 100644 index b3a3a1b..0000000 --- a/crates/dashboard/src/dashboard_html.rs +++ /dev/null @@ -1,663 +0,0 @@ -pub const DASHBOARD_HTML: &str = r##" - - - - -Swactor Runtime Dashboard - - - -
-
-

- Swactor Runtime Dashboard - - REPLAY -

- -
-
- - - -
-
-
-
-
- - -
-
-

Worker Utilization

-
-
- \u25A0 processing - \u25A0 delivery - \u25A0 spawns - \u25A0 overhead -
-
- -
-

Stats

-
-
0
Actors
-
0
Messages
-
0
Workers
-
0
Mailbox
-
-
- -
-

Actors

-
- - - -
AddressWorker
-
-
- -
-

Worker Details

-
-
- -
-

Activity Log

-
- - - -
SeqTimeLevelWorkerMessageFields
-
-
-
- - - - -"##; diff --git a/crates/dashboard/src/html.rs b/crates/dashboard/src/html.rs new file mode 100644 index 0000000..d4edf36 --- /dev/null +++ b/crates/dashboard/src/html.rs @@ -0,0 +1,2168 @@ +pub const ACTOR_DETAIL_HTML: &str = r##" + + + + +Actor Detail — Swactor Dashboard + + + +
+
+

+ Swactor Runtime Dashboard + +

+ +
+
+ +
+ + +
+ +
+
Address
—
+
Worker
—
+
Status
—
+
+
+
Last Message Type
—
+
+
+ +
+
0
Messages
+
0
Mailbox
+
0
Msg/s
+
—
Worker Load
+
+ +
+

Message Rate

+ +
+ +
+

Mailbox Depth

+ +
+ + + +
+

+ Logs + (0) +
+ + + + + +
+

+
+
+
+ + + + +"##; +pub const ACTORS_HTML: &str = r##" + + + + +Swactor Runtime – Actors + + + +
+
+

+ Swactor Runtime Dashboard + + REPLAY +

+ +
+
+ + + +
+
+
+
+
+ +
+ +
+

Actor Stats

+
+
0
Total Actors
+
0
Avg Mailbox
+
0
Max Mailbox
+
0
Active Workers
+
+
+ + +
+

Mailbox Depth Distribution

+ +
+ + +
+

Actors per Worker

+ +
+ + +
+

All Actors

+
+ + + + + +
+
+ + + + + + + + + + + + +
Address Worker Mailbox Msgs Last MsgDepth
+
+
+ + +
+ + +
+
+

Actor

+ +
+
+
Address
+
Worker
+
Mailbox
+
Messages
+
Last Type
+
Status
+
+
Mailbox History
+ +
+ + + + +"##; +pub const DASHBOARD_HTML: &str = r##" + + + + +Swactor Runtime Dashboard + + + +
+
+

+ Swactor Runtime Dashboard + + REPLAY +

+ +
+
+ + + +
+
+
+
+
+ + +
+
+

Worker Utilization

+
+
+ \u25A0 processing + \u25A0 delivery + \u25A0 spawns + \u25A0 overhead +
+
+ +
+

Stats

+
+
0
Actors
+
0
Messages
+
0
Workers
+
0
Mailbox
+
+
+ +
+

Actors

+
+ + + +
AddressWorker
+
+
+ +
+

Worker Details

+
+
+ +
+

Activity Log

+
+ + + +
SeqTimeLevelWorkerMessageFields
+
+
+
+ + + + +"##; +pub const TOPOLOGY_HTML: &str = r##" + + + + +Topology — Swactor Dashboard + + + +
+
+

Swactor Runtime Dashboard

+ +
+
+ +
+ +
+ Node size = actor count. Edge thickness = message volume. Green = local sends. Blue = cross-worker sends. +
+
+ + + + +"##; diff --git a/crates/dashboard/src/lib.rs b/crates/dashboard/src/lib.rs index 948bd81..daa7b9f 100644 --- a/crates/dashboard/src/lib.rs +++ b/crates/dashboard/src/lib.rs @@ -4,14 +4,10 @@ pub mod history; pub mod investigate; pub mod layer; pub mod plugin; -pub mod trace; pub mod warnings; -mod actor_detail_html; -mod actors_html; -mod dashboard_html; +mod html; mod server; pub mod topology; -mod topology_html; #[cfg(feature = "tui")] pub mod tui; @@ -28,11 +24,29 @@ use swactor::runtime::{Runtime, RuntimeHandle}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; +use serde::{Deserialize, Serialize}; +use swactor::stats::RuntimeStats; + use crate::collector::StatsCollector; use crate::history::{DashboardHistory, HistoryConfig}; -use crate::layer::{now_ms, DashboardLayer, EventStore}; +use crate::layer::{now_ms, DashboardEvent, DashboardLayer, EventStore}; use crate::plugin::PluginRegistry; -use crate::trace::{RuntimeTrace, TimestampedStats}; + +// ─── Trace Types ──────────────────────────────────────────────────────────── + +/// Complete trace of a runtime execution, suitable for saving/loading. +#[derive(Debug, Serialize, Deserialize)] +pub struct RuntimeTrace { + pub events: Vec, + pub stats_timeline: Vec, +} + +/// A stats snapshot with a wall-clock timestamp. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimestampedStats { + pub timestamp_ms: u64, + pub stats: RuntimeStats, +} /// Peer info sent through the join channel. pub struct JoinPeerInfo { @@ -150,7 +164,7 @@ impl DashboardHandle { } /// Start the HTTP server on a standalone tokio runtime (1 worker thread). - /// Use this when no external tokio runtime is available (e.g. TCP transport). + /// Use this when no external tokio runtime is available (e.g. non-async transport). pub fn start_http_standalone(&self) { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(1) diff --git a/crates/dashboard/src/server.rs b/crates/dashboard/src/server.rs index 79588c5..7b94abf 100644 --- a/crates/dashboard/src/server.rs +++ b/crates/dashboard/src/server.rs @@ -16,20 +16,17 @@ use tokio_stream::StreamExt; use swactor::runtime::Runtime; -use crate::actor_detail_html::ACTOR_DETAIL_HTML; -use crate::actors_html::ACTORS_HTML; use crate::collector::StatsCollector; use crate::command::CommandRouter; -use crate::dashboard_html::DASHBOARD_HTML; use crate::history::DashboardHistory; +use crate::html::{ACTOR_DETAIL_HTML, ACTORS_HTML, DASHBOARD_HTML, TOPOLOGY_HTML}; use crate::layer::EventStore; use crate::topology; -use crate::topology_html::TOPOLOGY_HTML; use crate::warnings::{WarningConfig, WarningDetector}; use crate::plugin::PluginRegistry; -use crate::trace::RuntimeTrace; +use crate::RuntimeTrace; /// Format a server-sent event. fn format_sse(event: &str, data: &str) -> Event { diff --git a/crates/dashboard/src/topology_html.rs b/crates/dashboard/src/topology_html.rs deleted file mode 100644 index f9f5f07..0000000 --- a/crates/dashboard/src/topology_html.rs +++ /dev/null @@ -1,282 +0,0 @@ -pub const TOPOLOGY_HTML: &str = r##" - - - - -Topology — Swactor Dashboard - - - -
-
-

Swactor Runtime Dashboard

- -
-
- -
- -
- Node size = actor count. Edge thickness = message volume. Green = local sends. Blue = cross-worker sends. -
-
- - - - -"##; diff --git a/crates/dashboard/src/trace.rs b/crates/dashboard/src/trace.rs deleted file mode 100644 index 09505d5..0000000 --- a/crates/dashboard/src/trace.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Deserialize, Serialize}; -use swactor::stats::RuntimeStats; - -use crate::layer::DashboardEvent; - -/// Complete trace of a runtime execution, suitable for saving/loading. -#[derive(Debug, Serialize, Deserialize)] -pub struct RuntimeTrace { - pub events: Vec, - pub stats_timeline: Vec, -} - -/// A stats snapshot with a wall-clock timestamp. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimestampedStats { - pub timestamp_ms: u64, - pub stats: RuntimeStats, -} diff --git a/crates/dashboard/tests/dashboard_core.rs b/crates/dashboard/tests/dashboard_core.rs index 1beff8f..9b18f56 100644 --- a/crates/dashboard/tests/dashboard_core.rs +++ b/crates/dashboard/tests/dashboard_core.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use dashboard::collector::StatsCollector; use dashboard::layer::{DashboardEvent, EventStore}; -use dashboard::trace::RuntimeTrace; +use dashboard::RuntimeTrace; use swactor::actor::ActorAddress; use swactor::stats::{ActorSnapshot, StatsHook}; diff --git a/crates/datastore/Cargo.toml b/crates/datastore/Cargo.toml index cfa67e5..854a647 100644 --- a/crates/datastore/Cargo.toml +++ b/crates/datastore/Cargo.toml @@ -8,8 +8,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } [dependencies] swactor = { path = "../..", features = ["serde", "transport"] } -ed25519-dalek = { version = "2", features = ["rand_core"] } -rand_core = { version = "0.6", features = ["getrandom"] } +swactor-transport = { path = "../transport" } serde = { version = "1", features = ["derive"] } serde_json = "1" blake3 = "1" @@ -35,6 +34,7 @@ stateright = "0.31" [features] node = ["dep:tiny_http", "dep:clap", "dep:ctrlc", "dep:toml"] cli = ["dep:clap", "dep:ureq"] +formal-verification = [] [[bin]] name = "swactor-store" diff --git a/crates/datastore/src/bin/store_cli.rs b/crates/datastore/src/bin/store_cli.rs index a1a2568..c4d5ebd 100644 --- a/crates/datastore/src/bin/store_cli.rs +++ b/crates/datastore/src/bin/store_cli.rs @@ -316,75 +316,6 @@ fn resolve_authorized_key(base: &str, name_input: &str, kp: &Keypair) -> String } } -// ── Key file helpers ──────────────────────────────────────────────────────── - -fn hex_decode(hex: &str) -> Option> { - if hex.len() % 2 != 0 { - return None; - } - let mut bytes = Vec::with_capacity(hex.len() / 2); - for chunk in hex.as_bytes().chunks(2) { - let hi = hex_digit(chunk[0])?; - let lo = hex_digit(chunk[1])?; - bytes.push((hi << 4) | lo); - } - Some(bytes) -} - -fn hex_digit(b: u8) -> Option { - match b { - b'0'..=b'9' => Some(b - b'0'), - b'a'..=b'f' => Some(b - b'a' + 10), - b'A'..=b'F' => Some(b - b'A' + 10), - _ => None, - } -} - -fn load_keypair(path: &std::path::Path) -> Keypair { - let data = fs::read_to_string(path).unwrap_or_else(|e| { - eprintln!("Error reading key file {}: {e}", path.display()); - std::process::exit(1); - }); - let json: serde_json::Value = serde_json::from_str(&data).unwrap_or_else(|e| { - eprintln!("Error parsing key file: {e}"); - std::process::exit(1); - }); - let secret_hex = json - .get("secret_key") - .and_then(|v| v.as_str()) - .unwrap_or_else(|| { - eprintln!("Key file missing secret_key field"); - std::process::exit(1); - }); - let secret_bytes = hex_decode(secret_hex).unwrap_or_else(|| { - eprintln!("Invalid secret_key hex in key file"); - std::process::exit(1); - }); - let secret: [u8; 32] = secret_bytes.try_into().unwrap_or_else(|_| { - eprintln!("secret_key must be exactly 32 bytes"); - std::process::exit(1); - }); - Keypair::from_bytes(&secret) -} - -// ── Auth signing ──────────────────────────────────────────────────────────── - -fn sign_action(keypair: &Keypair, action: DatastoreAction) -> String { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let mut nonce = [0u8; 16]; - getrandom::getrandom(&mut nonce).expect("failed to generate random nonce"); - let payload = SignedRequestPayload { - action, - timestamp, - nonce, - }; - let signed = sign_request(keypair, payload); - serde_json::to_string(&signed).expect("SignedRequest is always serializable") -} - fn main() { let args = Args::parse(); let base = args.url.trim_end_matches('/'); diff --git a/crates/datastore/src/cli.rs b/crates/datastore/src/cli.rs deleted file mode 100644 index 8c126c7..0000000 --- a/crates/datastore/src/cli.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! CLI command type definitions for `swactor-store`. -//! -//! Types only — no implementation. These define the CLI interface that will -//! be wired to the actor system in a future milestone. - -use std::collections::BTreeMap; -use std::path::PathBuf; - -use swactor::transport::NodeId; - -/// Top-level CLI commands for `swactor-store`. -#[derive(Debug, Clone)] -pub enum CliCommand { - /// Store a local file as a distributed object. - /// - /// ```text - /// swactor-store put [--name