c0c0156468
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>
55 lines
1.7 KiB
Python
55 lines
1.7 KiB
Python
from rich.console import Console
|
|
from rich.live import Live
|
|
from rich.markdown import Markdown
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
|
|
from pyra.security.injection import redact_api_keys
|
|
|
|
console = Console()
|
|
|
|
|
|
def render_streaming_response(stream) -> str:
|
|
"""Consume a litellm streaming response, render markdown progressively, return full text."""
|
|
full_text = ""
|
|
with Live(console=console, refresh_per_second=8) as live:
|
|
for chunk in stream:
|
|
delta = chunk.choices[0].delta.content or ""
|
|
full_text += delta
|
|
safe_text = redact_api_keys(full_text)
|
|
live.update(Markdown(safe_text))
|
|
|
|
return redact_api_keys(full_text)
|
|
|
|
|
|
def render_text_response(text: str) -> str:
|
|
"""Render a complete (non-streaming) AI response as markdown. Returns redacted text."""
|
|
safe_text = redact_api_keys(text)
|
|
if safe_text.strip():
|
|
console.print(Markdown(safe_text))
|
|
return safe_text
|
|
|
|
|
|
def render_injection_warning(warnings) -> None:
|
|
labels = ", ".join(w.pattern_label for w in warnings)
|
|
console.print(Panel(
|
|
f"[yellow]Possible prompt injection detected[/yellow]\n"
|
|
f"Pattern(s): [bold]{labels}[/bold]\n\n"
|
|
"[dim]The response is shown, but treat it with caution.\n"
|
|
"Details logged to ~/.pyra/security.log[/dim]",
|
|
border_style="yellow",
|
|
title="Security Warning",
|
|
))
|
|
|
|
|
|
def render_error(message: str) -> None:
|
|
console.print(Panel(f"[red]{message}[/red]", border_style="red"))
|
|
|
|
|
|
def render_info(message: str) -> None:
|
|
console.print(f"[dim]{message}[/dim]")
|
|
|
|
|
|
def render_system(message: str) -> None:
|
|
console.print(Panel(message, border_style="cyan"))
|