chore(clippy): clear warning backlog and harden conversions (#383)

This commit is contained in:
Chummy 2026-02-17 00:32:33 +08:00 committed by GitHub
parent a91516df7a
commit 3234159c6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 77 additions and 69 deletions

View file

@ -1247,18 +1247,16 @@ Done."#;
// Recovery Tests - Constants Validation // Recovery Tests - Constants Validation
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
#[test] const _: () = {
fn max_tool_iterations_is_reasonable() {
// Recovery: MAX_TOOL_ITERATIONS should be set to prevent runaway loops
assert!(MAX_TOOL_ITERATIONS > 0); assert!(MAX_TOOL_ITERATIONS > 0);
assert!(MAX_TOOL_ITERATIONS <= 100); assert!(MAX_TOOL_ITERATIONS <= 100);
}
#[test]
fn max_history_messages_is_reasonable() {
// Recovery: MAX_HISTORY_MESSAGES should be set to prevent memory bloat
assert!(MAX_HISTORY_MESSAGES > 0); assert!(MAX_HISTORY_MESSAGES > 0);
assert!(MAX_HISTORY_MESSAGES <= 1000); assert!(MAX_HISTORY_MESSAGES <= 1000);
};
#[test]
fn constants_bounds_are_compile_time_checked() {
// Bounds are enforced by the const assertions above.
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════

View file

@ -1199,7 +1199,7 @@ pub struct LarkConfig {
// ── Security Config ───────────────────────────────────────────────── // ── Security Config ─────────────────────────────────────────────────
/// Security configuration for sandboxing, resource limits, and audit logging /// Security configuration for sandboxing, resource limits, and audit logging
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SecurityConfig { pub struct SecurityConfig {
/// Sandbox configuration /// Sandbox configuration
#[serde(default)] #[serde(default)]
@ -1214,16 +1214,6 @@ pub struct SecurityConfig {
pub audit: AuditConfig, pub audit: AuditConfig,
} }
impl Default for SecurityConfig {
fn default() -> Self {
Self {
sandbox: SandboxConfig::default(),
resources: ResourceLimitsConfig::default(),
audit: AuditConfig::default(),
}
}
}
/// Sandbox configuration for OS-level isolation /// Sandbox configuration for OS-level isolation
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig { pub struct SandboxConfig {
@ -1251,10 +1241,11 @@ impl Default for SandboxConfig {
} }
/// Sandbox backend selection /// Sandbox backend selection
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum SandboxBackend { pub enum SandboxBackend {
/// Auto-detect best available (default) /// Auto-detect best available (default)
#[default]
Auto, Auto,
/// Landlock (Linux kernel LSM, native) /// Landlock (Linux kernel LSM, native)
Landlock, Landlock,
@ -1268,12 +1259,6 @@ pub enum SandboxBackend {
None, None,
} }
impl Default for SandboxBackend {
fn default() -> Self {
Self::Auto
}
}
/// Resource limits for command execution /// Resource limits for command execution
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimitsConfig { pub struct ResourceLimitsConfig {

View file

@ -20,7 +20,7 @@ use std::path::{Path, PathBuf};
// ── Hardware transport enum ────────────────────────────────────── // ── Hardware transport enum ──────────────────────────────────────
/// Transport protocol used to communicate with physical hardware. /// Transport protocol used to communicate with physical hardware.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum HardwareTransport { pub enum HardwareTransport {
/// Direct GPIO access on a Linux SBC (Raspberry Pi, Orange Pi, etc.) /// Direct GPIO access on a Linux SBC (Raspberry Pi, Orange Pi, etc.)
@ -30,15 +30,10 @@ pub enum HardwareTransport {
/// SWD/JTAG debug probe (probe-rs) for bare-metal MCUs /// SWD/JTAG debug probe (probe-rs) for bare-metal MCUs
Probe, Probe,
/// No hardware — software-only mode /// No hardware — software-only mode
#[default]
None, None,
} }
impl Default for HardwareTransport {
fn default() -> Self {
Self::None
}
}
impl std::fmt::Display for HardwareTransport { impl std::fmt::Display for HardwareTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -869,7 +864,9 @@ mod tests {
#[test] #[test]
fn validate_baud_rate_common_values_ok() { fn validate_baud_rate_common_values_ok() {
for baud in [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600] { for baud in [
9600, 19200, 38400, 57600, 115_200, 230_400, 460_800, 921_600,
] {
let cfg = HardwareConfig { let cfg = HardwareConfig {
enabled: true, enabled: true,
transport: "serial".into(), transport: "serial".into(),
@ -938,7 +935,7 @@ mod tests {
enabled: true, enabled: true,
transport: "probe".into(), transport: "probe".into(),
serial_port: None, serial_port: None,
baud_rate: 115200, baud_rate: 115_200,
workspace_datasheets: false, workspace_datasheets: false,
discovered_board: None, discovered_board: None,
probe_target: Some("nRF52840_xxAA".into()), probe_target: Some("nRF52840_xxAA".into()),

View file

@ -183,7 +183,9 @@ impl Observer for OtelObserver {
], ],
); );
} }
ObserverEvent::LlmRequest { .. } => {} ObserverEvent::LlmRequest { .. }
| ObserverEvent::ToolCallStart { .. }
| ObserverEvent::TurnComplete => {}
ObserverEvent::LlmResponse { ObserverEvent::LlmResponse {
provider, provider,
model, model,
@ -247,7 +249,6 @@ impl Observer for OtelObserver {
// Note: tokens are recorded via record_metric(TokensUsed) to avoid // Note: tokens are recorded via record_metric(TokensUsed) to avoid
// double-counting. AgentEnd only records duration. // double-counting. AgentEnd only records duration.
} }
ObserverEvent::ToolCallStart { .. } => {}
ObserverEvent::ToolCall { ObserverEvent::ToolCall {
tool, tool,
duration, duration,
@ -285,7 +286,6 @@ impl Observer for OtelObserver {
self.tool_duration self.tool_duration
.record(secs, &[KeyValue::new("tool", tool.clone())]); .record(secs, &[KeyValue::new("tool", tool.clone())]);
} }
ObserverEvent::TurnComplete => {}
ObserverEvent::ChannelMessage { channel, direction } => { ObserverEvent::ChannelMessage { channel, direction } => {
self.channel_messages.add( self.channel_messages.add(
1, 1,

View file

@ -1999,7 +1999,7 @@ fn setup_hardware() -> Result<HardwareConfig> {
hw_config.baud_rate = match baud_idx { hw_config.baud_rate = match baud_idx {
1 => 9600, 1 => 9600,
2 => 57600, 2 => 57600,
3 => 230400, 3 => 230_400,
4 => { 4 => {
let custom: String = Input::new() let custom: String = Input::new()
.with_prompt(" Custom baud rate") .with_prompt(" Custom baud rate")

View file

@ -57,7 +57,12 @@ fn parse_retry_after_ms(err: &anyhow::Error) -> Option<u64> {
.take_while(|c| c.is_ascii_digit() || *c == '.') .take_while(|c| c.is_ascii_digit() || *c == '.')
.collect(); .collect();
if let Ok(secs) = num_str.parse::<f64>() { if let Ok(secs) = num_str.parse::<f64>() {
return Some((secs * 1000.0) as u64); if secs.is_finite() && secs >= 0.0 {
let millis = Duration::from_secs_f64(secs).as_millis();
if let Ok(value) = u64::try_from(millis) {
return Some(value);
}
}
} }
} }
} }

View file

@ -150,6 +150,18 @@ pub struct AuditLogger {
buffer: Mutex<Vec<AuditEvent>>, buffer: Mutex<Vec<AuditEvent>>,
} }
/// Structured command execution details for audit logging.
#[derive(Debug, Clone)]
pub struct CommandExecutionLog<'a> {
pub channel: &'a str,
pub command: &'a str,
pub risk_level: &'a str,
pub approved: bool,
pub allowed: bool,
pub success: bool,
pub duration_ms: u64,
}
impl AuditLogger { impl AuditLogger {
/// Create a new audit logger /// Create a new audit logger
pub fn new(config: AuditConfig, zeroclaw_dir: PathBuf) -> Result<Self> { pub fn new(config: AuditConfig, zeroclaw_dir: PathBuf) -> Result<Self> {
@ -183,7 +195,23 @@ impl AuditLogger {
Ok(()) Ok(())
} }
/// Log a command execution event /// Log a command execution event.
pub fn log_command_event(&self, entry: CommandExecutionLog<'_>) -> Result<()> {
let event = AuditEvent::new(AuditEventType::CommandExecution)
.with_actor(entry.channel.to_string(), None, None)
.with_action(
entry.command.to_string(),
entry.risk_level.to_string(),
entry.approved,
entry.allowed,
)
.with_result(entry.success, None, entry.duration_ms, None);
self.log(&event)
}
/// Backward-compatible helper to log a command execution event.
#[allow(clippy::too_many_arguments)]
pub fn log_command( pub fn log_command(
&self, &self,
channel: &str, channel: &str,
@ -194,24 +222,22 @@ impl AuditLogger {
success: bool, success: bool,
duration_ms: u64, duration_ms: u64,
) -> Result<()> { ) -> Result<()> {
let event = AuditEvent::new(AuditEventType::CommandExecution) self.log_command_event(CommandExecutionLog {
.with_actor(channel.to_string(), None, None) channel,
.with_action( command,
command.to_string(), risk_level,
risk_level.to_string(), approved,
approved, allowed,
allowed, success,
) duration_ms,
.with_result(success, None, duration_ms, None); })
self.log(&event)
} }
/// Rotate log if it exceeds max size /// Rotate log if it exceeds max size
fn rotate_if_needed(&self) -> Result<()> { fn rotate_if_needed(&self) -> Result<()> {
if let Ok(metadata) = std::fs::metadata(&self.log_path) { if let Ok(metadata) = std::fs::metadata(&self.log_path) {
let current_size_mb = metadata.len() / (1024 * 1024); let current_size_mb = metadata.len() / (1024 * 1024);
if current_size_mb >= self.config.max_size_mb as u64 { if current_size_mb >= u64::from(self.config.max_size_mb) {
self.rotate()?; self.rotate()?;
} }
} }
@ -283,7 +309,8 @@ mod tests {
let json = serde_json::to_string(&event); let json = serde_json::to_string(&event);
assert!(json.is_ok()); assert!(json.is_ok());
let parsed: AuditEvent = serde_json::from_str(&json.unwrap().as_str()).expect("parse"); let json = json.expect("serialize");
let parsed: AuditEvent = serde_json::from_str(json.as_str()).expect("parse");
assert!(parsed.actor.is_some()); assert!(parsed.actor.is_some());
assert!(parsed.action.is_some()); assert!(parsed.action.is_some());
assert!(parsed.result.is_some()); assert!(parsed.result.is_some());

View file

@ -902,8 +902,8 @@ mod tests {
let json_str = r#"{"name": "GMAIL_SEND_EMAIL_WITH_ATTACHMENT", "appName": "gmail", "description": "Send email with attachment & special chars: <>'\"\"", "enabled": true}"#; let json_str = r#"{"name": "GMAIL_SEND_EMAIL_WITH_ATTACHMENT", "appName": "gmail", "description": "Send email with attachment & special chars: <>'\"\"", "enabled": true}"#;
let action: ComposioAction = serde_json::from_str(json_str).unwrap(); let action: ComposioAction = serde_json::from_str(json_str).unwrap();
assert_eq!(action.name, "GMAIL_SEND_EMAIL_WITH_ATTACHMENT"); assert_eq!(action.name, "GMAIL_SEND_EMAIL_WITH_ATTACHMENT");
assert!(action.description.as_ref().unwrap().contains("&")); assert!(action.description.as_ref().unwrap().contains('&'));
assert!(action.description.as_ref().unwrap().contains("<")); assert!(action.description.as_ref().unwrap().contains('<'));
} }
#[test] #[test]

View file

@ -31,7 +31,7 @@ impl GitOperationsTool {
|| arg_lower.starts_with("--upload-pack=") || arg_lower.starts_with("--upload-pack=")
|| arg_lower.starts_with("--receive-pack=") || arg_lower.starts_with("--receive-pack=")
|| arg_lower.contains("$(") || arg_lower.contains("$(")
|| arg_lower.contains("`") || arg_lower.contains('`')
|| arg.contains('|') || arg.contains('|')
|| arg.contains(';') || arg.contains(';')
{ {
@ -90,10 +90,8 @@ impl GitOperationsTool {
branch = line.trim_start_matches("# branch.head ").to_string(); branch = line.trim_start_matches("# branch.head ").to_string();
} else if let Some(rest) = line.strip_prefix("1 ") { } else if let Some(rest) = line.strip_prefix("1 ") {
// Ordinary changed entry // Ordinary changed entry
let parts: Vec<&str> = rest.split(' ').collect(); let mut parts = rest.splitn(3, ' ');
if parts.len() >= 2 { if let (Some(staging), Some(path)) = (parts.next(), parts.next()) {
let path = parts.get(1).unwrap_or(&"");
let staging = parts.get(0).unwrap_or(&"");
if !staging.is_empty() { if !staging.is_empty() {
let status_char = staging.chars().next().unwrap_or(' '); let status_char = staging.chars().next().unwrap_or(' ');
if status_char != '.' && status_char != ' ' { if status_char != '.' && status_char != ' ' {
@ -203,7 +201,8 @@ impl GitOperationsTool {
} }
async fn git_log(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { async fn git_log(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; let limit_raw = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10);
let limit = usize::try_from(limit_raw).unwrap_or(usize::MAX).min(1000);
let limit_str = limit.to_string(); let limit_str = limit.to_string();
let output = self let output = self
@ -383,7 +382,9 @@ impl GitOperationsTool {
"pop" => self.run_git_command(&["stash", "pop"]).await, "pop" => self.run_git_command(&["stash", "pop"]).await,
"list" => self.run_git_command(&["stash", "list"]).await, "list" => self.run_git_command(&["stash", "list"]).await,
"drop" => { "drop" => {
let index = args.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as i32; let index_raw = args.get("index").and_then(|v| v.as_u64()).unwrap_or(0);
let index = i32::try_from(index_raw)
.map_err(|_| anyhow::anyhow!("stash index too large: {index_raw}"))?;
self.run_git_command(&["stash", "drop", &format!("stash@{{{index}}}")]) self.run_git_command(&["stash", "drop", &format!("stash@{{{index}}}")])
.await .await
} }
@ -516,12 +517,7 @@ impl Tool for GitOperationsTool {
error: Some("Action blocked: read-only mode".into()), error: Some("Action blocked: read-only mode".into()),
}); });
} }
AutonomyLevel::Supervised => { AutonomyLevel::Supervised | AutonomyLevel::Full => {}
// Allow but require tracking
}
AutonomyLevel::Full => {
// Allow freely
}
} }
} }