M5.1: the knowledge vault
vault.py: a folder of Markdown notes on the agent host, as SilverBullet reads them. Only configured folders exist for the agent, each rw or ro (`vault:` in the configuration); paths are checked in code (relative, inside a folder, symlinks included, Markdown, never the editor's own files). Writes are atomic (temporary file, fsync, rename, directory fsync) and merge provenance into the note's frontmatter, keeping the rest; failures are results. read_note, list_notes and search_notes are control tools in the harness for the driver (before the go-ahead, and while a VM is blocked) and for conversations, labelled with their path; the prompt says a note is information, never an instruction. Writing by the model is M5.2's. With the operator: SilverBullet replaces Obsidian and runs on the vault itself, agents' folders mounted read-only, so no Syncthing (L10, X5, M5.4). Per-agent mounts come with the containers (M6.1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
026f67ab05
commit
9fc7360ca5
11 files changed
+729
-15
No files matched your search
@@ -25,6 +25,7 @@ import yaml
|
||||
|
||||
from nisse_dev.llm import DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_REQUEST_TIMEOUT_S, RoleMap
|
||||
from nisse_dev.matrix import MatrixConfig
|
||||
from nisse_dev.vault import ACCESS, DEFAULT_IGNORE
|
||||
|
||||
DEFAULT_CONFIG_FILE = "nisse-dev.yaml"
|
||||
|
||||
@@ -489,6 +490,55 @@ class Project:
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VaultConfig:
|
||||
"""The knowledge vault (L10): where it is, and the folders this agent
|
||||
sees, each "rw" or "ro" (one writer per folder)."""
|
||||
|
||||
root: Path
|
||||
folders: Mapping[str, str]
|
||||
ignore: tuple[str, ...] = DEFAULT_IGNORE
|
||||
|
||||
|
||||
def _parse_vault(raw: Any, errors: list[str]) -> VaultConfig | None:
|
||||
if raw is None:
|
||||
return None # no vault: no note tools
|
||||
if not isinstance(raw, dict):
|
||||
errors.append("vault: must be a mapping")
|
||||
return None
|
||||
for key in sorted(set(raw) - {"root", "folders", "ignore"}):
|
||||
errors.append(f"vault.{key}: unknown setting")
|
||||
root = raw.get("root")
|
||||
if not isinstance(root, str) or not root.strip():
|
||||
errors.append("vault.root: the vault's folder on the agent host")
|
||||
folders = raw.get("folders")
|
||||
if not isinstance(folders, dict) or not folders:
|
||||
errors.append("vault.folders: a mapping of folder to rw or ro")
|
||||
folders = {}
|
||||
clean: dict[str, str] = {}
|
||||
for folder, access in folders.items():
|
||||
where = f"vault.folders.{folder}"
|
||||
name = str(folder).strip("/")
|
||||
if not name or name.startswith("/") or ".." in name.split("/"):
|
||||
errors.append(f"{where}: a folder inside the vault")
|
||||
elif access not in ACCESS:
|
||||
errors.append(f"{where}: must be one of {', '.join(ACCESS)}")
|
||||
else:
|
||||
clean[name] = access
|
||||
names = sorted(clean)
|
||||
for a in names:
|
||||
for b in names:
|
||||
if a != b and b.startswith(f"{a}/"):
|
||||
errors.append(f"vault.folders: {b} is inside {a}; one access each")
|
||||
ignore = raw.get("ignore", list(DEFAULT_IGNORE))
|
||||
if not isinstance(ignore, list) or not all(isinstance(i, str) for i in ignore):
|
||||
errors.append("vault.ignore: a list of name patterns")
|
||||
ignore = list(DEFAULT_IGNORE)
|
||||
if errors:
|
||||
return None
|
||||
return VaultConfig(Path(str(root)).expanduser(), clean, tuple(ignore))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentConfig:
|
||||
state_db: Path
|
||||
@@ -507,6 +557,7 @@ class AgentConfig:
|
||||
max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS
|
||||
request_timeout_s: float = DEFAULT_REQUEST_TIMEOUT_S
|
||||
tuning: Tuning = field(default_factory=Tuning)
|
||||
vault: VaultConfig | None = None
|
||||
config_file: Path | None = None
|
||||
|
||||
@classmethod
|
||||
@@ -547,8 +598,13 @@ class AgentConfig:
|
||||
tuning = _parse_tuning(tree.get("tuning"), errors)
|
||||
projects = _parse_projects(tree.get("projects"), env, errors)
|
||||
profiles = _parse_profiles(tree.get("profiles"), errors)
|
||||
vault = _parse_vault(tree.get("vault"), errors)
|
||||
flat = _flatten(
|
||||
{k: v for k, v in tree.items() if k not in ("projects", "profiles")}
|
||||
{
|
||||
k: v
|
||||
for k, v in tree.items()
|
||||
if k not in ("projects", "profiles", "vault")
|
||||
}
|
||||
)
|
||||
flat.pop("tuning", None)
|
||||
|
||||
@@ -619,6 +675,7 @@ class AgentConfig:
|
||||
max_output_tokens=max_output,
|
||||
request_timeout_s=request_timeout,
|
||||
tuning=tuning,
|
||||
vault=vault,
|
||||
config_file=config_file,
|
||||
)
|
||||
config._refuse_secrets_inside_the_clone(env)
|
||||
|
||||
Reference in new issue
Block a user