diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e6098368..5929b4027 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,18 @@ -# Robonix CI — all build/check logic lives in rust/Makefile (make ci-rust / make ci-full) +# Robonix CI — build + lint checks name: CI on: push: - branches: [main, dev/**] + branches: [main, dev, dev/**] pull_request: - branches: [main, dev/**] + branches: [main, dev, dev/**] env: RUST_DIR: rust jobs: - rust: - name: Rust (make ci-rust) + check: + name: Rust (fmt + clippy + build) runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -20,44 +20,13 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - components: rustfmt + components: rustfmt, clippy - - name: make ci-rust - run: cd ${{ env.RUST_DIR }} && make ci-rust + - name: cargo fmt --check + run: cd ${{ env.RUST_DIR }} && cargo fmt --all -- --check - full: - name: Full (make ci-full) - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - - name: Install ROS2 Humble - run: | - sudo apt-get update - sudo apt-get install -y locales - sudo locale-gen en_US en_US.UTF-8 - sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 - export LANG=en_US.UTF-8 - sudo apt-get install -y curl gnupg2 lsb-release - sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null - sudo apt-get update - sudo apt-get install -y ros-humble-ros-base python3-pip - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Install colcon - run: | - source /opt/ros/humble/setup.bash - pip3 install --user colcon-common-extensions - echo "$HOME/.local/bin" >> $GITHUB_PATH - export PATH="$HOME/.local/bin:$PATH" - which colcon + - name: cargo clippy + run: cd ${{ env.RUST_DIR }} && cargo clippy --workspace -- -D warnings - - name: make ci-full - run: | - source /opt/ros/humble/setup.bash - cd ${{ env.RUST_DIR }} && make ci-full - env: - PYTHON3_EXECUTABLE: /usr/bin/python3 + - name: cargo build + run: cd ${{ env.RUST_DIR }} && cargo build --workspace diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 5ef00cded..4a7e0983a 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -17,12 +17,12 @@ export ROBONIX_META_GRPC_ADDR="${ROBONIX_META_GRPC_ADDR:-0.0.0.0:50051}" export ROBONIX_META_GRPC_ENDPOINT="${ROBONIX_META_GRPC_ENDPOINT:-127.0.0.1:50051}" if ! grep -q "### ROBONIX_DOCKER_ENV ###" ~/.bashrc 2>/dev/null; then -cat <> ~/.bashrc +cat <<'EOF' >> ~/.bashrc ### ROBONIX_DOCKER_ENV ### source /opt/ros/humble/setup.bash -export RMW_IMPLEMENTATION=${RMW_IMPLEMENTATION} -export ROBONIX_META_GRPC_ADDR=${ROBONIX_META_GRPC_ADDR} -export ROBONIX_META_GRPC_ENDPOINT=${ROBONIX_META_GRPC_ENDPOINT} +export RMW_IMPLEMENTATION="${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp}" +export ROBONIX_META_GRPC_ADDR="${ROBONIX_META_GRPC_ADDR:-0.0.0.0:50051}" +export ROBONIX_META_GRPC_ENDPOINT="${ROBONIX_META_GRPC_ENDPOINT:-127.0.0.1:50051}" EOF fi diff --git a/pyrightconfig.json b/pyrightconfig.json index 65b809c94..de5125347 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -3,5 +3,5 @@ "rust/examples/proto_gen", "rust/crates/robonix-interfaces", "rust/examples/packages/maniskill_vla_demo/maniskill_vla_demo" - ], + ] } diff --git a/rust/crates/robonix-atlas/build.rs b/rust/crates/robonix-atlas/build.rs index dc615ee47..bd6942c8a 100644 --- a/rust/crates/robonix-atlas/build.rs +++ b/rust/crates/robonix-atlas/build.rs @@ -23,7 +23,7 @@ fn main() -> Result<(), Box> { .build_client(true) .compile_protos( &[proto_dir.join("robonix_runtime.proto")], - &[proto_dir.clone()], + std::slice::from_ref(&proto_dir), )?; // robonix-codegen build-dependency REMOVED -- robonix-codegen syntax is deprecated. diff --git a/rust/crates/robonix-atlas/src/meta_runtime.rs b/rust/crates/robonix-atlas/src/meta_runtime.rs index c3551843d..49e7e0e2e 100644 --- a/rust/crates/robonix-atlas/src/meta_runtime.rs +++ b/rust/crates/robonix-atlas/src/meta_runtime.rs @@ -127,9 +127,7 @@ impl MetaRuntimeRegistry { } fn is_catalogued_robonix_interface(abstract_path: &str) -> bool { - ROBO_SYSTEM_INTERFACE_CATALOG - .iter() - .any(|&id| id == abstract_path) + ROBO_SYSTEM_INTERFACE_CATALOG.contains(&abstract_path) } /// For `grpc` / `ros2` / `mcp`, namespace must be under `robonix/...`. @@ -334,6 +332,7 @@ impl MetaRuntimeRegistry { } } + #[allow(clippy::too_many_arguments)] async fn register_node( &self, node_id: String, @@ -472,14 +471,14 @@ impl MetaRuntimeRegistry { } fn inject_endpoint(metadata_json: &str, endpoint: &str) -> String { - if let Ok(mut meta) = serde_json::from_str::(metadata_json) { - if let Some(obj) = meta.as_object_mut() { - obj.insert( - "endpoint".into(), - serde_json::Value::String(endpoint.to_string()), - ); - return serde_json::to_string(&meta).unwrap_or_else(|_| metadata_json.to_string()); - } + if let Ok(mut meta) = serde_json::from_str::(metadata_json) + && let Some(obj) = meta.as_object_mut() + { + obj.insert( + "endpoint".into(), + serde_json::Value::String(endpoint.to_string()), + ); + return serde_json::to_string(&meta).unwrap_or_else(|_| metadata_json.to_string()); } // If metadata_json is empty or not an object, create a minimal one. serde_json::json!({ "endpoint": endpoint }).to_string() diff --git a/rust/crates/robonix-buffer/src/buffer.rs b/rust/crates/robonix-buffer/src/buffer.rs index 3f59bfa67..b2204284e 100644 --- a/rust/crates/robonix-buffer/src/buffer.rs +++ b/rust/crates/robonix-buffer/src/buffer.rs @@ -118,18 +118,24 @@ pub struct BufferSpec { impl BufferSpec { pub fn frame_bytes(&self) -> usize { - self.width as usize - * self.height as usize - * self.channels as usize - * self.format.bytes_per_element() + (self.width as usize) + .checked_mul(self.height as usize) + .and_then(|v| v.checked_mul(self.channels as usize)) + .and_then(|v| v.checked_mul(self.format.bytes_per_element())) + .expect("buffer frame size overflow") } pub fn total_bytes(&self) -> usize { - HEADER_SIZE + self.frame_bytes() + HEADER_SIZE + .checked_add(self.frame_bytes()) + .expect("buffer total size overflow") } pub fn stride(&self) -> u32 { - self.width * self.channels * self.format.bytes_per_element() as u32 + self.width + .checked_mul(self.channels) + .and_then(|v| v.checked_mul(self.format.bytes_per_element() as u32)) + .expect("buffer stride overflow") } } @@ -223,3 +229,95 @@ pub enum BufferState { /// One or more consumers are reading / processing. Processing, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_bytes_normal() { + let spec = BufferSpec { + width: 1920, + height: 1080, + channels: 3, + format: BufferFormat::Rgb8, + }; + assert_eq!(spec.frame_bytes(), 1920 * 1080 * 3 * 1); + } + + #[test] + fn frame_bytes_float32() { + let spec = BufferSpec { + width: 640, + height: 480, + channels: 1, + format: BufferFormat::Float32, + }; + assert_eq!(spec.frame_bytes(), 640 * 480 * 1 * 4); + } + + #[test] + fn total_bytes_includes_header() { + let spec = BufferSpec { + width: 100, + height: 100, + channels: 3, + format: BufferFormat::Rgb8, + }; + assert_eq!(spec.total_bytes(), HEADER_SIZE + 100 * 100 * 3); + } + + #[test] + #[should_panic(expected = "buffer frame size overflow")] + fn frame_bytes_overflow_panics() { + let spec = BufferSpec { + width: u32::MAX, + height: u32::MAX, + channels: u32::MAX, + format: BufferFormat::Float64, // 8 bytes per element + }; + // This should panic with overflow instead of silently wrapping + let _ = spec.frame_bytes(); + } + + #[test] + #[should_panic(expected = "buffer stride overflow")] + fn stride_overflow_panics() { + let spec = BufferSpec { + width: u32::MAX, + height: 1, + channels: u32::MAX, + format: BufferFormat::Float64, + }; + let _ = spec.stride(); + } + + #[test] + fn stride_normal() { + let spec = BufferSpec { + width: 1920, + height: 1080, + channels: 3, + format: BufferFormat::Rgb8, + }; + assert_eq!(spec.stride(), 1920 * 3 * 1); + } + + #[test] + fn buffer_format_roundtrip() { + for v in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 17, 18, 19, 20, 255] { + let fmt = BufferFormat::from_u32(v).expect(&format!("format {v} should be valid")); + assert_eq!(fmt as u32, v); + } + assert!( + BufferFormat::from_u32(99).is_none(), + "unknown format should return None" + ); + } + + #[test] + fn header_size_is_64() { + assert_eq!(HEADER_SIZE, 64); + assert_eq!(std::mem::size_of::(), HEADER_SIZE); + } +} diff --git a/rust/crates/robonix-cli/src/cmd/chat.rs b/rust/crates/robonix-cli/src/cmd/chat.rs index c45409428..1cc8fa88f 100644 --- a/rust/crates/robonix-cli/src/cmd/chat.rs +++ b/rust/crates/robonix-cli/src/cmd/chat.rs @@ -98,15 +98,15 @@ async fn try_discover_pilot_once(atlas_endpoint: &str) -> Result { for node in &nodes { for iface in &node.interfaces { - if let Ok(meta) = serde_json::from_str::(&iface.metadata_json) { - if let Some(ep) = meta.get("endpoint").and_then(|v| v.as_str()) { - let uri = if ep.starts_with("http") { - ep.to_string() - } else { - format!("http://{ep}") - }; - return Ok(localhost_to_ipv4_loopback(&uri)); - } + if let Ok(meta) = serde_json::from_str::(&iface.metadata_json) + && let Some(ep) = meta.get("endpoint").and_then(|v| v.as_str()) + { + let uri = if ep.starts_with("http") { + ep.to_string() + } else { + format!("http://{ep}") + }; + return Ok(localhost_to_ipv4_loopback(&uri)); } } } @@ -131,68 +131,68 @@ async fn run_tui( loop { draw(terminal, &messages.borrow(), &input, scroll, busy)?; - if event::poll(std::time::Duration::from_millis(50))? { - if let Event::Key(key) = event::read()? { - if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { - let _ = notify_session_end(pilot_endpoint, &session_id).await; - break; - } - if busy { - match key.code { - KeyCode::PageUp => scroll = scroll.saturating_add(5), - KeyCode::PageDown => scroll = scroll.saturating_sub(5), - _ => {} - } - continue; - } + if event::poll(std::time::Duration::from_millis(50))? + && let Event::Key(key) = event::read()? + { + if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { + let _ = notify_session_end(pilot_endpoint, &session_id).await; + break; + } + if busy { match key.code { - KeyCode::Enter => { - let msg = input.trim().to_string(); - input.clear(); - scroll = 0; - if msg.is_empty() { - continue; - } - if msg == "quit" || msg == "exit" { - let _ = notify_session_end(pilot_endpoint, &session_id).await; - break; - } - messages.borrow_mut().push(ChatMessage { - role: Role::User, - text: msg.clone(), - }); - busy = true; - draw(terminal, &messages.borrow(), &input, scroll, busy)?; - - match run_intent_with_esc_abort( - pilot_endpoint, - &session_id, - &msg, - Rc::clone(&messages), - terminal, - &input, - &mut scroll, - ) - .await - { - Ok(()) => {} - Err(e) => { - messages.borrow_mut().push(ChatMessage { - role: Role::Status, - text: format!("Error: {e:#}"), - }); - } - } - busy = false; - } - KeyCode::Char(c) => input.push(c), - KeyCode::Backspace => { - input.pop(); - } KeyCode::PageUp => scroll = scroll.saturating_add(5), KeyCode::PageDown => scroll = scroll.saturating_sub(5), _ => {} } + continue; + } + match key.code { + KeyCode::Enter => { + let msg = input.trim().to_string(); + input.clear(); + scroll = 0; + if msg.is_empty() { + continue; + } + if msg == "quit" || msg == "exit" { + let _ = notify_session_end(pilot_endpoint, &session_id).await; + break; + } + messages.borrow_mut().push(ChatMessage { + role: Role::User, + text: msg.clone(), + }); + busy = true; + draw(terminal, &messages.borrow(), &input, scroll, busy)?; + + match run_intent_with_esc_abort( + pilot_endpoint, + &session_id, + &msg, + Rc::clone(&messages), + terminal, + &input, + &mut scroll, + ) + .await + { + Ok(()) => {} + Err(e) => { + messages.borrow_mut().push(ChatMessage { + role: Role::Status, + text: format!("Error: {e:#}"), + }); + } + } + busy = false; + } + KeyCode::Char(c) => input.push(c), + KeyCode::Backspace => { + input.pop(); + } + KeyCode::PageUp => scroll = scroll.saturating_add(5), + KeyCode::PageDown => scroll = scroll.saturating_sub(5), + _ => {} } } } @@ -331,8 +331,8 @@ async fn run_intent_with_esc_abort( } } _ = tokio::time::sleep(std::time::Duration::from_millis(25)) => { - if event::poll(std::time::Duration::ZERO)? { - if let Event::Key(key) = event::read()? { + if event::poll(std::time::Duration::ZERO)? + && let Event::Key(key) = event::read()? { match key.code { KeyCode::Esc => { let _ = abort_pilot_session(pilot_endpoint, session_id).await; @@ -354,7 +354,6 @@ async fn run_intent_with_esc_abort( _ => {} } } - } } } } @@ -391,7 +390,7 @@ fn apply_pilot_event( } EVT_FINAL_TEXT => { let t = event.final_text.clone(); - let has_agent = m.last().map_or(false, |x| matches!(x.role, Role::Agent)); + let has_agent = m.last().is_some_and(|x| matches!(x.role, Role::Agent)); if !has_agent && !t.is_empty() { m.push(ChatMessage { role: Role::Agent, diff --git a/rust/crates/robonix-cli/src/cmd/config.rs b/rust/crates/robonix-cli/src/cmd/config.rs index 03f2e1ca3..e06a8aa72 100644 --- a/rust/crates/robonix-cli/src/cmd/config.rs +++ b/rust/crates/robonix-cli/src/cmd/config.rs @@ -30,8 +30,7 @@ pub async fn execute(_config: Config, set_storage_path: Option, show: b let real_path = std::fs::canonicalize(&config_path).unwrap_or_else(|_| config_path.clone()); println!( - "{} {}", - "Current robonix system config are located at:", + "Current robonix system config are located at: {}", real_path.display().to_string().bold().cyan() ); println!("\n{}", "Configuration:".bold().cyan()); diff --git a/rust/crates/robonix-cli/src/cmd/graph.rs b/rust/crates/robonix-cli/src/cmd/graph.rs index 8e016a2d4..7fea209f3 100644 --- a/rust/crates/robonix-cli/src/cmd/graph.rs +++ b/rust/crates/robonix-cli/src/cmd/graph.rs @@ -699,8 +699,8 @@ fn render_svg(nodes: &[robonix_sdk::NodeInfo], channels: &[Channel], title: &str writeln!(s, "consumed by: {}", by + BPAD + 2.0 * LH + 8.5, esc(&consumers.join(", "))).unwrap(); } - } else if is_client { - if let Some(prov) = channels + } else if is_client + && let Some(prov) = channels .iter() .find(|c| c.consumer_id == n.node_id && c.interface_name == iface.name) .map(|c| { @@ -710,10 +710,9 @@ fn render_svg(nodes: &[robonix_sdk::NodeInfo], channels: &[Channel], title: &str .unwrap_or(&c.provider_id) .to_string() }) - { - writeln!(s, "provider: {}", + { + writeln!(s, "provider: {}", by + BPAD + 2.0 * LH + 8.5, esc(&prov)).unwrap(); - } } } diff --git a/rust/crates/robonix-cli/src/cmd/run_package.rs b/rust/crates/robonix-cli/src/cmd/run_package.rs index 615723801..72f5f78e2 100644 --- a/rust/crates/robonix-cli/src/cmd/run_package.rs +++ b/rust/crates/robonix-cli/src/cmd/run_package.rs @@ -194,11 +194,10 @@ pub async fn execute_start( .launch_profiles .as_ref() .and_then(|p| p.get("default")) + && let Some(launch) = profile.nodes.get(&node.id) { - if let Some(launch) = profile.nodes.get(&node.id) { - for (k, v) in &launch.env { - env.insert(k.clone(), v.clone()); - } + for (k, v) in &launch.env { + env.insert(k.clone(), v.clone()); } } diff --git a/rust/crates/robonix-cli/src/cmd/runtime.rs b/rust/crates/robonix-cli/src/cmd/runtime.rs index 531a6b73f..558eab657 100644 --- a/rust/crates/robonix-cli/src/cmd/runtime.rs +++ b/rust/crates/robonix-cli/src/cmd/runtime.rs @@ -245,15 +245,14 @@ fn iface_summary(metadata_json: &str, transport: &str) -> String { parts.push(format!("tools={} total", names.len())); } } - } else if transport.contains("grpc") { - if let Some(rpc) = meta + } else if transport.contains("grpc") + && let Some(rpc) = meta .get("contract") .and_then(|c| c.get("rpc_method")) .and_then(|v| v.as_str()) - { - let short = rpc.rsplit('/').next().unwrap_or(rpc); - parts.push(format!("rpc={short}")); - } + { + let short = rpc.rsplit('/').next().unwrap_or(rpc); + parts.push(format!("rpc={short}")); } parts.join(" ") @@ -411,7 +410,7 @@ pub async fn describe(endpoint: &str, node_id: Option<&str>, json: bool) -> Resu for s in &skills { let node = nodes.iter().find(|n| n.node_id == s.node_id); - let iface_str = node.map(|n| fmt_interfaces(n)).unwrap_or_default(); + let iface_str = node.map(fmt_interfaces).unwrap_or_default(); println!("== {} ({}) [{}] ==", s.node_id, s.namespace, s.kind); println!(" interfaces: {}", iface_str); let preview: String = s.skill_md.lines().take(5).collect::>().join("\n"); @@ -492,7 +491,7 @@ pub async fn inspect(endpoint: &str) -> Result<()> { let mut sdk = connect(endpoint).await?; let raw = sdk.inspect_runtime().await?; let parsed: serde_json::Value = - serde_json::from_str(&raw).unwrap_or_else(|_| serde_json::Value::String(raw)); + serde_json::from_str(&raw).unwrap_or(serde_json::Value::String(raw)); println!("{}", serde_json::to_string_pretty(&parsed)?); Ok(()) } @@ -514,8 +513,8 @@ pub async fn channels(endpoint: &str) -> Result<()> { } println!( - "{:<38} {:<12} {:<24} {:<18} {}", - "CHANNEL_ID", "TRANSPORT", "ENDPOINT", "PROVIDER", "CONSUMER" + "{:<38} {:<12} {:<24} {:<18} CONSUMER", + "CHANNEL_ID", "TRANSPORT", "ENDPOINT", "PROVIDER" ); println!("{}", "-".repeat(115)); for ch in &chs { diff --git a/rust/crates/robonix-cli/src/config.rs b/rust/crates/robonix-cli/src/config.rs index d699c7464..082382bbd 100644 --- a/rust/crates/robonix-cli/src/config.rs +++ b/rust/crates/robonix-cli/src/config.rs @@ -63,6 +63,7 @@ impl Config { Ok(()) } + #[allow(clippy::should_implement_trait)] pub fn default() -> Self { let default_path = dirs::home_dir() .unwrap_or_else(|| PathBuf::from("/tmp")) diff --git a/rust/crates/robonix-cli/src/database.rs b/rust/crates/robonix-cli/src/database.rs index 6ee06a1b9..d2a581f2c 100644 --- a/rust/crates/robonix-cli/src/database.rs +++ b/rust/crates/robonix-cli/src/database.rs @@ -161,14 +161,14 @@ impl PackageDatabase { } for name in db.packages.keys().cloned().collect::>() { - if !found_packages.contains(&name) { - if let Some(removed) = db.remove_package(&name) { - log::info!( - "Removed '{}' from database (not found: {})", - name, - removed.path.display() - ); - } + if !found_packages.contains(&name) + && let Some(removed) = db.remove_package(&name) + { + log::info!( + "Removed '{}' from database (not found: {})", + name, + removed.path.display() + ); } } diff --git a/rust/crates/robonix-cli/src/install.rs b/rust/crates/robonix-cli/src/install.rs index 7dc5ad079..639378b3c 100644 --- a/rust/crates/robonix-cli/src/install.rs +++ b/rust/crates/robonix-cli/src/install.rs @@ -35,7 +35,7 @@ impl PackageInstaller { let repo_url = repo.to_string(); let repo_name = repo .split('/') - .last() + .next_back() .context("Invalid GitHub repository format")? .trim_end_matches(".git"); diff --git a/rust/crates/robonix-cli/src/output.rs b/rust/crates/robonix-cli/src/output.rs index b647063ff..b204af15a 100644 --- a/rust/crates/robonix-cli/src/output.rs +++ b/rust/crates/robonix-cli/src/output.rs @@ -84,7 +84,7 @@ impl Spinner { let spinner_char = frames[frame % frames.len()]; let line = format!(" {} {}", spinner_char, message); print!("\r{}", line); - io::stdout().flush().unwrap(); + let _ = io::stdout().flush(); frame += 1; tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } @@ -101,8 +101,8 @@ impl Spinner { // Clear the line first (using ANSI escape code) print!("\r\x1b[K"); let line = format!(" {} {}", "✓".green(), final_message.green()); - print!("{}\n", line); - io::stdout().flush().unwrap(); + println!("{}", line); + let _ = io::stdout().flush(); } /// Stop the spinner and show error message @@ -113,8 +113,8 @@ impl Spinner { // Clear the line first (using ANSI escape code) print!("\r\x1b[K"); let line = format!(" {} {}", "✗".red(), final_message.red()); - print!("{}\n", line); - io::stdout().flush().unwrap(); + println!("{}", line); + let _ = io::stdout().flush(); } } diff --git a/rust/crates/robonix-cli/src/process.rs b/rust/crates/robonix-cli/src/process.rs index de0a89195..f3ef0d963 100644 --- a/rust/crates/robonix-cli/src/process.rs +++ b/rust/crates/robonix-cli/src/process.rs @@ -373,11 +373,10 @@ impl ProcessManager { .arg("-g") .arg(pgid.as_raw().to_string()) .output() + && !output.status.success() { - if !output.status.success() { - // No processes in group, they've all terminated - break; - } + // No processes in group, they've all terminated + break; } std::thread::sleep(check_interval); } @@ -393,10 +392,10 @@ impl ProcessManager { let reader = BufReader::new(&*output.stdout); let mut still_alive = Vec::new(); for line in reader.lines() { - if let Ok(pid_str) = line { - if let Ok(proc_pid) = pid_str.parse::() { - still_alive.push(proc_pid); - } + if let Ok(pid_str) = line + && let Ok(proc_pid) = pid_str.parse::() + { + still_alive.push(proc_pid); } } @@ -421,10 +420,9 @@ impl ProcessManager { .arg("-p") .arg(pid.to_string()) .output() + && output.status.success() { - if output.status.success() { - let _ = kill(pid_obj, Signal::SIGKILL); - } + let _ = kill(pid_obj, Signal::SIGKILL); } } @@ -443,12 +441,11 @@ impl ProcessManager { .arg("-p") .arg(pid.to_string()) .output() + && output.status.success() { - if output.status.success() { - let pgid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if let Ok(pgid) = pgid_str.parse::() { - return Ok(pgid); - } + let pgid_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if let Ok(pgid) = pgid_str.parse::() { + return Ok(pgid); } } @@ -470,15 +467,14 @@ impl ProcessManager { .arg(pgid.to_string()) .stdout(Stdio::piped()) .output() + && output.status.success() { - if output.status.success() { - let reader = BufReader::new(&*output.stdout); - for line in reader.lines() { - if let Ok(pid_str) = line { - if let Ok(pid) = pid_str.parse::() { - pids.push(pid); - } - } + let reader = BufReader::new(&*output.stdout); + for line in reader.lines() { + if let Ok(pid_str) = line + && let Ok(pid) = pid_str.parse::() + { + pids.push(pid); } } } @@ -512,38 +508,35 @@ impl ProcessManager { .arg("--no-headers") .stdout(Stdio::piped()) .output() + && output.status.success() { - if output.status.success() { - let reader = BufReader::new(&*output.stdout); - for line in reader.lines() { - if let Ok(line_str) = line { - // Parse line: PID PPID COMMAND... - // We need to handle spaces in command, so split by first two numbers - let trimmed = line_str.trim(); - if let Some(first_space) = trimmed.find(' ') { - if let Ok(proc_pid) = trimmed[..first_space].parse::() { - let rest = &trimmed[first_space + 1..].trim_start(); - if let Some(second_space) = rest.find(' ') { - if let Ok(proc_ppid) = rest[..second_space].parse::() { - let cmd = rest[second_space + 1..].trim().to_string(); - // Truncate long commands for display - let cmd_display = if cmd.len() > 60 { - format!("{}...", &cmd[..57]) - } else { - cmd - }; - processes.insert( - proc_pid, - ProcessInfo { - pid: proc_pid, - ppid: proc_ppid, - cmd: cmd_display, - }, - ); - } - } - } - } + let reader = BufReader::new(&*output.stdout); + for line_str in reader.lines().map_while(Result::ok) { + // Parse line: PID PPID COMMAND... + // We need to handle spaces in command, so split by first two numbers + let trimmed = line_str.trim(); + if let Some(first_space) = trimmed.find(' ') + && let Ok(proc_pid) = trimmed[..first_space].parse::() + { + let rest = &trimmed[first_space + 1..].trim_start(); + if let Some(second_space) = rest.find(' ') + && let Ok(proc_ppid) = rest[..second_space].parse::() + { + let cmd = rest[second_space + 1..].trim().to_string(); + // Truncate long commands for display + let cmd_display = if cmd.len() > 60 { + format!("{}...", &cmd[..57]) + } else { + cmd + }; + processes.insert( + proc_pid, + ProcessInfo { + pid: proc_pid, + ppid: proc_ppid, + cmd: cmd_display, + }, + ); } } } diff --git a/rust/crates/robonix-codegen/src/codegen/mcp_python_gen.rs b/rust/crates/robonix-codegen/src/codegen/mcp_python_gen.rs index 93bb84d73..1851d266f 100644 --- a/rust/crates/robonix-codegen/src/codegen/mcp_python_gen.rs +++ b/rust/crates/robonix-codegen/src/codegen/mcp_python_gen.rs @@ -70,10 +70,10 @@ fn ros_to_json_schema_type(t: &str) -> &'static str { fn cross_package_deps(spec: &MsgSpec) -> BTreeSet { let mut pkgs = BTreeSet::new(); for f in &spec.fields { - if let MsgTypeRef::Named { package, .. } = &f.type_ref { - if package != &spec.package { - pkgs.insert(package.clone()); - } + if let MsgTypeRef::Named { package, .. } = &f.type_ref + && package != &spec.package + { + pkgs.insert(package.clone()); } } pkgs diff --git a/rust/crates/robonix-codegen/src/codegen/proto_gen.rs b/rust/crates/robonix-codegen/src/codegen/proto_gen.rs index af950f165..2b5473c21 100644 --- a/rust/crates/robonix-codegen/src/codegen/proto_gen.rs +++ b/rust/crates/robonix-codegen/src/codegen/proto_gen.rs @@ -76,10 +76,10 @@ fn emit_srv_messages(out: &mut String, srv: &SrvSpec) { } fn import_named_type(imports: &mut BTreeSet, current_package: &str, tr: &MsgTypeRef) { - if let MsgTypeRef::Named { package, .. } = tr { - if package != current_package { - imports.insert(package.clone()); - } + if let MsgTypeRef::Named { package, .. } = tr + && package != current_package + { + imports.insert(package.clone()); } } diff --git a/rust/crates/robonix-executor/src/dispatch/builtin.rs b/rust/crates/robonix-executor/src/dispatch/builtin.rs index 159c0e9c0..33db36232 100644 --- a/rust/crates/robonix-executor/src/dispatch/builtin.rs +++ b/rust/crates/robonix-executor/src/dispatch/builtin.rs @@ -3,7 +3,54 @@ use crate::pilot::TaskCallResult; use serde::Deserialize; -use std::path::Path; +use std::path::{Path, PathBuf}; + +/// Workspace root for file operations. All file paths are resolved relative to +/// this directory, and path traversal beyond it is rejected. +fn workspace_root() -> PathBuf { + std::env::var("ROBONIX_WORKSPACE") + .map(PathBuf::from) + .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) +} + +/// Resolve a user-supplied path and ensure it stays within the workspace root. +/// Returns the canonical path on success, or an error if the path escapes the +/// allowed directory (path traversal). +fn safe_resolve(user_path: &str) -> anyhow::Result { + let root = workspace_root() + .canonicalize() + .unwrap_or_else(|_| workspace_root()); + let candidate = if Path::new(user_path).is_absolute() { + PathBuf::from(user_path) + } else { + root.join(user_path) + }; + // Canonicalize to resolve ".." components. If the file doesn't exist yet + // (write_file), canonicalize the parent directory instead. + let resolved = if candidate.exists() { + candidate.canonicalize()? + } else { + let parent = candidate + .parent() + .ok_or_else(|| anyhow::anyhow!("invalid path: no parent directory"))?; + let parent_resolved = parent.canonicalize().map_err(|_| { + anyhow::anyhow!("parent directory does not exist: {}", parent.display()) + })?; + parent_resolved.join( + candidate + .file_name() + .ok_or_else(|| anyhow::anyhow!("invalid path: no file name"))?, + ) + }; + if !resolved.starts_with(&root) { + anyhow::bail!( + "path traversal denied: {} resolves outside workspace {}", + user_path, + root.display() + ); + } + Ok(resolved) +} pub async fn execute(call_id: &str, name: &str, args_json: &str) -> TaskCallResult { let output = run(name, args_json).await; @@ -42,7 +89,9 @@ fn truncate(s: &str, max: usize) -> String { if s.len() <= max { s.to_string() } else { - format!("{}…(truncated, {} bytes total)", &s[..max], s.len()) + // Find a valid UTF-8 char boundary at or before `max` to avoid panic. + let end = s.floor_char_boundary(max); + format!("{}…(truncated, {} bytes total)", &s[..end], s.len()) } } @@ -72,32 +121,40 @@ struct CmdArgs { fn read_file(args: &str) -> anyhow::Result { let a: ReadArgs = serde_json::from_str(args)?; - Ok(truncate(&std::fs::read_to_string(&a.path)?, 8000)) + let path = safe_resolve(&a.path)?; + Ok(truncate(&std::fs::read_to_string(path)?, 8000)) } fn write_file(args: &str) -> anyhow::Result { let a: WriteArgs = serde_json::from_str(args)?; - if let Some(parent) = Path::new(&a.path).parent() { + let path = safe_resolve(&a.path)?; + if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).ok(); } - std::fs::write(&a.path, &a.content)?; - Ok(format!("wrote {} bytes to {}", a.content.len(), a.path)) + std::fs::write(&path, &a.content)?; + Ok(format!( + "wrote {} bytes to {}", + a.content.len(), + path.display() + )) } fn patch_file(args: &str) -> anyhow::Result { let a: PatchArgs = serde_json::from_str(args)?; - let content = std::fs::read_to_string(&a.path)?; + let path = safe_resolve(&a.path)?; + let content = std::fs::read_to_string(&path)?; if !content.contains(&a.old) { anyhow::bail!("old string not found in file"); } - std::fs::write(&a.path, content.replacen(&a.old, &a.new, 1))?; - Ok(format!("patched {}", a.path)) + std::fs::write(&path, content.replacen(&a.old, &a.new, 1))?; + Ok(format!("patched {}", path.display())) } fn list_dir(args: &str) -> anyhow::Result { let a: ListArgs = serde_json::from_str(args)?; let dir = a.path.as_deref().unwrap_or("."); - let mut items: Vec = std::fs::read_dir(dir)? + let resolved = safe_resolve(dir)?; + let mut items: Vec = std::fs::read_dir(resolved)? .flatten() .map(|e| { let ft = e @@ -111,13 +168,28 @@ fn list_dir(args: &str) -> anyhow::Result { Ok(items.join("\n")) } +/// Maximum command length to prevent abuse. +const MAX_COMMAND_LEN: usize = 8192; +/// Command execution timeout. +const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120); + async fn run_command(args: &str) -> anyhow::Result { let a: CmdArgs = serde_json::from_str(args)?; - let out = tokio::process::Command::new("bash") + if a.command.len() > MAX_COMMAND_LEN { + anyhow::bail!( + "command too long ({} bytes, max {})", + a.command.len(), + MAX_COMMAND_LEN + ); + } + let child = tokio::process::Command::new("bash") .arg("-c") .arg(&a.command) - .output() - .await?; + .output(); + let out = tokio::time::timeout(COMMAND_TIMEOUT, child) + .await + .map_err(|_| anyhow::anyhow!("command timed out after {}s", COMMAND_TIMEOUT.as_secs()))? + .map_err(|e| anyhow::anyhow!("failed to execute command: {e}"))?; let mut result = String::new(); let stdout = String::from_utf8_lossy(&out.stdout); let stderr = String::from_utf8_lossy(&out.stderr); @@ -136,3 +208,199 @@ async fn run_command(args: &str) -> anyhow::Result { } Ok(result) } + +#[cfg(test)] +mod tests { + use super::*; + + // ── Path traversal tests ───────────────────────────────────────────── + + #[test] + fn path_traversal_dotdot_is_rejected() { + // Set workspace to a temp dir so we have a known root + let tmp = std::env::temp_dir().join("rbnx_test_ws"); + std::fs::create_dir_all(&tmp).unwrap(); + unsafe { std::env::set_var("ROBONIX_WORKSPACE", tmp.to_str().unwrap()) }; + + // ../../../etc/passwd must be rejected + let result = safe_resolve("../../../etc/passwd"); + assert!( + result.is_err(), + "path traversal with ../../../etc/passwd should fail" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("path traversal denied") || err_msg.contains("does not exist"), + "error should mention traversal or nonexistent path, got: {err_msg}" + ); + } + + #[test] + fn path_traversal_absolute_outside_workspace_is_rejected() { + let tmp = std::env::temp_dir().join("rbnx_test_ws2"); + std::fs::create_dir_all(&tmp).unwrap(); + unsafe { std::env::set_var("ROBONIX_WORKSPACE", tmp.to_str().unwrap()) }; + + // /etc/hostname is a real file outside workspace + let result = safe_resolve("/etc/hostname"); + assert!( + result.is_err(), + "absolute path /etc/hostname outside workspace should fail" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("path traversal denied"), + "error should mention traversal, got: {err_msg}" + ); + } + + #[test] + fn path_resolve_within_workspace_is_allowed() { + let tmp = std::env::temp_dir().join("rbnx_test_ws3"); + std::fs::create_dir_all(&tmp).unwrap(); + // Create a test file inside workspace + let test_file = tmp.join("allowed.txt"); + std::fs::write(&test_file, "hello").unwrap(); + unsafe { std::env::set_var("ROBONIX_WORKSPACE", tmp.to_str().unwrap()) }; + + let result = safe_resolve("allowed.txt"); + assert!( + result.is_ok(), + "path within workspace should be allowed: {:?}", + result.err() + ); + + // Cleanup + let _ = std::fs::remove_file(&test_file); + let _ = std::fs::remove_dir(&tmp); + } + + // ── execute() integration tests for path traversal ─────────────────── + + #[tokio::test] + async fn read_file_rejects_path_traversal() { + let tmp = std::env::temp_dir().join("rbnx_test_read"); + std::fs::create_dir_all(&tmp).unwrap(); + unsafe { std::env::set_var("ROBONIX_WORKSPACE", tmp.to_str().unwrap()) }; + + let result = execute("test-1", "read_file", r#"{"path": "../../../etc/passwd"}"#).await; + assert!(!result.success, "read_file should fail for path traversal"); + assert!( + result.error.contains("traversal") || result.error.contains("does not exist"), + "error should explain traversal, got: {}", + result.error + ); + } + + #[tokio::test] + async fn write_file_rejects_path_traversal() { + let tmp = std::env::temp_dir().join("rbnx_test_write"); + std::fs::create_dir_all(&tmp).unwrap(); + unsafe { std::env::set_var("ROBONIX_WORKSPACE", tmp.to_str().unwrap()) }; + + let result = execute( + "test-2", + "write_file", + r#"{"path": "/tmp/outside_workspace_evil.txt", "content": "pwned"}"#, + ) + .await; + assert!( + !result.success, + "write_file should fail for path outside workspace" + ); + + // File should NOT have been created + assert!( + !std::path::Path::new("/tmp/outside_workspace_evil.txt").exists(), + "file outside workspace must not be created" + ); + } + + // ── Command length limit test ──────────────────────────────────────── + + #[tokio::test] + async fn run_command_rejects_oversized_command() { + let long_cmd = "a".repeat(MAX_COMMAND_LEN + 1); + let args = format!(r#"{{"command": "{}"}}"#, long_cmd); + let result = execute("test-3", "run_command", &args).await; + assert!(!result.success, "oversized command should be rejected"); + assert!( + result.error.contains("command too long"), + "error should mention 'command too long', got: {}", + result.error + ); + } + + #[tokio::test] + async fn run_command_accepts_normal_command() { + let result = execute("test-4", "run_command", r#"{"command": "echo hello"}"#).await; + assert!( + result.success, + "normal command should succeed: {}", + result.error + ); + assert!( + result.output.contains("hello"), + "output should contain 'hello', got: {}", + result.output + ); + } + + // ── UTF-8 truncation safety test ───────────────────────────────────── + + #[test] + fn truncate_ascii_works() { + let s = "hello world"; + assert_eq!(truncate(s, 100), "hello world"); + let t = truncate(s, 5); + assert!( + t.starts_with("hello"), + "should start with 'hello', got: {t}" + ); + assert!( + t.contains("truncated"), + "should contain 'truncated', got: {t}" + ); + } + + #[test] + fn truncate_multibyte_utf8_does_not_panic() { + // Each Chinese char is 3 bytes in UTF-8 + let s = "你好世界测试数据"; // 8 chars × 3 bytes = 24 bytes + // Truncate at byte 7 — in the middle of the 3rd char '世' + let result = truncate(s, 7); + // Should NOT panic, and should truncate at a valid boundary + assert!( + result.contains("truncated"), + "should be truncated, got: {result}" + ); + // The truncated prefix should be valid UTF-8 (it is, since we're returning a String) + assert!( + result.starts_with("你好"), + "should keep first 2 chars, got: {result}" + ); + } + + #[test] + fn truncate_emoji_boundary() { + // 🚀 is 4 bytes in UTF-8 + let s = "A🚀B🚀C"; // 1 + 4 + 1 + 4 + 1 = 11 bytes + // Truncate at byte 3, which is in the middle of 🚀 + let result = truncate(s, 3); + assert!(result.contains("truncated"), "should be truncated"); + // Should only keep "A" since 🚀 starts at byte 1 and ends at byte 5 + assert!( + result.starts_with("A"), + "should start with 'A', got: {result}" + ); + } + + // ── Unknown builtin test ───────────────────────────────────────────── + + #[tokio::test] + async fn unknown_builtin_returns_error() { + let result = execute("test-5", "evil_tool", "{}").await; + assert!(!result.success); + assert!(result.error.contains("unknown builtin")); + } +} diff --git a/rust/crates/robonix-executor/src/dispatch/mcp.rs b/rust/crates/robonix-executor/src/dispatch/mcp.rs index 3663edc29..2bd7f433f 100644 --- a/rust/crates/robonix-executor/src/dispatch/mcp.rs +++ b/rust/crates/robonix-executor/src/dispatch/mcp.rs @@ -30,7 +30,8 @@ pub async fn execute(call_id: &str, name: &str, args_json: &str, endpoint: &str) async fn call_mcp(name: &str, args_json: &str, endpoint: &str) -> anyhow::Result { let mut client = connect_mcp(endpoint).await?; - let args_val: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default(); + let args_val: serde_json::Value = serde_json::from_str(args_json) + .map_err(|e| anyhow::anyhow!("invalid tool arguments JSON: {e}"))?; let args_obj = args_val.as_object().cloned(); let result = client @@ -64,8 +65,7 @@ async fn call_mcp(name: &str, args_json: &str, endpoint: &str) -> anyhow::Result } async fn connect_mcp(endpoint: &str) -> anyhow::Result { - if endpoint.starts_with("stdio://") { - let cmd_str = &endpoint["stdio://".len()..]; + if let Some(cmd_str) = endpoint.strip_prefix("stdio://") { let mut parts = cmd_str.split_whitespace(); let program = parts.next().unwrap_or("python3"); let args: Vec<&str> = parts.collect(); @@ -73,11 +73,7 @@ async fn connect_mcp(endpoint: &str) -> anyhow::Result { command.args(&args); let transport = rmcp::transport::TokioChildProcess::new(command) .map_err(|e| anyhow::anyhow!("MCP stdio spawn failed: {e}"))?; - return Ok( - ().serve(transport) - .await - .map_err(|e| anyhow::anyhow!("MCP init failed: {e}"))?, - ); + return ().serve(transport).await.map_err(|e| anyhow::anyhow!("MCP init failed: {e}")); } let base = if endpoint.starts_with("http") { endpoint.to_string() @@ -93,8 +89,7 @@ async fn connect_mcp(endpoint: &str) -> anyhow::Result { rmcp::transport::streamable_http_client::StreamableHttpClientTransport::from_uri( uri.clone(), ); - Ok(() - .serve(transport) + ().serve(transport) .await - .map_err(|e| anyhow::anyhow!("MCP HTTP connect to '{uri}' failed: {e}"))?) + .map_err(|e| anyhow::anyhow!("MCP HTTP connect to '{uri}' failed: {e}")) } diff --git a/rust/crates/robonix-executor/src/main.rs b/rust/crates/robonix-executor/src/main.rs index ce7f2ee01..431e22118 100644 --- a/rust/crates/robonix-executor/src/main.rs +++ b/rust/crates/robonix-executor/src/main.rs @@ -91,10 +91,10 @@ fn atlas_endpoint() -> String { fn resolve_endpoint(vars: &[&str], default: &str) -> String { for v in vars { - if let Ok(val) = std::env::var(v) { - if !val.is_empty() { - return val; - } + if let Ok(val) = std::env::var(v) + && !val.is_empty() + { + return val; } } default.to_string() diff --git a/rust/crates/robonix-liaison/src/main.rs b/rust/crates/robonix-liaison/src/main.rs index c4bdf4b29..a7738f02c 100644 --- a/rust/crates/robonix-liaison/src/main.rs +++ b/rust/crates/robonix-liaison/src/main.rs @@ -342,10 +342,10 @@ async fn main() -> Result<()> { fn resolve_endpoint(vars: &[&str], default: &str) -> String { for v in vars { - if let Ok(val) = std::env::var(v) { - if !val.is_empty() { - return val; - } + if let Ok(val) = std::env::var(v) + && !val.is_empty() + { + return val; } } default.to_string() diff --git a/rust/crates/robonix-pilot/src/main.rs b/rust/crates/robonix-pilot/src/main.rs index 15d08ff9c..75b1a04c3 100644 --- a/rust/crates/robonix-pilot/src/main.rs +++ b/rust/crates/robonix-pilot/src/main.rs @@ -100,10 +100,10 @@ async fn main() -> Result<()> { fn resolve_endpoint(vars: &[&str], default: &str) -> String { for v in vars { - if let Ok(val) = std::env::var(v) { - if !val.is_empty() { - return val; - } + if let Ok(val) = std::env::var(v) + && !val.is_empty() + { + return val; } } default.to_string() diff --git a/rust/crates/robonix-pilot/src/planner.rs b/rust/crates/robonix-pilot/src/planner.rs index edb40707a..33769f081 100644 --- a/rust/crates/robonix-pilot/src/planner.rs +++ b/rust/crates/robonix-pilot/src/planner.rs @@ -311,6 +311,7 @@ pub async fn run_turn( const EX_STARTED: u32 = 0; const EX_RESULT: u32 = 1; + #[allow(dead_code)] const EX_COMPLETE: u32 = 2; while let Some(event) = exec_stream @@ -341,7 +342,7 @@ pub async fn run_turn( results.push(r); } } - EX_COMPLETE | _ => {} + _ => {} } } @@ -427,7 +428,10 @@ fn tool_result_to_messages(call_id: &str, output: &str) -> ToolResultHistory { if let Some(b64) = v.get("image_base64").and_then(|x| x.as_str()) { let fmt = v.get("format").and_then(|x| x.as_str()).unwrap_or("jpeg"); return ToolResultHistory { - tool_messages: vec![Message::tool_result(call_id, &format!("[{fmt} image attached]"))], + tool_messages: vec![Message::tool_result( + call_id, + &format!("[{fmt} image attached]"), + )], followup_messages: vec![Message::user_with_image( "Tool returned an image. Analyze this image together with the tool result above.", b64.to_string(), @@ -551,15 +555,15 @@ async fn prefetch_memory( .ok()? .into_inner(); while let Ok(Some(event)) = stream.message().await { - if event.event_kind == EX_RESULT { - if let Some(r) = event.result { - let out = decode_string_output(&r.output); - if r.success && !out.contains("No relevant memories") && !out.is_empty() { - log::debug!("[pilot] memory prefetch: {}", out); - return Some(out); - } - return None; + if event.event_kind == EX_RESULT + && let Some(r) = event.result + { + let out = decode_string_output(&r.output); + if r.success && !out.contains("No relevant memories") && !out.is_empty() { + log::debug!("[pilot] memory prefetch: {}", out); + return Some(out); } + return None; } } None diff --git a/rust/crates/robonix-pilot/src/session.rs b/rust/crates/robonix-pilot/src/session.rs index fd4ec50cf..8c835bfe6 100644 --- a/rust/crates/robonix-pilot/src/session.rs +++ b/rust/crates/robonix-pilot/src/session.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use tokio::sync::Mutex; /// One ongoing user session. +#[allow(dead_code)] pub struct Session { pub session_id: String, /// Full VLM conversation history (system prompt excluded — added per turn). @@ -44,6 +45,7 @@ pub struct SessionManager { sessions: Arc>>>>, } +#[allow(dead_code)] impl SessionManager { /// Get or create a session by ID. pub async fn get_or_create(&self, session_id: &str) -> Arc> { diff --git a/rust/crates/robonix-pilot/src/session_state.rs b/rust/crates/robonix-pilot/src/session_state.rs index 4480582a3..b71c11ed7 100644 --- a/rust/crates/robonix-pilot/src/session_state.rs +++ b/rust/crates/robonix-pilot/src/session_state.rs @@ -3,6 +3,7 @@ #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] #[repr(u32)] +#[allow(dead_code)] pub enum SessionState { #[default] Active = 0, diff --git a/rust/crates/robonix-pilot/src/skills.rs b/rust/crates/robonix-pilot/src/skills.rs index 16b835e9b..579e2ec8b 100644 --- a/rust/crates/robonix-pilot/src/skills.rs +++ b/rust/crates/robonix-pilot/src/skills.rs @@ -130,7 +130,7 @@ pub fn scan_extra_skill_dirs() -> Vec { }; let node_id = "com.robonix.local.workspace".to_string(); let mut merged: HashMap = HashMap::new(); - for part in raw.split(|c| c == ':' || c == ';') { + for part in raw.split([':', ';']) { let p = part.trim(); if p.is_empty() { continue; diff --git a/rust/crates/robonix-pilot/src/vlm.rs b/rust/crates/robonix-pilot/src/vlm.rs index 9989b740b..09000c4da 100644 --- a/rust/crates/robonix-pilot/src/vlm.rs +++ b/rust/crates/robonix-pilot/src/vlm.rs @@ -162,6 +162,7 @@ impl Message { parts: Some(vec![ChatPart::function_response(id, content)]), } } + #[allow(dead_code)] pub fn tool_result_with_image(id: &str, content: &str, image: String) -> Self { Self { role: "tool".into(), diff --git a/rust/crates/robonix-sdk/src/lib.rs b/rust/crates/robonix-sdk/src/lib.rs index f052a49ff..3547bc804 100644 --- a/rust/crates/robonix-sdk/src/lib.rs +++ b/rust/crates/robonix-sdk/src/lib.rs @@ -124,6 +124,7 @@ impl RobonixClient { .await } + #[allow(clippy::too_many_arguments)] pub async fn register_node_with_skills( &mut self, node_id: impl Into, diff --git a/rust/examples/packages/zero_copy_demo/zero_copy_demo/benchmark.py b/rust/examples/packages/zero_copy_demo/zero_copy_demo/benchmark.py index acfbe3197..15fc9385b 100644 --- a/rust/examples/packages/zero_copy_demo/zero_copy_demo/benchmark.py +++ b/rust/examples/packages/zero_copy_demo/zero_copy_demo/benchmark.py @@ -99,9 +99,12 @@ def run(args) -> dict: print(f"[!] Missing results: {f}", file=sys.stderr) sys.exit(1) - cam_data = json.load(open(cam_file)) - yolo_data = json.load(open(yolo_file)) - edge_data = json.load(open(edge_file)) + with open(cam_file) as f: + cam_data = json.load(f) + with open(yolo_file) as f: + yolo_data = json.load(f) + with open(edge_file) as f: + edge_data = json.load(f) # Skip warmup frames from each node's timing w = args.warmup diff --git a/rust/examples/packages/zero_copy_demo/zero_copy_demo/rbnx_buffer.py b/rust/examples/packages/zero_copy_demo/zero_copy_demo/rbnx_buffer.py index 731b4d172..cc03574b7 100644 --- a/rust/examples/packages/zero_copy_demo/zero_copy_demo/rbnx_buffer.py +++ b/rust/examples/packages/zero_copy_demo/zero_copy_demo/rbnx_buffer.py @@ -251,6 +251,8 @@ def open(self, shm_name: str, pin_for_gpu: bool = False) -> BufferHandle: if rc != 0: raise RuntimeError(f"Failed to GPU-pin opened buffer: {shm_name}") header_ptr = lib.rbnx_buf_header_ptr(self._ptr, handle_id) + if not header_ptr: + raise RuntimeError(f"Failed to read buffer header: {shm_name}") width = ctypes.c_uint32.from_address(header_ptr + 24).value height = ctypes.c_uint32.from_address(header_ptr + 28).value channels = ctypes.c_uint32.from_address(header_ptr + 32).value @@ -267,6 +269,8 @@ def attach(self, handle_id: int, pin_for_gpu: bool = False) -> BufferHandle: if rc != 0: raise RuntimeError(f"Failed to attach to buffer handle={handle_id}") header_ptr = lib.rbnx_buf_header_ptr(self._ptr, handle_id) + if not header_ptr: + raise RuntimeError(f"Failed to read buffer header for handle={handle_id}") width = ctypes.c_uint32.from_address(header_ptr + 24).value height = ctypes.c_uint32.from_address(header_ptr + 28).value channels = ctypes.c_uint32.from_address(header_ptr + 32).value