diff --git a/server/src/session/codex/translate.rs b/server/src/session/codex/translate.rs index 9445e25..47c2281 100644 --- a/server/src/session/codex/translate.rs +++ b/server/src/session/codex/translate.rs @@ -270,6 +270,7 @@ fn tool(item: &Value) -> Option<(String, String, Value)> { fn command_tool(item: &Value) -> (String, Value) { let command = item.get("command").cloned().unwrap_or(Value::Null); if let Some(script) = command.as_str() { + let script = rendered_bash_script(script).unwrap_or_else(|| script.to_string()); return ("Bash".to_string(), json!({"command": script})); } if let Some(argv) = command.as_array() @@ -290,6 +291,39 @@ fn command_tool(item: &Value) -> (String, Value) { ("Shell".to_string(), json!({"command": command})) } +/// App-server renders the executor argv into one shell-quoted string. Unwrap only the exact Bash +/// form Codex currently emits; anything with another argument or quoting form stays visible rather +/// than being decoded into a command that was not actually run. +fn rendered_bash_script(command: &str) -> Option { + let quoted = ["/usr/bin/bash -lc ", "/bin/bash -lc ", "bash -lc "] + .iter() + .find_map(|prefix| command.strip_prefix(prefix))?; + if !quoted.starts_with('\'') { + return (!quoted.is_empty() && !quoted.chars().any(char::is_whitespace)) + .then(|| quoted.to_string()); + } + + let mut rest = quoted; + let mut script = String::new(); + loop { + rest = rest.strip_prefix('\'')?; + let end = rest.find('\'')?; + script.push_str(&rest[..end]); + rest = &rest[end + 1..]; + if rest.is_empty() { + return Some(script); + } + if let Some(after_quote) = rest.strip_prefix("\\'") { + script.push('\''); + rest = after_quote; + } else { + let after_quote = rest.strip_prefix("\"'\"")?; + script.push('\''); + rest = after_quote; + } + } +} + fn shell_word(word: &Value) -> String { let Some(word) = word.as_str() else { return word.to_string(); @@ -539,6 +573,20 @@ mod tests { )) ); + let rendered = tool(&json!({ + "id": "rendered", + "type": "commandExecution", + "command": r#"/usr/bin/bash -lc 'printf '\''%s\n'\'' hello'"# + })); + assert_eq!( + rendered, + Some(( + "rendered".to_string(), + "Bash".to_string(), + json!({"command": "printf '%s\\n' hello"}) + )) + ); + let fish = tool(&line( r#"{"id":"fish","type":"commandExecution","command":["/usr/bin/fish","-c","pwd"]}"#, ));