reader.py and writer.py each independently computed the same path via
pyra_home(). Single definition in __init__.py; both modules import it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expand() had zero callers anywhere in the codebase. The skills/bash,
skills/powershell, and skills/python directories were created on every
startup but the skills/ tree is not part of the current architecture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents all third-party libraries, stdlib modules, internal utility
functions, and classes with signatures and import paths. Adds workflow
rules for bugfixes (≤50 lines), duplication avoidance, and commit
discipline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
litellm requires the api_key field even for local OpenAI-compatible
servers (LM Studio, llama.cpp). Use "local" as a sentinel value for
providers that don't require a real key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vault/reader.py, vault/writer.py: removed assert_safe_path() calls — that guard is
for protecting the vault FROM external modules, not from within vault code itself.
Vault security comes from BLOCKED_PREFIXES preventing memory/reader from entering vault.
test_path_traversal.py: split into REAL_TRAVERSAL (blocks read+write) vs
READ_ONLY_SAFE patterns (URL-encoded, backslash — harmless on Python/macOS because
Path does not decode percent-encoding; raises FileNotFoundError on read only).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- memory/index.py: auto-regenerate MEMORY_INDEX.md on every write
- memory/reader.py: list_memories(), read_memory(), load_context_for_session()
all go through assert_safe_path() + relative_to check
- memory/writer.py: write_memory(), append_memory() — relative names only,
no absolute paths or traversal, calls update_index() after every write
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- vault/reader.py: get_key() reads from ~/.pyra/vault/secrets/api_keys.json
- vault/writer.py: set_key(), delete_key() — only writer callable from setup
- Both call assert_safe_path() as defense-in-depth
- Keys file stays chmod 400; temporarily 600 during write then locked again
- Config.yaml never touched by either module
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Directory structure, pyproject.toml with hatchling build, and all
subpackage stubs for pyra Stage 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>