mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
core/tools: rename run_command_batch to run_argv_batch (steps/argv keys)
This commit is contained in:
@@ -42,7 +42,7 @@ from .process import close_pty as close_pty
|
|||||||
from .process import open_pty as open_pty
|
from .process import open_pty as open_pty
|
||||||
from .process import read_pty as read_pty
|
from .process import read_pty as read_pty
|
||||||
from .process import run_argv as run_argv
|
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 as write_pty
|
||||||
from .process import write_pty_keys as write_pty_keys
|
from .process import write_pty_keys as write_pty_keys
|
||||||
from .temp_workspace import cleanup_temporary_workspaces as cleanup_temporary_workspaces
|
from .temp_workspace import cleanup_temporary_workspaces as cleanup_temporary_workspaces
|
||||||
|
|||||||
@@ -192,20 +192,20 @@ def _batch_step_argv(item: object) -> list[str] | None:
|
|||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
return None
|
return None
|
||||||
step = cast("dict[str, object]", item)
|
step = cast("dict[str, object]", item)
|
||||||
command = step.get("command")
|
argv = step.get("argv")
|
||||||
if not isinstance(command, list) or not command:
|
if not isinstance(argv, list) or not argv:
|
||||||
return None
|
return None
|
||||||
argv: list[str] = []
|
parts: list[str] = []
|
||||||
for part in cast("list[object]", command):
|
for part in cast("list[object]", argv):
|
||||||
if not isinstance(part, str):
|
if not isinstance(part, str):
|
||||||
return None
|
return None
|
||||||
argv.append(part)
|
parts.append(part)
|
||||||
return argv or None
|
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."""
|
"""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):
|
if isinstance(raw, str):
|
||||||
try:
|
try:
|
||||||
loaded: object = json.loads(raw)
|
loaded: object = json.loads(raw)
|
||||||
@@ -218,11 +218,11 @@ def _run_command_batch_reason(args: Mapping[str, object]) -> str | None:
|
|||||||
reason
|
reason
|
||||||
for index, item in enumerate(cast("list[object]", raw))
|
for index, item in enumerate(cast("list[object]", raw))
|
||||||
if (argv := _batch_step_argv(item)) is not None
|
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:
|
if not risky:
|
||||||
return None
|
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)
|
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)
|
return _write_file_reason(args)
|
||||||
if name == "run_argv":
|
if name == "run_argv":
|
||||||
return _run_argv_reason(args)
|
return _run_argv_reason(args)
|
||||||
if name == "run_command_batch":
|
if name == "run_argv_batch":
|
||||||
return _run_command_batch_reason(args)
|
return _run_argv_batch_reason(args)
|
||||||
if name == "open_pty":
|
if name == "open_pty":
|
||||||
return _open_pty_reason(args)
|
return _open_pty_reason(args)
|
||||||
if name == "fetch":
|
if name == "fetch":
|
||||||
|
|||||||
@@ -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 .pty_terminal import sanitize_pty_output_for_tool as sanitize_pty_output_for_tool
|
||||||
from .read_pty import read_pty as read_pty
|
from .read_pty import read_pty as read_pty
|
||||||
from .run_argv import run_argv as run_argv
|
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 import write_pty as write_pty
|
||||||
from .write_pty_keys import write_pty_keys as write_pty_keys
|
from .write_pty_keys import write_pty_keys as write_pty_keys
|
||||||
|
|
||||||
PROCESS_TOOLS = [
|
PROCESS_TOOLS = [
|
||||||
run_argv,
|
run_argv,
|
||||||
run_command_batch,
|
run_argv_batch,
|
||||||
open_pty,
|
open_pty,
|
||||||
read_pty,
|
read_pty,
|
||||||
write_pty,
|
write_pty,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ async def execute_argv(
|
|||||||
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
|
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
|
||||||
mix_stderr: bool = False,
|
mix_stderr: bool = False,
|
||||||
) -> CommandStepResult:
|
) -> 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:
|
if not command:
|
||||||
msg = "command argv must not be empty"
|
msg = "command argv must not be empty"
|
||||||
raise WorkspaceError(msg)
|
raise WorkspaceError(msg)
|
||||||
|
|||||||
+24
-24
@@ -50,23 +50,23 @@ def _as_str_dict(value: object) -> dict[str, str] | None:
|
|||||||
|
|
||||||
def _parse_step(raw: object, index: int) -> dict[str, Any]:
|
def _parse_step(raw: object, index: int) -> dict[str, Any]:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
msg = f"commands[{index}] must be an object"
|
msg = f"steps[{index}] must be an object"
|
||||||
raise WorkspaceError(msg)
|
raise WorkspaceError(msg)
|
||||||
data = cast("dict[str, object]", raw)
|
data = cast("dict[str, object]", raw)
|
||||||
command = data.get("command")
|
argv = data.get("argv")
|
||||||
if not isinstance(command, list) or not command:
|
if not isinstance(argv, list) or not argv:
|
||||||
msg = f"commands[{index}].command must be a non-empty argv list"
|
msg = f"steps[{index}].argv must be a non-empty argv list"
|
||||||
raise WorkspaceError(msg)
|
raise WorkspaceError(msg)
|
||||||
command_parts = cast("list[object]", command)
|
argv_parts = cast("list[object]", argv)
|
||||||
argv = [part for part in command_parts if isinstance(part, str)]
|
argv_str = [part for part in argv_parts if isinstance(part, str)]
|
||||||
if len(argv) != len(command_parts):
|
if len(argv_str) != len(argv_parts):
|
||||||
msg = f"commands[{index}].command must be a list of strings"
|
msg = f"steps[{index}].argv must be a list of strings"
|
||||||
raise WorkspaceError(msg)
|
raise WorkspaceError(msg)
|
||||||
cwd = data.get("cwd")
|
cwd = data.get("cwd")
|
||||||
stdin = data.get("stdin")
|
stdin = data.get("stdin")
|
||||||
stop = data.get("stop_on_error", None)
|
stop = data.get("stop_on_error", None)
|
||||||
return {
|
return {
|
||||||
"command": argv,
|
"argv": argv_str,
|
||||||
"cwd": str(cwd) if isinstance(cwd, str) and cwd else None,
|
"cwd": str(cwd) if isinstance(cwd, str) and cwd else None,
|
||||||
"env": _as_str_dict(data.get("env")),
|
"env": _as_str_dict(data.get("env")),
|
||||||
"stdin": None if stdin is None else str(stdin),
|
"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]:
|
def _normalize_steps(steps: list[dict[str, object]] | str) -> list[object]:
|
||||||
if isinstance(commands, str):
|
if isinstance(steps, str):
|
||||||
try:
|
try:
|
||||||
loaded: object = json.loads(commands)
|
loaded: object = json.loads(steps)
|
||||||
except json.JSONDecodeError as exc:
|
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
|
raise WorkspaceError(msg) from exc
|
||||||
if not isinstance(loaded, list):
|
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)
|
raise WorkspaceError(msg)
|
||||||
return cast("list[object]", loaded)
|
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:
|
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):
|
for index, step in enumerate(steps):
|
||||||
stdin_text = prev_capture if prev_piped else step["stdin"]
|
stdin_text = prev_capture if prev_piped else step["stdin"]
|
||||||
result = await execute_argv(
|
result = await execute_argv(
|
||||||
step["command"],
|
step["argv"],
|
||||||
cwd=step["cwd"] if step["cwd"] is not None else cwd,
|
cwd=step["cwd"] if step["cwd"] is not None else cwd,
|
||||||
env=_merge_env(env, step["env"]),
|
env=_merge_env(env, step["env"]),
|
||||||
stdin=stdin_text,
|
stdin=stdin_text,
|
||||||
@@ -155,8 +155,8 @@ async def _run_batch_steps(
|
|||||||
|
|
||||||
|
|
||||||
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO)
|
||||||
async def run_command_batch(
|
async def run_argv_batch(
|
||||||
commands: list[dict[str, object]] | str,
|
steps: list[dict[str, object]] | str,
|
||||||
*,
|
*,
|
||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
env: dict[str, str] | None = None,
|
env: dict[str, str] | None = None,
|
||||||
@@ -164,10 +164,10 @@ async def run_command_batch(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Run a serial batch of argv commands (no shell).
|
"""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
|
"cwd": ".", # optional, else batch cwd
|
||||||
"env": {"FOO": "1"}, # optional overlay
|
"env": {"FOO": "1"}, # optional overlay
|
||||||
"stdin": "...", # optional; unused if previous pipe_out
|
"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.
|
Default ``stop_on_error`` is true. Max 20 steps; total output budget 32k chars.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
raw_steps = _normalize_commands(commands)
|
raw_steps = _normalize_steps(steps)
|
||||||
if not raw_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:
|
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)]
|
steps = [_parse_step(item, i) for i, item in enumerate(raw_steps)]
|
||||||
if steps[-1]["pipe_out"]:
|
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)
|
results, stopped_early = await _run_batch_steps(steps, cwd=cwd, env=env, stop_on_error=stop_on_error)
|
||||||
body = "\n".join(
|
body = "\n".join(
|
||||||
@@ -48,23 +48,23 @@ def test_classify_safe_tools() -> None:
|
|||||||
assert classify_danger("run_argv", {"argv": ["ls", "-la"]}) is 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(
|
reason = classify_danger(
|
||||||
"run_command_batch",
|
"run_argv_batch",
|
||||||
{
|
{
|
||||||
"commands": [
|
"steps": [
|
||||||
{"command": ["echo", "ok"]},
|
{"argv": ["echo", "ok"]},
|
||||||
{"command": ["bash", "-c", "echo risky"]},
|
{"argv": ["bash", "-c", "echo risky"]},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert reason is not None
|
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 "risky" in reason or "bash" in reason
|
||||||
assert (
|
assert (
|
||||||
classify_danger(
|
classify_danger(
|
||||||
"run_command_batch",
|
"run_argv_batch",
|
||||||
{"commands": [{"command": ["echo", "ok"]}]},
|
{"steps": [{"argv": ["echo", "ok"]}]},
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from plyngent.tools.process import (
|
|||||||
open_pty,
|
open_pty,
|
||||||
read_pty,
|
read_pty,
|
||||||
run_argv,
|
run_argv,
|
||||||
run_command_batch,
|
run_argv_batch,
|
||||||
write_pty,
|
write_pty,
|
||||||
write_pty_keys,
|
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
|
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
|
del workspace
|
||||||
out = await call_async(
|
out = await call_async(
|
||||||
run_command_batch,
|
run_argv_batch,
|
||||||
[
|
[
|
||||||
{"command": _py("print('a')")},
|
{"argv": _py("print('a')")},
|
||||||
{"command": _py("print('b')")},
|
{"argv": _py("print('b')")},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert "steps=2" in out
|
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
|
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
|
del workspace
|
||||||
out = await call_async(
|
out = await call_async(
|
||||||
run_command_batch,
|
run_argv_batch,
|
||||||
[
|
[
|
||||||
{"command": _py("import sys; sys.exit(2)")},
|
{"argv": _py("import sys; sys.exit(2)")},
|
||||||
{"command": _py("print('should-not-run')")},
|
{"argv": _py("print('should-not-run')")},
|
||||||
],
|
],
|
||||||
stop_on_error=True,
|
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
|
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
|
del workspace
|
||||||
out = await call_async(
|
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,
|
"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
|
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
|
del workspace
|
||||||
out = await call_async(
|
out = await call_async(
|
||||||
run_command_batch,
|
run_argv_batch,
|
||||||
[{"command": _py("print(1)"), "pipe_out": True}],
|
[{"argv": _py("print(1)"), "pipe_out": True}],
|
||||||
)
|
)
|
||||||
assert "error:" in out
|
assert "error:" in out
|
||||||
assert "pipe_out" 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
|
del workspace
|
||||||
out = await call_async(
|
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,
|
"mix_stderr": True,
|
||||||
"pipe_out": 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
|
assert "ran=2" in out
|
||||||
|
|||||||
Reference in New Issue
Block a user