Skip to content

fix(acp): parse shell command for terminal/create to fix acpx compatibility#1688

Open
zhzy0077 wants to merge 1 commit intoMoonshotAI:mainfrom
zhzy0077:fix/acpx-terminal-command
Open

fix(acp): parse shell command for terminal/create to fix acpx compatibility#1688
zhzy0077 wants to merge 1 commit intoMoonshotAI:mainfrom
zhzy0077:fix/acpx-terminal-command

Conversation

@zhzy0077
Copy link
Copy Markdown

@zhzy0077 zhzy0077 commented Mar 31, 2026

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:

[error] RUNTIME: Internal error
[tool] Shell: ls -la (failed)
  output:
    Error running tool: Internal error

The root cause is that kimi-cli sends the entire command as a single string to the ACP client's terminal/create method:

{"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, CreateTerminalRequest has:

  • 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:

resp = await self._acp_conn.create_terminal(
    command=params.command,  # "ls -la"
    ...
)

After:

parts = shlex.split(params.command)  # ["ls", "-la"]
cmd = parts[0]  # "ls"
args = parts[1:]  # ["-la"]
resp = await self._acp_conn.create_terminal(
    command=cmd,
    args=args,
    ...
)

This properly separates the executable from its arguments, allowing acpx to spawn the command correctly.

Testing

Tested with:

acpx --approve-all kimi exec "Run ls -la"

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.


Open with Devin

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')
Copilot AI review requested due to automatic review settings March 31, 2026 16:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 shlex parsing of params.command into command and args for acp_conn.create_terminal.
  • Pass the parsed args explicitly to create_terminal instead of sending the whole command line as command.

💡 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)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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",
)

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +97
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,
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to 96
# 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(
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +93 to +95
parts = shlex.split(params.command)
cmd = parts[0] if parts else ""
args = parts[1:] if len(parts) > 1 else []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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'.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


try:
# Parse command string into command and args for ACP
parts = shlex.split(params.command)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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")
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants