mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/tools: classify destructive tools and clarify policy denies
Add classify_danger for delete/move/overwrite paths, get_path_denylist, and clearer path/command denial messages.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from .danger import classify_danger as classify_danger
|
||||
from .file import FILE_TOOLS as FILE_TOOLS
|
||||
from .file import copy_path as copy_path
|
||||
from .file import delete_path as delete_path
|
||||
@@ -21,6 +22,7 @@ from .workspace import WorkspaceError as WorkspaceError
|
||||
from .workspace import check_command_allowed as check_command_allowed
|
||||
from .workspace import clear_workspace_root as clear_workspace_root
|
||||
from .workspace import get_command_denylist as get_command_denylist
|
||||
from .workspace import get_path_denylist as get_path_denylist
|
||||
from .workspace import get_workspace_root as get_workspace_root
|
||||
from .workspace import resolve_path as resolve_path
|
||||
from .workspace import set_command_denylist as set_command_denylist
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from plyngent.tools.workspace import WorkspaceError, resolve_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def _write_file_reason(args: Mapping[str, object]) -> str | None:
|
||||
path = args.get("path")
|
||||
if not isinstance(path, str) or not path:
|
||||
return None
|
||||
try:
|
||||
target = resolve_path(path)
|
||||
except WorkspaceError:
|
||||
return None
|
||||
if target.exists() or target.is_symlink():
|
||||
return f"overwrite existing file {path!r}"
|
||||
return None
|
||||
|
||||
|
||||
def classify_danger(name: str, args: Mapping[str, object]) -> str | None:
|
||||
"""Return a short reason if ``name``/``args`` need user confirm, else ``None``.
|
||||
|
||||
Hard denylists (paths/commands) still raise independently. This only covers
|
||||
soft confirms for mutating tools that policy otherwise allows.
|
||||
"""
|
||||
reasons: dict[str, str | None] = {
|
||||
"delete_path": (
|
||||
f"delete path {args.get('path', '')!r} recursively"
|
||||
if bool(args.get("recursive", False))
|
||||
else f"delete path {args.get('path', '')!r}"
|
||||
),
|
||||
"move_path": f"move {args.get('src', '')!r} -> {args.get('dst', '')!r}",
|
||||
"copy_path": (
|
||||
f"copy with overwrite {args.get('src', '')!r} -> {args.get('dst', '')!r}"
|
||||
if bool(args.get("overwrite", False))
|
||||
else None
|
||||
),
|
||||
"write_file": _write_file_reason(args),
|
||||
}
|
||||
if name not in reasons:
|
||||
return None
|
||||
return reasons[name]
|
||||
@@ -68,6 +68,11 @@ def set_path_denylist(patterns: list[str] | tuple[str, ...] | None) -> None:
|
||||
_state.path_denylist = tuple(patterns or ())
|
||||
|
||||
|
||||
def get_path_denylist() -> tuple[str, ...]:
|
||||
"""Return the current path substring denylist."""
|
||||
return _state.path_denylist
|
||||
|
||||
|
||||
def set_command_denylist(names: list[str] | tuple[str, ...] | frozenset[str] | None) -> None:
|
||||
"""Set denied command basenames (None restores defaults)."""
|
||||
_state.command_denylist = DEFAULT_COMMAND_DENYLIST if names is None else frozenset(names)
|
||||
@@ -87,12 +92,12 @@ def resolve_path(path: str | Path) -> Path:
|
||||
try:
|
||||
_ = resolved.relative_to(root)
|
||||
except ValueError as exc:
|
||||
msg = f"path escapes workspace root: {path}"
|
||||
msg = f"path escapes workspace root ({root}): {path}"
|
||||
raise WorkspaceError(msg) from exc
|
||||
resolved_str = str(resolved)
|
||||
for pattern in _state.path_denylist:
|
||||
if pattern and pattern in resolved_str:
|
||||
msg = f"path denied by policy: {path}"
|
||||
msg = f"path denied by policy (matched {pattern!r}): {path}"
|
||||
raise WorkspaceError(msg)
|
||||
return resolved
|
||||
|
||||
@@ -104,5 +109,5 @@ def check_command_allowed(argv: list[str]) -> None:
|
||||
raise WorkspaceError(msg)
|
||||
binary = Path(argv[0]).name
|
||||
if binary in _state.command_denylist:
|
||||
msg = f"command denied by policy: {binary}"
|
||||
msg = f"command denied by policy (basename {binary!r} is blocked)"
|
||||
raise WorkspaceError(msg)
|
||||
|
||||
Reference in New Issue
Block a user