Skip to content
65 changes: 46 additions & 19 deletions src/cortex-cli/src/utils/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ pub fn get_cortex_home() -> PathBuf {
/// // Returns: /home/user/documents/file.txt
/// ```
pub fn expand_tilde(path: &str) -> String {
if path.starts_with("~/")
&& let Some(home) = dirs::home_dir()
{
return home.join(&path[2..]).to_string_lossy().to_string();
if path == "~" {
// Handle bare "~" - return home directory
if let Some(home) = dirs::home_dir() {
return home.to_string_lossy().to_string();
}
} else if path.starts_with("~/") {
// Handle "~/" prefix - expand to home directory + rest of path
if let Some(home) = dirs::home_dir() {
return home.join(&path[2..]).to_string_lossy().to_string();
}
}
path.to_string()
}
Expand All @@ -58,8 +64,12 @@ pub fn expand_tilde(path: &str) -> String {
pub fn validate_path_safety(path: &Path, base_dir: Option<&Path>) -> Result<(), String> {
let path_str = path.to_string_lossy();

// Check for path traversal attempts
if path_str.contains("..") {
// Check for path traversal attempts by examining path components
// This correctly handles filenames containing ".." like "file..txt"
if path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err("Path contains traversal sequence '..'".to_string());
}

Expand Down Expand Up @@ -257,8 +267,15 @@ mod tests {

#[test]
fn test_expand_tilde_with_tilde_only() {
// Test tilde alone - should remain unchanged (not "~/")
assert_eq!(expand_tilde("~"), "~");
// Test bare "~" - should expand to home directory
let result = expand_tilde("~");
if let Some(home) = dirs::home_dir() {
let expected = home.to_string_lossy().to_string();
assert_eq!(result, expected);
} else {
// If no home dir, original is returned
assert_eq!(result, "~");
}
}

#[test]
Expand Down Expand Up @@ -320,20 +337,30 @@ mod tests {

#[test]
fn test_validate_path_safety_detects_various_traversal_patterns() {
// Different traversal patterns
let patterns = ["foo/../bar", "...", "foo/bar/../baz", "./foo/../../../etc"];
// Patterns that ARE path traversal (contain ".." as a component)
let traversal_patterns = ["foo/../bar", "foo/bar/../baz", "./foo/../../../etc", ".."];

for pattern in patterns {
for pattern in traversal_patterns {
let path = Path::new(pattern);
let result = validate_path_safety(path, None);
// Only patterns containing ".." should fail
if pattern.contains("..") {
assert!(
result.is_err(),
"Expected traversal detection for: {}",
pattern
);
}
assert!(
result.is_err(),
"Expected traversal detection for: {}",
pattern
);
}

// Patterns that are NOT path traversal (contain ".." in filenames only)
let safe_patterns = ["file..txt", "..hidden", "test...file", "foo/bar..baz/file"];

for pattern in safe_patterns {
let path = Path::new(pattern);
let result = validate_path_safety(path, None);
assert!(
result.is_ok(),
"False positive: '{}' should not be detected as traversal",
pattern
);
}
}

Expand Down
20 changes: 17 additions & 3 deletions src/cortex-compact/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! Compaction configuration.

use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};

/// Configuration for auto-compaction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionConfig {
/// Whether auto-compaction is enabled.
#[serde(default = "default_true")]
pub enabled: bool,
/// Token threshold to trigger compaction (percentage of max).
#[serde(default = "default_threshold")]
/// Token threshold to trigger compaction (ratio 0.0-1.0 of max context).
#[serde(default = "default_threshold", deserialize_with = "deserialize_threshold_percent")]
pub threshold_percent: f32,
/// Minimum tokens to keep after compaction.
#[serde(default = "default_min_tokens")]
Expand All @@ -25,6 +25,20 @@ pub struct CompactionConfig {
pub preserve_recent_turns: usize,
}

/// Deserialize threshold_percent with validation (must be 0.0-1.0).
fn deserialize_threshold_percent<'de, D>(deserializer: D) -> Result<f32, D::Error>
where
D: Deserializer<'de>,
{
let value = f32::deserialize(deserializer)?;
if !(0.0..=1.0).contains(&value) {
return Err(serde::de::Error::custom(
"threshold_percent must be between 0.0 and 1.0",
));
}
Ok(value)
}

fn default_true() -> bool {
true
}
Expand Down
4 changes: 2 additions & 2 deletions src/cortex-engine/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ pub struct Config {
/// Provider configuration.
pub model_provider: ModelProviderInfo,
/// Context window size.
pub model_context_window: Option<i64>,
pub model_context_window: Option<u64>,
/// Auto-compact token limit.
pub model_auto_compact_token_limit: Option<i64>,
pub model_auto_compact_token_limit: Option<u64>,
/// Approval policy.
pub approval_policy: AskForApproval,
/// Sandbox policy.
Expand Down
4 changes: 2 additions & 2 deletions src/cortex-engine/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ pub struct PermissionConfig {
pub struct ConfigToml {
pub model: Option<String>,
pub model_provider: Option<String>,
pub model_context_window: Option<i64>,
pub model_auto_compact_token_limit: Option<i64>,
pub model_context_window: Option<u64>,
pub model_auto_compact_token_limit: Option<u64>,
pub approval_policy: Option<AskForApproval>,
pub sandbox_mode: Option<SandboxMode>,
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
Expand Down
5 changes: 5 additions & 0 deletions src/cortex-engine/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod artifacts;
pub mod context;
pub mod handlers;
pub mod registry;
pub mod response_store;
pub mod router;
pub mod spec;
pub mod unified_executor;
Expand All @@ -45,6 +46,10 @@ pub use artifacts::{
pub use context::ToolContext;
pub use handlers::*;
pub use registry::{PluginTool, ToolRegistry};
pub use response_store::{
CLEANUP_INTERVAL, DEFAULT_TTL, MAX_STORE_SIZE, StoreInfo, StoreStats, StoredResponse,
ToolResponseStore, ToolResponseStoreConfig, create_shared_store, create_shared_store_with_config,
};
pub use router::ToolRouter;
pub use spec::{ToolCall, ToolDefinition, ToolHandler, ToolResult};
pub use unified_executor::{ExecutorConfig, UnifiedToolExecutor};
Loading
Loading