core/cli: hand the trash switch to the tools

`[trash] enabled` now reaches `delete_path` through the bound instance's
workspace policy, the same channel `[agent] path_denylist` already travels: set
when the session state is built and re-read by `reload_config_from_disk`, so a
config reload in the middle of a chat changes the next delete too.

The policy flag is what `delete_path` consults, so a host that never sets it
gets the safe default (deletes go to the trash) instead of a silent no-op.
This commit is contained in:
2026-10-21 20:22:50 +08:00
parent c4a287c464
commit 17813281d8
7 changed files with 54 additions and 4 deletions
+3 -1
View File
@@ -347,8 +347,10 @@ async def _run_chat( # noqa: C901, PLR0915 — chat orchestration
quiet=quiet,
mcp_manager=mcp_manager,
)
# Path denylist and policy confirm live on instance.workspace (no process bag).
# Path denylist, trash switch and policy confirm live on
# instance.workspace (no process bag).
state.instance_state.workspace.path_denylist = tuple(store.agent_config.path_denylist or ())
state.instance_state.workspace.trash_enabled = store.trash_config.enabled
from plyngent.tools.net import set_ssrf_assume_public_cidrs
# Clash Fake-IP etc.: treat configured CIDRs as public for fetch_local SSRF.
+1
View File
@@ -655,6 +655,7 @@ class ReplState:
self.config.reload()
self.instance_state.workspace.path_denylist = tuple(self.config.agent_config.path_denylist or ())
self.instance_state.workspace.trash_enabled = self.config.trash_config.enabled
# Roots and their switches came from the file that was just re-read.
self._install_skills()
+2
View File
@@ -101,6 +101,7 @@ from .workspace import denylist_match as denylist_match
from .workspace import get_command_denylist as get_command_denylist
from .workspace import get_path_denylist as get_path_denylist
from .workspace import get_policy_confirm_timeout as get_policy_confirm_timeout
from .workspace import get_trash_enabled as get_trash_enabled
from .workspace import get_workspace_root as get_workspace_root
from .workspace import grant_policy_command as grant_policy_command
from .workspace import granted_access_mode as granted_access_mode
@@ -116,5 +117,6 @@ from .workspace import set_path_denylist as set_path_denylist
from .workspace import set_policy_confirm_hook as set_policy_confirm_hook
from .workspace import set_policy_confirm_timeout as set_policy_confirm_timeout
from .workspace import set_static_read_roots as set_static_read_roots
from .workspace import set_trash_enabled as set_trash_enabled
from .workspace import set_workspace_root as set_workspace_root
from .workspace import within_workspace_roots as within_workspace_roots
+10 -3
View File
@@ -5,7 +5,13 @@ from pathlib import Path
from plyngent.agent import ToolTag, invalidate_lineno_read, tool
from plyngent.tools.trash import move_to_trash, under_system_temp
from plyngent.tools.workspace import AccessMode, WorkspaceError, get_workspace_root, resolve_path
from plyngent.tools.workspace import (
AccessMode,
WorkspaceError,
get_trash_enabled,
get_workspace_root,
resolve_path,
)
def _kind(path: Path) -> str:
@@ -164,7 +170,8 @@ async def delete_path(path: str, *, recursive: bool = False, permanent: bool = F
path, nothing is deleted and the error says how to delete on purpose.
Paths under the OS temporary directory are deleted for good either way: they
are scratch space, not something to restore.
are scratch space, not something to restore. A host with ``[trash] enabled =
false`` removes the path outright, as ``permanent=true`` does.
"""
try:
target = resolve_path(path, required=AccessMode.WRITE)
@@ -181,7 +188,7 @@ async def delete_path(path: str, *, recursive: bool = False, permanent: bool = F
target,
path,
recursive=recursive,
permanent=permanent or under_system_temp(target),
permanent=permanent or not get_trash_enabled() or under_system_temp(target),
)
except OSError as exc:
return f"error: delete failed: {exc}"
+12
View File
@@ -120,6 +120,8 @@ class WorkspacePolicy:
# read them, nothing may write them. Not a grant, so they stay out of
# ``/grants``; writes go through the owning tool's own confirm.
static_read: list[Path] = field(default_factory=list)
# ``[trash] enabled``: deletes go to the system trash unless cleared.
trash_enabled: bool = True
def _bound_instance() -> InstanceState | None:
@@ -188,6 +190,16 @@ def get_path_denylist() -> tuple[str, ...]:
return active_workspace_policy().path_denylist
def set_trash_enabled(*, enabled: bool) -> None:
"""Set whether ``delete_path`` moves to the system trash (``[trash]``)."""
active_workspace_policy().trash_enabled = bool(enabled)
def get_trash_enabled() -> bool:
"""Whether deletes go to the system trash rather than being final."""
return active_workspace_policy().trash_enabled
def set_command_denylist(names: list[str] | tuple[str, ...] | frozenset[str] | None) -> None:
"""Set denied command basenames (None restores defaults)."""
policy = active_workspace_policy()