Files
curo1305 4744cf819b docs: update CLAUDE.md for Stage 6 daemon infrastructure
- Current Status: add Stage 6 daemon infrastructure in progress
- Architecture table: expand daemon/__init__.py stub to all 5 daemon modules
- Code Inventory: add daemon.core, daemon.pid, daemon.ipc, daemon.service
  sections with function signatures and purposes
- Internal classes: add PluginSupervisor and PidFile; expand DaemonConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:10:49 +02:00

30 KiB
Raw Permalink Blame History

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 3 — Memory Database: complete (2026-05-18) Stage 6 — Daemon infrastructure: in progress (feat/daemon branch) Next: Stage 4 — Vault Encryption (skipped for now); messaging bots (Stage 6 remainder)

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 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; daemon itself is Stage 6)
  • 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 at Stage 2; implemented in Stage 6)

Stage 3 — Memory Database COMPLETE

  • src/pyra/memory/database.py: SQLite + FTS5 via memory_meta + memory_fts tables
  • memory_meta columns: path, category, size_bytes, modified, summary, keywords, embedding BLOB (reserved for Stage 8)
  • list_memories() queries DB; lookup_memories() uses FTS5 with JSON-index fallback
  • write_memory() / append_memory() upsert to DB on every write
  • bootstrap() calls init_db() + migrate_from_files() (one-shot migration of existing .md files)
  • .md files remain the canonical store; DB is the search index

Stage 4 — 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 5 — Skills System

YAML-defined multi-plugin workflows with event triggers and AI-driven selection. Skills compose existing plugin tools into automated pipelines with conditional branching and human-in-the-loop decision points.

Stage 6 — Daemon + Messaging Bots

Always-on asyncio daemon, IPC socket, launchd/systemd service. Bundled bots: matrix_bot, telegram_bot, signal_bot. Sender allowlist, bcrypt passphrase challenge, rate limiting (20 msg/hr), injection scanning on all incoming messages, tool approval over messaging (2-min timeout).

Stage 7 — Security Audit Sub-agent

pyra security audit — sandboxed agent scanning for prompt injection in memory files, unexpected vault access in security.log, outdated CVEs, permission drift on ~/.pyra/. Report written to ~/.pyra/security_audit.md (not AI-readable during normal chat).

Stage 8 — Web UI / Advanced Features

Optional local web interface (FastAPI + HTMX or similar). Embedding-based memory search via sqlite-vec. Multi-profile support (work vs personal).


Plugin Catalog (not stage-gated — ships when ready)

Plugins are developed independently on plugin/<name> branches and merged to main only when complete. All integrations are standalone Python plugin scripts in ~/.pyra/plugins/ — not hardcoded in src/pyra/. Plugin credentials are stored in the vault under namespaced keys (plugin:{name}:{key}).

Plugin Branch Status
nextcloud plugin/nextcloud planned
email plugin/email planned
websearch plugin/websearch planned
headless_browser plugin/headless_browser planned
server_manager plugin/server_manager planned
matrix_bot plugin/matrix_bot planned
telegram_bot plugin/telegram_bot planned
signal_bot plugin/signal_bot planned
ssh_tool plugin/ssh_tool planned
docker_tool plugin/docker_tool planned
gdrive plugin/gdrive planned
onedrive plugin/onedrive planned
dropbox_tool plugin/dropbox_tool planned

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, GeneralConfig, PluginConfig, DaemonConfig; plugin_settings dict
config/tui.py textual-based /config TUI — ConfigApp, GENERAL_FIELDS, launch_config_tui()
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/planner.py TaskPlanner — multi-step plan approval loop, per-step AI execution and verification
chat/renderer.py Streaming + non-streaming markdown via rich, injection warning panel
chat/history.py Conversation list, token budget trimming, tool message support
memory/database.py SQLite+FTS5 — init_db(), upsert(), remove(), search(), list_all(), migrate_from_files()
memory/reader.py list_memories() (DB-backed), read_memory(), lookup_memories() (FTS5), load_context_for_session()
memory/writer.py write_memory(), append_memory() — writes file + upserts to DB
memory/index.py Auto-regenerate MEMORY_INDEX.md + memory_index.json 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/pid.py Atomic PID file — write, read, stale detection (POSIX + Windows), context manager
daemon/ipc.py IPC transport — Unix socket chmod 600 + UID-check (Linux/macOS) or TCP loopback + port file (Windows); newline-delimited JSON protocol
daemon/service.py OS service file generation + install/uninstall — launchd plist (macOS), systemd user unit (Linux), schtasks XML (Windows)
daemon/core.py asyncio event loop entry point, PluginSupervisor (per-task restart, max 10×, 5s back-off, reload), IPC command dispatch, signal handling
daemon/__init__.py Public daemon API exports

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:
    {"name": "<name>", "version": "1.0.0", "description": "...", "author": "you"}
    
  2. Create ~/.pyra/plugins/<name>/plugin.py exporting get_plugin() -> BasePlugin:
    from pyra.plugins.base import BasePlugin, ConfigField, 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 config_fields(self):
            # Declare user-adjustable settings. Values are saved to config.yaml
            # under plugin_settings["<name>"] and rendered in /config → plugin tab.
            return [
                ConfigField("api_url", "API URL", "text", "https://example.com",
                            description="Base URL for the service"),
                ConfigField("verify_ssl", "Verify SSL", "bool", True),
            ]
    
    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)
  • Implement config_fields() for any user-adjustable settings beyond credentials. Return a list of ConfigField objects — the /config TUI renders them automatically and saves values to config.yaml under plugin_settings["<name>"]. Plugins that need no configuration can omit this method (base no-op is used).

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

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
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

Workflow Rules

Testing

  • Write tests for every new feature. A feature without tests is incomplete — do not commit without them.
  • New tests go in tests/unit/ for pure-logic helpers and tests/security/ for security-boundary code.
  • All existing tests must continue to pass — run pytest tests/ -v before committing.
  • Test pure functions directly; do not test interactive I/O (questionary, Rich output) — only test the logic helpers those flows call.
  • For Rich output, capture side effects by monkeypatching console.print rather than using capsys.

Bugfixes

  • Stay under 50 lines changed. Find the root cause and fix it directly.
  • If the fix seems to require more than 50 lines, it is probably a refactor, not a bugfix — stop and discuss with the user before proceeding.
  • Do not write workarounds, fallback layers, or compatibility shims to route around a bug. Remove the cause.

Committing Changes

  • Commit after every logical unit of work — do not batch unrelated changes into one commit and do not wait until the end of a session.
  • One commit per concern. If a session touches a file for two different reasons (e.g. a bugfix and a cleanup), those are two separate commits — staged and committed independently, even if the file is the same.
  • Use the project commit convention: feat(module):, fix(module):, test:, docs:, chore: followed by a short description.
  • Always git add only the files relevant to that commit — never git add . blindly.
  • Always push after committing — every commit goes to the remote Gitea repository immediately.

Git Worktrees — Required for All Branch Work

Never switch branches in the main working directory. Always use a git worktree so that multiple sessions (plugins, features, bugfixes) can run in parallel without interfering with each other or with main.

# Create a worktree for a plugin branch
git worktree add ../pyra-plugin-nextcloud -b plugin/nextcloud

# Create a worktree for a feature branch
git worktree add ../pyra-feat-vault -b feat/vault-encryption

# List active worktrees
git worktree list

# Remove a worktree after merging
git worktree remove ../pyra-plugin-nextcloud

Each worktree is a full checkout at a separate path. Work on it exactly like the main repo — commit, push, run tests — without touching the main worktree.

Rules:

  • The main working directory (/Users/nik/Documents/Progamming/pyra) always stays on main.
  • Do not run git checkout <branch> in the main directory — create a worktree instead.
  • When a Claude Code session is asked to work on a branch, it must create (or reuse) a worktree for that branch before making any changes.

Plugin Branches

  • Every plugin is developed on its own branch: plugin/<name> (e.g. plugin/nextcloud), in its own worktree (e.g. ../pyra-plugin-nextcloud).
  • A plugin branch is never merged to main until the plugin is complete and tested.
  • main always contains only production-ready core source code (src/pyra/ framework).
  • If plugin work uncovers a bug in core Pyra code, fix it on a dedicated fix/... branch off main (in its own worktree), merge to main, push, then rebase the plugin branch.
  • Plugin branches may be pushed to remote for backup/review at any time.
  • Do not merge plugin branches to main prematurely — a half-working plugin on main is worse than one that isn't there yet.

Avoid Duplication — Check the Inventory First

Before writing any new utility function, class, or import block, check the Code Inventory section below. Everything listed there already exists and is importable. Writing a duplicate wastes code and introduces divergence.


Code Inventory

Third-party libraries (pyproject.toml dependencies)

Library Min version Used in Purpose
litellm 1.40.0 chat/session.py, setup/wizard.py Multi-provider LLM completion (streaming + non-streaming) and tool-use dispatch
rich 13.0.0 chat/renderer.py, cli.py, setup/wizard.py, plugins/executor.py Terminal UI — Console, Panel, Markdown, Live, Text
click 8.1.0 cli.py CLI entrypoint, @click.group, @click.command, arguments
prompt_toolkit 3.0.0 chat/session.py REPL input loop — PromptSession, FileHistory
questionary 2.0.0 setup/wizard.py Interactive select / text / password prompts
ruamel.yaml 0.18.0 config/manager.py Round-trip YAML read/write (preserves comments and formatting)
pydantic 2.0.0 config/schema.py Config validation via BaseModel
httpx 0.27.0 setup/wizard.py HTTP GET for local-server connectivity checks
textual 1.0.0 config/tui.py Full-screen TUI framework — tabs, inputs, switches, data tables for /config

Optional plugin extras (declared in pyproject.toml [project.optional-dependencies]):

Extra Libraries Intended for
nextcloud caldav, webdav4, vobject CalDAV / CardDAV / WebDAV
matrix matrix-nio, aiofiles Matrix bot
telegram python-telegram-bot Telegram bot
ssh paramiko SSH plugin
docker docker Docker plugin
gdrive google-api-python-client, google-auth-oauthlib Google Drive
onedrive msal OneDrive device-flow auth
dropbox dropbox Dropbox

Standard library modules in use

Module Used in Notes
pathlib.Path everywhere Default for all paths — never use os.path string joins
os utils/paths.py Only for os.name (Windows guard)
json vault/reader.py, vault/writer.py, plugins/loader.py, plugins/executor.py, plugins/install.py Vault file, manifests, tool args/results
re security/injection.py Compiled injection-detection patterns
datetime security/injection.py, memory/reader.py, memory/index.py, plugins/loader.py, plugins/executor.py Log timestamps, file mtimes
dataclasses security/injection.py, memory/reader.py, plugins/base.py @dataclassInjectionWarning, MemoryFile, Tool
importlib.util plugins/loader.py Dynamic plugin loading (spec_from_file_location)
sys cli.py, plugins/loader.py sys.exit, sys.modules for dynamic module registration
shutil plugins/install.py copytree, rmtree for bundled plugin installation
typing plugins/base.py, chat/history.py, plugins/registry.py Protocol, Callable, Coroutine, Any, TYPE_CHECKING

Internal utility functions — import, do not rewrite

utils.paths

Function Signature Purpose
pyra_home () -> Path Returns ~/.pyra/
ensure_dir (path: Path, mode=0o700) -> Path mkdir -p + chmod in one call
safe_chmod (path: Path, mode: int) -> None chmod that silently skips on Windows

security.boundaries

Function Signature Purpose
assert_safe_path (path: Path) -> None Raises VaultAccessError if path resolves into vault
check_vault_lock () -> None Raises PyraSecurityError if vault sentinel is missing

Exceptions: VaultAccessError(PermissionError), PyraSecurityError(RuntimeError)

security.injection

Function Signature Purpose
scan_response (text: str) -> list[InjectionWarning] Runs 15 compiled regex patterns, logs hits to security.log
redact_api_keys (text: str) -> str Replaces key-shaped strings with [REDACTED]

Dataclass: InjectionWarning(pattern_label: str, matched_text: str)

config.manager

Function Signature Purpose
load_config () -> PyraConfig Reads config.yaml, validates via Pydantic; raises FileNotFoundError if missing
save_config (cfg: PyraConfig) -> None Writes config.yaml, enforces chmod 600
config_exists () -> bool True if config.yaml exists
config_path () -> Path Absolute path to config.yaml

config.tui

Symbol Purpose
launch_config_tui () -> None — opens the full-screen configuration TUI; blocks until user presses q/Escape
GENERAL_FIELDS List of _CoreField entries — the single place to add new core settings to the General tab

config.dirs

Function Signature Purpose
bootstrap () -> None Creates ~/.pyra/ directory tree and checks vault sentinel; called at every startup

vault.reader / vault.writer

Function Module Signature Purpose
get_key vault.reader (provider_id: str) -> str | None Sole vault reader — never call open(api_keys.json) anywhere else
set_key vault.writer (provider_id: str, api_key: str) -> None Stores or overwrites a key in the vault
delete_key vault.writer (provider_id: str) -> bool Removes a key; returns True if it existed

memory.database

Function Signature Purpose
init_db () -> None Creates memory.db with memory_meta + memory_fts tables; chmod 600
upsert (path, *, content, category, size_bytes, modified, summary, keywords) -> None Insert or replace one entry in both tables
remove (path: str) -> None Delete entry from both tables
search (query: str, limit: int = 20) -> list[dict] FTS5 MATCH search; returns [{file, summary, keywords, snippet}]
list_all () -> list[dict] All rows from memory_meta ordered by path
migrate_from_files () -> None One-shot: populate DB from existing .md files if DB is empty

memory.reader

Function Signature Purpose
list_memories () -> list[MemoryFile] Queries DB (memory_meta); falls back to file scan if DB empty
read_memory (name: str) -> str Reads memory file by relative path; validates against vault/traversal
lookup_memories (query: str) -> list[dict] FTS5 full-text search; falls back to JSON index substring search
load_context_for_session () -> str Concatenates all memory files into a system-prompt block

Dataclass: MemoryFile(name, path, category, size_bytes, modified)

memory.writer

Function Signature Purpose
write_memory (name: str, content: str, summary: str, keywords: list[str]) -> Path Creates/overwrites a memory .md file, updates index and DB
append_memory (name: str, content: str) -> Path Appends to a memory file (creates if missing), updates index and DB

memory.index

Function Signature Purpose
update_index () -> None Regenerates MEMORY_INDEX.md and memory_index.json — called automatically by writer functions

setup.providers

Symbol Kind Purpose
PROVIDERS list[Provider] All registered providers in display order
PROVIDERS_BY_ID dict[str, Provider] Fast id lookup
get_provider (provider_id: str) -> Provider Raises KeyError for unknown ids
Provider frozen dataclass id, display_name, requires_key, default_model, litellm_prefix, base_url, key_env_var, connectivity_check, group

plugins.loader

Function Signature Purpose
load_plugins (plugins_dir: Path) -> list[PyraPlugin] Discovers all valid plugin directories
load_plugin_by_name (name: str, plugins_dir: Path) -> PyraPlugin | None Loads a single plugin; returns None on any failure

plugins.install

Function Signature Purpose
get_bundled_plugins_dir () -> Path Path to src/pyra/bundled_plugins/
install_bundled_plugin (name, bundled_dir, plugins_dir) -> None Copies bundled plugin dir to ~/.pyra/plugins/, sets permissions
list_bundled_plugins (bundled_dir: Path) -> list[str] Names of all bundled plugins that have a manifest.json
read_manifest (plugin_dir: Path) -> dict Reads manifest.json; returns {} if missing

daemon.core

Function Signature Purpose
run_foreground () -> None Entry point for pyra daemon run — loads config + plugins, writes PID file, runs asyncio loop
start_background () -> None Spawns pyra daemon run as a detached subprocess (start_new_session on POSIX, DETACHED_PROCESS on Windows)

daemon.pid

Function Signature Purpose
resolve_pid_path (cfg_path: str) -> Path Expand ~ and resolve to absolute Path

daemon.ipc

Function Signature Purpose
send_command (address, msg, timeout=5.0) -> IpcResponse Synchronous CLI helper — asyncio.run(IpcClient.send(...))
get_socket_path (cfg: str) -> Path Expand ~ and return Unix socket path
is_unix_socket () -> bool True on Linux/macOS (sys.platform != 'nt')
get_port_file_path () -> Path Path to ~/.pyra/daemon.port (Windows TCP port file)

daemon.service

Function Signature Purpose
detect_platform () -> Literal["macos","linux","windows"] Detect current OS
find_pyra_executable () -> str shutil.which("pyra") → sibling fallback → sys.executable -m pyra
install_service () -> None Generate + register OS service (reads config for log/pid paths)
uninstall_service () -> None Deregister OS service
render_launchd_plist (exe, log_file, pid_file) -> str macOS plist template
render_systemd_unit (exe, log_file) -> str Linux systemd unit template
render_schtasks_xml (exe) -> str Windows Task Scheduler XML template (write as UTF-16)

chat.renderer — rendering functions and shared console

Import console from here; do not create a second rich.Console() in new code.

Symbol Purpose
console Module-level rich.Console — the single shared terminal instance
render_streaming_response(stream) Renders a litellm streaming response with Live + Markdown, returns final text
render_text_response(text) Renders a complete string as Markdown
render_injection_warning(warnings) Yellow Panel showing detected pattern labels
render_error(message) Red Panel
render_info(message) Dim plain text line
render_system(message) Cyan Panel

Internal classes

Class Module Notes
PyraConfig config.schema Top-level config; fields: ai, general, memory, security, plugins, daemon, plugin_settings
GeneralConfig config.schema general: block — user_name, assistant_name
ProviderConfig config.schema ai: block — provider_id, model, base_url
PluginConfig config.schema plugins: block — enabled, require_approval, log_executions
DaemonConfig config.schema daemon: block — enabled, socket_path, log_file, pid_file, ipc_port
MemoryConfig config.schema memory: block — max_tokens_in_context, auto_load
SecurityConfig config.schema security: block — injection_detection, log_injections
ConversationHistory chat.history Holds message list; builds API payload via build_for_api(); trims to token budget
PluginRegistry plugins.registry Singleton (instance() / reset()); aggregates tools, slash commands, system prompt additions
ToolExecutor plugins.executor Approval gate + injection scan + logging; call via execute() or execute_tool_call_batch()
ConfigField plugins.base Dataclass — declares one plugin config option (key, label, type, default, options, description); returned by config_fields()
Tool plugins.base Dataclass — name, description, parameters (JSON Schema), handler, requires_approval
PyraPlugin plugins.base @runtime_checkable Protocol — the plugin interface
BasePlugin plugins.base Concrete base with no-op defaults; plugins should inherit this
TaskPlanner chat.planner Multi-step plan runner; make_tool_handler() returns the callable wired into the chat session; presents plan for user approval, executes each step via litellm with up to 5 tool-use iterations, verifies output before proceeding
PluginSupervisor daemon.core asyncio supervisor — add_task(name, factory), start(), stop(), reload(), status(); restarts crashed tasks up to 10× with 5s back-off
PidFile daemon.pid write() (atomic), read(), is_stale(), remove(), context manager; PidFileError(OSError) raised when live PID already exists