mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
core/config: add the [trash] section
`[trash]` owns the one policy `delete_path` needs from the human: whether a delete is a move to the desktop's trash (`enabled = true`, the default) or a removal that cannot be undone (`false`, which is what the tool did before). Unknown keys degrade to the defaults, the way `[mcp]` does, so a typo cannot silently switch a destructive policy on. `reload()` re-reads the section.
This commit is contained in:
@@ -25,6 +25,7 @@ from .models import ProviderConfig as ProviderConfig
|
||||
from .models import ReasoningEffortConfig as ReasoningEffortConfig
|
||||
from .models import SkillDiscoverySource as SkillDiscoverySource
|
||||
from .models import SkillsConfig as SkillsConfig
|
||||
from .models import TrashConfig as TrashConfig
|
||||
from .models import compose_agent_system_content as compose_agent_system_content
|
||||
from .path import get_default_path as _get_default_path
|
||||
from .reasoning import ReasoningConfig as ReasoningConfig
|
||||
|
||||
@@ -304,6 +304,21 @@ class NotifyConfig(Struct, omit_defaults=True):
|
||||
protocol: str = ""
|
||||
|
||||
|
||||
class TrashConfig(Struct, omit_defaults=True):
|
||||
"""Whether a delete lands in the system trash or is final.
|
||||
|
||||
``enabled`` (the default) sends every path ``delete_path`` removes to the
|
||||
desktop's own trash — the freedesktop trash on Linux, the Finder trash on
|
||||
macOS, the Recycle Bin on Windows — so a deletion can be undone there.
|
||||
``false`` restores the old behaviour of removing the path outright.
|
||||
|
||||
Scratch paths under the OS temporary directory, and a call that passes
|
||||
``permanent=true``, are deleted for good either way.
|
||||
"""
|
||||
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class ModelConfig(Struct, omit_defaults=True):
|
||||
"""Capability flags and optional routing overrides for a model.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from .models import (
|
||||
PluginsConfig,
|
||||
Provider,
|
||||
SkillsConfig,
|
||||
TrashConfig,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -83,6 +84,22 @@ def _parse_notify(raw: dict[str, object]) -> NotifyConfig:
|
||||
return config
|
||||
|
||||
|
||||
def _parse_trash(raw: dict[str, object]) -> TrashConfig:
|
||||
"""Parse the ``[trash]`` section, falling back to defaults.
|
||||
|
||||
Unknown keys are rejected like ``[mcp]`` does: a typo leaves deletes going
|
||||
to the trash instead of half-applying a policy about destructive ones.
|
||||
"""
|
||||
try:
|
||||
config = msgspec.convert(raw, TrashConfig)
|
||||
except msgspec.ValidationError:
|
||||
return TrashConfig()
|
||||
known = set(TrashConfig.__struct_fields__)
|
||||
if set(raw) - known:
|
||||
return TrashConfig()
|
||||
return config
|
||||
|
||||
|
||||
def _parse_mcp(raw: dict[str, object]) -> McpConfig:
|
||||
"""Parse the ``[mcp]`` section, falling back to defaults.
|
||||
|
||||
@@ -178,6 +195,7 @@ class ConfigStore:
|
||||
_mcp: McpConfig
|
||||
_skills: SkillsConfig
|
||||
_notify: NotifyConfig
|
||||
_trash: TrashConfig
|
||||
_providers: dict[str, Provider]
|
||||
_bad_providers: dict[str, object]
|
||||
_recoverable_providers: dict[str, Provider]
|
||||
@@ -193,6 +211,7 @@ class ConfigStore:
|
||||
self._mcp = _parse_mcp(cast("dict[str, object]", raw.get("mcp", {})))
|
||||
self._skills = _parse_skills(cast("dict[str, object]", raw.get("skills", {})))
|
||||
self._notify = _parse_notify(cast("dict[str, object]", raw.get("notify", {})))
|
||||
self._trash = _parse_trash(cast("dict[str, object]", raw.get("trash", {})))
|
||||
self._providers, self._bad_providers, self._recoverable_providers = _parse_providers(document)
|
||||
|
||||
@property
|
||||
@@ -264,6 +283,13 @@ class ConfigStore:
|
||||
"""Typed notifications section (grace, channels, actions)."""
|
||||
return self._notify
|
||||
|
||||
# -- trash (read-only) --
|
||||
|
||||
@property
|
||||
def trash_config(self) -> TrashConfig:
|
||||
"""Typed trash section (whether a delete can be undone)."""
|
||||
return self._trash
|
||||
|
||||
def set_plugins_enable(self, names: Sequence[str]) -> PluginsConfig:
|
||||
"""Replace the plugin enable list (in-memory; call :meth:`write` to persist)."""
|
||||
cleaned = [name.strip() for name in names if name.strip()]
|
||||
@@ -469,6 +495,7 @@ class ConfigStore:
|
||||
self._networking = _parse_networking(cast("dict[str, object]", raw.get("networking", {})))
|
||||
self._mcp = _parse_mcp(cast("dict[str, object]", raw.get("mcp", {})))
|
||||
self._notify = _parse_notify(cast("dict[str, object]", raw.get("notify", {})))
|
||||
self._trash = _parse_trash(cast("dict[str, object]", raw.get("trash", {})))
|
||||
self._providers, self._bad_providers, self._recoverable_providers = _parse_providers(self._document)
|
||||
|
||||
# -- internal sync helpers --
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from plyngent.config import load
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_trash_section_defaults(tmp_path: Path) -> None:
|
||||
"""Deletes go to the system trash unless the section says otherwise."""
|
||||
path = tmp_path / "c.toml"
|
||||
_ = path.write_text("", encoding="utf-8")
|
||||
assert load(path).trash_config.enabled is True
|
||||
|
||||
|
||||
def test_trash_section_parse(tmp_path: Path) -> None:
|
||||
path = tmp_path / "c.toml"
|
||||
_ = path.write_text("[trash]\nenabled = false\n", encoding="utf-8")
|
||||
assert load(path).trash_config.enabled is False
|
||||
|
||||
|
||||
def test_trash_section_typo_falls_back(tmp_path: Path) -> None:
|
||||
"""An unknown key or a bad type degrades to the defaults, like ``[mcp]``."""
|
||||
path = tmp_path / "c.toml"
|
||||
_ = path.write_text("[trash]\nenable = false\n", encoding="utf-8")
|
||||
assert load(path).trash_config.enabled is True
|
||||
|
||||
path2 = tmp_path / "d.toml"
|
||||
_ = path2.write_text('[trash]\nenabled = "no"\n', encoding="utf-8")
|
||||
assert load(path2).trash_config.enabled is True
|
||||
|
||||
|
||||
def test_trash_section_survives_reload(tmp_path: Path) -> None:
|
||||
path = tmp_path / "c.toml"
|
||||
_ = path.write_text("[trash]\nenabled = false\n", encoding="utf-8")
|
||||
store = load(path)
|
||||
assert store.trash_config.enabled is False
|
||||
_ = path.write_text("[trash]\nenabled = true\n", encoding="utf-8")
|
||||
store.reload()
|
||||
assert store.trash_config.enabled is True
|
||||
Reference in New Issue
Block a user