Files
Pyra/tests/conftest.py
T
curo1305 c0c0156468 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>
2026-05-17 15:35:20 +02:00

72 lines
2.3 KiB
Python

import json
import os
from pathlib import Path
import pytest
@pytest.fixture
def tmp_pyra_home(tmp_path, monkeypatch):
"""Redirect pyra_home() to a temporary directory for isolation."""
fake_home = tmp_path / ".pyra"
# Patch Path.home() so pyra_home() returns our tmp dir
monkeypatch.setattr(
"pyra.utils.paths.Path",
type("FakePath", (Path,), {"home": staticmethod(lambda: tmp_path)}),
)
# Also patch the module-level constants already computed
import pyra.security.boundaries as b
import pyra.memory.index as mi
import pyra.memory.reader as mr
import pyra.memory.writer as mw
import pyra.vault.reader as vr
import pyra.vault.writer as vw
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"
mi._INDEX_FILE = fake_home / "memory" / "MEMORY_INDEX.md"
mr._MEMORY_ROOT = fake_home / "memory"
mw._MEMORY_ROOT = fake_home / "memory"
vr._KEYS_FILE = fake_home / "vault" / "secrets" / "api_keys.json"
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
(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
@pytest.fixture
def lmstudio_available():
"""Skip test if LM Studio is not reachable."""
import httpx
try:
r = httpx.get("http://localhost:1234/v1/models", timeout=2.0)
r.raise_for_status()
return True
except Exception:
pytest.skip("LM Studio not reachable at localhost:1234")