fix(security): enforce action guards in file_write and scheduler (#269)

This commit is contained in:
Chummy 2026-02-16 14:57:58 +08:00 committed by GitHub
parent 60f3282ad4
commit 3bdabdc7ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 132 additions and 0 deletions

View file

@ -53,6 +53,22 @@ impl Tool for FileWriteTool {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'content' parameter"))?;
if !self.security.can_act() {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Action blocked: autonomy is read-only".into()),
});
}
if self.security.is_rate_limited() {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Rate limit exceeded: too many actions in the last hour".into()),
});
}
// Security check: validate path is within workspace
if !self.security.is_path_allowed(path) {
return Ok(ToolResult {
@ -122,6 +138,14 @@ impl Tool for FileWriteTool {
}
}
if !self.security.record_action() {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Rate limit exceeded: action budget exhausted".into()),
});
}
match tokio::fs::write(&resolved_target, content).await {
Ok(()) => Ok(ToolResult {
success: true,
@ -150,6 +174,19 @@ mod tests {
})
}
fn test_security_with(
workspace: std::path::PathBuf,
autonomy: AutonomyLevel,
max_actions_per_hour: u32,
) -> Arc<SecurityPolicy> {
Arc::new(SecurityPolicy {
autonomy,
workspace_dir: workspace,
max_actions_per_hour,
..SecurityPolicy::default()
})
}
#[test]
fn file_write_name() {
let tool = FileWriteTool::new(test_security(std::env::temp_dir()));
@ -324,4 +361,50 @@ mod tests {
let _ = tokio::fs::remove_dir_all(&root).await;
}
#[tokio::test]
async fn file_write_blocks_readonly_mode() {
let dir = std::env::temp_dir().join("zeroclaw_test_file_write_readonly");
let _ = tokio::fs::remove_dir_all(&dir).await;
tokio::fs::create_dir_all(&dir).await.unwrap();
let tool = FileWriteTool::new(test_security_with(dir.clone(), AutonomyLevel::ReadOnly, 20));
let result = tool
.execute(json!({"path": "out.txt", "content": "should-block"}))
.await
.unwrap();
assert!(!result.success);
assert!(result.error.as_deref().unwrap_or("").contains("read-only"));
assert!(!dir.join("out.txt").exists());
let _ = tokio::fs::remove_dir_all(&dir).await;
}
#[tokio::test]
async fn file_write_blocks_when_rate_limited() {
let dir = std::env::temp_dir().join("zeroclaw_test_file_write_rate_limited");
let _ = tokio::fs::remove_dir_all(&dir).await;
tokio::fs::create_dir_all(&dir).await.unwrap();
let tool = FileWriteTool::new(test_security_with(
dir.clone(),
AutonomyLevel::Supervised,
0,
));
let result = tool
.execute(json!({"path": "out.txt", "content": "should-block"}))
.await
.unwrap();
assert!(!result.success);
assert!(result
.error
.as_deref()
.unwrap_or("")
.contains("Rate limit exceeded"));
assert!(!dir.join("out.txt").exists());
let _ = tokio::fs::remove_dir_all(&dir).await;
}
}