mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/tools: shell confirm argv uses $(command); full -c body indented
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import shlex
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
from plyngent.tools.workspace import WorkspaceError, resolve_path
|
from plyngent.tools.workspace import WorkspaceError, resolve_path
|
||||||
@@ -8,8 +9,6 @@ from plyngent.tools.workspace import WorkspaceError, resolve_path
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
|
|
||||||
_DISPLAY_ARGV_MAX = 400
|
|
||||||
_CODE_PREVIEW_MAX = 240
|
|
||||||
|
|
||||||
_SHELL_BASENAMES: frozenset[str] = frozenset(
|
_SHELL_BASENAMES: frozenset[str] = frozenset(
|
||||||
{
|
{
|
||||||
@@ -81,30 +80,58 @@ def _find_dash_c_code(argv: Sequence[str]) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _indent_block(text: str, *, prefix: str = " ") -> str:
|
||||||
|
"""Indent every line of *text* (for multi-line -c bodies in confirm prompts)."""
|
||||||
|
if not text:
|
||||||
|
return prefix
|
||||||
|
return "\n".join(
|
||||||
|
prefix + line if line else prefix.rstrip() for line in text.splitlines()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_argv_for_confirm(argv: Sequence[str], *, code: str | None) -> str:
|
||||||
|
"""One-line argv summary; replace -c payload with ``$(command)`` when present."""
|
||||||
|
if code is None:
|
||||||
|
return shlex.join(list(argv))
|
||||||
|
parts: list[str] = []
|
||||||
|
skip_next = False
|
||||||
|
for part in argv:
|
||||||
|
if skip_next:
|
||||||
|
skip_next = False
|
||||||
|
continue
|
||||||
|
if part == "-c":
|
||||||
|
parts.append("-c")
|
||||||
|
parts.append("$(command)")
|
||||||
|
skip_next = True
|
||||||
|
continue
|
||||||
|
parts.append(part)
|
||||||
|
return shlex.join(parts)
|
||||||
|
|
||||||
|
|
||||||
def _shell_or_dash_c_reason(argv: Sequence[str], *, via: str) -> str | None:
|
def _shell_or_dash_c_reason(argv: Sequence[str], *, via: str) -> str | None:
|
||||||
"""Confirm interactive shells/REPLs and ``-c`` one-liners so the user can inspect argv.
|
"""Confirm interactive shells and ``*-c`` one-liners so the user can inspect argv.
|
||||||
|
|
||||||
Multi-line reason (shown inside the CLI confirm box). ``via`` is a short
|
Multi-line reason (shown inside the CLI confirm box). ``via`` is a short
|
||||||
label (tool name), not repeated on every line.
|
label such as ``run_command`` or ``open_pty``.
|
||||||
|
|
||||||
|
For ``-c`` scripts, argv shows ``$(command)`` instead of inlining the body;
|
||||||
|
the full script is printed below untruncated, with every line indented.
|
||||||
"""
|
"""
|
||||||
if not argv:
|
if not argv:
|
||||||
return None
|
return None
|
||||||
base = _basename(argv[0])
|
base = _basename(argv[0])
|
||||||
display = " ".join(argv)
|
|
||||||
if len(display) > _DISPLAY_ARGV_MAX:
|
|
||||||
display = display[:_DISPLAY_ARGV_MAX] + "…"
|
|
||||||
|
|
||||||
code = _find_dash_c_code(argv)
|
code = _find_dash_c_code(argv)
|
||||||
|
display = _format_argv_for_confirm(argv, code=code)
|
||||||
|
|
||||||
if code is not None:
|
if code is not None:
|
||||||
preview = code if len(code) <= _CODE_PREVIEW_MAX else code[:_CODE_PREVIEW_MAX] + "…"
|
body = _indent_block(code, prefix=" ")
|
||||||
return f"{via}: {base} -c (review code before allow)\nargv:\n {display}\n-c code:\n {preview}"
|
return f"{via}: {base} -c (review code before allow)\n argv: {display}\n command:\n{body}"
|
||||||
|
|
||||||
if base in _SHELL_BASENAMES and len(argv) == 1:
|
if base in _SHELL_BASENAMES and len(argv) == 1:
|
||||||
return f"{via}: interactive {base!r} (review before allow)\nargv:\n {display}"
|
return f"{via}: interactive {base!r} (review before allow)\n argv: {display}"
|
||||||
|
|
||||||
# e.g. python -i, bash --login without -c still needs a glance.
|
if base in _SHELL_BASENAMES and "-c" not in argv[1:]:
|
||||||
if base in _SHELL_BASENAMES:
|
return f"{via}: shell/runtime {base!r} without -c (review before allow)\n argv: {display}"
|
||||||
return f"{via}: shell/runtime {base!r} (review before allow)\nargv:\n {display}"
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ def test_classify_delete_and_move() -> None:
|
|||||||
def test_classify_copy() -> None:
|
def test_classify_copy() -> None:
|
||||||
# Without overwrite flag, copy is not soft-confirmed.
|
# Without overwrite flag, copy is not soft-confirmed.
|
||||||
assert classify_danger("copy_path", {"src": "a", "dst": "b"}) is None
|
assert classify_danger("copy_path", {"src": "a", "dst": "b"}) is None
|
||||||
assert classify_danger(
|
assert classify_danger("copy_path", {"src": "a", "dst": "b", "overwrite": False}) is None
|
||||||
"copy_path", {"src": "a", "dst": "b", "overwrite": False}
|
|
||||||
) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_classify_write_overwrite_only(tmp_path: Path) -> None:
|
def test_classify_write_overwrite_only(tmp_path: Path) -> None:
|
||||||
@@ -115,3 +113,22 @@ async def test_confirm_deny_with_comment() -> None:
|
|||||||
assert "denied" in out
|
assert "denied" in out
|
||||||
assert "user comment:" in out
|
assert "user comment:" in out
|
||||||
assert "too destructive" in out
|
assert "too destructive" in out
|
||||||
|
|
||||||
|
def test_shell_confirm_formats_command_placeholder() -> None:
|
||||||
|
script = "line1" + "
|
||||||
|
" + "line2" + "
|
||||||
|
" + "line3"
|
||||||
|
reason = classify_danger(
|
||||||
|
"run_command",
|
||||||
|
{"command": ["bash", "-c", script]},
|
||||||
|
)
|
||||||
|
assert reason is not None
|
||||||
|
assert "$(command)" in reason
|
||||||
|
assert "line1" in reason and "line2" in reason and "line3" in reason
|
||||||
|
argv_line = next(ln for ln in reason.splitlines() if "argv:" in ln)
|
||||||
|
assert "line1" not in argv_line
|
||||||
|
lines = reason.splitlines()
|
||||||
|
idx = lines.index(" command:")
|
||||||
|
body = lines[idx + 1 :]
|
||||||
|
assert body
|
||||||
|
assert all(ln.startswith(" ") for ln in body)
|
||||||
|
|||||||
Reference in New Issue
Block a user