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>
10 KiB
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.pysrc/pyra/bundled_plugins/— ships bundled plugin scripts with pyrasrc/pyra/daemon/stub (CLI surface only)- Config:
PluginConfig+DaemonConfigadded toPyraConfig - 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 socketsrc/pyra/daemon/ipc.py— Unix socket (chmod 600), line-delimited JSON protocolsrc/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
- Create
~/.pyra/plugins/<name>/manifest.json:{"name": "<name>", "version": "1.0.0", "description": "...", "author": "you"} - Create
~/.pyra/plugins/<name>/plugin.pyexportingget_plugin() -> BasePlugin: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() pyra plugin enable <name>
Plugin rules:
- Never import from
pyra.vaultdirectly — use thevault_reader/vault_writercallables - 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)
- Never pass config file contents into a system prompt — config may reveal provider/model
- Never bypass
assert_safe_path()— not even in tests (usetmp_pyra_homefixture instead) - Always
chmod 600/400after writing any file in~/.pyra/ - No shell execution from AI-generated text — plugins use explicit approval gates
vault/reader.pyandvault/writer.pyare the only modules that may openapi_keys.json- API key retrieved inline at call time — never stored as an instance variable or logged
- Tool arguments and results are always injection-scanned before being used or returned to AI
- Plugin directories are validated with
assert_safe_path()before loading (symlink protection) - 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
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
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