"""Shared knowledge / FAQ snapshot for system-prompt injection. This layer is intentionally GLOBAL across principal profiles: it lives under the Hermes root (``~/.hermes/shared_knowledge`` by default), not under an active profile home. That lets different principals keep isolated private memories while still seeing the same non-private FAQ / reusable knowledge base. """ from __future__ import annotations from pathlib import Path from typing import Iterable, Optional from hermes_constants import get_default_hermes_root from tools.threat_patterns import scan_for_threats _ALLOWED_SUFFIXES = {".md", ".markdown", ".txt"} _DEFAULT_CHAR_LIMIT = 12_000 def resolve_shared_knowledge_dir(path_override: Optional[str] = None) -> Path: """Return the global shared-knowledge directory. ``path_override`` may be absolute or relative. Relative paths resolve under the Hermes root (not the active profile home) so every principal still sees the same directory. """ root = get_default_hermes_root() raw = str(path_override or "").strip() if not raw: return root / "shared_knowledge" path = Path(raw).expanduser() if not path.is_absolute(): path = root / path return path def iter_shared_knowledge_files(directory: Path) -> Iterable[Path]: """Yield supported shared-knowledge files deterministically.""" if not directory.exists() or not directory.is_dir(): return [] files = [ p for p in directory.rglob("*") if p.is_file() and p.suffix.lower() in _ALLOWED_SUFFIXES ] return sorted(files, key=lambda p: tuple(part.lower() for part in p.relative_to(directory).parts)) def _sanitize_shared_knowledge(content: str, rel_name: str) -> str: findings = scan_for_threats(content, scope="context") if findings: return ( f"[BLOCKED: {rel_name} contained potential prompt injection " f"({', '.join(findings)}). Content not loaded.]" ) return content def build_shared_knowledge_block( *, enabled: bool, path_override: Optional[str] = None, char_limit: Optional[int] = None, ) -> str: """Build the shared-knowledge system-prompt block, or ``""`` when absent.""" if not enabled: return "" directory = resolve_shared_knowledge_dir(path_override) files = list(iter_shared_knowledge_files(directory)) if not files: return "" budget = int(char_limit or _DEFAULT_CHAR_LIMIT) if budget <= 0: budget = _DEFAULT_CHAR_LIMIT sections = [ "## SHARED KNOWLEDGE", ( "Global, non-private FAQ/knowledge shared across principals. Use this for " "reusable facts; keep principal-specific preferences and private details in " "profile memory instead." ), f"Path: {directory}", ] remaining = budget added = 0 omitted = 0 for path in files: rel_name = path.relative_to(directory).as_posix() try: raw = path.read_text(encoding="utf-8", errors="ignore").strip() except OSError: omitted += 1 continue if not raw: continue sanitized = _sanitize_shared_knowledge(raw, rel_name) chunk = f"### {rel_name}\n{sanitized}" if len(chunk) <= remaining: sections.append(chunk) remaining -= len(chunk) added += 1 continue if remaining > 200: trimmed = sanitized[: max(0, remaining - len(rel_name) - 32)].rstrip() if trimmed: sections.append(f"### {rel_name}\n{trimmed}\n[TRUNCATED]") added += 1 omitted += 1 break if not added: return "" if omitted > 0 or added < len(files): sections.append( f"[Shared knowledge truncated: loaded {added} file(s); {len(files) - added} omitted due to size/read limits.]" ) return "\n\n".join(part for part in sections if part).strip()