refactor: extract kb_framework package, build graph in memory
Scripts become thin CLIs over kb_framework (graph, query, query_tools, vault, instructions) so the hosted read-only connector shares one implementation with the stdio server. The MCP server builds the graph from Markdown on demand and invalidates it on writes; refresh_graph rebuilds in memory and reports validation problems. Pre-commit hook validates only; index/graph.json is no longer read, written or staged (generate-graph.py --write remains for operators). Golden fixtures pin graph and tool payloads to the previous output. Agent and skill docs updated to match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""Filesystem access to a vault, confined to its root.
|
||||
|
||||
`resolve_in_root` is the single place that turns a vault-relative path into an
|
||||
absolute one and refuses empty, absolute or escaping values, so no tool can
|
||||
reach outside the vault. `read_note_payload` returns the `read_note` payload
|
||||
shape shared by the stdio server and the connector: UTF-8 text inline, or a
|
||||
binary descriptor (size, sha256, hint) for captured originals under `raw/`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def resolve_in_root(root: Path, raw_path: Any, *, label: str = "path") -> Path:
|
||||
"""Turn a vault-relative path into an absolute path inside `root`."""
|
||||
value = str(raw_path or "").strip()
|
||||
if not value:
|
||||
raise ValueError(f"'{label}' is required")
|
||||
candidate = Path(value)
|
||||
if candidate.is_absolute():
|
||||
raise ValueError(f"'{label}' must be vault-relative, got absolute '{value}'")
|
||||
root = Path(root).resolve()
|
||||
target = (root / candidate).resolve()
|
||||
if target != root and not target.is_relative_to(root):
|
||||
raise ValueError(f"'{label}' resolves outside repository root: '{value}'")
|
||||
return target
|
||||
|
||||
|
||||
def relative_to_root(root: Path, target: Path) -> str:
|
||||
return target.relative_to(Path(root).resolve()).as_posix()
|
||||
|
||||
|
||||
def read_note_payload(root: Path, raw_path: Any) -> Dict[str, Any]:
|
||||
"""The `read_note` payload for a vault-relative path."""
|
||||
target = resolve_in_root(root, raw_path)
|
||||
if not target.exists():
|
||||
raise FileNotFoundError(f"note not found at {target}")
|
||||
relative = relative_to_root(root, target)
|
||||
data = target.read_bytes()
|
||||
try:
|
||||
text = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return {
|
||||
"path": relative,
|
||||
"binary": True,
|
||||
"bytes": len(data),
|
||||
"sha256": hashlib.sha256(data).hexdigest(),
|
||||
"hint": (
|
||||
f"{target} is not UTF-8 text (e.g. a PDF or image captured under "
|
||||
"raw/). Open it with a file-reading tool that handles binaries; "
|
||||
"this server returns text only."
|
||||
),
|
||||
}
|
||||
return {"path": relative, "binary": False, "content": text}
|
||||
Reference in New Issue
Block a user