core/config: top-level [plugins] enable/disable allowlist

This commit is contained in:
2026-07-24 16:19:50 +08:00
parent 278c930fae
commit 4215a8cea0
9 changed files with 100 additions and 38 deletions
+4 -6
View File
@@ -212,17 +212,15 @@ class ReplState:
from plyngent.tools.danger import classify_danger
from plyngent.tools.plugins import load_plugin_tools
agent_cfg = self.config.agent_config
plugins_cfg = self.config.plugins_config
catalog = register_builtin_tools()
# Allowlisted entry points (plugin ids); disable is plugin-id only, not tool names.
_ = load_plugin_tools(
agent_cfg.tool_plugins,
disable=agent_cfg.tool_plugins_disable,
plugins_cfg.enable,
disable=plugins_cfg.disable,
)
# Local surface: builtins + allowlisted plugins (import registers into catalog).
tools = catalog.select(surface="local")
disable = {name.strip() for name in agent_cfg.tool_plugins_disable if name.strip()}
if disable:
tools = [tool for tool in tools if tool.name not in disable]
yolo = self.effective_yolo() != "off"
# Always attach soft-confirm path so non-YOLO tools still prompt under YOLO mode.
return ToolRegistry(
+1
View File
@@ -15,6 +15,7 @@ from .models import HttpTimeoutConfig as HttpTimeoutConfig
from .models import ModelConfig as ModelConfig
from .models import OpenAICompatibleProvider as OpenAICompatibleProvider
from .models import OpenAIProvider as OpenAIProvider
from .models import PluginsConfig as PluginsConfig
from .models import ProviderConfig as ProviderConfig
from .models import compose_agent_system_content as compose_agent_system_content
from .path import get_default_path as _get_default_path
+15 -6
View File
@@ -87,12 +87,6 @@ class AgentConfig(Struct, omit_defaults=True):
path_denylist: list[str] = field(default_factory=list)
max_context_tokens: int = 200_000
# Third-party tool plugins (entry-point group ``plyngent.tools``).
# Default empty = load none. Use ``["*"]`` to load all discovered plugins.
tool_plugins: list[str] = field(default_factory=list)
# Entry-point names never loaded even when listed / ``*``.
tool_plugins_disable: list[str] = field(default_factory=list)
# How to inject todo stack nags into model context (see agent/todo_nag.py).
# developer | user | synthetic_tool | none (legacy "system" → developer)
todo_nag_strategy: str = "developer"
@@ -103,6 +97,21 @@ class AgentConfig(Struct, omit_defaults=True):
compact_seed_text: str = ""
class PluginsConfig(Struct, omit_defaults=True):
"""Third-party plugins (not tool-specific config).
Hosts allowlist plugin **entry-point names** (today: group ``plyngent.tools``;
other extension points may reuse the same allowlist later).
- ``enable`` empty / omitted → load **no** plugins (safe default).
- ``enable = ["*"]`` → load every discovered entry point for that group.
- ``disable`` always wins over ``enable`` / ``*``.
"""
enable: list[str] = field(default_factory=list)
disable: list[str] = field(default_factory=list)
class ModelConfig(Struct, omit_defaults=True):
"""Capability flags for a model within a provider."""
+29 -1
View File
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, cast
import msgspec
import tomlkit
from .models import AgentConfig, DatabaseConfig, ModelConfig, Provider
from .models import AgentConfig, DatabaseConfig, ModelConfig, PluginsConfig, Provider
if TYPE_CHECKING:
from collections.abc import Mapping, MutableMapping, Sequence
@@ -33,6 +33,14 @@ def _parse_agent(raw: dict[str, object]) -> AgentConfig:
return AgentConfig()
def _parse_plugins(raw: dict[str, object]) -> PluginsConfig:
"""Parse the ``[plugins]`` section, falling back to defaults."""
try:
return msgspec.convert(raw, PluginsConfig)
except msgspec.ValidationError:
return PluginsConfig()
def _parse_providers(
document: tomlkit.TOMLDocument,
) -> tuple[dict[str, Provider], dict[str, object], dict[str, Provider]]:
@@ -100,6 +108,7 @@ class ConfigStore:
_document: tomlkit.TOMLDocument
_database: DatabaseConfig
_agent: AgentConfig
_plugins: PluginsConfig
_providers: dict[str, Provider]
_bad_providers: dict[str, object]
_recoverable_providers: dict[str, Provider]
@@ -110,6 +119,7 @@ class ConfigStore:
raw: dict[str, object] = document.unwrap()
self._database = _parse_database(cast("dict[str, object]", raw.get("database", {})))
self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {})))
self._plugins = _parse_plugins(cast("dict[str, object]", raw.get("plugins", {})))
self._providers, self._bad_providers, self._recoverable_providers = _parse_providers(document)
@property
@@ -136,6 +146,18 @@ class ConfigStore:
"""Typed agent profile (system prompt, tool budgets, etc.)."""
return self._agent
# -- plugins (read-only) --
@property
def plugins(self) -> MappingProxyType[str, object]:
"""Read-only mapping view of plugin allowlist configuration."""
return MappingProxyType(msgspec.structs.asdict(self._plugins))
@property
def plugins_config(self) -> PluginsConfig:
"""Typed plugin allowlist (enable / disable entry-point names)."""
return self._plugins
# -- providers (read/write) --
@property
@@ -269,6 +291,7 @@ class ConfigStore:
raw: dict[str, object] = self._document.unwrap()
self._database = _parse_database(cast("dict[str, object]", raw.get("database", {})))
self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {})))
self._plugins = _parse_plugins(cast("dict[str, object]", raw.get("plugins", {})))
self._providers, self._bad_providers, self._recoverable_providers = _parse_providers(self._document)
# -- internal sync helpers --
@@ -300,6 +323,10 @@ class ConfigStore:
"""Sync ``[agent]`` to the document."""
self._sync_section("agent", self._agent)
def _sync_plugins_section(self) -> None:
"""Sync ``[plugins]`` to the document."""
self._sync_section("plugins", self._plugins)
def _sync_providers_section(self) -> None:
"""Sync ``[providers]`` to the document (ready + recoverable)."""
section = self._toml_table("providers")
@@ -323,4 +350,5 @@ class ConfigStore:
"""Incrementally sync all sections into the document."""
self._sync_database_section()
self._sync_agent_section()
self._sync_plugins_section()
self._sync_providers_section()