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>
58 lines
2.2 KiB
Python
58 lines
2.2 KiB
Python
"""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}
|