Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,58 @@ impl Default for DriverConfig {
}
}

/// Path to the persistent JSON config file shared by the CLI and MCP session.
pub fn config_file_path() -> std::path::PathBuf {
let home = std::env::var("HOME").unwrap_or_default();
std::path::PathBuf::from(format!("{home}/.cua-driver/config.json"))
}

/// Load `DriverConfig` from `~/.cua-driver/config.json`, falling back to
/// defaults for any missing or unrecognised keys. Called at MCP startup so
/// that `cua-driver config set capture_mode vision` (CLI) carries over into
/// the next MCP session without requiring a per-call `set_config`.
pub fn load_driver_config() -> DriverConfig {
let mut cfg = DriverConfig::default();
let path = config_file_path();
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return cfg, // no file yet — use defaults
};
let json: serde_json::Value = match serde_json::from_str(&text) {
Ok(v) => v,
Err(_) => return cfg, // malformed file — use defaults
};
if let Some(v) = json.get("capture_mode").and_then(|v| v.as_str()) {
cfg.capture_mode = v.to_owned();
}
if let Some(v) = json.get("max_image_dimension").and_then(|v| v.as_u64()) {
if let Ok(v32) = u32::try_from(v) {
cfg.max_image_dimension = v32;
}
}
Comment on lines +147 to +151

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all potentially truncating conversions around config dimension handling.
rg -nP 'max_image_dimension|as_u64\(\)|as u32|u32::try_from' libs/cua-driver-rs/crates/platform-macos/src/tools

Repository: trycua/cua

Length of output: 8153


Add bounds check on max_image_dimension cast to prevent silent truncation.

At lines 147–149, the cast from u64 to u32 can silently truncate large values without validation. Use a fallible conversion to ensure the loaded config value fits within the valid range.

Suggested fix
    if let Some(v) = json.get("max_image_dimension").and_then(|v| v.as_u64()) {
-       cfg.max_image_dimension = v as u32;
+       if let Ok(v32) = u32::try_from(v) {
+           cfg.max_image_dimension = v32;
+       }
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(v) = json.get("max_image_dimension").and_then(|v| v.as_u64()) {
cfg.max_image_dimension = v as u32;
}
if let Some(v) = json.get("max_image_dimension").and_then(|v| v.as_u64()) {
if let Ok(v32) = u32::try_from(v) {
cfg.max_image_dimension = v32;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs` around lines 147 -
149, The code reads max_image_dimension from json and casts a u64 to u32 which
can silently truncate; change the conversion to a fallible check (e.g. use
u32::try_from or TryFrom::try_from on the value returned by
json.get("max_image_dimension").and_then(|v| v.as_u64())) and handle the Err
case instead of blindly casting: if conversion succeeds set
cfg.max_image_dimension, otherwise return or propagate a config parse error (or
log and skip) so oversized values are detected rather than truncated.

cfg
}

/// Persist a single key/value pair to `~/.cua-driver/config.json`.
/// Merges with any existing file contents so other keys are preserved.
/// Returns `Err` if the directory cannot be created or the file cannot be written.
pub fn write_driver_config_key(key: &str, value: &serde_json::Value) -> Result<(), String> {
let path = config_file_path();
let mut json: serde_json::Value = path
.exists()
.then(|| std::fs::read_to_string(&path).ok())
.flatten()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_else(|| serde_json::json!({}));
json[key] = value.clone();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let body = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
std::fs::write(&path, body).map_err(|e| e.to_string())?;
Ok(())
}

/// Shared state passed to all tools.
pub struct ToolState {
pub element_cache: Arc<ElementCache>,
Expand All @@ -136,7 +188,9 @@ impl Default for ToolState {
cursor_registry: Arc::new(CursorRegistry::new()),
zoom_registry: Arc::new(ZoomRegistry::new()),
resize_registry: Arc::new(ResizeRegistry::new()),
config: Arc::new(std::sync::RwLock::new(DriverConfig::default())),
// Load persisted config from ~/.cua-driver/config.json so that
// `cua-driver config set` changes carry over into MCP sessions.
config: Arc::new(std::sync::RwLock::new(load_driver_config())),
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions libs/cua-driver-rs/crates/platform-macos/src/tools/set_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use mcp_server::{protocol::ToolResult, tool::{Tool, ToolDef}};
use serde_json::Value;
use std::sync::Arc;

use super::ToolState;
use super::{write_driver_config_key, ToolState};

pub struct SetConfigTool {
state: Arc<ToolState>,
Expand Down Expand Up @@ -49,9 +49,19 @@ impl Tool for SetConfigTool {
let mut cfg = self.state.config.write().unwrap();
if let Some(mode) = args.get("capture_mode").and_then(|v| v.as_str()) {
cfg.capture_mode = mode.to_owned();
if let Err(e) = write_driver_config_key("capture_mode", &Value::String(mode.to_owned())) {
tracing::warn!("set_config: failed to persist capture_mode: {e}");
}
}
if let Some(dim) = args.get("max_image_dimension").and_then(|v| v.as_u64()) {
cfg.max_image_dimension = dim as u32;
if let Ok(dim32) = u32::try_from(dim) {
cfg.max_image_dimension = dim32;
if let Err(e) = write_driver_config_key("max_image_dimension", &Value::Number(dim.into())) {
tracing::warn!("set_config: failed to persist max_image_dimension: {e}");
}
} else {
return ToolResult::error(format!("max_image_dimension {dim} exceeds u32::MAX"));
}
}
ToolResult::text(format!(
"Config updated: capture_mode={}, max_image_dimension={}",
Expand Down
15 changes: 13 additions & 2 deletions libs/cua-driver-rs/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,19 @@ VERSION="${TAG#${TAG_PREFIX}}"

# Prefer the bare-binary tarball (single `cua-driver` file at the root) —
# the directory tarball would require unpacking and copying, but the bare
# form is curl-pipe-able. Both forms are published per release.
TARBALL="cua-driver-rs-${VERSION}-${LABEL}-binary.tar.gz"
# form is curl-pipe-able.
#
# macOS: the release workflow publishes one universal binary (arm64 + x86_64
# lipo'd together) named `darwin-universal-binary`. There are NO per-arch
# bare-binary tarballs for macOS — only the directory tarballs are split
# by arch (darwin-arm64 / darwin-x86_64). The universal binary works on
# both Apple Silicon and Intel, so we always fetch it on macOS.
#
# Linux: per-arch bare-binary tarballs exist (e.g. linux-x86_64-binary).
case "$LABEL" in
darwin-*) TARBALL="cua-driver-rs-${VERSION}-darwin-universal-binary.tar.gz" ;;
*) TARBALL="cua-driver-rs-${VERSION}-${LABEL}-binary.tar.gz" ;;
esac
URL="https://github.com/$REPO/releases/download/$TAG/$TARBALL"

log "downloading $URL"
Expand Down
Loading