From df8f6bf9592e6ab8be5e60e6fc6f06463f96004d Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 4 Aug 2026 21:40:25 +0800 Subject: [PATCH] core/tools: rename run_command_batch to run_argv_batch (steps/argv keys) --- src/plyngent/tools/__init__.py | 2 +- src/plyngent/tools/danger.py | 24 +++++----- src/plyngent/tools/process/__init__.py | 4 +- src/plyngent/tools/process/command_exec.py | 2 +- ...run_command_batch.py => run_argv_batch.py} | 48 +++++++++---------- tests/test_tools/test_danger.py | 16 +++---- tests/test_tools/test_process.py | 40 ++++++++-------- 7 files changed, 68 insertions(+), 68 deletions(-) rename src/plyngent/tools/process/{run_command_batch.py => run_argv_batch.py} (84%) diff --git a/src/plyngent/tools/__init__.py b/src/plyngent/tools/__init__.py index 56f221b..a5ef65c 100644 --- a/src/plyngent/tools/__init__.py +++ b/src/plyngent/tools/__init__.py @@ -42,7 +42,7 @@ from .process import close_pty as close_pty from .process import open_pty as open_pty from .process import read_pty as read_pty from .process import run_argv as run_argv -from .process import run_command_batch as run_command_batch +from .process import run_argv_batch as run_argv_batch from .process import write_pty as write_pty from .process import write_pty_keys as write_pty_keys from .temp_workspace import cleanup_temporary_workspaces as cleanup_temporary_workspaces diff --git a/src/plyngent/tools/danger.py b/src/plyngent/tools/danger.py index 921f356..3eb3cc3 100644 --- a/src/plyngent/tools/danger.py +++ b/src/plyngent/tools/danger.py @@ -192,20 +192,20 @@ def _batch_step_argv(item: object) -> list[str] | None: if not isinstance(item, dict): return None step = cast("dict[str, object]", item) - command = step.get("command") - if not isinstance(command, list) or not command: + argv = step.get("argv") + if not isinstance(argv, list) or not argv: return None - argv: list[str] = [] - for part in cast("list[object]", command): + parts: list[str] = [] + for part in cast("list[object]", argv): if not isinstance(part, str): return None - argv.append(part) - return argv or None + parts.append(part) + return parts or None -def _run_command_batch_reason(args: Mapping[str, object]) -> str | None: +def _run_argv_batch_reason(args: Mapping[str, object]) -> str | None: """One confirm for the whole batch if any step is shell/REPL/-c.""" - raw = args.get("commands") + raw = args.get("steps") if isinstance(raw, str): try: loaded: object = json.loads(raw) @@ -218,11 +218,11 @@ def _run_command_batch_reason(args: Mapping[str, object]) -> str | None: reason for index, item in enumerate(cast("list[object]", raw)) if (argv := _batch_step_argv(item)) is not None - and (reason := _shell_or_dash_c_reason(argv, via=f"run_command_batch[{index}]")) is not None + and (reason := _shell_or_dash_c_reason(argv, via=f"run_argv_batch[{index}]")) is not None ] if not risky: return None - header = f"run_command_batch: {len(risky)} risky step(s) (review before allow)" + header = f"run_argv_batch: {len(risky)} risky step(s) (review before allow)" return header + "\n" + "\n".join(risky) @@ -264,8 +264,8 @@ def classify_danger(name: str, args: Mapping[str, object]) -> str | None: # noq return _write_file_reason(args) if name == "run_argv": return _run_argv_reason(args) - if name == "run_command_batch": - return _run_command_batch_reason(args) + if name == "run_argv_batch": + return _run_argv_batch_reason(args) if name == "open_pty": return _open_pty_reason(args) if name == "fetch": diff --git a/src/plyngent/tools/process/__init__.py b/src/plyngent/tools/process/__init__.py index c68b77e..ea1c9f0 100644 --- a/src/plyngent/tools/process/__init__.py +++ b/src/plyngent/tools/process/__init__.py @@ -5,13 +5,13 @@ from .pty_terminal import decode_write_data as decode_write_data from .pty_terminal import sanitize_pty_output_for_tool as sanitize_pty_output_for_tool from .read_pty import read_pty as read_pty from .run_argv import run_argv as run_argv -from .run_command_batch import run_command_batch as run_command_batch +from .run_argv_batch import run_argv_batch as run_argv_batch from .write_pty import write_pty as write_pty from .write_pty_keys import write_pty_keys as write_pty_keys PROCESS_TOOLS = [ run_argv, - run_command_batch, + run_argv_batch, open_pty, read_pty, write_pty, diff --git a/src/plyngent/tools/process/command_exec.py b/src/plyngent/tools/process/command_exec.py index 270cfbc..099df48 100644 --- a/src/plyngent/tools/process/command_exec.py +++ b/src/plyngent/tools/process/command_exec.py @@ -83,7 +83,7 @@ async def execute_argv( timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, mix_stderr: bool = False, ) -> CommandStepResult: - """Run one argv command; no shell. Used by run_argv and run_command_batch.""" + """Run one argv command; no shell. Used by run_argv and run_argv_batch.""" if not command: msg = "command argv must not be empty" raise WorkspaceError(msg) diff --git a/src/plyngent/tools/process/run_command_batch.py b/src/plyngent/tools/process/run_argv_batch.py similarity index 84% rename from src/plyngent/tools/process/run_command_batch.py rename to src/plyngent/tools/process/run_argv_batch.py index bbba8bd..2e1e88c 100644 --- a/src/plyngent/tools/process/run_command_batch.py +++ b/src/plyngent/tools/process/run_argv_batch.py @@ -50,23 +50,23 @@ def _as_str_dict(value: object) -> dict[str, str] | None: def _parse_step(raw: object, index: int) -> dict[str, Any]: if not isinstance(raw, dict): - msg = f"commands[{index}] must be an object" + msg = f"steps[{index}] must be an object" raise WorkspaceError(msg) data = cast("dict[str, object]", raw) - command = data.get("command") - if not isinstance(command, list) or not command: - msg = f"commands[{index}].command must be a non-empty argv list" + argv = data.get("argv") + if not isinstance(argv, list) or not argv: + msg = f"steps[{index}].argv must be a non-empty argv list" raise WorkspaceError(msg) - command_parts = cast("list[object]", command) - argv = [part for part in command_parts if isinstance(part, str)] - if len(argv) != len(command_parts): - msg = f"commands[{index}].command must be a list of strings" + argv_parts = cast("list[object]", argv) + argv_str = [part for part in argv_parts if isinstance(part, str)] + if len(argv_str) != len(argv_parts): + msg = f"steps[{index}].argv must be a list of strings" raise WorkspaceError(msg) cwd = data.get("cwd") stdin = data.get("stdin") stop = data.get("stop_on_error", None) return { - "command": argv, + "argv": argv_str, "cwd": str(cwd) if isinstance(cwd, str) and cwd else None, "env": _as_str_dict(data.get("env")), "stdin": None if stdin is None else str(stdin), @@ -77,18 +77,18 @@ def _parse_step(raw: object, index: int) -> dict[str, Any]: } -def _normalize_commands(commands: list[dict[str, object]] | str) -> list[object]: - if isinstance(commands, str): +def _normalize_steps(steps: list[dict[str, object]] | str) -> list[object]: + if isinstance(steps, str): try: - loaded: object = json.loads(commands) + loaded: object = json.loads(steps) except json.JSONDecodeError as exc: - msg = f"commands must be a JSON array: {exc}" + msg = f"steps must be a JSON array: {exc}" raise WorkspaceError(msg) from exc if not isinstance(loaded, list): - msg = "commands must be a JSON array of step objects" + msg = "steps must be a JSON array of step objects" raise WorkspaceError(msg) return cast("list[object]", loaded) - return cast("list[object]", commands) + return cast("list[object]", steps) def _format_step(index: int, result: CommandStepResult, *, pipe_out: bool) -> str: @@ -131,7 +131,7 @@ async def _run_batch_steps( for index, step in enumerate(steps): stdin_text = prev_capture if prev_piped else step["stdin"] result = await execute_argv( - step["command"], + step["argv"], cwd=step["cwd"] if step["cwd"] is not None else cwd, env=_merge_env(env, step["env"]), stdin=stdin_text, @@ -155,8 +155,8 @@ async def _run_batch_steps( @tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) -async def run_command_batch( - commands: list[dict[str, object]] | str, +async def run_argv_batch( + steps: list[dict[str, object]] | str, *, cwd: str = ".", env: dict[str, str] | None = None, @@ -164,10 +164,10 @@ async def run_command_batch( ) -> str: """Run a serial batch of argv commands (no shell). - Each element of ``commands`` is an object:: + Each element of ``steps`` is an object:: { - "command": ["git", "status"], # required argv + "argv": ["git", "status"], # required argv "cwd": ".", # optional, else batch cwd "env": {"FOO": "1"}, # optional overlay "stdin": "...", # optional; unused if previous pipe_out @@ -181,15 +181,15 @@ async def run_command_batch( Default ``stop_on_error`` is true. Max 20 steps; total output budget 32k chars. """ try: - raw_steps = _normalize_commands(commands) + raw_steps = _normalize_steps(steps) if not raw_steps: - return "error: commands must not be empty" + return "error: steps must not be empty" if len(raw_steps) > DEFAULT_MAX_BATCH_STEPS: - return f"error: at most {DEFAULT_MAX_BATCH_STEPS} commands per batch" + return f"error: at most {DEFAULT_MAX_BATCH_STEPS} steps per batch" steps = [_parse_step(item, i) for i, item in enumerate(raw_steps)] if steps[-1]["pipe_out"]: - return "error: last command has pipe_out=true but there is no next step" + return "error: last step has pipe_out=true but there is no next step" results, stopped_early = await _run_batch_steps(steps, cwd=cwd, env=env, stop_on_error=stop_on_error) body = "\n".join( diff --git a/tests/test_tools/test_danger.py b/tests/test_tools/test_danger.py index 22fe9cd..cf0beb7 100644 --- a/tests/test_tools/test_danger.py +++ b/tests/test_tools/test_danger.py @@ -48,23 +48,23 @@ def test_classify_safe_tools() -> None: assert classify_danger("run_argv", {"argv": ["ls", "-la"]}) is None -def test_classify_run_command_batch_risky() -> None: +def test_classify_run_argv_batch_risky() -> None: reason = classify_danger( - "run_command_batch", + "run_argv_batch", { - "commands": [ - {"command": ["echo", "ok"]}, - {"command": ["bash", "-c", "echo risky"]}, + "steps": [ + {"argv": ["echo", "ok"]}, + {"argv": ["bash", "-c", "echo risky"]}, ] }, ) assert reason is not None - assert "run_command_batch" in reason + assert "run_argv_batch" in reason assert "risky" in reason or "bash" in reason assert ( classify_danger( - "run_command_batch", - {"commands": [{"command": ["echo", "ok"]}]}, + "run_argv_batch", + {"steps": [{"argv": ["echo", "ok"]}]}, ) is None ) diff --git a/tests/test_tools/test_process.py b/tests/test_tools/test_process.py index 4c89843..2975ecf 100644 --- a/tests/test_tools/test_process.py +++ b/tests/test_tools/test_process.py @@ -10,7 +10,7 @@ from plyngent.tools.process import ( open_pty, read_pty, run_argv, - run_command_batch, + run_argv_batch, write_pty, write_pty_keys, ) @@ -78,13 +78,13 @@ async def test_open_pty_string_command_error(workspace: object) -> None: assert "only accepts an array" in out -async def test_run_command_batch_serial(workspace: object) -> None: +async def test_run_argv_batch_serial(workspace: object) -> None: del workspace out = await call_async( - run_command_batch, + run_argv_batch, [ - {"command": _py("print('a')")}, - {"command": _py("print('b')")}, + {"argv": _py("print('a')")}, + {"argv": _py("print('b')")}, ], ) assert "steps=2" in out @@ -95,13 +95,13 @@ async def test_run_command_batch_serial(workspace: object) -> None: assert "a" in out and "b" in out -async def test_run_command_batch_stop_on_error(workspace: object) -> None: +async def test_run_argv_batch_stop_on_error(workspace: object) -> None: del workspace out = await call_async( - run_command_batch, + run_argv_batch, [ - {"command": _py("import sys; sys.exit(2)")}, - {"command": _py("print('should-not-run')")}, + {"argv": _py("import sys; sys.exit(2)")}, + {"argv": _py("print('should-not-run')")}, ], stop_on_error=True, ) @@ -110,17 +110,17 @@ async def test_run_command_batch_stop_on_error(workspace: object) -> None: assert "should-not-run" not in out -async def test_run_command_batch_pipe_out(workspace: object) -> None: +async def test_run_argv_batch_pipe_out(workspace: object) -> None: del workspace out = await call_async( - run_command_batch, + run_argv_batch, [ { - "command": _py("print('piped-payload', end='')"), + "argv": _py("print('piped-payload', end='')"), "pipe_out": True, }, { - "command": _py("import sys; print(sys.stdin.read())"), + "argv": _py("import sys; print(sys.stdin.read())"), }, ], ) @@ -129,27 +129,27 @@ async def test_run_command_batch_pipe_out(workspace: object) -> None: assert "pipe_out=true" in out -async def test_run_command_batch_last_pipe_out_error(workspace: object) -> None: +async def test_run_argv_batch_last_pipe_out_error(workspace: object) -> None: del workspace out = await call_async( - run_command_batch, - [{"command": _py("print(1)"), "pipe_out": True}], + run_argv_batch, + [{"argv": _py("print(1)"), "pipe_out": True}], ) assert "error:" in out assert "pipe_out" in out -async def test_run_command_batch_mix_stderr(workspace: object) -> None: +async def test_run_argv_batch_mix_stderr(workspace: object) -> None: del workspace out = await call_async( - run_command_batch, + run_argv_batch, [ { - "command": _py("import sys; sys.stdout.write('out'); sys.stderr.write('err')"), + "argv": _py("import sys; sys.stdout.write('out'); sys.stderr.write('err')"), "mix_stderr": True, "pipe_out": True, }, - {"command": _py("import sys; print(repr(sys.stdin.read()))")}, + {"argv": _py("import sys; print(repr(sys.stdin.read()))")}, ], ) assert "ran=2" in out