Files
Pyra/CLAUDE.md
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

236 lines
10 KiB
Markdown

# Pyra — Developer Guide
## What Is This
Pyra is a personal AI assistant CLI combining a multi-provider AI chat interface with
a plugin/integration system (Stage 2+) and an encrypted vault (Stage 3+).
## Current Status
**Stage 2.1 — Plugin Framework: complete** (2026-05-17)
Next: Stage 2.2 (Nextcloud plugin) + Stage 2.3 (Email plugin)
## Project Roadmap
### Stage 1 — Core CLI ✅ COMPLETE
Working `pyra` executable with provider setup wizard, streaming chat REPL, .md-based
memory in `~/.pyra/memory/`, and hard security boundaries around the vault.
### Stage 2 — Plugin System & Integrations (IN PROGRESS)
Pyra runs as a system daemon so messaging bots are always-on. All integrations are
standalone Python plugin scripts in `~/.pyra/plugins/` — not hardcoded in `src/pyra/`.
The AI uses tool-use (function calling) to invoke plugins; every execution requires
explicit user approval (y/N prompt). Plugin credentials are stored in the vault under
namespaced keys (`plugin:{name}:{key}`).
#### Stage 2.1 — Plugin Framework ✅ COMPLETE
- `src/pyra/plugins/` package: `base.py`, `loader.py`, `registry.py`, `executor.py`, `install.py`
- `src/pyra/bundled_plugins/` — ships bundled plugin scripts with pyra
- `src/pyra/daemon/` stub (CLI surface only)
- Config: `PluginConfig` + `DaemonConfig` added to `PyraConfig`
- Bootstrap: `~/.pyra/plugins/` and `~/.pyra/logs/` created on startup
- Chat session: AI tool-use loop (up to 10 iterations), approval gate, plugin slash commands
- CLI: `pyra plugin list/install/enable/disable/setup`, `pyra daemon *` stubs
#### Stage 2.2 — Nextcloud Plugin (next)
Bundled plugin: `src/pyra/bundled_plugins/nextcloud/plugin.py`
CalDAV calendar, CardDAV contacts, WebDAV files.
Install deps: `uv pip install -e ".[nextcloud]"`
#### Stage 2.3 — Email Plugin (next, parallel with 2.2)
Bundled plugin: `src/pyra/bundled_plugins/email/plugin.py`
IMAP (Hotmail.de/Outlook), folder browsing, smart event extraction to calendar.
No new deps (uses stdlib imaplib).
#### Stage 2.4 — Daemon + Messaging Bots
- `src/pyra/daemon/server.py` — asyncio event loop, plugin tasks, IPC socket
- `src/pyra/daemon/ipc.py` — Unix socket (chmod 600), line-delimited JSON protocol
- `src/pyra/daemon/service.py` — launchd plist (macOS) / systemd unit (Linux)
- Bundled plugins: `matrix_bot`, `telegram_bot`, `signal_bot`
- Security: sender allowlist, bcrypt passphrase challenge, rate limiting (20 msg/hr),
injection scanning on all incoming messages, tool approval over messaging (2-min timeout)
#### Stage 2.5 — Infrastructure Plugins
Bundled plugins: `ssh_tool` (paramiko), `docker_tool` (docker SDK),
`kubernetes_tool` (kubectl subprocess), `service_manager` (systemctl/launchctl),
`smb_mount` (mount subprocess)
#### Stage 2.6 — Cloud Storage Plugins
Bundled plugins: `gdrive` (Google OAuth2), `onedrive` (MSAL device flow), `dropbox_tool`
### Stage 3 — Vault Encryption
Encrypt `~/.pyra/vault/secrets/` using `age` (or GPG fallback). Pyra decrypts in memory
at call time only — no plaintext ever written to disk after initial setup. Secret
rotation support. Per-key passphrases optional.
### Stage 4 — Security Audit Sub-agent
A separate `pyra security audit` command that spins up a sandboxed AI agent whose sole
job is scanning for vulnerabilities: prompt injection in memory files, unexpected vault
access attempts in `security.log`, outdated dependency CVEs, permission drift on `~/.pyra/`.
Report written to `~/.pyra/security_audit.md` (not AI-readable during normal chat).
### Stage 5 — Web UI / Advanced Features
Optional local web interface (FastAPI + HTMX or similar). Embedding-based memory search
(ChromaDB or sqlite-vec). Scheduled automations via cron-style plugin scheduling.
Multi-profile support (work vs personal).
---
## Architecture
### Source: `src/pyra/`
| Module | Purpose |
|--------|---------|
| `cli.py` | Click entrypoint. Subcommands: `setup`, `chat`, `memory`, `plugin`, `daemon` |
| `setup/providers.py` | Provider registry — pure data, no I/O |
| `setup/wizard.py` | questionary-based interactive setup wizard |
| `config/schema.py` | Pydantic v2 models — `PyraConfig`, `PluginConfig`, `DaemonConfig` |
| `config/manager.py` | ruamel.yaml round-trip config read/write, chmod 600 enforced |
| `config/dirs.py` | `bootstrap()` — creates `~/.pyra/` tree, checks vault sentinel every startup |
| `chat/session.py` | prompt_toolkit REPL loop, AI tool-use loop, plugin slash commands |
| `chat/renderer.py` | Streaming + non-streaming markdown via rich, injection warning panel |
| `chat/history.py` | Conversation list, token budget trimming, tool message support |
| `memory/reader.py` | `list_memories()`, `read_memory()`, `load_context_for_session()` |
| `memory/writer.py` | `write_memory()`, `append_memory()` — relative names only, no traversal |
| `memory/index.py` | Auto-regenerate `MEMORY_INDEX.md` on every write |
| `vault/reader.py` | `get_key(key)` — sole accessor of `vault/secrets/api_keys.json` |
| `vault/writer.py` | `set_key()`, `delete_key()` — only called from setup wizard + plugin setup |
| `security/boundaries.py` | `assert_safe_path()`, `check_vault_lock()`, `BLOCKED_PREFIXES` |
| `security/injection.py` | `scan_response()` — 15 regex patterns, 4 categories, logs to `security.log` |
| `utils/paths.py` | `pyra_home()`, `ensure_dir()`, `safe_chmod()`, `expand()` |
| `plugins/base.py` | `Tool` dataclass, `PyraPlugin` Protocol, `BasePlugin` helper class |
| `plugins/loader.py` | Discovers + loads plugins via importlib; failures isolated per plugin |
| `plugins/registry.py` | Singleton: aggregates tools, slash commands, system prompt additions |
| `plugins/executor.py` | Approval gate: scan args → prompt → execute → scan result → log |
| `plugins/install.py` | Copies bundled plugins to `~/.pyra/plugins/` |
| `bundled_plugins/` | Standalone plugin scripts shipped with pyra (installed on demand) |
| `daemon/__init__.py` | Daemon package stub (implementation in Stage 2.4) |
### Runtime: `~/.pyra/`
```
~/.pyra/
├── config.yaml chmod 600 ← provider_id, model, base_url, enabled plugins
├── security.log chmod 600 ← injection event log
├── memory/ chmod 700
│ ├── user/profile.md
│ ├── context/
│ ├── knowledge/
│ └── MEMORY_INDEX.md
├── plugins/ chmod 700 ← active plugins (each is a dir with manifest.json + plugin.py)
│ └── <name>/
│ ├── manifest.json
│ └── plugin.py
├── logs/ chmod 700
│ ├── tool_executions.log chmod 600 ← every tool call: approved/declined, args, result preview
│ └── plugin_errors.log chmod 600 ← plugin load failures
└── vault/ chmod 700 ← AI CANNOT ACCESS
├── .vault_lock chmod 400 ← sentinel; missing = refuse to start
└── secrets/
└── api_keys.json chmod 400 ← ALL secrets (AI keys + plugin credentials)
```
### Plugin Credential Naming Convention
Plugin credentials live in the vault under namespaced keys:
```
plugin:{plugin_name}:{key_name}
```
Examples: `plugin:nextcloud:password`, `plugin:matrix_bot:access_token`
The vault's `get_key()` / `set_key()` accept any string — the namespace is enforced
by convention in each plugin's `setup()` method.
### Writing a Plugin
1. Create `~/.pyra/plugins/<name>/manifest.json`:
```json
{"name": "<name>", "version": "1.0.0", "description": "...", "author": "you"}
```
2. Create `~/.pyra/plugins/<name>/plugin.py` exporting `get_plugin() -> BasePlugin`:
```python
from pyra.plugins.base import BasePlugin, Tool
class MyPlugin(BasePlugin):
name = "<name>"
description = "..."
version = "1.0.0"
def on_load(self, vault_reader):
self._secret = vault_reader("plugin:<name>:secret")
def tools(self):
return [
Tool("my_tool", "Does X", {"type": "object", "properties": {}},
self._do_x, requires_approval=True)
]
def _do_x(self):
return "result"
def setup(self, console, vault_writer):
secret = console.input("Enter secret: ")
vault_writer("plugin:<name>:secret", secret)
def get_plugin():
return MyPlugin()
```
3. `pyra plugin enable <name>`
**Plugin rules:**
- Never import from `pyra.vault` directly — use the `vault_reader`/`vault_writer` callables
- All write/destructive tools must set `requires_approval=True`
- Return strings from tool handlers (truncated to 4000 chars by executor)
---
## Security Rules (never break these)
1. **Never pass config file contents into a system prompt** — config may reveal provider/model
2. **Never bypass `assert_safe_path()`** — not even in tests (use `tmp_pyra_home` fixture instead)
3. **Always `chmod 600/400`** after writing any file in `~/.pyra/`
4. **No shell execution from AI-generated text** — plugins use explicit approval gates
5. **`vault/reader.py` and `vault/writer.py` are the only modules that may open `api_keys.json`**
6. **API key retrieved inline at call time** — never stored as an instance variable or logged
7. **Tool arguments and results are always injection-scanned** before being used or returned to AI
8. **Plugin directories are validated with `assert_safe_path()`** before loading (symlink protection)
9. **Messaging bot security**: sender allowlist + bcrypt passphrase + rate limiting (Stage 2.4)
## Adding a New Provider
Edit `src/pyra/setup/providers.py`. Add a new `Provider` dataclass entry with all required fields.
litellm handles dispatch automatically via the `litellm_prefix` field.
Add a test in `tests/unit/test_providers.py` to verify the new entry.
## Installing for Development
```bash
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
pyra setup
# Install optional plugin dependencies:
uv pip install -e ".[nextcloud]" # Nextcloud plugin
uv pip install -e ".[ssh]" # SSH plugin
uv pip install -e ".[all-plugins]" # Everything
```
## Running Tests
```bash
pytest tests/ -v # all unit + security tests (161 tests)
pytest tests/integration/test_lmstudio.py # requires LM Studio at localhost:1234
```
## Commit Convention
```
feat(module): short description
fix(module): short description
test: description
docs: description
chore: description
```