feat(terminal): accept argv-list form to bypass bash - #25864
Open
sparkeros wants to merge 1 commit into
Open
Conversation
Add a sibling `argv` parameter on the terminal tool. When the model
passes a non-empty argv list, the command bypasses bash entirely:
subprocess.Popen is called with shell=False, so arguments reach the
target program byte-for-byte exactly as the model assembled them.
Why: today every terminal call is `bash -c <string>`, which means the
model has to escape its arguments into a shell-safe string. A single
apostrophe in a JSON body is enough to break the outer quoting and the
call fails with `unexpected EOF while looking for matching '`. argv
form removes the shell from the equation:
argv=["curl", "-X", "POST", "-H", "Content-Type: application/json",
"-d", '{"body":"It\'s done"}',
"http://localhost:3100/api/issues/AOS-8"]
Bash never sees the body — curl receives the bytes verbatim.
Plumbing:
- BaseEnvironment gains `execute_argv(argv, cwd, timeout, stdin_data)`
alongside `execute(command, ...)`. It bypasses the shell-only
machinery: no sudo prompt rewriting, no compound-background rewrite,
no CWD-tracking wrapper, no snapshot env. Trade-off: argv processes
cannot change the session's cwd; that's correct semantics for a
single-program invocation.
- BaseEnvironment gains `_run_argv(argv, ...)` abstract-ish hook with
a default implementation that degrades to `_run_bash(shlex.join(argv))`
for backends that haven't implemented native argv. Backends that can
spawn argv directly (LocalEnvironment via subprocess.Popen, future
docker/ssh implementations) override this to skip the bash wrap and
recover the ~30-80 ms per-call overhead.
- LocalEnvironment overrides `_run_argv` with a direct
`subprocess.Popen(argv, shell=False, ...)` call. Mirrors the
cwd-recovery, env-merge, and process-group setup of `_run_bash` so
process management (kill, timeout, output capture) is identical.
Tool-level guard:
- argv form is foreground-only for now. background=true and pty=true
go through process_registry which expects shell strings; supporting
them in argv form is a separate plumbing change. The handler returns
a clear error if the model combines argv with either flag.
- argv and command are mutually exclusive — the handler returns an
error if both are non-empty.
- Schema-wise, `command` stays required (provider-safe; some sanitisers
reject mixed-form `oneOf`). The model passes empty `command` plus
populated `argv` for the new path.
- The `command` description now mentions argv as the alternative, and
the `argv` description warns about no shell features (pipes, &&,
redirection) so the model knows when to fall back to `command`.
Tests in tests/tools/test_terminal_argv_form.py cover validation
(both/neither, non-string entries, background/pty rejection), end-to-end
dispatch (argv goes to execute_argv, string command goes to execute,
apostrophe-in-payload survives byte-for-byte), schema shape, and the
local._run_argv shell=False guarantee.
Contributor
|
Thanks for isolating a real shell-quoting pain point. Current main still runs local foreground terminal commands through Problems
Suggested changes
Automated hermes-sweeper review. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds a sibling `argv` parameter on the `terminal` tool. When the model passes a non-empty argv list, the command bypasses bash entirely: `subprocess.Popen` is called with `shell=False`, so arguments reach the target program byte-for-byte exactly as the model assembled them.
Why: today every terminal call is `bash -c `, which means the model has to escape its arguments into a shell-safe string. A single apostrophe in a JSON body is enough to break the outer quoting and the call fails with `unexpected EOF while looking for matching '`. argv form removes the shell from the equation:
```python
argv=["curl", "-X", "POST", "-H", "Content-Type: application/json",
"-d", '{"body":"It\'s done"}',
"http://localhost:3100/api/issues/AOS-8"]
```
Bash never sees the body — curl receives the bytes verbatim. Also saves the bash fork+parse overhead (~30–80 ms per call).
Complements PR #25861 (the `http` tool) and #25862 (quoting-error hint). Independent — works without either.
Type of Change
Changes Made
How to Test
`pytest tests/tools/test_terminal_argv_form.py -q` — 13 passed.
Checklist
Behavior notes