diff --git a/Cargo.lock b/Cargo.lock index b8016b4cb..4dd7d1c08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,30 +10,30 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common 0.1.7", - "generic-array", + "crypto-common 0.2.2", + "inout", ] [[package]] name = "aes" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] name = "aes-gcm" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ "aead", "aes", @@ -594,11 +594,12 @@ dependencies = [ [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.1.7", + "block-buffer 0.12.0", + "crypto-common 0.2.2", "inout", ] @@ -645,6 +646,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" @@ -697,9 +704,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -735,6 +742,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -876,7 +889,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -886,18 +898,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.2", "hybrid-array", + "rand_core 0.10.1", ] [[package]] name = "ctr" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "cty" version = "0.2.2" @@ -1313,11 +1336,10 @@ dependencies = [ [[package]] name = "ghash" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "opaque-debug", "polyval", ] @@ -1726,9 +1748,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", @@ -1739,11 +1761,11 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -2096,7 +2118,7 @@ dependencies = [ "parking_lot", "percent-encoding", "quick-xml", - "rand 0.10.1", + "rand 0.10.2", "reqwest", "ring", "serde", @@ -2129,12 +2151,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -2912,13 +2928,12 @@ dependencies = [ [[package]] name = "polyval" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", + "cpubits", + "cpufeatures 0.3.0", "universal-hash", ] @@ -3131,9 +3146,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -3160,15 +3175,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "rand_core" version = "0.9.5" @@ -3380,9 +3386,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.8.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" +checksum = "f00a32c3b81b7b254076a65abd5ab2551209146713ba38f73818657e865e9433" dependencies = [ "async-trait", "base64", @@ -3394,7 +3400,7 @@ dependencies = [ "http-body-util", "pastey", "pin-project-lite", - "rand 0.10.1", + "rand 0.10.2", "rmcp-macros", "schemars", "serde", @@ -3411,9 +3417,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.8.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" +checksum = "2ee70afb7956da9f30d5348a2539b5eb90d9038f834463657ab717076ac3b1ad" dependencies = [ "darling", "proc-macro2", @@ -4508,7 +4514,7 @@ dependencies = [ "libmimalloc-sys", "mimalloc", "proptest", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "tempfile", @@ -4627,7 +4633,7 @@ dependencies = [ "indicatif", "libc", "proptest", - "rand 0.10.1", + "rand 0.10.2", "rand_chacha 0.10.0", "rayon", "rustc-hash", @@ -4665,7 +4671,7 @@ dependencies = [ "dirs-next", "libc", "memmap2", - "rand 0.10.1", + "rand 0.10.2", "security-framework", "tempfile", "tracing", @@ -4771,12 +4777,12 @@ checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.1.7", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f307ab35b..523183c5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -235,7 +235,7 @@ thiserror = "2.0.18" anyhow = "1.0.103" # ───── MCP (Model Context Protocol) ───── -rmcp = { version = "1.8.0", features = ["server", "transport-io", "macros"] } +rmcp = { version = "2.1.0", features = ["server", "transport-io", "macros"] } schemars = "1.2.1" # ───── HTTP / Tower (for MCP Streamable HTTP gateway) ───── @@ -258,7 +258,7 @@ clap = { version = "4.6.1", features = [ "unicode", "wrap_help", ] } -indicatif = "0.18.4" +indicatif = "0.18.6" devicons = "0.6.12" # ANSI terminal colouring. Used by the `uffs-ci-pipeline` tool; kept at # workspace scope so a future UI surface that wants colored output inherits @@ -328,8 +328,8 @@ num_cpus = "1.17.0" uuid = { version = "1.23.4", features = ["v4"] } # ───── Security / Crypto ───── -aes-gcm = "0.10" -rand = "0.10.1" +aes-gcm = "0.11.0" +rand = "0.10.2" rand_chacha = "0.10.0" security-framework = "3.7.0" diff --git a/crates/uffs-broker/src/broker/service.rs b/crates/uffs-broker/src/broker/service.rs index dc2c62679..a76285dfb 100644 --- a/crates/uffs-broker/src/broker/service.rs +++ b/crates/uffs-broker/src/broker/service.rs @@ -45,7 +45,10 @@ fn wide(text: &str) -> Vec { /// stderr-only message comes out empty (as seen on a failed `sc create`). #[cfg(windows)] fn sc_output(output: &std::process::Output) -> String { + // AUDIT-OK(bytes): operator-facing diagnostic text only — the combined + // output is formatted into an error message, never parsed or matched. let stdout = String::from_utf8_lossy(&output.stdout); + // AUDIT-OK(bytes): same display-only argument as stdout above. let stderr = String::from_utf8_lossy(&output.stderr); format!("{} {}", stdout.trim(), stderr.trim()) .trim() diff --git a/crates/uffs-cli/src/commands/uninstall/sweep.rs b/crates/uffs-cli/src/commands/uninstall/sweep.rs index 120145866..f84f530c6 100644 --- a/crates/uffs-cli/src/commands/uninstall/sweep.rs +++ b/crates/uffs-cli/src/commands/uninstall/sweep.rs @@ -200,9 +200,13 @@ fn probe_version_bounded(path: &Path) -> ProbeOutcome { let Ok(output) = child.wait_with_output() else { return ProbeOutcome::None; }; + // AUDIT-OK(bytes): ASCII version-token extraction from a probed binary's + // output. A lossy decode cannot fabricate a `MAJOR.MINOR.PATCH` token + // (U+FFFD is not a digit), only fail to yield one -> "legacy" label. let mut text = String::from_utf8_lossy(&output.stdout).into_owned(); if text.trim().is_empty() { // Some tools print `--version` to stderr; fall back to it. + // AUDIT-OK(bytes): same ASCII-token argument as stdout above. text = String::from_utf8_lossy(&output.stderr).into_owned(); } crate::commands::update::binaries::parse_version(&text) @@ -397,7 +401,10 @@ fn payload_paths(payload: uffs_client::protocol::response::SearchPayload) -> Vec Payload::ShmemBlob(path) => { let mut buf: Vec = Vec::new(); if uffs_client::shmem::stream_paths_blob_into(Path::new(&path), &mut buf).is_ok() { - blob_lines_to_paths(&String::from_utf8_lossy(&buf)) + // Strict parse: these paths feed the EXTRA delete list, so a + // corrupt blob is rejected outright rather than risking a + // lossy-mangled path. The daemon always emits valid UTF-8. + core::str::from_utf8(&buf).map_or_else(|_| Vec::new(), blob_lines_to_paths) } else { Vec::new() } diff --git a/crates/uffs-cli/src/commands/update/acquire.rs b/crates/uffs-cli/src/commands/update/acquire.rs index 6f44eb11a..124379ec7 100644 --- a/crates/uffs-cli/src/commands/update/acquire.rs +++ b/crates/uffs-cli/src/commands/update/acquire.rs @@ -91,7 +91,10 @@ pub(super) fn latest_version() -> Option { if !output.status.success() { return None; } - String::from_utf8_lossy(&output.stdout) + // Strict parse: the helper is our own binary emitting ASCII `latest=` + // lines; non-UTF-8 output means something is wrong -> treat as no answer. + core::str::from_utf8(&output.stdout) + .ok()? .lines() .find_map(|line| line.strip_prefix("latest=")) .map(|tag| tag.trim().to_owned()) diff --git a/crates/uffs-cli/src/commands/update/binaries.rs b/crates/uffs-cli/src/commands/update/binaries.rs index 4bc033cad..7a91e141d 100644 --- a/crates/uffs-cli/src/commands/update/binaries.rs +++ b/crates/uffs-cli/src/commands/update/binaries.rs @@ -98,9 +98,13 @@ pub(crate) fn enumerate(dir: &Path) -> Vec { /// version token is found. pub(crate) fn probe_version(path: &Path) -> Option { let output = Command::new(path).arg("--version").output().ok()?; + // AUDIT-OK(bytes): ASCII version-token extraction from a probed binary's + // output. A lossy decode cannot fabricate a `MAJOR.MINOR.PATCH` token + // (U+FFFD is not a digit), only fail to yield one -> `None`. let mut text = String::from_utf8_lossy(&output.stdout).into_owned(); if text.trim().is_empty() { // Some tools print `--version` to stderr; fall back to it. + // AUDIT-OK(bytes): same ASCII-token argument as stdout above. text = String::from_utf8_lossy(&output.stderr).into_owned(); } parse_version(&text) diff --git a/crates/uffs-cli/src/commands/update/procinfo.rs b/crates/uffs-cli/src/commands/update/procinfo.rs index 614ffe3d4..7a936ac88 100644 --- a/crates/uffs-cli/src/commands/update/procinfo.rs +++ b/crates/uffs-cli/src/commands/update/procinfo.rs @@ -117,6 +117,9 @@ pub(crate) fn find_pids_by_name(stem: &str) -> Vec { .ok() .filter(|out| out.status.success()) .map(|out| { + // AUDIT-OK(bytes): pgrep emits one ASCII pid per line; a lossy + // decode cannot fabricate digits (U+FFFD fails `parse::`), + // it can only drop a line — best-effort by design. String::from_utf8_lossy(&out.stdout) .lines() .filter_map(|line| line.trim().parse::().ok()) @@ -173,6 +176,9 @@ fn sc_output(args: &[&str]) -> Option { if !out.status.success() { return None; } + // AUDIT-OK(bytes): Windows console tools emit OEM/ANSI-codepage bytes, so + // a strict UTF-8 parse would fail on any localized system; the text is a + // best-effort display/substring probe, and lossy can only fail a match. let text = String::from_utf8_lossy(&out.stdout).into_owned(); (!text.trim().is_empty()).then_some(text) } @@ -189,6 +195,8 @@ fn unix_cmdline(pid: u32) -> Option { let joined = raw .split(|byte| *byte == 0) .filter(|seg| !seg.is_empty()) + // AUDIT-OK(bytes): kernel cmdline segments are OS-arbitrary bytes + // with no encoding guarantee; best-effort display/probe text. .map(|seg| String::from_utf8_lossy(seg).into_owned()) .collect::>() .join(" "); @@ -218,6 +226,8 @@ fn ps_field(pid: u32, field: &str) -> Option { if !out.status.success() { return None; } + // AUDIT-OK(bytes): `ps` field bytes have no encoding guarantee (they echo + // the process's own argv); best-effort display/probe text. let text = String::from_utf8_lossy(&out.stdout).trim().to_owned(); (!text.is_empty()).then_some(text) } diff --git a/crates/uffs-mcp/src/handler/mod.rs b/crates/uffs-mcp/src/handler/mod.rs index 7c9d7d1ba..11e78ea13 100644 --- a/crates/uffs-mcp/src/handler/mod.rs +++ b/crates/uffs-mcp/src/handler/mod.rs @@ -16,11 +16,10 @@ use alloc::sync::Arc; use core::sync::atomic::{AtomicU64, Ordering}; use rmcp::model::{ - AnnotateAble as _, CallToolRequestParams, CallToolResult, GetPromptRequestParams, - GetPromptResult, Implementation, ListPromptsResult, ListResourceTemplatesResult, - ListResourcesResult, ListToolsResult, PaginatedRequestParams, RawResource, RawResourceTemplate, - ReadResourceRequestParams, ReadResourceResult, ResourceContents, ServerCapabilities, - ServerInfo, + CallToolRequestParams, CallToolResult, GetPromptRequestParams, GetPromptResult, Implementation, + ListPromptsResult, ListResourceTemplatesResult, ListResourcesResult, ListToolsResult, + PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResult, Resource, + ResourceContents, ResourceTemplate, ServerCapabilities, ServerInfo, }; use rmcp::service::RequestContext; use rmcp::{ErrorData as McpError, RoleServer, ServerHandler}; @@ -349,19 +348,31 @@ impl ServerHandler for UffsMcpServer { ) )] async fn on_roots_list_changed(&self, context: rmcp::service::NotificationContext) { - // Ask the client for the current list of roots. + // Ask the client for the current list of roots. This match is the ONLY + // place in uffs-mcp that speaks the SEP-2577-deprecated Roots wire API + // (`Peer::list_roots` + `rmcp::model::Root`); `roots.rs` is transport- + // agnostic (`AdvertisedRoot`). When rmcp removes the API, delete this + // hook — workspace scoping then flows from the explicit path filters + // the search tools already accept. #[expect( deprecated, - reason = "rmcp 1.8.0 deprecated Peer::list_roots per MCP SEP-2577 (the Roots \ - capability is being phased out). It still functions, and uffs-mcp's \ - roots -> NTFS-prefix mapping depends on it; there is no replacement API \ - yet. Migrate or drop the roots feature when rmcp removes the method (at \ - which point this becomes a hard error, not a silenced warning)." + reason = "SEP-2577 deprecates MCP Roots with no replacement API; clients still \ + advertise roots today, so this single boundary hook converts them to \ + the transport-agnostic AdvertisedRoot. Delete the hook when rmcp \ + removes the method (hard error then, not a silenced warning)." )] match context.peer.list_roots().await { Ok(result) => { + let advertised: Vec = result + .roots + .iter() + .map(|root| roots::AdvertisedRoot { + uri: root.uri.clone(), + name: root.name.clone(), + }) + .collect(); let mut state = self.roots.write().await; - roots::update_roots_state(&mut state, &result.roots); + roots::update_roots_state(&mut state, &advertised); let mapped = state .roots .iter() @@ -483,49 +494,42 @@ impl ServerHandler for UffsMcpServer { self.touch(); Ok(ListResourcesResult { resources: vec![ - RawResource::new("uffs://schema/fields", "Field Catalog") + Resource::new("uffs://schema/fields", "Field Catalog") .with_description( "Complete catalog of fields available for searching, filtering, \ sorting, and aggregating — includes types and capabilities", ) - .with_mime_type("application/json") - .no_annotation(), - RawResource::new("uffs://drives", "Indexed Drives") + .with_mime_type("application/json"), + Resource::new("uffs://drives", "Indexed Drives") .with_description( "Live listing of currently indexed NTFS drives with record counts", ) - .with_mime_type("application/json") - .no_annotation(), - RawResource::new("uffs://status", "Daemon Status") + .with_mime_type("application/json"), + Resource::new("uffs://status", "Daemon Status") .with_description( "Daemon health, state, uptime, memory, PID, and drive-loading progress", ) - .with_mime_type("application/json") - .no_annotation(), - RawResource::new("uffs://schema/search", "Search Request Schema") + .with_mime_type("application/json"), + Resource::new("uffs://schema/search", "Search Request Schema") .with_description("JSON Schema for the uffs_search tool input parameters") - .with_mime_type("application/json") - .no_annotation(), - RawResource::new("uffs://schema/aggregate", "Aggregate Request Schema") + .with_mime_type("application/json"), + Resource::new("uffs://schema/aggregate", "Aggregate Request Schema") .with_description("JSON Schema for the uffs_aggregate tool input parameters") - .with_mime_type("application/json") - .no_annotation(), - RawResource::new("uffs://presets/aggregate", "Aggregate Presets") + .with_mime_type("application/json"), + Resource::new("uffs://presets/aggregate", "Aggregate Presets") .with_description( "Built-in aggregate presets (overview, by_type, by_extension, \ storage, etc.) with descriptions", ) - .with_mime_type("application/json") - .no_annotation(), + .with_mime_type("application/json"), // ── Agent cookbook (query examples) ────────────────── - RawResource::new("uffs://cookbook", "Query Cookbook") + Resource::new("uffs://cookbook", "Query Cookbook") .with_description( "Curated example MCP tool calls organized by workflow — \ ready-to-use arguments objects, tips, and multi-step patterns. \ Read this first to learn how to compose effective UFFS queries.", ) - .with_mime_type("application/json") - .no_annotation(), + .with_mime_type("application/json"), ], next_cursor: None, meta: None, @@ -544,14 +548,13 @@ impl ServerHandler for UffsMcpServer { self.touch(); Ok(ListResourceTemplatesResult { resource_templates: vec![ - RawResourceTemplate::new("uffs://info/{path}", "File/Directory Info") + ResourceTemplate::new("uffs://info/{path}", "File/Directory Info") .with_description( "Full metadata for a file or directory by path. \ The {path} parameter is a percent-encoded Windows path \ with forward slashes (e.g. C:/Users/me/file.txt).", ) - .with_mime_type("application/json") - .no_annotation(), + .with_mime_type("application/json"), ], next_cursor: None, meta: None, diff --git a/crates/uffs-mcp/src/handler/prompts.rs b/crates/uffs-mcp/src/handler/prompts.rs index 9ed3afd05..fffdab57f 100644 --- a/crates/uffs-mcp/src/handler/prompts.rs +++ b/crates/uffs-mcp/src/handler/prompts.rs @@ -23,10 +23,7 @@ pub(crate) fn u64_arg(args: &serde_json::Map, key: &str, default: /// Build a single user-role prompt message. fn user_msg(text: String) -> Vec { - vec![PromptMessage::new_text( - rmcp::model::PromptMessageRole::User, - text, - )] + vec![PromptMessage::new_text(rmcp::model::Role::User, text)] } /// Build the messages for a given prompt name and arguments. diff --git a/crates/uffs-mcp/src/roots.rs b/crates/uffs-mcp/src/roots.rs index ba85d719f..d14218189 100644 --- a/crates/uffs-mcp/src/roots.rs +++ b/crates/uffs-mcp/src/roots.rs @@ -8,6 +8,14 @@ //! workspace. Unmappable roots (e.g. macOS paths for NTFS capture data) //! produce warnings rather than silent incorrect scoping. +// SEP-2577 deprecates the MCP Roots capability upstream with NO replacement +// API (workspace context moves to explicit tool inputs). This module is +// deliberately **transport-agnostic**: it never touches `rmcp::model::Root`. +// The one place that still speaks the deprecated wire API is the narrow +// `on_roots_list_changed` hook in `handler/mod.rs` — when rmcp removes the +// API, that hook is deleted and everything here keeps working, fed by +// explicit path filters instead. + extern crate alloc; use alloc::sync::Arc; @@ -32,6 +40,18 @@ pub struct RootScope { pub drive_letter: Option, } +/// A workspace root as advertised by the client, decoupled from the +/// transport: `rmcp::model::Root` is SEP-2577-deprecated, so only the +/// `handler` boundary hook converts from it — this module and its tests +/// never name the deprecated type. +#[derive(Debug, Clone)] +pub(crate) struct AdvertisedRoot { + /// Root URI as sent by the client (e.g. `"file:///C:/Users/me/project"`). + pub(crate) uri: String, + /// Optional display name from the client. + pub(crate) name: Option, +} + /// Shared roots state held by the MCP server. /// /// `Clone` is derived so the MCP handler can snapshot the current @@ -83,8 +103,8 @@ fn parse_file_uri_to_ntfs_path(uri: &str) -> Option { Some(trimmed.to_owned()) } -/// Resolve a single [`rmcp::model::Root`] into a [`RootScope`]. -fn resolve_root(root: &rmcp::model::Root) -> RootScope { +/// Resolve a single [`AdvertisedRoot`] into a [`RootScope`]. +fn resolve_root(root: &AdvertisedRoot) -> RootScope { let ntfs_prefix = parse_file_uri_to_ntfs_path(&root.uri); let drive_letter = ntfs_prefix .as_ref() @@ -100,7 +120,7 @@ fn resolve_root(root: &rmcp::model::Root) -> RootScope { } /// Update the [`RootsState`] from a list of roots received from the client. -pub(crate) fn update_roots_state(state: &mut RootsState, roots: &[rmcp::model::Root]) { +pub(crate) fn update_roots_state(state: &mut RootsState, roots: &[AdvertisedRoot]) { state.advertised = true; state.roots.clear(); state.warnings.clear(); @@ -277,11 +297,10 @@ fn longest_common_prefix(paths: &[&String]) -> Option { mod tests { use super::*; - fn make_root(uri: &str, name: Option<&str>) -> rmcp::model::Root { - let root = rmcp::model::Root::new(uri); - match name { - Some(label) => root.with_name(label), - None => root, + fn make_root(uri: &str, name: Option<&str>) -> AdvertisedRoot { + AdvertisedRoot { + uri: uri.to_owned(), + name: name.map(str::to_owned), } } diff --git a/crates/uffs-mcp/src/tools/aggregate.rs b/crates/uffs-mcp/src/tools/aggregate.rs index fd4b93696..86b7c3a7f 100644 --- a/crates/uffs-mcp/src/tools/aggregate.rs +++ b/crates/uffs-mcp/src/tools/aggregate.rs @@ -5,7 +5,7 @@ use core::fmt::Write as _; -use rmcp::model::{CallToolResult, Content}; +use rmcp::model::{CallToolResult, ContentBlock}; use schemars::JsonSchema; use serde::Deserialize; use uffs_client::connect::UffsClient; @@ -202,7 +202,7 @@ pub(crate) async fn run( out }; - let mut result = CallToolResult::success(vec![Content::text(output)]); + let mut result = CallToolResult::success(vec![ContentBlock::text(output)]); result.structured_content = Some(serde_json::to_value(structured)?); Ok(result) } diff --git a/crates/uffs-mcp/src/tools/drives.rs b/crates/uffs-mcp/src/tools/drives.rs index 5a73c90e9..c22a5db6b 100644 --- a/crates/uffs-mcp/src/tools/drives.rs +++ b/crates/uffs-mcp/src/tools/drives.rs @@ -5,7 +5,7 @@ use core::fmt::Write as _; -use rmcp::model::{CallToolResult, Content}; +use rmcp::model::{CallToolResult, ContentBlock}; use uffs_client::connect::UffsClient; use crate::error::BridgeError; @@ -45,7 +45,7 @@ pub(crate) async fn run(client: &mut UffsClient) -> Result Result = resources - .resources - .iter() - .map(|r| r.raw.uri.as_str()) - .collect(); + let uris: Vec<_> = resources.resources.iter().map(|r| r.uri.as_str()).collect(); // Live metadata resources assert!(uris.contains(&"uffs://drives")); assert!(uris.contains(&"uffs://status")); @@ -114,7 +109,9 @@ async fn mcp_resources_list() { fn extract_text(rc: &rmcp::model::ResourceContents) -> &str { match rc { rmcp::model::ResourceContents::TextResourceContents { text, .. } => text.as_str(), - _ => panic!("expected TextResourceContents"), + rmcp::model::ResourceContents::BlobResourceContents { .. } | _ => { + panic!("expected TextResourceContents") + } } } @@ -200,7 +197,7 @@ async fn mcp_resource_templates_list() { let uris: Vec<&str> = templates .resource_templates .iter() - .map(|t| t.raw.uri_template.as_str()) + .map(|t| t.uri_template.as_str()) .collect(); assert!( uris.contains(&"uffs://info/{path}"), diff --git a/crates/uffs-mft/src/platform/process.rs b/crates/uffs-mft/src/platform/process.rs index 2a2754c79..312a578ac 100644 --- a/crates/uffs-mft/src/platform/process.rs +++ b/crates/uffs-mft/src/platform/process.rs @@ -15,6 +15,7 @@ #![cfg(windows)] +use std::os::windows::ffi::OsStringExt as _; use std::path::PathBuf; use windows::Win32::Foundation::{CloseHandle, HANDLE}; @@ -80,7 +81,10 @@ fn image_path_of(handle: HANDLE) -> Option { outcome.ok()?; let written = usize::try_from(size).ok()?; let slice = buf.get(..written)?; - Some(PathBuf::from(String::from_utf16_lossy(slice))) + // Lossless: Windows paths are arbitrary u16 sequences (not necessarily + // valid Unicode); `OsString::from_wide` preserves every code unit, so the + // resulting `PathBuf` compares exactly against real paths — no decode. + Some(PathBuf::from(std::ffi::OsString::from_wide(slice))) } /// Snapshot all process ids on the system via `EnumProcesses`, growing diff --git a/crates/uffs-security/src/crypto.rs b/crates/uffs-security/src/crypto.rs index 92d80d6e7..c4b1af0e4 100644 --- a/crates/uffs-security/src/crypto.rs +++ b/crates/uffs-security/src/crypto.rs @@ -30,8 +30,8 @@ use std::io; -use aes_gcm::aead::generic_array::GenericArray; -use aes_gcm::{AeadInPlace as _, Aes256Gcm, KeyInit as _, Nonce}; +use aes_gcm::aead::inout::InOutBuf; +use aes_gcm::{AeadInOut as _, Aes256Gcm, KeyInit as _, Tag}; // ──────────────────────────────────────────────────────────────────────────── // Constants @@ -147,18 +147,17 @@ pub fn encrypt_cache(plaintext: &[u8], key: &[u8; 32]) -> io::Result> { output.extend_from_slice(plaintext); // Encrypt in-place - let cipher = Aes256Gcm::new(GenericArray::from_slice(key)); - let nonce = Nonce::from_slice(&nonce_bytes); + let cipher = Aes256Gcm::new(key.into()); let tag = cipher - .encrypt_in_place_detached( - nonce, + .encrypt_inout_detached( + (&nonce_bytes).into(), &aad, - output.get_mut(ciphertext_start..).ok_or_else(|| { + InOutBuf::from(output.get_mut(ciphertext_start..).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, "ciphertext offset out of bounds", ) - })?, + })?), ) .map_err(|enc_err| io::Error::other(format!("AES-GCM encrypt failed: {enc_err}")))?; @@ -272,12 +271,17 @@ pub fn decrypt_cache(data: &[u8], key: &[u8; 32]) -> io::Result> { .get(header_size + payload_len..header_size + payload_len + TAG_SIZE) .ok_or_else(|| bad_data("tag OOB"))?; - let cipher = Aes256Gcm::new(GenericArray::from_slice(key)); - let nonce = Nonce::from_slice(nonce_bytes); + let cipher = Aes256Gcm::new(key.into()); + let auth_tag: &Tag = tag.try_into().map_err(|_e| bad_data("tag size mismatch"))?; let mut plaintext = ciphertext.to_vec(); cipher - .decrypt_in_place_detached(nonce, aad, &mut plaintext, GenericArray::from_slice(tag)) + .decrypt_inout_detached( + nonce_bytes.into(), + aad, + plaintext.as_mut_slice().into(), + auth_tag, + ) .map_err(|_e| bad_data("AES-GCM authentication failed (wrong key or tampered data)"))?; Ok(plaintext) @@ -414,11 +418,10 @@ mod tests { let ciphertext_start = v1_data.len(); v1_data.extend_from_slice(plaintext); - let cipher = Aes256Gcm::new(GenericArray::from_slice(&key_bytes)); - let nonce = Nonce::from_slice(&nonce_bytes); + let cipher = Aes256Gcm::new((&key_bytes).into()); let ct_region = v1_data.get_mut(ciphertext_start..).expect("ct region"); let tag = cipher - .encrypt_in_place_detached(nonce, &aad, ct_region) + .encrypt_inout_detached((&nonce_bytes).into(), &aad, ct_region.into()) .expect("encrypt"); v1_data.extend_from_slice(&tag); diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index c50853ca9..4ab46cae8 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -1,6 +1,12 @@ # cargo-vet audits file +[[audits.aes-gcm]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +delta = "0.10.3 -> 0.11.0" +notes = "Delta audit (cargo vet diff 0.10.3 -> 0.11.0). Single-file crate; src change is only src/lib.rs (rest is docs/tests/Wycheproof vectors). Changes are the RustCrypto 2024-editions migration: aead 0.6 (AeadInPlace -> AeadInOut with InOutBuf), cipher 0.5 + hybrid-array (GenericArray -> Array). Crypto flow is IDENTICAL: init_ctr -> Ctr32BE keystream + GHASH compute_tag; decrypt still verifies the tag via subtle::ConstantTimeEq BEFORE applying the keystream. Length limits corrected to NIST SP 800-38D exactly (P_MAX 2^36 -> 2^36-32 bytes, i.e. tightened; A_MAX 2^36 -> 2^61-1 per spec). New: hazmat feature gates U4/U8 short tags with an SP 800-38D usage warning; manual Debug impl via finish_non_exhaustive leaks no key material. grep confirms ZERO unsafe in 0.11.0 src (crate was previously deny(unsafe_code), still none). No fs/net/process/env surface. Publisher tarcieri (Tony Arcieri, lead RustCrypto maintainer, same publisher as 0.10.3)." + [[audits.anyhow]] who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" criteria = "safe-to-deploy" @@ -13,6 +19,12 @@ criteria = "safe-to-run" delta = "2.2.1 -> 2.2.2" notes = "Delta audit reviewed via cargo vet diff. Replaces the silent exemption bump from PR #166 (which was a supply-chain #[allow] — see docs/architecture/security/supply-chain-posture.md). Changes: (1) #[track_caller] attributes on assert/failure/success paths + replacement of `.unwrap_or_else(AssertError::panic)` chains with explicit `match` blocks so caller location is preserved through panic; (2) edition-2024 idiom updates (Self::Variant / Self {..}); (3) include-list path normalization in Cargo.toml (/build.rs etc.); (4) internal lints.clippy config tightening. No semantic changes to assertion logic, no new unsafe, no new network/FS access paths. assert_cmd is test-only (safe-to-run)." +[[audits.async-trait]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +version = "0.1.89" +notes = "Full audit (1196 LOC proc-macro). Zero real unsafe (all three 'unsafe' mentions are documentation and parsing of the unsafe *keyword token* in trait definitions). Zero capability at expansion time: pure deterministic token transformation desugaring async trait methods to Pin> returns; the crate's own docs state (and the expansion code confirms) no unsafe is emitted into user code. Publisher verified on crates.io: dtolnay." + [[audits.autocfg]] who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" criteria = "safe-to-deploy" @@ -55,6 +67,12 @@ criteria = "safe-to-deploy" delta = "0.9.0 -> 0.9.1" notes = """Delta audit (cargo vet diff 0.9.0 -> 0.9.1, 3 src files). src/lib.rs: CompactString::repeat now computes capacity with self.len().checked_mul(n).expect("capacity overflow") instead of an unchecked multiply — an overflow-safety hardening. src/repr/heap.rs: realloc uses Capacity::new(new_capacity) and adds a regression test (test_realloc_shrink_to_min_heap_gap) covering the shrink-to-MIN_HEAP_SIZE boundary. src/tests.rs additions are test-only. No NEW unsafe blocks (the crate's existing inline-repr unsafe is unchanged), no new deps, no new ambient capability. Publisher ParkMyCar (compact_str maintainer).""" +[[audits.console]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +delta = "0.16.3 -> 0.16.4" +notes = "Delta audit (cargo vet diff 0.16.3 -> 0.16.4, 435-line diff). (1) term.rs: TermInner construction deduplicated into new()/new_buffered()/with_buffer() helpers - pure refactor; new pub fn is_dumb() reads TERM env var (read-only env access, same capability class the crate already had via is_a_color_terminal). (2) utils.rs: Style::from_dotted_str gains is_ascii() guards on '#RRGGBB'/'on_#RRGGBB' parsing - fixes a panic on non-ASCII input that slices mid-UTF-8 char (robustness improvement for untrusted style strings), plus regression tests. (3) windows_term: as_handle() helper removed in favor of out.as_raw_handle() at existing call sites - unchanged unsafe surface (same SetConsoleCursorPosition/SetConsoleCursorInfo calls as before); read_secure backspace arm converted to a match guard, logic identical. No new unsafe blocks, no new fs/net/process capability. Publisher djc (Dirkjan Ochtman, console-rs maintainer, same publisher as 0.16.3)." + [[audits.crypto-common]] who = "Robert Nio " criteria = "safe-to-deploy" @@ -79,6 +97,18 @@ criteria = "safe-to-deploy" version = "0.5.2" notes = "Reviewed v0.5.2 source. Transitive dep of num_cpus. Two files: errno.rs is pure i32 constants (EPERM, ENOENT, ...); lib.rs is #![no_std] FFI declarations for the Hermit unikernel syscall interface (sys_mmap, sys_getpagesize, sys_errno, thread scheduling primitives, ...) plus two unsafe wrapper fns for get/set_priority. No network I/O, no filesystem I/O, no std dependency. On non-Hermit targets the extern C symbols are never linked and the functions are inert — num_cpus only touches hermit-abi when target_os=hermit, which none of our shipping targets hit. Apache-2.0 OR MIT; author Stefan Lankes, Hermit OS project lead." +[[audits.indicatif]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +delta = "0.18.4 -> 0.18.6" +notes = "Delta audit (cargo vet diff 0.18.4 -> 0.18.6, 1547-line diff; src changes limited to draw_target.rs, format.rs, iter.rs, multi.rs, style.rs - rest is examples/tests/CI). All rendering/formatting logic: (1) draw_target.rs switches hidden-detection to console::is_dumb() (pairs with the console 0.16.4 bump) and adds CJK-aware wrapped_metrics (line-height + last-line-width accounting via AnsiCodeIterator + UnicodeWidthChar); (2) format.rs fixes HumanFloatCount negative-sign grouping ('-,100' bug) and precision-0 rounding, uses stable div_duration_f64; (3) iter.rs converts map-and-return closures to Result::inspect - behavior identical; (4) multi.rs replaces is_hidden()/width() forwarders with direct pub(crate) draw_target field access + big doc addition, adds Multi arm to is_stderr(); (5) style.rs moves segment/measure/width helpers verbatim and converts Template::from_str to the FromStr trait. Zero unsafe, zero fs/net/process/env additions. Publisher djc, same as 0.18.4." + +[[audits.itoa]] +who = "Robert M1 <50460704+githubrobbi@users.noreply.github.com>" +criteria = "safe-to-deploy" +version = "1.0.18" +notes = "Full audit (488 LOC, no_std, zero deps). No fs/net/process/env capability; no build.rs. All 13 unsafe sites reviewed: digit-pair table reads are get_unchecked(pair*2+{0,1}) with pair<100 from divmod100 into a 200-byte ASCII table (in-bounds by construction); buffer writes are checked-index MaybeUninit writes covering exactly [offset, len); slice_buffer_to_str's from_utf8_unchecked is sound because only ASCII table bytes and b'0'+digit are ever written; Buffer::format's pointer cast goes from the largest buffer ([MaybeUninit; i128 MAX_STR_LEN]) to a smaller same-alignment array, and the unreachable_unchecked is an optimizer hint upheld because write() slices within the buffer. Publisher verified on crates.io: dtolnay." + [[audits.libmimalloc-sys]] who = "Robert Nio " criteria = "safe-to-deploy" @@ -295,6 +325,12 @@ criteria = "safe-to-deploy" delta = "0.39.2 -> 0.39.4" notes = "Delta audit (cargo vet diff 0.39.2 -> 0.39.4, 3 files +35/-10). Cargo.toml/Cargo.toml.orig: version bump only. src/parser/dtd.rs is the only source change — three robustness fixes to the DTD internal-subset parser, all panic-prevention: (a) when 9+ bytes already accumulated in UndecidedMarkup state without matching one of