Commit Graph

25 Commits

Author SHA1 Message Date
curo1305 399ed8b5df test: add memory database tests and update conftest for DB isolation
conftest patches mdb._DB_PATH and calls init_db() after directory creation
so all existing tests continue to work with the new DB layer. New
test_memory_db.py covers upsert, search, remove, migration, and the
updated list_memories/lookup_memories integration paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:23:57 +02:00
curo1305 b9b0918d3a feat(memory): wire database into reader, writer, and bootstrap
- reader: list_memories() queries memory_meta; lookup_memories() uses FTS5 with
  fallback to JSON index substring search
- writer: write_memory() and append_memory() upsert to DB after every file write
- dirs: bootstrap() calls init_db() + migrate_from_files() on startup

Existing .md files remain the canonical store; SQLite is the search index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:23:49 +02:00
curo1305 45e6ec32ec feat(memory): add SQLite+FTS5 database layer
New memory/database.py with memory_meta table (path, category, size_bytes,
modified, summary, keywords, embedding BLOB reserved for Stage 8) and
memory_fts virtual table for full-text search. Public API: init_db, upsert,
remove, search, list_all, migrate_from_files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:23:43 +02:00
curo1305 84785967c3 docs: restructure roadmap and add plugin branch workflow rules
Stages now reflect architectural milestones only (Memory DB → Vault →
Skills → Daemon → Audit → Web UI). Plugins move to a perpetual catalog
with per-plugin git branches. Always-push rule replaces the old
no-push default. Adds Plugin Branches workflow section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:02:58 +02:00
curo1305 e56e9779ec feat(memory): add JSON index and runtime memory_lookup/read/write tools
Gives Pyra an active memory brain: memory_index.json tracks summary +
keywords per file (like an inode table), and three built-in tools let
the AI look up, read, and overwrite memory mid-session. write_memory
accepts summary/keywords; update_index() merges the JSON index without
losing existing metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:01:54 +02:00
curo1305 ad024807bc feat(chat): add agent orchestration system with plan_and_execute
Introduces TaskPlanner and AgentSpec so Pyra can decompose multi-step
tasks into sequential steps, each executed with a focused sub-agent
context rather than the full conversation history.

- plugins/base.py: AgentSpec dataclass + agent_spec() on Protocol/BasePlugin
- plugins/registry.py: register_builtin, get_agent, list_agents
- chat/planner.py: TaskPlanner with plan approval, per-step tool-use loop,
  verification call, and agent-aware routing
- chat/session.py: wires plan_and_execute as a built-in tool after load_all
- chat/history.py: planning hint in system prompt + dynamic agents listing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:03:42 +02:00
curo1305 72dae1e048 perf(plugins): cache tool index in PluginRegistry for O(1) find_tool
load_all() now builds a _tools: dict[str, Tool] index at startup.
get_all_tools() returns list(_tools.values()) and find_tool() is a
direct dict.get() instead of rebuilding the full tool list from every
plugin on every tool call during a session.

Updated test helper to populate _tools alongside _plugins to match
the actual load_all() behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 18:09:51 +02:00
curo1305 bbe9bcfe0a refactor(memory): centralize _MEMORY_ROOT; fix mkdir order in append_memory
_MEMORY_ROOT was defined independently in reader.py, writer.py, and
index.py. Moved to memory/__init__.py; all three import from there.

Also fixes a bug in append_memory where path.write_text() was called
before path.parent.mkdir(), which would crash when creating a file in
a new subdirectory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 18:09:45 +02:00
curo1305 18b2b94194 refactor(vault): centralize _KEYS_FILE constant in vault/__init__.py
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>
2026-05-17 18:09:41 +02:00
curo1305 7bdb2c3faf chore: remove dead expand() function and skills/ bootstrap dirs
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>
2026-05-17 18:09:36 +02:00
curo1305 27cc925965 docs: add workflow rules and full code inventory to CLAUDE.md
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>
2026-05-17 18:09:31 +02:00
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
curo1305 30cda28ec8 fix(setup,chat): pass dummy api_key for local providers
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>
2026-05-17 13:54:18 +02:00
curo1305 6e138bcec2 fix: remove self-defeating assert_safe_path from vault modules, clarify traversal test scope
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>
2026-05-17 12:59:59 +02:00
curo1305 27b32cb4d1 docs: CLAUDE.md with full 5-stage roadmap and README
CLAUDE.md: architecture table, security rules, all module descriptions,
roadmap Stages 1-5, adding-provider guide, commit convention.
README.md: quick start, provider table, command reference, security overview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:56:08 +02:00
curo1305 251e509ee0 test: comprehensive test suite
Unit tests:
- test_security_boundaries.py: vault block, vault lock sentinel
- test_security_injection.py: all 4 injection categories, case-insensitive
- test_vault_rw.py: roundtrip, file permissions (chmod 400), no key in config
- test_config.py: schema roundtrip, no api_key field, chmod 600 on config.yaml
- test_memory_reader.py: list, read, sandboxing, context loading
- test_memory_writer.py: write, append, index update, traversal blocked, chmod 600
- test_providers.py: required fields, unique IDs, litellm prefix format
- test_renderer.py: key redaction for sk-ant-, sk-, AIza patterns

Security tests:
- test_vault_ai_isolation.py: 7 traversal patterns blocked via memory read/write
- test_path_traversal.py: 20+ traversal patterns — all rejected for read and write
- test_prompt_injection.py: 21-item corpus + 5 clean texts (no false positives)

Integration tests:
- test_lmstudio.py: live call to localhost:1234, streaming, full stack session,
  injection scan on real output (skips if LM Studio not running)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:55:06 +02:00
curo1305 e792c5e0c9 feat(cli): wire all subcommands
pyra (default→chat), pyra setup, pyra chat, pyra memory list/read/write/append
All routes call bootstrap() first; PyraSecurityError exits with clear message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:52:26 +02:00
curo1305 0fe6332316 feat(chat): streaming REPL with rich renderer
- chat/renderer.py: Live streaming markdown, injection warning panel, redaction
- chat/history.py: ConversationHistory with system prompt + memory context injection,
  token budget trimming
- chat/session.py: prompt_toolkit REPL, slash commands (/quit /clear /help /memory list),
  vault key retrieval inline at call time (not stored), injection scan after each response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:52:08 +02:00
curo1305 1448bb4650 feat(security): prompt injection scanner and API key redaction
- 15 regex patterns across 4 categories: instruction-override, role-switch,
  jailbreak, exfiltration, credential-fishing
- scan_response() returns InjectionWarning list and logs to ~/.pyra/security.log
- redact_api_keys() strips sk-ant-, sk-, AIza patterns before display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:51:23 +02:00
curo1305 18c39cc152 feat(memory): sandboxed markdown memory system
- 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>
2026-05-17 12:50:55 +02:00
curo1305 7617a80595 feat(vault): API key storage in vault only
- 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>
2026-05-17 12:50:24 +02:00
curo1305 43a8a363ba feat(setup): provider registry and interactive setup wizard
- setup/providers.py: registry for 8 providers (3 local, 5 cloud), frozen dataclasses
- setup/wizard.py: questionary-based wizard — provider select, model input,
  API key collected via vault.writer (not config.yaml), connectivity check,
  test call via litellm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:50:04 +02:00
curo1305 ae565b0d68 feat(config): directory bootstrap and config manager
- config/schema.py: Pydantic v2 models — no API keys, only provider_id/model/base_url
- config/manager.py: ruamel.yaml round-trip load/save, chmod 600 enforced on write
- config/dirs.py: bootstrap() creates ~/.pyra/ tree, vault sentinel checked every startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:49:22 +02:00
curo1305 a96b540234 feat(security): vault wall, path guard, and utils
- utils/paths.py: pyra_home(), ensure_dir(), safe_chmod(), expand()
- security/boundaries.py: VaultAccessError, PyraSecurityError,
  assert_safe_path() (called before every file read), check_vault_lock()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:48:50 +02:00
curo1305 0a04e04490 chore: init project skeleton
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>
2026-05-17 12:48:32 +02:00