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:
harald 2026-02-25 13:13:19 +01:00
parent 05e1102af9
commit 5b896f3378
2 changed files with 41 additions and 13 deletions

View file

@ -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)
} }

View file

@ -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"
)),
})
}
} }
} }
} }