feat(plugins): Stage 2.1 — plugin framework and AI tool-use
Introduces a standalone plugin system where every integration lives as an independent Python script in ~/.pyra/plugins/, not hardcoded in core. Plugin framework (src/pyra/plugins/): - base.py: Tool dataclass, PyraPlugin Protocol, BasePlugin helper - loader.py: importlib-based discovery; one bad plugin never crashes pyra - registry.py: singleton aggregating tools, slash commands, system prompts - executor.py: approval gate — scans args, prompts y/N, scans result, logs - install.py: copies bundled_plugins/ to ~/.pyra/plugins/ on install Chat integration: - AI tool-use loop (litellm function calling, up to 10 iterations) - Plugin system prompt additions injected per session - Plugin slash commands merged with static commands CLI additions: - pyra plugin list/install/enable/disable/setup - pyra daemon start/stop/status/restart/install/uninstall (stubs for 2.4) Config: PluginConfig + DaemonConfig added to PyraConfig (backwards-compatible) Bootstrap: ~/.pyra/plugins/ and ~/.pyra/logs/ created on startup Security: tool args and results always injection-scanned; plugin dirs validated with assert_safe_path() before loading (symlink protection) Tests: 37 new tests (loader, registry, executor, plugin isolation security) 161 total, all passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -26,6 +26,9 @@ def tmp_pyra_home(tmp_path, monkeypatch):
|
||||
import pyra.security.injection as si
|
||||
import pyra.config.manager as cm
|
||||
|
||||
import pyra.plugins.loader as pl
|
||||
import pyra.plugins.executor as pe
|
||||
|
||||
b.VAULT_PATH = fake_home / "vault"
|
||||
b.BLOCKED_PREFIXES = [b.VAULT_PATH]
|
||||
mi._MEMORY_ROOT = fake_home / "memory"
|
||||
@@ -36,15 +39,22 @@ def tmp_pyra_home(tmp_path, monkeypatch):
|
||||
vw._KEYS_FILE = fake_home / "vault" / "secrets" / "api_keys.json"
|
||||
si._LOG_FILE = fake_home / "security.log"
|
||||
cm._CONFIG_PATH = fake_home / "config.yaml"
|
||||
pl._LOG_FILE = fake_home / "logs" / "plugin_errors.log"
|
||||
pe._LOG_FILE = fake_home / "logs" / "tool_executions.log"
|
||||
|
||||
# Bootstrap the directory structure
|
||||
from pyra.config.dirs import bootstrap
|
||||
(fake_home / "vault").mkdir(parents=True)
|
||||
(fake_home / "vault" / "secrets").mkdir()
|
||||
(fake_home / "vault" / ".vault_lock").touch(mode=0o400)
|
||||
(fake_home / "memory" / "user").mkdir(parents=True)
|
||||
(fake_home / "memory" / "context").mkdir()
|
||||
(fake_home / "memory" / "knowledge").mkdir()
|
||||
(fake_home / "plugins").mkdir()
|
||||
(fake_home / "logs").mkdir()
|
||||
|
||||
# Reset plugin registry singleton so tests don't share state
|
||||
from pyra.plugins.registry import PluginRegistry
|
||||
PluginRegistry.reset()
|
||||
|
||||
return fake_home
|
||||
|
||||
|
||||
Reference in New Issue
Block a user