diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b3f99e1..166c68b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,11 @@ "permissions": { "allow": [ "Bash(chmod:*)", - "Bash(cargo build:*)" + "Bash(cargo build:*)", + "Bash(rustc:*)", + "Bash(cargo check:*)", + "Bash(git push:*)", + "Bash(rm:*)" ], "deny": [] } diff --git a/Cargo.toml b/Cargo.toml index f4936c4..c093723 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ regex = "1.10" once_cell = "1.20" # Command line argument parsing -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.5", features = ["derive", "env"] } # Optional external tool support headless_chrome = { version = "1.0", optional = true } @@ -96,6 +96,7 @@ embedded-fonts = [] pure-rust-pdf = [] external-tools = ["headless_chrome", "wkhtmltopdf"] full = ["embedded-fonts", "pure-rust-pdf", "external-tools", "tera"] +build-bin = [] [build-dependencies] anyhow = "1.0" @@ -103,6 +104,11 @@ anyhow = "1.0" [[bin]] name = "docx-mcp" path = "src/main.rs" +required-features = ["build-bin"] + +[lib] +name = "docx_mcp" +path = "src/lib.rs" [dev-dependencies] # Testing framework diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..262e327 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod security; + +pub use security::{Args, SecurityConfig, SecurityMiddleware, SecurityError}; diff --git a/src/security.rs b/src/security.rs index 582d9d9..231c59b 100644 --- a/src/security.rs +++ b/src/security.rs @@ -361,30 +361,30 @@ impl SecurityConfig { /// Get a summary of current security settings pub fn get_summary(&self) -> String { - let mut summary = Vec::new(); + let mut summary: Vec = Vec::new(); if self.readonly_mode { - summary.push("📖 READONLY MODE"); + summary.push("📖 READONLY MODE".to_string()); } if self.sandbox_mode { - summary.push("🔒 SANDBOX MODE"); + summary.push("🔒 SANDBOX MODE".to_string()); } if let Some(ref whitelist) = self.command_whitelist { - summary.push(&format!("✅ Whitelist: {} commands", whitelist.len())); + summary.push(format!("✅ Whitelist: {} commands", whitelist.len())); } if let Some(ref blacklist) = self.command_blacklist { - summary.push(&format!("🚫 Blacklist: {} commands", blacklist.len())); + summary.push(format!("🚫 Blacklist: {} commands", blacklist.len())); } if !self.allow_external_tools { - summary.push("🔧 No external tools"); + summary.push("🔧 No external tools".to_string()); } if !self.allow_network { - summary.push("🌐 No network access"); + summary.push("🌐 No network access".to_string()); } if summary.is_empty() { diff --git a/tests/args_tests.rs b/tests/args_tests.rs new file mode 100644 index 0000000..17d5061 --- /dev/null +++ b/tests/args_tests.rs @@ -0,0 +1,100 @@ +use docx_mcp::security::{Args, SecurityConfig}; +use clap::Parser; +use std::env; + +fn reset_env() { + for (k, _) in env::vars() { + if k.starts_with("DOCX_MCP_") { + env::remove_var(k); + } + } +} + +#[test] +fn parses_flags_and_lists() { + reset_env(); + + let argv = [ + "docx-mcp", + "--readonly", + "--sandbox", + "--no-external-tools", + "--no-network", + "--whitelist", + "open_document,extract_text,get_metadata", + "--blacklist", + "save_document,add_paragraph", + "--max-size", + "1048576", + "--max-docs", + "10", + ]; + + let args = Args::parse_from(&argv); + assert!(args.readonly); + assert!(args.sandbox); + assert!(args.no_external_tools); + assert!(args.no_network); + assert_eq!(args.max_size, Some(1_048_576)); + assert_eq!(args.max_docs, Some(10)); + + let wl = args.whitelist.clone().unwrap(); + assert_eq!(wl, vec![ + "open_document".to_string(), + "extract_text".to_string(), + "get_metadata".to_string(), + ]); + + let bl = args.blacklist.clone().unwrap(); + assert_eq!(bl, vec![ + "save_document".to_string(), + "add_paragraph".to_string(), + ]); + + let cfg = SecurityConfig::from_args(args); + assert!(cfg.readonly_mode); + assert!(cfg.sandbox_mode); + assert!(!cfg.allow_external_tools); + assert!(!cfg.allow_network); + assert_eq!(cfg.max_document_size, 1_048_576); + assert_eq!(cfg.max_open_documents, 10); + + let wlset = cfg.command_whitelist.unwrap(); + assert!(wlset.contains("open_document")); + assert!(wlset.contains("extract_text")); + assert!(wlset.contains("get_metadata")); + + let blset = cfg.command_blacklist.unwrap(); + assert!(blset.contains("save_document")); + assert!(blset.contains("add_paragraph")); +} + +#[test] +fn parses_from_environment() { + reset_env(); + + env::set_var("DOCX_MCP_READONLY", "true"); + env::set_var("DOCX_MCP_SANDBOX", "true"); + env::set_var("DOCX_MCP_NO_EXTERNAL_TOOLS", "true"); + env::set_var("DOCX_MCP_NO_NETWORK", "true"); + env::set_var("DOCX_MCP_WHITELIST", "open_document,extract_text"); + env::set_var("DOCX_MCP_BLACKLIST", "save_document"); + env::set_var("DOCX_MCP_MAX_SIZE", "2048"); + env::set_var("DOCX_MCP_MAX_DOCS", "7"); + + let cfg = SecurityConfig::from_env(); + + assert!(cfg.readonly_mode); + assert!(cfg.sandbox_mode); + assert!(!cfg.allow_external_tools); + assert!(!cfg.allow_network); + assert_eq!(cfg.max_document_size, 2048); + assert_eq!(cfg.max_open_documents, 7); + + let wl = cfg.command_whitelist.unwrap(); + assert!(wl.contains("open_document")); + assert!(wl.contains("extract_text")); + + let bl = cfg.command_blacklist.unwrap(); + assert!(bl.contains("save_document")); +}