Fix quoted Bash tool titles
This commit is contained in:
1 parent
4c15150338
commit
f9c8f640ce
3 files changed
+67
-52
No files matched your search
@@ -80,7 +80,11 @@ fun parseToolInput(tool: String, input: String): ToolInput {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val (subjectKey, language) = SUBJECTS[tool] ?: (null to null)
|
val (subjectKey, language) = SUBJECTS[tool] ?: (null to null)
|
||||||
val subject = subjectKey?.let { json.optString(it) }?.takeIf { it.isNotBlank() }
|
val subject =
|
||||||
|
subjectKey
|
||||||
|
?.let { json.optString(it) }
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { if (tool == "Bash") renderedBashScript(it) ?: it else it }
|
||||||
val description = DESCRIPTIONS.firstNotNullOfOrNull {
|
val description = DESCRIPTIONS.firstNotNullOfOrNull {
|
||||||
json.optString(it).takeIf { v -> v.isNotBlank() }
|
json.optString(it).takeIf { v -> v.isNotBlank() }
|
||||||
}
|
}
|
||||||
@@ -98,6 +102,28 @@ fun parseToolInput(tool: String, input: String): ToolInput {
|
|||||||
return ToolInput(subject, language, description, timeout, rest)
|
return ToolInput(subject, language, description, timeout, rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes Codex's rendered Bash argv from old transcript rows.
|
||||||
|
*
|
||||||
|
* New events arrive normalized by the server, but persisted transcripts keep the input originally
|
||||||
|
* written to them. Only the outer pair are presentation quoting: quotes inside the command belong
|
||||||
|
* to the command and must not be parsed as an early end delimiter.
|
||||||
|
*/
|
||||||
|
internal fun renderedBashScript(command: String): String? {
|
||||||
|
val prefix =
|
||||||
|
listOf("/usr/bin/bash -lc ", "/bin/bash -lc ", "bash -lc ").firstOrNull {
|
||||||
|
command.startsWith(it)
|
||||||
|
} ?: return null
|
||||||
|
val quoted = command.removePrefix(prefix)
|
||||||
|
return quoted
|
||||||
|
.takeIf {
|
||||||
|
it.length >= 2 &&
|
||||||
|
((it.startsWith('\'') && it.endsWith('\'')) ||
|
||||||
|
(it.startsWith('"') && it.endsWith('"')))
|
||||||
|
}
|
||||||
|
?.substring(1, quoted.lastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tool call's input: its subject highlighted, then whatever else it carried.
|
* A tool call's input: its subject highlighted, then whatever else it carried.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.example.aiapp
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class ToolInputTest {
|
||||||
|
@Test
|
||||||
|
fun `bash wrapper ignores double quotes inside its outer pair`() {
|
||||||
|
assertEquals(
|
||||||
|
"rg -n \"needle\" server app",
|
||||||
|
renderedBashScript("/usr/bin/bash -lc \"rg -n \"needle\" server app\""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `bash wrapper ignores single quotes inside its outer pair`() {
|
||||||
|
assertEquals(
|
||||||
|
"printf 'hello'",
|
||||||
|
renderedBashScript("/bin/bash -lc 'printf 'hello''"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unquoted or unfamiliar commands stay intact`() {
|
||||||
|
assertNull(renderedBashScript("/usr/bin/bash -lc echo hello"))
|
||||||
|
assertNull(renderedBashScript("/usr/bin/fish -lc 'echo hello'"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -292,59 +292,19 @@ fn command_tool(item: &Value) -> (String, Value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// App-server renders the executor argv into one shell-quoted string. Unwrap only an exact Bash
|
/// App-server renders the executor argv into one shell-quoted string. Unwrap only an exact Bash
|
||||||
/// invocation containing one shell word; extra arguments stay visible rather than being decoded
|
/// invocation whose script has matching outer quotes; extra arguments stay visible rather than
|
||||||
/// into a command that was not actually run.
|
/// being mistaken for part of the script.
|
||||||
fn rendered_bash_script(command: &str) -> Option<String> {
|
fn rendered_bash_script(command: &str) -> Option<String> {
|
||||||
let quoted = ["/usr/bin/bash -lc ", "/bin/bash -lc ", "bash -lc "]
|
let quoted = ["/usr/bin/bash -lc ", "/bin/bash -lc ", "bash -lc "]
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|prefix| command.strip_prefix(prefix))?;
|
.find_map(|prefix| command.strip_prefix(prefix))?;
|
||||||
if quoted.starts_with('"') {
|
if quoted.len() >= 2
|
||||||
return double_quoted_shell_word(quoted);
|
&& ((quoted.starts_with('\'') && quoted.ends_with('\''))
|
||||||
|
|| (quoted.starts_with('"') && quoted.ends_with('"')))
|
||||||
|
{
|
||||||
|
return Some(quoted[1..quoted.len() - 1].to_string());
|
||||||
}
|
}
|
||||||
if !quoted.starts_with('\'') {
|
(!quoted.is_empty() && !quoted.chars().any(char::is_whitespace)).then(|| quoted.to_string())
|
||||||
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 double_quoted_shell_word(quoted: &str) -> Option<String> {
|
|
||||||
let mut chars = quoted.strip_prefix('"')?.chars();
|
|
||||||
let mut script = String::new();
|
|
||||||
while let Some(character) = chars.next() {
|
|
||||||
match character {
|
|
||||||
'"' => return chars.next().is_none().then_some(script),
|
|
||||||
'\\' => match chars.next()? {
|
|
||||||
escaped @ ('$' | '`' | '"' | '\\') => script.push(escaped),
|
|
||||||
'\n' => {}
|
|
||||||
escaped => {
|
|
||||||
script.push('\\');
|
|
||||||
script.push(escaped);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
character => script.push(character),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shell_word(word: &Value) -> String {
|
fn shell_word(word: &Value) -> String {
|
||||||
@@ -599,21 +559,21 @@ mod tests {
|
|||||||
let rendered = tool(&json!({
|
let rendered = tool(&json!({
|
||||||
"id": "rendered",
|
"id": "rendered",
|
||||||
"type": "commandExecution",
|
"type": "commandExecution",
|
||||||
"command": r#"/usr/bin/bash -lc 'printf '\''%s\n'\'' hello'"#
|
"command": "/usr/bin/bash -lc 'printf \"%s\\n\" hello'"
|
||||||
}));
|
}));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered,
|
rendered,
|
||||||
Some((
|
Some((
|
||||||
"rendered".to_string(),
|
"rendered".to_string(),
|
||||||
"Bash".to_string(),
|
"Bash".to_string(),
|
||||||
json!({"command": "printf '%s\\n' hello"})
|
json!({"command": "printf \"%s\\n\" hello"})
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
let double_quoted = tool(&json!({
|
let double_quoted = tool(&json!({
|
||||||
"id": "double-quoted",
|
"id": "double-quoted",
|
||||||
"type": "commandExecution",
|
"type": "commandExecution",
|
||||||
"command": r#"/usr/bin/bash -lc "printf '%s\\n' \"\$HOME\" \\path""#
|
"command": r#"/usr/bin/bash -lc "printf '%s\n' "$HOME" \path""#
|
||||||
}));
|
}));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
double_quoted,
|
double_quoted,
|
||||||
|
|||||||
Reference in new issue
Block a user