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>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
|
||||
from pyra.memory.index import update_index
|
||||
from pyra.security.boundaries import assert_safe_path
|
||||
from pyra.utils.paths import pyra_home, safe_chmod
|
||||
|
||||
_MEMORY_ROOT = pyra_home() / "memory"
|
||||
|
||||
|
||||
def _resolve_and_validate(name: str) -> Path:
|
||||
if name.startswith("/") or name.startswith("~"):
|
||||
raise ValueError(f"Memory name must be relative, got: {name!r}")
|
||||
path = (_MEMORY_ROOT / name).resolve()
|
||||
assert_safe_path(path)
|
||||
try:
|
||||
path.relative_to(_MEMORY_ROOT.resolve())
|
||||
except ValueError:
|
||||
raise PermissionError(f"Path escapes memory directory: {name!r}")
|
||||
if path.suffix.lower() != ".md":
|
||||
path = path.with_suffix(".md")
|
||||
return path
|
||||
|
||||
|
||||
def write_memory(name: str, content: str) -> Path:
|
||||
path = _resolve_and_validate(name)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
safe_chmod(path, 0o600)
|
||||
update_index()
|
||||
return path
|
||||
|
||||
|
||||
def append_memory(name: str, content: str) -> Path:
|
||||
path = _resolve_and_validate(name)
|
||||
if path.exists():
|
||||
existing = path.read_text()
|
||||
path.write_text(existing.rstrip() + "\n\n" + content)
|
||||
else:
|
||||
path.write_text(content)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
safe_chmod(path, 0o600)
|
||||
update_index()
|
||||
return path
|
||||
Reference in New Issue
Block a user