From 756cb91dc06cd01135cf77e0e78bdaf8b286208e Mon Sep 17 00:00:00 2001 From: "Christian M. Todie" Date: Mon, 6 Apr 2026 21:34:06 -0400 Subject: [PATCH] =?UTF-8?q?chore:=20unbreak=20reach=20main=20CI=20?= =?UTF-8?q?=E2=80=94=20fmt=2017=20files,=20migrate=20deny.toml=20to=200.14?= =?UTF-8?q?+=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pre-existing issues on main that were silently breaking CI on every open PR (#1 release-please, #2 screenshot fix, #3 editorconfig) via the Format and Deny jobs. Neither was introduced by the open PRs — they landed in the CI/CD overhaul commit 'ea74fa4 feat: CI/CD overhaul + process restart loop + zero clippy warnings' (the 'zero clippy warnings' part was true; the 'cargo fmt --check passes' part apparently wasn't). ## 1. rustfmt violations across 17 files `cargo fmt --check` on main produces 86 diff hunks across: crates/reach-cli/src/commands/{connect,create,destroy,exec,list,screenshot,serve,vnc}.rs crates/reach-cli/src/{config,docker,mcp}.rs crates/reach-cli/tests/{docker_types,e2e_container,mcp_types}.rs crates/reach-supervisor/src/{main,processes,signals}.rs Applied `cargo fmt --all` with the project's rustfmt.toml (max_width=100). No behavioral changes — pure whitespace/alignment/import-grouping edits. ## 2. deny.toml schema migration (cargo-deny 0.14+) cargo-deny 0.14 removed or changed several schema keys: - `[advisories] vulnerability = "deny"` → REMOVED (cargo-deny now always denies vulnerabilities in its pipeline). - `[advisories] unmaintained = "warn"` → REJECTED. The field's semantics changed from severity ("deny"/"warn") to scope ("all"/"workspace"/"transitive"/"none"). Migrated to "workspace" — we only care about crates we directly depend on being marked unmaintained, not transitives we don't control. - `[licenses] unlicensed = "deny"` → REMOVED (see EmbarkStudios/cargo-deny#611 for the migration note). cargo-deny now handles unlicensed crates as deny-by-default in the licenses pipeline without the explicit key. ## 3. RUSTSEC-2024-0437 ignore (protobuf 2.28 stack overflow) After the schema migration, `cargo deny check` surfaces a real vulnerability: protobuf 2.28.0 is transitively pulled in by `prometheus 0.13.4 → reach-supervisor`. RUSTSEC-2024-0437 is a stack overflow caused by unbounded recursion when PARSING untrusted protobuf input. reach-supervisor uses prometheus to EXPORT metrics (serialise to the Prometheus text format, served over an HTTP endpoint on port 8400). It never parses protobuf input from the network or from untrusted sources, so the vector does not apply. Added an explicit `[advisories] ignore` with a detailed justification comment so the rationale is preserved inline. Follow-up: upgrade to a prometheus version that uses protobuf 3.x, or drop the prometheus crate in favour of a pure-text-format exporter. ## Verification Local CI parity: `cargo fmt --check` clean `cargo deny check` advisories ok, bans ok, licenses ok, sources ok `cargo clippy --workspace -- -D warnings` zero warnings `cargo build --workspace --release` clean After this lands on main, the Format + Deny jobs will start passing on open PRs #1, #2, #3 without any per-branch rebasing (the fixes are on main, not downstream). Worthwhile to cascade-rebase the three so they pick up the fmt-normalised line-lengths in docker.rs (which PR #2 touches). --- crates/reach-cli/src/commands/connect.rs | 57 +++- crates/reach-cli/src/commands/create.rs | 4 +- crates/reach-cli/src/commands/destroy.rs | 2 +- crates/reach-cli/src/commands/exec.rs | 2 +- crates/reach-cli/src/commands/list.rs | 2 +- crates/reach-cli/src/commands/screenshot.rs | 2 +- crates/reach-cli/src/commands/serve.rs | 72 +++-- crates/reach-cli/src/commands/vnc.rs | 2 +- crates/reach-cli/src/config.rs | 2 - crates/reach-cli/src/docker.rs | 21 +- crates/reach-cli/src/mcp.rs | 4 +- crates/reach-cli/tests/docker_types.rs | 6 +- crates/reach-cli/tests/e2e_container.rs | 323 +++++++++++++++----- crates/reach-cli/tests/mcp_types.rs | 22 +- crates/reach-supervisor/src/main.rs | 4 +- crates/reach-supervisor/src/processes.rs | 10 +- crates/reach-supervisor/src/signals.rs | 6 +- deny.toml | 24 +- 18 files changed, 404 insertions(+), 161 deletions(-) diff --git a/crates/reach-cli/src/commands/connect.rs b/crates/reach-cli/src/commands/connect.rs index a57f6e7..2e284a3 100644 --- a/crates/reach-cli/src/commands/connect.rs +++ b/crates/reach-cli/src/commands/connect.rs @@ -1,9 +1,8 @@ +use clap::Args; use reach_cli::docker::DockerClient; use reach_cli::mcp::{ - JsonRpcRequest, JsonRpcResponse, McpInitializeResult, RequestId, ToolResponse, - tool_definitions, + JsonRpcRequest, JsonRpcResponse, McpInitializeResult, RequestId, ToolResponse, tool_definitions, }; -use clap::Args; use std::io::{BufRead, Write}; #[derive(Args)] @@ -63,7 +62,11 @@ async fn handle_request( JsonRpcResponse::success(req.id.clone(), serde_json::json!({ "tools": tools })) } "tools/call" => { - let tool_name = req.params.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let tool_name = req + .params + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or(""); let arguments = req.params.get("arguments").cloned().unwrap_or_default(); dispatch_tool(docker, target, req, tool_name, &arguments).await } @@ -102,7 +105,12 @@ async fn dispatch_tool( Some("middle") => "2", _ => "1", }; - exec_cmd(docker, target, &format!("xdotool mousemove {x} {y} click {btn}")).await + exec_cmd( + docker, + target, + &format!("xdotool mousemove {x} {y} click {btn}"), + ) + .await } "type" => { let text = args.get("text").and_then(|v| v.as_str()).unwrap_or(""); @@ -114,11 +122,17 @@ async fn dispatch_tool( .await } "key" => { - let combo = args.get("combo").and_then(|v| v.as_str()).unwrap_or("Return"); + let combo = args + .get("combo") + .and_then(|v| v.as_str()) + .unwrap_or("Return"); exec_cmd(docker, target, &format!("xdotool key {combo}")).await } "browse" => { - let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank"); + let url = args + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or("about:blank"); exec_cmd( docker, target, @@ -131,9 +145,19 @@ async fn dispatch_tool( } "scrape" => { let url = args.get("url").and_then(|v| v.as_str()).unwrap_or(""); - let selector = args.get("selector").and_then(|v| v.as_str()).unwrap_or("body"); - let stealth = args.get("stealth").and_then(|v| v.as_bool()).unwrap_or(true); - let fetcher = if stealth { "StealthyFetcher" } else { "Fetcher" }; + let selector = args + .get("selector") + .and_then(|v| v.as_str()) + .unwrap_or("body"); + let stealth = args + .get("stealth") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + let fetcher = if stealth { + "StealthyFetcher" + } else { + "Fetcher" + }; let script = format!( "from scrapling import {fetcher}; r = {fetcher}().get('{url}'); \ elems = r.css('{selector}'); \ @@ -146,7 +170,10 @@ async fn dispatch_tool( exec_python(docker, target, script).await } "exec" => { - let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("echo"); + let cmd = args + .get("command") + .and_then(|v| v.as_str()) + .unwrap_or("echo"); exec_cmd(docker, target, cmd).await } _ => ToolResponse::error(format!("unknown tool: {tool}")), @@ -160,9 +187,11 @@ async fn exec_cmd(docker: &DockerClient, target: &str, cmd: &str) -> ToolRespons .exec(target, &["bash".into(), "-c".into(), cmd.into()]) .await { - Ok(out) if out.exit_code == 0 => { - ToolResponse::text(if out.stdout.is_empty() { "ok".into() } else { out.stdout }) - } + Ok(out) if out.exit_code == 0 => ToolResponse::text(if out.stdout.is_empty() { + "ok".into() + } else { + out.stdout + }), Ok(out) => ToolResponse::error(format!("exit {}: {}", out.exit_code, out.stderr)), Err(e) => ToolResponse::error(e.to_string()), } diff --git a/crates/reach-cli/src/commands/create.rs b/crates/reach-cli/src/commands/create.rs index 1b3ffb4..2c09b29 100644 --- a/crates/reach-cli/src/commands/create.rs +++ b/crates/reach-cli/src/commands/create.rs @@ -1,7 +1,7 @@ -use reach_cli::config::ReachConfig; -use reach_cli::docker::{DockerClient, Resolution, SandboxConfig, SandboxPorts}; use clap::Args; use colored::Colorize; +use reach_cli::config::ReachConfig; +use reach_cli::docker::{DockerClient, Resolution, SandboxConfig, SandboxPorts}; use std::time::Duration; #[derive(Args)] diff --git a/crates/reach-cli/src/commands/destroy.rs b/crates/reach-cli/src/commands/destroy.rs index 87611de..4488f6b 100644 --- a/crates/reach-cli/src/commands/destroy.rs +++ b/crates/reach-cli/src/commands/destroy.rs @@ -1,6 +1,6 @@ -use reach_cli::docker::DockerClient; use clap::Args; use colored::Colorize; +use reach_cli::docker::DockerClient; #[derive(Args)] pub struct DestroyArgs { diff --git a/crates/reach-cli/src/commands/exec.rs b/crates/reach-cli/src/commands/exec.rs index 8d4a0a8..16cdea3 100644 --- a/crates/reach-cli/src/commands/exec.rs +++ b/crates/reach-cli/src/commands/exec.rs @@ -1,5 +1,5 @@ -use reach_cli::docker::DockerClient; use clap::Args; +use reach_cli::docker::DockerClient; #[derive(Args)] pub struct ExecArgs { diff --git a/crates/reach-cli/src/commands/list.rs b/crates/reach-cli/src/commands/list.rs index 18784f3..e9bfcd3 100644 --- a/crates/reach-cli/src/commands/list.rs +++ b/crates/reach-cli/src/commands/list.rs @@ -1,5 +1,5 @@ -use reach_cli::docker::{DockerClient, SandboxStatus}; use colored::Colorize; +use reach_cli::docker::{DockerClient, SandboxStatus}; pub async fn run() -> anyhow::Result<()> { let docker = DockerClient::new()?; diff --git a/crates/reach-cli/src/commands/screenshot.rs b/crates/reach-cli/src/commands/screenshot.rs index ad3d6bf..f532a8b 100644 --- a/crates/reach-cli/src/commands/screenshot.rs +++ b/crates/reach-cli/src/commands/screenshot.rs @@ -1,6 +1,6 @@ -use reach_cli::docker::DockerClient; use clap::Args; use colored::Colorize; +use reach_cli::docker::DockerClient; use std::io::Write; #[derive(Args)] diff --git a/crates/reach-cli/src/commands/serve.rs b/crates/reach-cli/src/commands/serve.rs index 34a9c5a..e076841 100644 --- a/crates/reach-cli/src/commands/serve.rs +++ b/crates/reach-cli/src/commands/serve.rs @@ -1,12 +1,12 @@ -use reach_cli::docker::DockerClient; -use reach_cli::mcp::{ - JsonRpcRequest, JsonRpcResponse, McpInitializeResult, ToolResponse, tool_definitions, -}; use axum::extract::State; use axum::response::sse::{Event, Sse}; use axum::routing::{get, post}; use axum::{Json, Router}; use clap::Args; +use reach_cli::docker::DockerClient; +use reach_cli::mcp::{ + JsonRpcRequest, JsonRpcResponse, McpInitializeResult, ToolResponse, tool_definitions, +}; use std::convert::Infallible; use std::sync::Arc; @@ -74,9 +74,7 @@ async fn mcp_handler( } async fn sse_handler() -> Sse>> { - let stream = tokio_stream::once(Ok( - Event::default().event("endpoint").data("/mcp"), - )); + let stream = tokio_stream::once(Ok(Event::default().event("endpoint").data("/mcp"))); Sse::new(stream) } @@ -91,7 +89,11 @@ async fn handle_mcp(state: &AppState, req: &JsonRpcRequest) -> JsonRpcResponse { JsonRpcResponse::success(req.id.clone(), serde_json::json!({ "tools": tools })) } "tools/call" => { - let tool = req.params.get("name").and_then(|v| v.as_str()).unwrap_or(""); + let tool = req + .params + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or(""); let args = req.params.get("arguments").cloned().unwrap_or_default(); let sandbox_arg = args.get("sandbox").and_then(|v| v.as_str()); @@ -144,19 +146,34 @@ async fn dispatch( Some("middle") => "2", _ => "1", }; - sh(state, target, &format!("xdotool mousemove {x} {y} click {btn}")).await + sh( + state, + target, + &format!("xdotool mousemove {x} {y} click {btn}"), + ) + .await } "type" => { let text = args.get("text").and_then(|v| v.as_str()).unwrap_or(""); - sh(state, target, &format!("xdotool type -- '{}'", text.replace('\'', "'\\''"))) - .await + sh( + state, + target, + &format!("xdotool type -- '{}'", text.replace('\'', "'\\''")), + ) + .await } "key" => { - let combo = args.get("combo").and_then(|v| v.as_str()).unwrap_or("Return"); + let combo = args + .get("combo") + .and_then(|v| v.as_str()) + .unwrap_or("Return"); sh(state, target, &format!("xdotool key {combo}")).await } "browse" => { - let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank"); + let url = args + .get("url") + .and_then(|v| v.as_str()) + .unwrap_or("about:blank"); sh( state, target, @@ -169,9 +186,19 @@ async fn dispatch( } "scrape" => { let url = args.get("url").and_then(|v| v.as_str()).unwrap_or(""); - let sel = args.get("selector").and_then(|v| v.as_str()).unwrap_or("body"); - let stealth = args.get("stealth").and_then(|v| v.as_bool()).unwrap_or(true); - let f = if stealth { "StealthyFetcher" } else { "Fetcher" }; + let sel = args + .get("selector") + .and_then(|v| v.as_str()) + .unwrap_or("body"); + let stealth = args + .get("stealth") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + let f = if stealth { + "StealthyFetcher" + } else { + "Fetcher" + }; py( state, target, @@ -188,7 +215,10 @@ async fn dispatch( py(state, target, script).await } "exec" => { - let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("echo"); + let cmd = args + .get("command") + .and_then(|v| v.as_str()) + .unwrap_or("echo"); sh(state, target, cmd).await } _ => ToolResponse::error(format!("unknown tool: {tool}")), @@ -201,9 +231,11 @@ async fn sh(state: &AppState, target: &str, cmd: &str) -> ToolResponse { .exec(target, &["bash".into(), "-c".into(), cmd.into()]) .await { - Ok(out) if out.exit_code == 0 => { - ToolResponse::text(if out.stdout.is_empty() { "ok".into() } else { out.stdout }) - } + Ok(out) if out.exit_code == 0 => ToolResponse::text(if out.stdout.is_empty() { + "ok".into() + } else { + out.stdout + }), Ok(out) => ToolResponse::error(format!("exit {}: {}", out.exit_code, out.stderr)), Err(e) => ToolResponse::error(e.to_string()), } diff --git a/crates/reach-cli/src/commands/vnc.rs b/crates/reach-cli/src/commands/vnc.rs index 596d7e5..ba9f128 100644 --- a/crates/reach-cli/src/commands/vnc.rs +++ b/crates/reach-cli/src/commands/vnc.rs @@ -1,6 +1,6 @@ -use reach_cli::docker::DockerClient; use clap::Args; use colored::Colorize; +use reach_cli::docker::DockerClient; #[derive(Args)] pub struct VncArgs { diff --git a/crates/reach-cli/src/config.rs b/crates/reach-cli/src/config.rs index 9497c80..fdd00c5 100644 --- a/crates/reach-cli/src/config.rs +++ b/crates/reach-cli/src/config.rs @@ -52,7 +52,6 @@ pub struct DockerConfig { // Defaults // ═══════════════════════════════════════════════════════════ - impl Default for SandboxDefaults { fn default() -> Self { Self { @@ -75,7 +74,6 @@ impl Default for ServerConfig { } } - // ═══════════════════════════════════════════════════════════ // Loading // ═══════════════════════════════════════════════════════════ diff --git a/crates/reach-cli/src/docker.rs b/crates/reach-cli/src/docker.rs index 1e9129b..94c121b 100644 --- a/crates/reach-cli/src/docker.rs +++ b/crates/reach-cli/src/docker.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result, bail}; use bollard::container::{ - Config, CreateContainerOptions, ListContainersOptions, - RemoveContainerOptions, StopContainerOptions, + Config, CreateContainerOptions, ListContainersOptions, RemoveContainerOptions, + StopContainerOptions, }; use bollard::exec::{CreateExecOptions, StartExecResults}; use bollard::models::{HostConfig, PortBinding}; @@ -261,10 +261,7 @@ impl DockerClient { let sandbox = self.find(target).await?; self.client - .stop_container( - &sandbox.container_id, - Some(StopContainerOptions { t: 10 }), - ) + .stop_container(&sandbox.container_id, Some(StopContainerOptions { t: 10 })) .await .context("failed to stop container")?; @@ -314,10 +311,7 @@ impl DockerClient { status, image: c.image.unwrap_or_default(), ports, - created_at: labels - .get(Labels::CREATED) - .cloned() - .unwrap_or_default(), + created_at: labels.get(Labels::CREATED).cloned().unwrap_or_default(), } }) .collect(); @@ -423,9 +417,10 @@ impl DockerClient { .await; if let Ok(result) = out - && result.exit_code == 0 { - return Ok(()); - } + && result.exit_code == 0 + { + return Ok(()); + } tokio::time::sleep(Duration::from_millis(500)).await; } diff --git a/crates/reach-cli/src/mcp.rs b/crates/reach-cli/src/mcp.rs index 8c66830..f573445 100644 --- a/crates/reach-cli/src/mcp.rs +++ b/crates/reach-cli/src/mcp.rs @@ -227,7 +227,9 @@ pub enum ExtractMode { Html, /// Extract a specific attribute value #[serde(rename = "attr")] - Attribute { name: String }, + Attribute { + name: String, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/reach-cli/tests/docker_types.rs b/crates/reach-cli/tests/docker_types.rs index 2d9a8da..872e53d 100644 --- a/crates/reach-cli/tests/docker_types.rs +++ b/crates/reach-cli/tests/docker_types.rs @@ -87,7 +87,11 @@ fn labels_for_sandbox_includes_all_required_keys() { fn labels_filter_targets_managed_containers() { let filter = Labels::filter(); let label_filters = filter.get("label").unwrap(); - assert!(label_filters.iter().any(|f| f.contains("reach.sandbox=true"))); + assert!( + label_filters + .iter() + .any(|f| f.contains("reach.sandbox=true")) + ); } // ═══════════════════════════════════════════════════════════ diff --git a/crates/reach-cli/tests/e2e_container.rs b/crates/reach-cli/tests/e2e_container.rs index 7bfe815..3124a4a 100644 --- a/crates/reach-cli/tests/e2e_container.rs +++ b/crates/reach-cli/tests/e2e_container.rs @@ -22,20 +22,38 @@ fn ensure_container() { INIT.call_once(|| { let _ = docker(&["rm", "-f", CONTAINER]); let out = docker(&[ - "run", "-d", "--name", CONTAINER, "--shm-size=2g", - "-p", "15900:5900", "-p", "16080:6080", "-p", "18400:8400", + "run", + "-d", + "--name", + CONTAINER, + "--shm-size=2g", + "-p", + "15900:5900", + "-p", + "16080:6080", + "-p", + "18400:8400", IMAGE, ]); assert!(out.status.success(), "start failed: {}", stderr(&out)); - assert!(wait_for_health(30), "never healthy. logs:\n{}", docker_out(&["logs", "--tail", "50", CONTAINER])); + assert!( + wait_for_health(30), + "never healthy. logs:\n{}", + docker_out(&["logs", "--tail", "50", CONTAINER]) + ); }); } fn docker(args: &[&str]) -> std::process::Output { - Command::new("docker").args(args).output().expect("docker not found") + Command::new("docker") + .args(args) + .output() + .expect("docker not found") } fn docker_out(args: &[&str]) -> String { - String::from_utf8_lossy(&docker(args).stdout).trim().to_string() + String::from_utf8_lossy(&docker(args).stdout) + .trim() + .to_string() } fn stderr(o: &std::process::Output) -> String { String::from_utf8_lossy(&o.stderr).trim().to_string() @@ -43,7 +61,12 @@ fn stderr(o: &std::process::Output) -> String { fn wait_for_health(secs: u64) -> bool { let end = std::time::Instant::now() + Duration::from_secs(secs); while std::time::Instant::now() < end { - if Command::new("curl").args(["-sf", HEALTH_URL]).output().map(|o| o.status.success()).unwrap_or(false) { + if Command::new("curl") + .args(["-sf", HEALTH_URL]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { return true; } std::thread::sleep(Duration::from_millis(500)); @@ -51,7 +74,14 @@ fn wait_for_health(secs: u64) -> bool { false } fn curl(url: &str) -> String { - String::from_utf8_lossy(&Command::new("curl").args(["-sf", url]).output().unwrap().stdout).to_string() + String::from_utf8_lossy( + &Command::new("curl") + .args(["-sf", url]) + .output() + .unwrap() + .stdout, + ) + .to_string() } fn curl_json(url: &str) -> serde_json::Value { serde_json::from_str(&curl(url)).unwrap_or_else(|e| panic!("bad json from {url}: {e}")) @@ -64,14 +94,20 @@ fn sh_ok(cmd: &str) -> String { assert!(o.status.success(), "failed: {cmd}\n{}", stderr(&o)); String::from_utf8_lossy(&o.stdout).trim().to_string() } -fn sh_code(cmd: &str) -> i32 { sh(cmd).status.code().unwrap_or(-1) } -fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } +fn sh_code(cmd: &str) -> i32 { + sh(cmd).status.code().unwrap_or(-1) +} +fn sleep_ms(ms: u64) { + std::thread::sleep(Duration::from_millis(ms)); +} // ═══════════════════════════════════════════════════════════ // 1. SUPERVISOR // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t01_health_json() { +#[test] +#[ignore] +fn t01_health_json() { ensure_container(); let h = curl_json(HEALTH_URL); assert_eq!(h["service"], "reach-supervisor"); @@ -79,29 +115,50 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } assert!(h["display"].as_str().unwrap().starts_with(':')); } -#[test] #[ignore] fn t02_four_processes_running() { +#[test] +#[ignore] +fn t02_four_processes_running() { ensure_container(); let h = curl_json(HEALTH_URL); let procs = h["processes"].as_array().unwrap(); assert_eq!(procs.len(), 4); for name in ["xvfb", "openbox", "x11vnc", "novnc"] { - let p = procs.iter().find(|p| p["name"] == name).unwrap_or_else(|| panic!("missing {name}")); + let p = procs + .iter() + .find(|p| p["name"] == name) + .unwrap_or_else(|| panic!("missing {name}")); assert_eq!(p["status"], "running", "{name} not running"); assert!(p["pid"].as_u64().unwrap() > 0); assert_eq!(p["restart_count"], 0, "{name} restarted"); } } -#[test] #[ignore] fn t03_healthy_status() { +#[test] +#[ignore] +fn t03_healthy_status() { ensure_container(); assert_eq!(curl_json(HEALTH_URL)["status"], "healthy"); } -#[test] #[ignore] fn t04_metrics_200() { +#[test] +#[ignore] +fn t04_metrics_200() { ensure_container(); let code = String::from_utf8_lossy( - &Command::new("curl").args(["-sf", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:18400/metrics"]).output().unwrap().stdout - ).to_string(); + &Command::new("curl") + .args([ + "-sf", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "http://localhost:18400/metrics", + ]) + .output() + .unwrap() + .stdout, + ) + .to_string(); assert_eq!(code.trim(), "200"); } @@ -109,20 +166,28 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 2. DISPLAY // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t05_x11_socket() { +#[test] +#[ignore] +fn t05_x11_socket() { ensure_container(); assert_eq!(sh_code("test -S /tmp/.X11-unix/X99"), 0); } -#[test] #[ignore] fn t06_resolution() { +#[test] +#[ignore] +fn t06_resolution() { ensure_container(); assert_eq!(sh_ok("DISPLAY=:99 xdotool getdisplaygeometry"), "1280 720"); } -#[test] #[ignore] fn t07_24bit_color() { +#[test] +#[ignore] +fn t07_24bit_color() { ensure_container(); // xdpyinfo may not be installed; use xrandr or python to check depth - let depth = sh_ok("DISPLAY=:99 python3 -c \"import subprocess; o=subprocess.check_output(['xdotool','getdisplaygeometry']).decode(); print('24')\""); + let depth = sh_ok( + "DISPLAY=:99 python3 -c \"import subprocess; o=subprocess.check_output(['xdotool','getdisplaygeometry']).decode(); print('24')\"", + ); assert!(depth.contains("24")); } @@ -130,12 +195,19 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 3. VNC // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t08_vnc_port() { +#[test] +#[ignore] +fn t08_vnc_port() { ensure_container(); - assert_eq!(sh_code("timeout 2 bash -c 'echo > /dev/tcp/localhost/5900'"), 0); + assert_eq!( + sh_code("timeout 2 bash -c 'echo > /dev/tcp/localhost/5900'"), + 0 + ); } -#[test] #[ignore] fn t09_novnc_html() { +#[test] +#[ignore] +fn t09_novnc_html() { ensure_container(); let body = curl(NOVNC_URL); assert!(body.contains("html") || body.contains("noVNC") || body.contains("Directory")); @@ -145,16 +217,22 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 4. SCREENSHOT // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t10_png_magic_bytes() { +#[test] +#[ignore] +fn t10_png_magic_bytes() { ensure_container(); sh_ok("DISPLAY=:99 scrot -z /tmp/e2e_shot.png"); assert_eq!(sh_ok("od -A n -t x1 -N 4 /tmp/e2e_shot.png"), "89 50 4e 47"); } -#[test] #[ignore] fn t11_png_dimensions() { +#[test] +#[ignore] +fn t11_png_dimensions() { ensure_container(); sh_ok("DISPLAY=:99 scrot -z /tmp/e2e_dim.png"); - let dims = sh_ok("python3 -c \"import struct; f=open('/tmp/e2e_dim.png','rb'); f.read(16); w,h=struct.unpack('>II',f.read(8)); print(f'{w}x{h}')\""); + let dims = sh_ok( + "python3 -c \"import struct; f=open('/tmp/e2e_dim.png','rb'); f.read(16); w,h=struct.unpack('>II',f.read(8)); print(f'{w}x{h}')\"", + ); assert_eq!(dims, "1280x720"); } @@ -162,24 +240,35 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 5. INPUT // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t12_mouse_move() { +#[test] +#[ignore] +fn t12_mouse_move() { ensure_container(); sh_ok("DISPLAY=:99 xdotool mousemove 100 200"); let pos = sh_ok("DISPLAY=:99 xdotool getmouselocation"); - assert!(pos.contains("x:100") && pos.contains("y:200"), "bad pos: {pos}"); + assert!( + pos.contains("x:100") && pos.contains("y:200"), + "bad pos: {pos}" + ); } -#[test] #[ignore] fn t13_mouse_click() { +#[test] +#[ignore] +fn t13_mouse_click() { ensure_container(); sh_ok("DISPLAY=:99 xdotool mousemove 640 360 click 1"); } -#[test] #[ignore] fn t14_keyboard_type() { +#[test] +#[ignore] +fn t14_keyboard_type() { ensure_container(); sh_ok("DISPLAY=:99 xdotool type 'reach e2e'"); } -#[test] #[ignore] fn t15_key_combo() { +#[test] +#[ignore] +fn t15_key_combo() { ensure_container(); sh_ok("DISPLAY=:99 xdotool key ctrl+l"); } @@ -188,31 +277,48 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 6. CHROME // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t16_chrome_version() { +#[test] +#[ignore] +fn t16_chrome_version() { ensure_container(); assert!(sh_ok("google-chrome --version").contains("Google Chrome")); } -#[test] #[ignore] fn t17_chrome_headed() { +#[test] +#[ignore] +fn t17_chrome_headed() { ensure_container(); - let _ = sh("pkill -f chrome"); sleep_ms(500); - sh_ok("DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &"); + let _ = sh("pkill -f chrome"); + sleep_ms(500); + sh_ok( + "DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &", + ); sleep_ms(3000); sh_ok("DISPLAY=:99 scrot -z /tmp/e2e_chrome.png"); let bytes: u64 = sh_ok("stat -c %s /tmp/e2e_chrome.png").parse().unwrap(); - assert!(bytes > 10_000, "screenshot too small ({bytes}b), chrome didn't render"); + assert!( + bytes > 10_000, + "screenshot too small ({bytes}b), chrome didn't render" + ); } -#[test] #[ignore] fn t18_chrome_headless_dom() { +#[test] +#[ignore] +fn t18_chrome_headless_dom() { ensure_container(); let html = String::from_utf8_lossy(&sh("timeout 15 google-chrome --headless --dump-dom --no-sandbox https://example.com 2>/dev/null").stdout).to_string(); assert!(html.contains("Example Domain")); } -#[test] #[ignore] fn t19_chrome_click_navigates() { +#[test] +#[ignore] +fn t19_chrome_click_navigates() { ensure_container(); - let _ = sh("pkill -f chrome"); sleep_ms(500); - sh_ok("DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &"); + let _ = sh("pkill -f chrome"); + sleep_ms(500); + sh_ok( + "DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &", + ); sleep_ms(3000); sh_ok("DISPLAY=:99 xdotool mousemove 266 353 click 1"); sleep_ms(3000); @@ -225,29 +331,43 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 7. PLAYWRIGHT // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t20_playwright_import() { +#[test] +#[ignore] +fn t20_playwright_import() { ensure_container(); let out = sh_ok("python3 -c 'from playwright.sync_api import sync_playwright; print(\"ok\")'"); assert_eq!(out, "ok"); } -#[test] #[ignore] fn t21_playwright_title() { +#[test] +#[ignore] +fn t21_playwright_title() { ensure_container(); - let title = sh_ok("python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n print(pg.title()); b.close()\nPY"); + let title = sh_ok( + "python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n print(pg.title()); b.close()\nPY", + ); assert_eq!(title, "Example Domain"); } -#[test] #[ignore] fn t22_playwright_selectors() { +#[test] +#[ignore] +fn t22_playwright_selectors() { ensure_container(); - let out = sh_ok("python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n h1=pg.query_selector('h1').inner_text()\n n=len(pg.query_selector_all('a'))\n print(f'{h1}|{n}'); b.close()\nPY"); + let out = sh_ok( + "python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n h1=pg.query_selector('h1').inner_text()\n n=len(pg.query_selector_all('a'))\n print(f'{h1}|{n}'); b.close()\nPY", + ); let parts: Vec<&str> = out.split('|').collect(); assert_eq!(parts[0], "Example Domain"); assert!(parts[1].parse::().unwrap() > 0); } -#[test] #[ignore] fn t23_playwright_screenshot() { +#[test] +#[ignore] +fn t23_playwright_screenshot() { ensure_container(); - sh_ok("python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n pg.screenshot(path='/tmp/e2e_pw.png'); b.close()\nPY"); + sh_ok( + "python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n pg.screenshot(path='/tmp/e2e_pw.png'); b.close()\nPY", + ); assert_eq!(sh_ok("od -A n -t x1 -N 4 /tmp/e2e_pw.png"), "89 50 4e 47"); } @@ -255,19 +375,32 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 8. SCRAPLING // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t24_scrapling_version() { +#[test] +#[ignore] +fn t24_scrapling_version() { ensure_container(); assert!(sh_ok("python3 -c 'import scrapling; print(scrapling.__version__)'").starts_with("0.")); } -#[test] #[ignore] fn t25_scrapling_h1() { +#[test] +#[ignore] +fn t25_scrapling_h1() { ensure_container(); - assert_eq!(sh_ok("python3 -c \"from scrapling import Fetcher; r=Fetcher().get('https://example.com'); print(r.css('h1')[0].text)\""), "Example Domain"); + assert_eq!( + sh_ok( + "python3 -c \"from scrapling import Fetcher; r=Fetcher().get('https://example.com'); print(r.css('h1')[0].text)\"" + ), + "Example Domain" + ); } -#[test] #[ignore] fn t26_scrapling_multi_selector() { +#[test] +#[ignore] +fn t26_scrapling_multi_selector() { ensure_container(); - let out = sh_ok("python3 << 'PY'\nfrom scrapling import Fetcher\nr=Fetcher().get('https://example.com')\nprint(f\"{r.css('h1')[0].text}|{len(r.css('p'))}|{len(r.css('a'))}\")\nPY"); + let out = sh_ok( + "python3 << 'PY'\nfrom scrapling import Fetcher\nr=Fetcher().get('https://example.com')\nprint(f\"{r.css('h1')[0].text}|{len(r.css('p'))}|{len(r.css('a'))}\")\nPY", + ); let p: Vec<&str> = out.split('|').collect(); assert_eq!(p[0], "Example Domain"); assert!(p[1].parse::().unwrap() > 0); @@ -278,12 +411,16 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 9. NODE // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t27_node_version() { +#[test] +#[ignore] +fn t27_node_version() { ensure_container(); assert!(sh_ok("node --version").starts_with('v')); } -#[test] #[ignore] fn t28_computer_use_mcp() { +#[test] +#[ignore] +fn t28_computer_use_mcp() { ensure_container(); let out = sh_ok("npm list -g --depth=0 2>/dev/null | grep computer-use-mcp || echo missing"); assert!(!out.contains("missing"), "computer-use-mcp not installed"); @@ -293,22 +430,30 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 10. SECURITY // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t29_sandbox_user() { +#[test] +#[ignore] +fn t29_sandbox_user() { ensure_container(); assert_eq!(sh_ok("whoami"), "sandbox"); } -#[test] #[ignore] fn t30_not_root() { +#[test] +#[ignore] +fn t30_not_root() { ensure_container(); assert_ne!(sh_ok("id -u"), "0"); } -#[test] #[ignore] fn t31_home_writable() { +#[test] +#[ignore] +fn t31_home_writable() { ensure_container(); sh_ok("touch ~/e2e_test && rm ~/e2e_test"); } -#[test] #[ignore] fn t32_system_dirs_readonly() { +#[test] +#[ignore] +fn t32_system_dirs_readonly() { ensure_container(); assert_ne!(sh_code("touch /usr/bin/e2e 2>/dev/null"), 0); assert_ne!(sh_code("touch /etc/e2e 2>/dev/null"), 0); @@ -318,37 +463,62 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 11. WORKFLOWS // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t33_workflow_scrape_then_visual() { +#[test] +#[ignore] +fn t33_workflow_scrape_then_visual() { ensure_container(); // Scrape - assert_eq!(sh_ok("python3 -c \"from scrapling import Fetcher; print(Fetcher().get('https://example.com').css('h1')[0].text)\""), "Example Domain"); + assert_eq!( + sh_ok( + "python3 -c \"from scrapling import Fetcher; print(Fetcher().get('https://example.com').css('h1')[0].text)\"" + ), + "Example Domain" + ); // Visual - let _ = sh("pkill -f chrome"); sleep_ms(500); - sh_ok("DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &"); + let _ = sh("pkill -f chrome"); + sleep_ms(500); + sh_ok( + "DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &", + ); sleep_ms(3000); sh_ok("DISPLAY=:99 scrot -z /tmp/e2e_wf1.png"); assert!(sh_ok("stat -c %s /tmp/e2e_wf1.png").parse::().unwrap() > 10_000); } -#[test] #[ignore] fn t34_workflow_type_url() { +#[test] +#[ignore] +fn t34_workflow_type_url() { ensure_container(); - let _ = sh("pkill -f chrome"); sleep_ms(500); - sh_ok("DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 about:blank &"); + let _ = sh("pkill -f chrome"); + sleep_ms(500); + sh_ok( + "DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 about:blank &", + ); sleep_ms(2000); - sh_ok("DISPLAY=:99 xdotool key ctrl+l"); sleep_ms(300); - sh_ok("DISPLAY=:99 xdotool type --delay 50 'https://example.com'"); sleep_ms(300); - sh_ok("DISPLAY=:99 xdotool key Return"); sleep_ms(3000); + sh_ok("DISPLAY=:99 xdotool key ctrl+l"); + sleep_ms(300); + sh_ok("DISPLAY=:99 xdotool type --delay 50 'https://example.com'"); + sleep_ms(300); + sh_ok("DISPLAY=:99 xdotool key Return"); + sleep_ms(3000); sh_ok("DISPLAY=:99 scrot -z /tmp/e2e_wf2.png"); assert!(sh_ok("stat -c %s /tmp/e2e_wf2.png").parse::().unwrap() > 10_000); } -#[test] #[ignore] fn t35_workflow_headed_and_headless_coexist() { +#[test] +#[ignore] +fn t35_workflow_headed_and_headless_coexist() { ensure_container(); - let _ = sh("pkill -f chrome"); sleep_ms(500); - sh_ok("DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &"); + let _ = sh("pkill -f chrome"); + sleep_ms(500); + sh_ok( + "DISPLAY=:99 google-chrome --no-sandbox --disable-gpu --no-first-run --window-size=1280,720 https://example.com &", + ); sleep_ms(1000); // Headless playwright while headed chrome is running - let h1 = sh_ok("python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n print(pg.query_selector('h1').inner_text()); b.close()\nPY"); + let h1 = sh_ok( + "python3 << 'PY'\nfrom playwright.sync_api import sync_playwright\nwith sync_playwright() as p:\n b=p.chromium.launch(headless=True)\n pg=b.new_page(); pg.goto('https://example.com')\n print(pg.query_selector('h1').inner_text()); b.close()\nPY", + ); assert_eq!(h1, "Example Domain"); // Headed chrome still alive assert!(!sh_ok("pgrep -f 'chrome.*no-sandbox' | head -1").is_empty()); @@ -358,9 +528,14 @@ fn sleep_ms(ms: u64) { std::thread::sleep(Duration::from_millis(ms)); } // 99. SHUTDOWN (must run last) // ═══════════════════════════════════════════════════════════ -#[test] #[ignore] fn t99_graceful_shutdown() { +#[test] +#[ignore] +fn t99_graceful_shutdown() { ensure_container(); assert!(docker(&["stop", "-t", "10", CONTAINER]).status.success()); - assert_eq!(docker_out(&["inspect", "-f", "{{.State.ExitCode}}", CONTAINER]), "0"); + assert_eq!( + docker_out(&["inspect", "-f", "{{.State.ExitCode}}", CONTAINER]), + "0" + ); let _ = docker(&["rm", "-f", CONTAINER]); } diff --git a/crates/reach-cli/tests/mcp_types.rs b/crates/reach-cli/tests/mcp_types.rs index 511d21e..ba1a626 100644 --- a/crates/reach-cli/tests/mcp_types.rs +++ b/crates/reach-cli/tests/mcp_types.rs @@ -9,10 +9,7 @@ use reach_cli::mcp::*; #[test] fn jsonrpc_response_success_has_no_error_field() { - let resp = JsonRpcResponse::success( - RequestId::Number(1), - serde_json::json!({"status": "ok"}), - ); + let resp = JsonRpcResponse::success(RequestId::Number(1), serde_json::json!({"status": "ok"})); let json = serde_json::to_string(&resp).unwrap(); assert!(json.contains("\"result\"")); assert!(!json.contains("\"error\"")); @@ -139,15 +136,13 @@ fn screenshot_tool_has_no_required_fields() { #[test] fn screenshot_params_default_format_is_png() { - let params: ScreenshotParams = - serde_json::from_str("{}").unwrap(); + let params: ScreenshotParams = serde_json::from_str("{}").unwrap(); assert!(matches!(params.format, ImageFormat::Png)); } #[test] fn click_params_default_button_is_left() { - let params: ClickParams = - serde_json::from_str(r#"{"x": 100, "y": 200}"#).unwrap(); + let params: ClickParams = serde_json::from_str(r#"{"x": 100, "y": 200}"#).unwrap(); assert!(matches!(params.button, MouseButton::Left)); assert_eq!(params.x, 100); assert_eq!(params.y, 200); @@ -155,8 +150,7 @@ fn click_params_default_button_is_left() { #[test] fn browse_params_default_headed_is_true() { - let params: BrowseParams = - serde_json::from_str(r#"{"url": "https://example.com"}"#).unwrap(); + let params: BrowseParams = serde_json::from_str(r#"{"url": "https://example.com"}"#).unwrap(); assert!(params.headed); } @@ -169,8 +163,7 @@ fn scrape_params_default_stealth_is_true() { #[test] fn exec_params_default_timeout_is_30() { - let params: ExecParams = - serde_json::from_str(r#"{"command": "ls"}"#).unwrap(); + let params: ExecParams = serde_json::from_str(r#"{"command": "ls"}"#).unwrap(); assert_eq!(params.timeout, 30); } @@ -241,10 +234,7 @@ fn sse_endpoint_message_carries_uri() { #[test] fn sse_message_wraps_jsonrpc_response() { - let resp = JsonRpcResponse::success( - RequestId::Number(1), - serde_json::json!({"tools": []}), - ); + let resp = JsonRpcResponse::success(RequestId::Number(1), serde_json::json!({"tools": []})); let msg = SseMessage::message(&resp); assert_eq!(msg.event, "message"); assert!(msg.data.contains("\"jsonrpc\"")); diff --git a/crates/reach-supervisor/src/main.rs b/crates/reach-supervisor/src/main.rs index ff4bb03..e27c09d 100644 --- a/crates/reach-supervisor/src/main.rs +++ b/crates/reach-supervisor/src/main.rs @@ -40,7 +40,9 @@ async fn main() -> anyhow::Result<()> { loop { tokio::time::sleep(Duration::from_secs(2)).await; let mut sup = supervision_shared.write().await; - if let Ok(restarted) = sup.check_and_restart().await && restarted > 0 { + if let Ok(restarted) = sup.check_and_restart().await + && restarted > 0 + { tracing::info!(restarted, "restarted crashed processes"); } } diff --git a/crates/reach-supervisor/src/processes.rs b/crates/reach-supervisor/src/processes.rs index f12d5e6..3e9260b 100644 --- a/crates/reach-supervisor/src/processes.rs +++ b/crates/reach-supervisor/src/processes.rs @@ -23,7 +23,10 @@ pub struct ProcessSpec { #[derive(Debug, Clone)] pub enum RestartPolicy { - Always { max_restarts: u32, backoff: Duration }, + Always { + max_restarts: u32, + backoff: Duration, + }, } #[derive(Debug, Clone)] @@ -203,10 +206,7 @@ impl Supervisor { backoff: Duration::from_secs(1), }, depends_on: vec![], - ready_check: ReadyCheck::FileExists(format!( - "/tmp/.X11-unix/X{}", - self.display - )), + ready_check: ReadyCheck::FileExists(format!("/tmp/.X11-unix/X{}", self.display)), }, ProcessSpec { name: "openbox", diff --git a/crates/reach-supervisor/src/signals.rs b/crates/reach-supervisor/src/signals.rs index b2eb2b5..6deeade 100644 --- a/crates/reach-supervisor/src/signals.rs +++ b/crates/reach-supervisor/src/signals.rs @@ -2,10 +2,8 @@ use anyhow::Result; /// Wait for SIGTERM or SIGINT, then return for graceful shutdown. pub async fn wait_for_shutdown() -> Result<()> { - let mut sigterm = - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; - let mut sigint = - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?; + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; + let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?; tokio::select! { _ = sigterm.recv() => tracing::info!("received SIGTERM"), diff --git a/deny.toml b/deny.toml index 5e0b85c..6972210 100644 --- a/deny.toml +++ b/deny.toml @@ -1,10 +1,28 @@ +# cargo-deny advisories configuration. +# `unmaintained` takes scope values {all, workspace, transitive, none} — +# "workspace" fails the build only when a direct workspace dependency is +# marked unmaintained in the advisory DB, without failing on transitive +# deps the workspace doesn't control. `vulnerability` was removed from the +# schema in cargo-deny 0.14+ (cargo-deny now always denies vulnerabilities +# in its default pipeline). [advisories] -vulnerability = "deny" -unmaintained = "warn" +unmaintained = "workspace" yanked = "warn" +ignore = [ + # protobuf 2.28.0 is pulled in transitively by prometheus 0.13 (our + # metrics exporter in reach-supervisor). RUSTSEC-2024-0437 is a stack + # overflow on parsing untrusted protobuf INPUT — reach-supervisor only + # EXPORTS metrics (serialises to text format over HTTP) and never + # parses protobuf input from the network, so the vector does not + # apply. Follow-up: upgrade to a metrics crate that uses protobuf 3.x + # or drop prometheus in favour of a text-only exporter. Tracked in + # the reach roadmap. + "RUSTSEC-2024-0437", +] +# cargo-deny 0.14+ removed the `unlicensed` key; the tool now treats +# unlicensed crates as deny-by-default inside the licenses pipeline. [licenses] -unlicensed = "deny" allow = [ "MIT", "Apache-2.0",