fix(acp): parse shell command for terminal/create to fix acpx compatibility#1688
fix(acp): parse shell command for terminal/create to fix acpx compatibility#1688zhzy0077 wants to merge 1 commit intoMoonshotAI:mainfrom
Conversation
According to ACP spec, CreateTerminalRequest requires separate 'command' and 'args' fields. Previously, kimi-cli sent the entire command as a single string (e.g., 'ls -la'), causing acpx to fail with ENOENT when trying to spawn a non-existent binary named 'ls -la'. The fix uses shlex.split() to properly parse the command string into executable and arguments before sending to the ACP client. Fixes: acpx terminal tool failing with 'Internal error' for commands with arguments (e.g., 'ls -la', 'cat file.txt')
There was a problem hiding this comment.
Pull request overview
Fixes ACP terminal compatibility when invoking the Shell tool via ACP clients (e.g., acpx) by splitting a command string into an executable + argument list before calling terminal/create.
Changes:
- Add
shlexparsing ofparams.commandintocommandandargsforacp_conn.create_terminal. - Pass the parsed
argsexplicitly tocreate_terminalinstead of sending the whole command line ascommand.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| try: | ||
| # Parse command string into command and args for ACP | ||
| parts = shlex.split(params.command) |
There was a problem hiding this comment.
shlex.split() can raise ValueError (e.g., unmatched quotes). Right now that exception will bubble up and likely surface as a generic ACP "Internal error". Consider catching ValueError here and returning a user-facing tool error like "Invalid command" (including the parse error in debug logs).
| parts = shlex.split(params.command) | |
| try: | |
| parts = shlex.split(params.command) | |
| except ValueError as e: | |
| # Handle malformed command strings (e.g., unmatched quotes) gracefully. | |
| return builder.error( | |
| f"Invalid command: {e}", | |
| brief="Invalid command syntax", | |
| ) |
| parts = shlex.split(params.command) | ||
| cmd = parts[0] if parts else "" | ||
| args = parts[1:] if len(parts) > 1 else [] | ||
| resp = await self._acp_conn.create_terminal( | ||
| command=params.command, | ||
| command=cmd, |
There was a problem hiding this comment.
Because the earlier guard is if not params.command, a whitespace-only command (e.g. " ") will pass, but shlex.split() will return [], resulting in command="" being sent to create_terminal. Consider stripping before splitting and erroring if no parts are produced.
| # Parse command string into command and args for ACP | ||
| parts = shlex.split(params.command) | ||
| cmd = parts[0] if parts else "" | ||
| args = parts[1:] if len(parts) > 1 else [] | ||
| resp = await self._acp_conn.create_terminal( |
There was a problem hiding this comment.
This new command/args parsing is core to ACP terminal compatibility but currently isn’t covered by tests. Consider adding a focused unit test (mocking _acp_conn.create_terminal) that asserts "ls -la" becomes command="ls" and args=["-la"], plus a quoted-arg case (e.g. echo "a b").
| parts = shlex.split(params.command) | ||
| cmd = parts[0] if parts else "" | ||
| args = parts[1:] if len(parts) > 1 else [] |
There was a problem hiding this comment.
🔴 shlex.split() breaks shell compound commands (pipes, redirects, &&, ||, etc.)
The LLM generates commands intended for shell interpretation (the Shell tool description tells it to write bash commands, and the original Shell tool wraps them in bash -c <command> at src/kimi_cli/tools/shell/__init__.py:235). Using shlex.split() to tokenize the command and pass the first token as command and the rest as args will break any command that uses shell features. For example, cat file.txt | grep pattern gets split into cmd='cat', args=['file.txt', '|', 'grep', 'pattern'], which would execute cat with those literal arguments rather than piping through grep. Similarly, echo hello && echo world, ls > output.txt, for i in 1 2 3; do echo $i; done, variable expansion (echo $HOME), and glob patterns (ls *.py) will all silently produce wrong results or fail. Since the Shell tool's contract with the LLM is to accept full shell command strings, splitting them into exec-style argv is a semantic mismatch.
Prompt for agents
In src/kimi_cli/acp/tools.py lines 92-98, instead of using shlex.split() to parse the command string, the command should be wrapped in a shell invocation to preserve shell semantics (pipes, redirects, &&, ||, variable expansion, globs, etc.). The ACP create_terminal should receive the shell executable (e.g. 'bash' or the shell from the environment) as the command and ['-c', params.command] as the args. This mirrors what the original Shell tool does in src/kimi_cli/tools/shell/__init__.py:232-235 where it uses _shell_args to produce ('bash', '-c', command). You may need to plumb the shell path from the Environment/Shell tool configuration into the Terminal tool, or hard-code a reasonable default like '/bin/sh'.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
|
||
| try: | ||
| # Parse command string into command and args for ACP | ||
| parts = shlex.split(params.command) |
There was a problem hiding this comment.
🟡 shlex.split() raises unhandled ValueError on malformed quotes in command
shlex.split() raises ValueError ("No closing quotation") when the command string contains unmatched quotes, e.g. echo "hello. This exception is not caught in the try block at line 91, which only handles TimeoutError in its inner block. The ValueError will propagate up as an unhandled exception instead of returning a clean tool error response via builder.error(). While there may be a generic exception handler further up the call stack, this produces an unhelpful error message rather than the user-friendly format the tool is designed to return.
| parts = shlex.split(params.command) | |
| try: | |
| parts = shlex.split(params.command) | |
| except ValueError as e: | |
| return builder.error(f"Invalid command syntax: {e}", brief="Invalid command") |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e7bc68f566
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| try: | ||
| # Parse command string into command and args for ACP | ||
| parts = shlex.split(params.command) |
There was a problem hiding this comment.
Preserve Windows paths when parsing terminal command
Using shlex.split(params.command) with default POSIX behavior removes backslashes, so Windows-style commands are mangled before terminal/create is called (for example, C:\Windows\System32\whoami.exe becomes C:WindowsSystem32whoami.exe). This breaks ACP terminal execution on Windows for absolute executable paths and arguments containing \, even when the original command string is valid.
Useful? React with 👍 / 👎.
Problem
When using acpx (Agent Client Protocol client) with kimi-cli, shell commands with arguments fail with "Internal error".
Example:
acpx --approve-all kimi prompt "Run ls -la"Error:
The root cause is that kimi-cli sends the entire command as a single string to the ACP client's
terminal/createmethod:{"command": "ls -la", "args": []}acpx then tries to spawn a binary literally named "ls -la" (with spaces), which doesn't exist, resulting in
ENOENT.ACP Specification
According to the ACP spec,
CreateTerminalRequesthas:command: The executable to run (string)args: Array of command arguments (string[])The spec expects these to be separate fields, not a combined command string.
Solution
Parse the shell command string using
shlex.split()before sending to ACP:Before:
After:
This properly separates the executable from its arguments, allowing acpx to spawn the command correctly.
Testing
Tested with:
Result: Command now executes successfully instead of failing with "Internal error".
Related
Fixes compatibility with ACP clients like acpx that expect proper command/args separation per the ACP specification.