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
21 changes: 17 additions & 4 deletions libs/cua-driver/rust/crates/pip-preview/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@
use std::sync::OnceLock;

/// Canonical `~/.cua-driver/config.json` path matching what the per-platform
/// `set_config` tools write to. Returns `None` when `$HOME` is unset
/// (sandboxed CI).
/// `set_config` tools write to. Resolves `$HOME` first (Unix/macOS) and falls
/// back to `%USERPROFILE%` (Windows, where `HOME` is usually unset). Returns
/// `None` when neither is set (sandboxed CI).
pub fn default_config_path() -> Option<std::path::PathBuf> {
std::env::var("HOME")
.ok()
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(|h| std::path::PathBuf::from(h).join(".cua-driver").join("config.json"))
Comment on lines 23 to 26

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Treat empty HOME / USERPROFILE as unset.

Line 24 and Line 25 use var_os directly, so HOME="" or USERPROFILE="" becomes .cua-driver/config.json relative to the current working directory instead of returning None. That breaks the documented contract here and can persist config to the wrong location.

Suggested fix
 pub fn default_config_path() -> Option<std::path::PathBuf> {
     std::env::var_os("HOME")
         .or_else(|| std::env::var_os("USERPROFILE"))
+        .filter(|h| !h.is_empty())
         .map(|h| std::path::PathBuf::from(h).join(".cua-driver").join("config.json"))
 }
📝 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
pub fn default_config_path() -> Option<std::path::PathBuf> {
std::env::var("HOME")
.ok()
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(|h| std::path::PathBuf::from(h).join(".cua-driver").join("config.json"))
pub fn default_config_path() -> Option<std::path::PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.filter(|h| !h.is_empty())
.map(|h| std::path::PathBuf::from(h).join(".cua-driver").join("config.json"))
}
🤖 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/rust/crates/pip-preview/src/lib.rs` around lines 23 - 26, The
default_config_path function is treating empty HOME or USERPROFILE values as
valid, which can produce a relative .cua-driver/config.json path instead of
None. Update default_config_path to check the environment variables for both
presence and non-empty content before building the PathBuf, and return None when
either HOME or USERPROFILE is unset or empty so the function’s contract is
preserved.

}

/// Read a single key from `~/.cua-driver/config.json` as a raw JSON value,
/// returning `None` when the file is missing/malformed or the key is absent.
/// Used by the per-platform `load_driver_config` helpers to rehydrate the
/// in-memory `DriverConfig` at process startup so `set_config` writes survive
/// across stateless `cua-driver call` invocations.
pub fn read_config_value(key: &str) -> Option<serde_json::Value> {
let path = default_config_path()?;
let text = std::fs::read_to_string(&path).ok()?;
let json: serde_json::Value = serde_json::from_str(&text).ok()?;
json.get(key).cloned()
}

/// Merge a single `key`/`value` into `~/.cua-driver/config.json`,
/// preserving any other keys that are already there. Used by the
/// per-platform `set_config` tools to persist `experimental_pip` /
Expand Down
40 changes: 37 additions & 3 deletions libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ impl Default for DriverConfig {
fn default() -> Self { Self { capture_mode: "som".into(), max_image_dimension: 1568 } }
}

/// Load `DriverConfig` from `~/.cua-driver/config.json`, falling back to
/// defaults for any missing/malformed keys. Called once at `ToolState`
/// construction (i.e. on every fresh `cua-driver call` process) so that a
/// prior `set_config capture_mode=vision` survives across stateless one-shot
/// invocations — matching the macOS daemon's startup load. See #2008.
pub fn load_driver_config() -> DriverConfig {
let mut cfg = DriverConfig::default();
if let Some(v) = pip_preview::read_config_value("capture_mode").and_then(|v| v.as_str().map(str::to_owned)) {
cfg.capture_mode = v;
}
if let Some(v) = pip_preview::read_config_value("max_image_dimension").and_then(|v| v.as_u64()) {
if let Ok(v32) = u32::try_from(v) { cfg.max_image_dimension = v32; }
}
cfg
}

pub struct ResizeRegistry {
ratios: std::sync::Mutex<std::collections::HashMap<u32, f64>>,
}
Expand Down Expand Up @@ -91,7 +107,7 @@ impl ToolState {
resize_registry: Arc::new(ResizeRegistry::new()),
zoom_registry: Arc::new(ZoomRegistry::new()),
mouse_hold: std::sync::Mutex::new(Default::default()),
config: Arc::new(RwLock::new(DriverConfig::default())),
config: Arc::new(RwLock::new(load_driver_config())),
})
}
}
Expand Down Expand Up @@ -3410,11 +3426,23 @@ impl Tool for SetConfigTool {
) {
match key {
"capture_mode" => match val.as_str() {
Some(s) => { cfg.capture_mode = s.to_owned(); parts.push(format!("capture_mode={s}")); }
Some(s) => {
cfg.capture_mode = s.to_owned();
if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(s.to_owned())) {
tracing::warn!("set_config: failed to persist capture_mode: {e}");
}
parts.push(format!("capture_mode={s}"));
}
None => return ToolResult::error(format!("`capture_mode` must be a string, got {val}.")),
},
"max_image_dimension" => match val.as_u64() {
Some(n) => { cfg.max_image_dimension = n as u32; parts.push(format!("max_image_dimension={n}")); }
Some(n) => {
cfg.max_image_dimension = n as u32;
if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(n)) {
tracing::warn!("set_config: failed to persist max_image_dimension: {e}");
}
parts.push(format!("max_image_dimension={n}"));
}
Comment on lines +3429 to +3445

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.

🗄️ Data Integrity & Integration | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# capture_mode consumers / validation in cua-driver
rg -nP --type=rust -C3 'capture_mode' libs/cua-driver/rust/crates | rg -nP -C3 'match|enum|som|vision|ax|parse|validate'

Repository: trycua/cua

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs'

# Map the relevant symbols first, then inspect the smallest useful ranges.
ast-grep outline "$FILE" --view expanded | sed -n '1,220p'

printf '\n--- load_driver_config / set_config ranges ---\n'
rg -n -C 4 'fn load_driver_config|capture_mode|max_image_dimension|set_config|DriverConfig' "$FILE"

Repository: trycua/cua

Length of output: 21516


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs'

# Show the relevant code around config load and set_config.
sed -n '150,220p' "$FILE"
printf '\n---\n'
sed -n '5000,5065p' "$FILE"

Repository: trycua/cua

Length of output: 3675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs'

python3 - <<'PY'
from pathlib import Path
file = Path("libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs")
text = file.read_text()
for needle in [
    "pub fn load_driver_config()",
    "\"capture_mode\" => match val.as_str()",
    "\"max_image_dimension\" => match val.as_u64()",
]:
    idx = text.find(needle)
    print(f"== {needle} == {idx}")
    if idx != -1:
        start = max(0, text.rfind("\n", 0, idx-400))
        end = text.find("\n", idx+900)
        print(text[start:end])
        print("\n---")
PY

Repository: trycua/cua

Length of output: 4447


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs'

echo '--- load_driver_config ---'
sed -n '169,190p' "$FILE"

echo
echo '--- set_config capture_mode / max_image_dimension ---'
sed -n '5030,5058p' "$FILE"

echo
echo '--- capture_mode consumers ---'
rg -n -C 2 'do_tree|do_shot|capture_mode !=|capture_mode ==' "$FILE"

Repository: trycua/cua

Length of output: 2424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs'

# Narrowly inspect the exact file under review.
rg -n -C 3 'pub fn load_driver_config|capture_mode|max_image_dimension|set_config|do_tree|do_shot' "$FILE"

Repository: trycua/cua

Length of output: 10954


Validate config values before persisting.

  • capture_mode is accepted as any string on the {key, value} path and reloaded unchanged. Unknown values don’t fail startup, but they are persisted and later behave like som because only "vision" and "ax" are special-cased.
  • max_image_dimension truncates u64 -> u32 in memory, while the full u64 is written to disk. Values above u32::MAX come back as the default on reload; reject overflow or persist the same u32 value.
🤖 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/rust/crates/platform-linux/src/tools/impl_.rs` around lines
3429 - 3445, Validate the config inputs in the set_config path before writing
them out: in the branches handling capture_mode and max_image_dimension, reject
unsupported or overflowing values instead of persisting them unchanged. For
capture_mode, only accept the known string values already recognized by the
config loader and return a ToolResult::error for anything else so unknown modes
cannot be saved. For max_image_dimension, check that the incoming u64 fits in
u32 before assigning to cfg.max_image_dimension and before calling
pip_preview::write_config_key, and persist the same u32 value that is stored in
memory.

None => return ToolResult::error(format!("`max_image_dimension` must be an integer, got {val}.")),
},
"experimental_pip" => match val.as_bool() {
Expand Down Expand Up @@ -3447,11 +3475,17 @@ impl Tool for SetConfigTool {
}
// Legacy per-field shape.
if let Some(mode) = args.opt_str("capture_mode") {
if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(mode.clone())) {
tracing::warn!("set_config: failed to persist capture_mode: {e}");
}
parts.push(format!("capture_mode={mode}"));
cfg.capture_mode = mode;
}
if let Some(dim) = args.opt_u64("max_image_dimension") {
cfg.max_image_dimension = dim as u32;
if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(dim)) {
tracing::warn!("set_config: failed to persist max_image_dimension: {e}");
}
parts.push(format!("max_image_dimension={dim}"));
}
if let Some(enabled) = args.get("experimental_pip").and_then(|v| v.as_bool()) {
Expand Down
46 changes: 41 additions & 5 deletions libs/cua-driver/rust/crates/platform-windows/src/tools/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ impl Default for DriverConfig {
fn default() -> Self { Self { capture_mode: "som".into(), max_image_dimension: 1568 } }
}

/// Load `DriverConfig` from `~/.cua-driver/config.json`, falling back to
/// defaults for any missing/malformed keys. Called once at `ToolState`
/// construction (i.e. on every fresh `cua-driver call` process) so that a
/// prior `set_config capture_mode=vision` survives across stateless one-shot
/// invocations — matching the macOS daemon's startup load. See #2008.
pub fn load_driver_config() -> DriverConfig {
let mut cfg = DriverConfig::default();
if let Some(v) = pip_preview::read_config_value("capture_mode").and_then(|v| v.as_str().map(str::to_owned)) {
cfg.capture_mode = v;
}
if let Some(v) = pip_preview::read_config_value("max_image_dimension").and_then(|v| v.as_u64()) {
if let Ok(v32) = u32::try_from(v) { cfg.max_image_dimension = v32; }
}
cfg
}

pub struct ResizeRegistry {
ratios: std::sync::Mutex<std::collections::HashMap<u32, f64>>,
}
Expand Down Expand Up @@ -219,7 +235,7 @@ impl ToolState {
cursor_registry: Arc::new(CursorRegistry::new()),
resize_registry: Arc::new(ResizeRegistry::new()),
zoom_registry: Arc::new(ZoomRegistry::new()),
config: Arc::new(RwLock::new(DriverConfig::default())),
config: Arc::new(RwLock::new(load_driver_config())),
})
}
}
Expand Down Expand Up @@ -5014,11 +5030,23 @@ impl Tool for SetConfigTool {
) {
match key {
"capture_mode" => match val.as_str() {
Some(s) => { cfg.capture_mode = s.to_owned(); applied = true; }
Some(s) => {
cfg.capture_mode = s.to_owned();
if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(s.to_owned())) {
tracing::warn!("set_config: failed to persist capture_mode: {e}");
}
applied = true;
}
None => return ToolResult::error(format!("`capture_mode` must be a string, got {val}.")),
},
"max_image_dimension" => match val.as_u64() {
Some(n) => { cfg.max_image_dimension = n as u32; applied = true; }
Some(n) => {
cfg.max_image_dimension = n as u32;
if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(n)) {
tracing::warn!("set_config: failed to persist max_image_dimension: {e}");
}
applied = true;
}
None => return ToolResult::error(format!("`max_image_dimension` must be an integer, got {val}.")),
},
"experimental_pip" => match val.as_bool() {
Expand Down Expand Up @@ -5051,10 +5079,18 @@ impl Tool for SetConfigTool {
}
// Legacy per-field shape.
if let Some(mode) = args.get("capture_mode").and_then(|v| v.as_str()) {
cfg.capture_mode = mode.to_owned(); applied = true;
cfg.capture_mode = mode.to_owned();
if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(mode.to_owned())) {
tracing::warn!("set_config: failed to persist capture_mode: {e}");
}
applied = true;
}
if let Some(dim) = args.get("max_image_dimension").and_then(|v| v.as_u64()) {
cfg.max_image_dimension = dim as u32; applied = true;
cfg.max_image_dimension = dim as u32;
if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(dim)) {
tracing::warn!("set_config: failed to persist max_image_dimension: {e}");
}
applied = true;
}
if let Some(enabled) = args.get("experimental_pip").and_then(|v| v.as_bool()) {
if let Err(e) = pip_preview::write_config_key("experimental_pip", Value::Bool(enabled)) {
Expand Down
Loading