feat(observability): add debug/trace logging to shell tool and command policy
Shell tool now logs at debug level: command invocations, policy allow/block decisions with reasons, exit codes, and output sizes. Trace level adds full stdout/stderr content and risk assessment details. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
05e1102af9
commit
5b896f3378
2 changed files with 41 additions and 13 deletions
|
|
@ -312,16 +312,20 @@ impl SecurityPolicy {
|
||||||
approved: bool,
|
approved: bool,
|
||||||
) -> Result<CommandRiskLevel, String> {
|
) -> Result<CommandRiskLevel, String> {
|
||||||
if !self.is_command_allowed(command) {
|
if !self.is_command_allowed(command) {
|
||||||
|
tracing::debug!(command, "Shell command blocked by allowlist");
|
||||||
return Err(format!("Command not allowed by security policy: {command}"));
|
return Err(format!("Command not allowed by security policy: {command}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let risk = self.command_risk_level(command);
|
let risk = self.command_risk_level(command);
|
||||||
|
tracing::trace!(command, ?risk, approved, "Shell command risk assessed");
|
||||||
|
|
||||||
if risk == CommandRiskLevel::High {
|
if risk == CommandRiskLevel::High {
|
||||||
if self.block_high_risk_commands {
|
if self.block_high_risk_commands {
|
||||||
|
tracing::debug!(command, "Shell command blocked: high-risk disallowed by policy");
|
||||||
return Err("Command blocked: high-risk command is disallowed by policy".into());
|
return Err("Command blocked: high-risk command is disallowed by policy".into());
|
||||||
}
|
}
|
||||||
if self.autonomy == AutonomyLevel::Supervised && !approved {
|
if self.autonomy == AutonomyLevel::Supervised && !approved {
|
||||||
|
tracing::debug!(command, "Shell command blocked: high-risk needs approval");
|
||||||
return Err(
|
return Err(
|
||||||
"Command requires explicit approval (approved=true): high-risk operation"
|
"Command requires explicit approval (approved=true): high-risk operation"
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -334,11 +338,13 @@ impl SecurityPolicy {
|
||||||
&& self.require_approval_for_medium_risk
|
&& self.require_approval_for_medium_risk
|
||||||
&& !approved
|
&& !approved
|
||||||
{
|
{
|
||||||
|
tracing::debug!(command, "Shell command blocked: medium-risk needs approval");
|
||||||
return Err(
|
return Err(
|
||||||
"Command requires explicit approval (approved=true): medium-risk operation".into(),
|
"Command requires explicit approval (approved=true): medium-risk operation".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::debug!(command, ?risk, "Shell command allowed by policy");
|
||||||
Ok(risk)
|
Ok(risk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,10 @@ impl Tool for ShellTool {
|
||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
tracing::debug!(command, approved, "Shell tool invoked");
|
||||||
|
|
||||||
if self.security.is_rate_limited() {
|
if self.security.is_rate_limited() {
|
||||||
|
tracing::warn!(command, "Shell command rejected: rate limit exceeded");
|
||||||
return Ok(ToolResult {
|
return Ok(ToolResult {
|
||||||
success: false,
|
success: false,
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
|
|
@ -122,9 +125,22 @@ impl Tool for ShellTool {
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(output)) => {
|
Ok(Ok(output)) => {
|
||||||
|
let exit_code = output.status.code();
|
||||||
|
let success = output.status.success();
|
||||||
|
tracing::debug!(
|
||||||
|
command,
|
||||||
|
?exit_code,
|
||||||
|
success,
|
||||||
|
stdout_bytes = output.stdout.len(),
|
||||||
|
stderr_bytes = output.stderr.len(),
|
||||||
|
"Shell command completed"
|
||||||
|
);
|
||||||
|
|
||||||
let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
let mut stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
let mut stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
|
||||||
|
tracing::trace!(command, stdout = %stdout, stderr = %stderr, "Shell command output");
|
||||||
|
|
||||||
// Truncate output to prevent OOM
|
// Truncate output to prevent OOM
|
||||||
if stdout.len() > MAX_OUTPUT_BYTES {
|
if stdout.len() > MAX_OUTPUT_BYTES {
|
||||||
stdout.truncate(stdout.floor_char_boundary(MAX_OUTPUT_BYTES));
|
stdout.truncate(stdout.floor_char_boundary(MAX_OUTPUT_BYTES));
|
||||||
|
|
@ -136,7 +152,7 @@ impl Tool for ShellTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ToolResult {
|
Ok(ToolResult {
|
||||||
success: output.status.success(),
|
success,
|
||||||
output: stdout,
|
output: stdout,
|
||||||
error: if stderr.is_empty() {
|
error: if stderr.is_empty() {
|
||||||
None
|
None
|
||||||
|
|
@ -145,18 +161,24 @@ impl Tool for ShellTool {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => Ok(ToolResult {
|
Ok(Err(e)) => {
|
||||||
success: false,
|
tracing::warn!(command, error = %e, "Shell command failed to execute");
|
||||||
output: String::new(),
|
Ok(ToolResult {
|
||||||
error: Some(format!("Failed to execute command: {e}")),
|
success: false,
|
||||||
}),
|
output: String::new(),
|
||||||
Err(_) => Ok(ToolResult {
|
error: Some(format!("Failed to execute command: {e}")),
|
||||||
success: false,
|
})
|
||||||
output: String::new(),
|
}
|
||||||
error: Some(format!(
|
Err(_) => {
|
||||||
"Command timed out after {SHELL_TIMEOUT_SECS}s and was killed"
|
tracing::warn!(command, timeout_secs = SHELL_TIMEOUT_SECS, "Shell command timed out");
|
||||||
)),
|
Ok(ToolResult {
|
||||||
}),
|
success: false,
|
||||||
|
output: String::new(),
|
||||||
|
error: Some(format!(
|
||||||
|
"Command timed out after {SHELL_TIMEOUT_SECS}s and was killed"
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue