From ea0d451a2ebc0c6cc1f982c31b0a7a9afd914943 Mon Sep 17 00:00:00 2001 From: dimknaf <136385722+dimknaf@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:48:32 +0100 Subject: [PATCH 1/4] feat(integrations): Hermes Agent memory provider + sandbox harness Native Hermes MemoryProvider (integrations/hermes/braindb): agent-only gateway -- braindb_ask -> /agent/query (the whole of BrainDB) and braindb_ingest -> file upload. Loads its usage skill live from BrainDB, so there is no duplicated prompt. Relies on two small ADDITIVE BrainDB endpoints, isolated in a new braindb/routers/integrations.py (+1 include_router line in main.py): GET /api/v1/skill/{name} and POST /api/v1/entities/datasources/upload. No existing route, schema, or behaviour is modified. Adds a hardened, tool-stripped docker sandbox (integrations/hermes/sandbox) to run Hermes against a throwaway BrainDB on an isolated host. Tests for both endpoints; full suite green. --- .gitignore | 4 + braindb/main.py | 5 +- braindb/routers/integrations.py | 93 ++++++ integrations/hermes/braindb/README.md | 66 +++++ integrations/hermes/braindb/__init__.py | 267 ++++++++++++++++++ integrations/hermes/braindb/plugin.yaml | 5 + integrations/hermes/sandbox/.env.example | 9 + integrations/hermes/sandbox/README.md | 98 +++++++ integrations/hermes/sandbox/config.yaml | 29 ++ .../sandbox/docker-compose.hermes-test.yml | 44 +++ tests/test_integrations.py | 66 +++++ 11 files changed, 685 insertions(+), 1 deletion(-) create mode 100644 braindb/routers/integrations.py create mode 100644 integrations/hermes/braindb/README.md create mode 100644 integrations/hermes/braindb/__init__.py create mode 100644 integrations/hermes/braindb/plugin.yaml create mode 100644 integrations/hermes/sandbox/.env.example create mode 100644 integrations/hermes/sandbox/README.md create mode 100644 integrations/hermes/sandbox/config.yaml create mode 100644 integrations/hermes/sandbox/docker-compose.hermes-test.yml create mode 100644 tests/test_integrations.py diff --git a/.gitignore b/.gitignore index 350887f..dee2fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,7 @@ data_bench/ # Test stack (docker-compose.test.yml): separate host data dir for test ingests data_test/ + +# Hermes sandbox (integrations/hermes/sandbox): throwaway agent profile dir — +# holds a .env with the LLM key + provider state; never commit it. +integrations/hermes/sandbox/hermes-data/ diff --git a/braindb/main.py b/braindb/main.py index 21040e5..ff47f77 100644 --- a/braindb/main.py +++ b/braindb/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from braindb.routers import agent, entities, memory, relations, wiki +from braindb.routers import agent, entities, integrations, memory, relations, wiki from braindb.services.embedding_service import get_embedding_service logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") @@ -26,6 +26,9 @@ app.include_router(memory.router) app.include_router(agent.router) app.include_router(wiki.router) +# External-integration endpoints (Hermes memory provider + similar clients). +# Additive only; see braindb/routers/integrations.py. +app.include_router(integrations.router) @app.on_event("startup") diff --git a/braindb/routers/integrations.py b/braindb/routers/integrations.py new file mode 100644 index 0000000..4212e0f --- /dev/null +++ b/braindb/routers/integrations.py @@ -0,0 +1,93 @@ +""" +External-integration endpoints — consumed by the Hermes memory provider (see +`integrations/hermes/`) and similar external clients. These are NOT used by +BrainDB's own app, agent, or watcher; they exist purely so an outside agent can +(a) self-configure from BrainDB's shipped skill text (single source of truth, no +copied prompt) and (b) hand a file to BrainDB's existing ingestion pipeline over +HTTP without needing a shared filesystem. + +Kept isolated in this dedicated router (and registered with a single +`include_router` line in main.py) so the integration surface is obvious and the +core routers stay untouched. Everything here is additive — no existing route, +schema, or behaviour is affected. +""" +import os +from pathlib import Path + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import PlainTextResponse + +router = APIRouter(prefix="/api/v1", tags=["integrations"]) + +# Mirror the watcher's watch dir + allow-list so an upload lands exactly where +# the existing ingestion pipeline (braindb/ingest_watcher.py) already looks. +_WATCH_DIR = Path(os.getenv("INGEST_WATCH_DIR", "data/sources")) +_ALLOWED_EXTS = {".md", ".txt", ".json", ".yaml", ".yml", ".csv", ".log", ".html", ".xml"} +_MAX_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB — generous for text docs, blocks abuse +_SKILLS_DIR = Path("skills") + + +def _safe_basename(raw: str) -> str: + """Reduce a client-supplied name to a bare filename — no path traversal. + + We only ever join the basename to a fixed directory, so traversal is + impossible even before this check; the explicit guard just rejects obvious + junk early with a clear 400. + """ + base = os.path.basename((raw or "").replace("\\", "/").strip()) + if not base or base in (".", "..") or base.startswith("."): + raise HTTPException(400, "Invalid or unsafe name") + return base + + +@router.get("/skill/{name}", response_class=PlainTextResponse) +def get_skill(name: str) -> str: + """Serve a shipped skill's markdown (e.g. `name=braindb-agent`). + + Lets an external client load its usage instructions live from BrainDB rather + than bundling a copy that could drift. Read-only. + """ + safe = _safe_basename(name) + path = _SKILLS_DIR / safe / "SKILL.md" + if not path.is_file(): + raise HTTPException(404, f"No skill named {safe!r}") + return path.read_text(encoding="utf-8") + + +@router.post("/entities/datasources/upload", status_code=202) +async def upload_datasource(request: Request, filename: str): + """Accept a file (raw request body) and drop it into `data/sources/` so the + existing watcher ingests + fact-extracts it. + + Adds NO ingestion logic of its own — it only lands the bytes where the + pipeline already polls. Extraction is asynchronous (the watcher picks the + file up on its next tick). Raw body + `filename` query param is used + deliberately so this needs no multipart dependency. + """ + fname = _safe_basename(filename) + ext = Path(fname).suffix.lower() + if ext not in _ALLOWED_EXTS: + raise HTTPException(400, f"Unsupported extension {ext!r}; allowed: {sorted(_ALLOWED_EXTS)}") + + data = await request.body() + if not data: + raise HTTPException(400, "Empty body") + if len(data) > _MAX_UPLOAD_BYTES: + raise HTTPException(413, f"File too large (> {_MAX_UPLOAD_BYTES} bytes)") + + _WATCH_DIR.mkdir(parents=True, exist_ok=True) + dest = _WATCH_DIR / fname + # No-overwrite on name collision (mirrors the watcher's own dedup). + if dest.exists(): + stem, suffix = dest.stem, dest.suffix + i = 1 + while (cand := _WATCH_DIR / f"{stem}.{i}{suffix}").exists(): + i += 1 + dest = cand + dest.write_bytes(data) + + return { + "filename": dest.name, + "status": "accepted", + "message": "Uploaded to data/sources/; the watcher will ingest and fact-extract it shortly.", + } diff --git a/integrations/hermes/braindb/README.md b/integrations/hermes/braindb/README.md new file mode 100644 index 0000000..0acca46 --- /dev/null +++ b/integrations/hermes/braindb/README.md @@ -0,0 +1,66 @@ +# BrainDB memory provider for Hermes Agent + +Use a self-hosted [BrainDB](https://github.com/dimknaf/braindb) as the long-term +memory backend for [Hermes Agent](https://github.com/NousResearch/hermes-agent). + +It's a small native Hermes `MemoryProvider`: the Hermes agent gets two tools that +talk to a running BrainDB over HTTP. Nothing about BrainDB's API is re-implemented +here — the gateway tool delegates to BrainDB's own agent, which already wields its +full toolset. + +## Install + +**Option A — drop-in folder** + +```bash +cp -r braindb ~/.hermes/plugins/memory/braindb +``` + +**Option B — pip** (if packaged): `pip install hermes-plugins-braindb`. + +Then activate it: + +```bash +hermes memory setup # pick "braindb" +``` + +## Configure + +One setting, resolved in this order: `BRAINDB_BASE_URL` env var → +`$HERMES_HOME/braindb.json` (`{"base_url": "..."}`) → default `http://localhost:8000`. + +```bash +export BRAINDB_BASE_URL=http://localhost:8000 +``` + +## Tools the agent gets + +- **`braindb_ask(query)`** — ask BrainDB in natural language to recall or remember. + This is the gateway to *all* of BrainDB: its internal agent does the search, + storage, graph-linking and reasoning, and returns an answer. +- **`braindb_ingest(file_path)`** — upload a local text file to BrainDB. BrainDB + ingests and **fact-extracts it asynchronously**, so the extracted facts show up + on a *later* `braindb_ask` recall, not immediately. Supported extensions: + `.md .txt .json .yaml .yml .csv .log .html .xml`. + +Usage instructions for the agent are loaded **live** from BrainDB's +`/api/v1/skill/braindb-agent` endpoint (single source of truth, cached locally), +so there's no duplicated prompt to keep in sync. + +## Deployment notes + +- **Shared store, single user.** All memory is one BrainDB instance; this provider + does no per-user scoping. +- **BrainDB has no authentication.** Run Hermes and BrainDB **co-located**, with + BrainDB on loopback (`localhost`). For a *remote* BrainDB you must put your own + auth / tunnel in front of it — that's out of scope here. +- The provider makes all calls over HTTP itself; it never shells out via the + agent's terminal, so it works regardless of Hermes' terminal sandbox. + +## Requires on the BrainDB side + +Two small additive endpoints (shipped with BrainDB in +`braindb/routers/integrations.py`): `GET /api/v1/skill/{name}` (serves the skill +text) and `POST /api/v1/entities/datasources/upload` (lands a file in +`data/sources/` for the watcher to ingest). The BrainDB **watcher** must be +running for ingestion to happen (it is by default in `docker-compose`). diff --git a/integrations/hermes/braindb/__init__.py b/integrations/hermes/braindb/__init__.py new file mode 100644 index 0000000..9904ce7 --- /dev/null +++ b/integrations/hermes/braindb/__init__.py @@ -0,0 +1,267 @@ +""" +BrainDB memory provider for Hermes Agent (https://github.com/NousResearch/hermes-agent). + +Install: drop this `braindb/` folder into `~/.hermes/plugins/memory/braindb/` +(or pip-install it), then `hermes memory setup` and pick "braindb". + +Design (deliberately small): +- AGENT-ONLY gateway. `braindb_ask` forwards a natural-language request to + BrainDB's own `/agent/query`, whose internal agent wields BrainDB's full + toolset — so the *whole* of BrainDB (recall / save / relate / reason) is + reachable through one tool, with zero duplication of its logic here. +- `braindb_ingest` uploads a local file to BrainDB; BrainDB's watcher then + ingests + fact-extracts it asynchronously. +- Usage instructions are loaded LIVE from BrainDB's `/skill` endpoint (single + source of truth, cached locally) — no copied/duplicated prompt — plus a small + Hermes-specific note. + +The provider makes all HTTP calls itself (via `requests`); it never relies on +the agent's shell/terminal. Co-locate Hermes and BrainDB (BrainDB has no auth — +keep it on loopback or behind your own auth/tunnel for remote use). +""" +from __future__ import annotations + +import json +import logging +import os +from pathlib import Path + +import requests + +from agent.memory_provider import MemoryProvider + +logger = logging.getLogger(__name__) + +_DEFAULT_BASE_URL = "http://localhost:8000" +_SKILL_NAME = "braindb-agent" +_ASK_TIMEOUT = 90 # /agent/query runs an LLM loop — allow headroom +_HTTP_TIMEOUT = 15 # skill fetch / file upload +_HEALTH_TIMEOUT = 2 # readiness ping + +# Last-resort instructions, used ONLY if BrainDB is unreachable AND no cached +# skill exists. The live skill (loaded in initialize) is the real source. +_BUILTIN_INSTRUCTIONS = ( + "BrainDB is your persistent memory. Use `braindb_ask` with a natural-language " + "request to recall facts/context or to remember something. Use `braindb_ingest` " + "with a local file path to add a document to memory." +) + +# Hermes-specific addendum — net-new glue, NOT a copy of the skill. +_HERMES_NOTES = ( + "\n\n## Using BrainDB from Hermes\n" + "- `braindb_ask(query)` is the main interface: ask in plain language to recall or " + "save; BrainDB's own agent does the work and returns an answer.\n" + "- `braindb_ingest(file_path)` uploads a local file to BrainDB; ingestion + fact " + "extraction run asynchronously, so new facts appear on a *later* `braindb_ask` " + "recall, not immediately.\n" +) + +_ASK_SCHEMA = { + "name": "braindb_ask", + "description": ( + "Query your BrainDB persistent memory in natural language. Use it to RECALL " + "(facts, context, past decisions — anything you may have stored) and to REMEMBER " + "(save a new fact/thought, connect ideas). BrainDB's own agent handles search, " + "storage, graph links and reasoning, and returns a written answer." + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": ( + "A natural-language question or instruction, e.g. 'what do we know " + "about project X?' or 'remember that the deadline is Friday'." + ), + }, + }, + "required": ["query"], + }, +} + +_INGEST_SCHEMA = { + "name": "braindb_ingest", + "description": ( + "Add a local document to BrainDB memory. Provide the path to a text file on this " + "machine; it is uploaded to BrainDB, which ingests it and extracts facts in the " + "background — so the extracted facts become available on a LATER braindb_ask " + "recall, not instantly. Supported: .md .txt .json .yaml .yml .csv .log .html .xml." + ), + "parameters": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Absolute or relative path to a local file to ingest.", + }, + }, + "required": ["file_path"], + }, +} + + +class BrainDBMemoryProvider(MemoryProvider): + """Exposes a self-hosted BrainDB instance to Hermes as a memory provider.""" + + def __init__(self) -> None: + self._base_url = _DEFAULT_BASE_URL + self._instructions = _BUILTIN_INSTRUCTIONS + self._hermes_home = "" + + @property + def name(self) -> str: + return "braindb" + + def is_available(self) -> bool: + """True only if a BrainDB is actually answering — so Hermes won't + activate a dead provider. Bounded by a short timeout.""" + try: + r = requests.get(f"{self._resolve_base_url()}/health", timeout=_HEALTH_TIMEOUT) + return r.status_code == 200 + except requests.RequestException: + return False + + def initialize(self, session_id: str, **kwargs) -> None: + self._hermes_home = kwargs.get("hermes_home", "") or "" + self._base_url = self._resolve_base_url() + self._instructions = self._load_instructions() + + def get_config_schema(self): + return [ + { + "key": "base_url", + "description": "BrainDB base URL (the running BrainDB API).", + "default": _DEFAULT_BASE_URL, + "env_var": "BRAINDB_BASE_URL", + }, + ] + + def save_config(self, values, hermes_home: str) -> None: + try: + data = {k: v for k, v in (values or {}).items() if v is not None} + (Path(hermes_home) / "braindb.json").write_text( + json.dumps(data, indent=2), encoding="utf-8" + ) + except Exception as e: # config persistence is best-effort + logger.warning("braindb: could not write config: %s", e) + + def system_prompt_block(self) -> str: + return f"{self._instructions}{_HERMES_NOTES}" + + def get_tool_schemas(self): + return [_ASK_SCHEMA, _INGEST_SCHEMA] + + def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: + try: + if tool_name == "braindb_ask": + return self._ask(args) + if tool_name == "braindb_ingest": + return self._ingest(args) + return _err(f"Unknown tool: {tool_name}") + except Exception as e: # a tool call must never crash the turn + logger.exception("braindb: tool %s failed", tool_name) + return _err(str(e)) + + # ---- internals ------------------------------------------------------- + + def _resolve_base_url(self) -> str: + """env BRAINDB_BASE_URL → $HERMES_HOME/braindb.json → default.""" + env = os.environ.get("BRAINDB_BASE_URL") + if env: + return env.rstrip("/") + home = self._hermes_home or os.environ.get("HERMES_HOME", "") + if home: + cfg = Path(home) / "braindb.json" + if cfg.is_file(): + try: + val = json.loads(cfg.read_text(encoding="utf-8")).get("base_url") + if val: + return str(val).rstrip("/") + except Exception: + pass + return _DEFAULT_BASE_URL + + def _cache_path(self) -> Path: + home = self._hermes_home or os.environ.get( + "HERMES_HOME", str(Path.home() / ".hermes") + ) + return Path(home) / "braindb_skill.md" + + def _load_instructions(self) -> str: + """Single source of truth: fetch the skill from BrainDB, cache it so a + transient outage still has the last copy, fall back to a built-in line. + Only needed when BrainDB is up (the tools call BrainDB anyway), so this + introduces no new failure mode.""" + try: + r = requests.get( + f"{self._base_url}/api/v1/skill/{_SKILL_NAME}", timeout=_HTTP_TIMEOUT + ) + if r.status_code == 200 and r.text.strip(): + try: + self._cache_path().write_text(r.text, encoding="utf-8") + except Exception: + pass + return r.text + except requests.RequestException: + pass + try: + cached = self._cache_path() + if cached.is_file(): + return cached.read_text(encoding="utf-8") + except Exception: + pass + return _BUILTIN_INSTRUCTIONS + + def _ask(self, args: dict) -> str: + query = (args or {}).get("query", "").strip() + if not query: + return _err("Missing required parameter: query") + try: + r = requests.post( + f"{self._base_url}/api/v1/agent/query", + json={"query": query}, + timeout=_ASK_TIMEOUT, + ) + r.raise_for_status() + except requests.Timeout: + return _err(f"BrainDB agent timed out after {_ASK_TIMEOUT}s; try a simpler query.") + except requests.RequestException as e: + return _err(f"BrainDB request failed: {e}") + answer = (r.json() or {}).get("answer", "") + return json.dumps({"answer": answer}) + + def _ingest(self, args: dict) -> str: + raw = (args or {}).get("file_path", "").strip() + if not raw: + return _err("Missing required parameter: file_path") + path = Path(raw).expanduser() + if not path.is_file(): + return _err(f"File not found: {raw}") + try: + data = path.read_bytes() + r = requests.post( + f"{self._base_url}/api/v1/entities/datasources/upload", + params={"filename": path.name}, + data=data, + headers={"Content-Type": "application/octet-stream"}, + timeout=_HTTP_TIMEOUT, + ) + r.raise_for_status() + except requests.RequestException as e: + return _err(f"BrainDB upload failed: {e}") + body = r.json() if r.content else {} + return json.dumps({ + "status": "uploaded", + "filename": body.get("filename", path.name), + "message": "Uploaded; BrainDB will ingest and fact-extract it in the background.", + }) + + +def _err(message: str) -> str: + """Tool-error contract: a JSON string (never a raised exception).""" + return json.dumps({"error": message}) + + +def register(ctx) -> None: + """Hermes plugin entry point.""" + ctx.register_memory_provider(BrainDBMemoryProvider()) diff --git a/integrations/hermes/braindb/plugin.yaml b/integrations/hermes/braindb/plugin.yaml new file mode 100644 index 0000000..42e46c3 --- /dev/null +++ b/integrations/hermes/braindb/plugin.yaml @@ -0,0 +1,5 @@ +name: braindb +version: 1.0.0 +description: "BrainDB — self-hosted memory: natural-language recall/save via its own agent, plus file ingestion. Talks to a running BrainDB over HTTP." +pip_dependencies: + - requests diff --git a/integrations/hermes/sandbox/.env.example b/integrations/hermes/sandbox/.env.example new file mode 100644 index 0000000..c777bd6 --- /dev/null +++ b/integrations/hermes/sandbox/.env.example @@ -0,0 +1,9 @@ +# Copy to ./hermes-data/.env (Hermes reads /opt/data/.env at startup). +# NEVER commit a filled-in copy — it holds your API key. + +# Hermes' OWN brain — DeepInfra key (custom OpenAI-compatible provider in config.yaml). +CUSTOM_API_KEY= + +# Where the braindb memory provider reaches BrainDB (its container on local-network). +# Also set in the compose `environment:`; kept here so Hermes loads it into the env too. +BRAINDB_BASE_URL=http://braindb_api:8000 diff --git a/integrations/hermes/sandbox/README.md b/integrations/hermes/sandbox/README.md new file mode 100644 index 0000000..130964b --- /dev/null +++ b/integrations/hermes/sandbox/README.md @@ -0,0 +1,98 @@ +# Hermes ↔ BrainDB sandbox (run it on an isolated throwaway host) + +A self-contained way to actually run **Hermes Agent** against BrainDB to test the +[`braindb` memory provider](../braindb/) — **safely**. + +## ⚠ Why a throwaway host + +Hermes is an autonomous agent: by default its `terminal` / `execute_code` / browser tools +run **on the host**, and its docs say plainly *"the only security boundary against an +adversarial LLM is the OS."* So: + +1. We run Hermes **in a container** with `cap_drop: ALL` + `no-new-privileges`. +2. We **strip** the dangerous toolsets in `config.yaml` (it can only use `braindb_*`). +3. Most importantly, we run the whole thing on a **throwaway, low-trust host** (a + Raspberry Pi) — never a work laptop. If Hermes misbehaves, the blast radius is the Pi. + +We also run a **separate, throwaway BrainDB** here — not your real personal BrainDB — so the +test never pollutes real memory or re-exposes an unauthenticated service. + +## What runs + +- **BrainDB** (api + bundled Postgres + watcher) via BrainDB's own compose, `internal-db` + profile, `LLM_PROFILE=deepinfra`. On the `local-network` docker network. +- **Hermes** (`nousresearch/hermes-agent`, this folder's compose) joined to `local-network`, + reaching BrainDB at `http://braindb_api:8000`, brain on **deepinfra** (a ≥64k model). + +Both LLMs are on deepinfra — **the workstation/bench is never touched.** + +## Deploy (on the Pi) + +```bash +# 0) Get the repo onto the Pi (e.g. rsync from your machine) and cd into it. +cd ~/braindb + +# 1) Network BrainDB expects +docker network create local-network 2>/dev/null || true + +# 2) BrainDB env (repo root .env) — bundled DB + deepinfra + wiki off +cat > .env <<'EOF' +COMPOSE_PROFILES=internal-db +LLM_PROFILE=deepinfra +DEEPINFRA_API_KEY= +WIKI_ENABLED=false +EOF + +# 3) Build + start BrainDB (first build on arm64 takes a while; downloads the +# embedding model on first /health) +docker compose up -d --build +curl -s http://localhost:8000/health # {"status":"ok","embeddings":true} + +# 4) Hermes profile dir: config + .env + (plugin is bind-mounted by the compose) +cd integrations/hermes/sandbox +mkdir -p hermes-data +cp config.yaml hermes-data/config.yaml +cp .env.example hermes-data/.env +# edit hermes-data/.env -> set CUSTOM_API_KEY= + +# 5) Start Hermes +docker compose -f docker-compose.hermes-test.yml up -d +``` + +## Verify + +```bash +# Hermes can reach BrainDB +docker exec hermes_test curl -s http://braindb_api:8000/health + +# Only the braindb tools are present; shell/code/browser are OFF +docker exec -it hermes_test hermes tools + +# braindb is the active memory provider +docker exec hermes_test hermes memory status + +# Talk to it — it should call braindb_ask -> BrainDB answers +docker exec -it hermes_test hermes chat -m "What do you remember about me? Save: my favourite colour is teal." + +# Ingest: a file INSIDE the container -> braindb_ingest -> BrainDB watcher extracts (async) +docker exec hermes_test sh -c 'echo "Project Falcon ships on Friday." > /tmp/note.md' +docker exec -it hermes_test hermes chat -m "Ingest the file at /tmp/note.md into memory." +``` + +## Teardown + +```bash +docker compose -f docker-compose.hermes-test.yml down -v +rm -rf hermes-data +# (and `docker compose down -v` at the repo root to drop the throwaway BrainDB) +``` + +## Notes + +- Hermes' brain needs a **≥64k-context** model (it refuses smaller). The default + `meta-llama/Llama-3.3-70B-Instruct` (128k) qualifies; change it in `config.yaml`. +- If `hermes memory status` doesn't show `braindb`, the plugin may need enabling + (`docker exec -it hermes_test hermes plugins`); the provider is at + `/opt/data/plugins/memory/braindb` (bind-mounted read-only). +- Remote BrainDB would need auth in front of it; here everything is co-located on one + isolated host, so loopback/internal-network access is fine. diff --git a/integrations/hermes/sandbox/config.yaml b/integrations/hermes/sandbox/config.yaml new file mode 100644 index 0000000..7a17275 --- /dev/null +++ b/integrations/hermes/sandbox/config.yaml @@ -0,0 +1,29 @@ +# Hermes Agent config for the BrainDB sandbox test (throwaway profile). +# Copied to ./hermes-data/config.yaml before `docker compose up`. +# +# Brain : deepinfra via a custom OpenAI-compatible endpoint (model >= 64k ctx). +# Memory : the braindb provider (its braindb_ask / braindb_ingest tools). +# Safety : every dangerous toolset is STRIPPED below, so the agent can only +# reach BrainDB — it cannot run shell, code, the browser, or write files. + +model: + provider: custom + # DeepInfra-hosted, 128k context (well over Hermes' 64k floor) and strong at + # tool use. Swap for any >=64k deepinfra model you prefer. + default: meta-llama/Llama-3.3-70B-Instruct + base_url: https://api.deepinfra.com/v1/openai + # API key is read from CUSTOM_API_KEY in ./hermes-data/.env (not hard-coded here). + +memory: + provider: braindb + +agent: + # Strip everything that could touch the host or the open internet. The braindb + # provider's tools are NOT part of these toolsets, so they remain available. + disabled_toolsets: + - terminal # arbitrary shell on the host + - execute_code # arbitrary Python (+ a known terminal-approval bypass, #4146) + - browser # web automation + - computer_use # desktop control + - web # web_search / web_extract (network egress) + - file # write_file / patch (filesystem writes) diff --git a/integrations/hermes/sandbox/docker-compose.hermes-test.yml b/integrations/hermes/sandbox/docker-compose.hermes-test.yml new file mode 100644 index 0000000..38f317d --- /dev/null +++ b/integrations/hermes/sandbox/docker-compose.hermes-test.yml @@ -0,0 +1,44 @@ +# Hermes Agent sandbox — runs Hermes in a HARDENED, TOOL-STRIPPED container and +# joins it to BrainDB's docker network so it reaches the API at +# http://braindb_api:8000. Hermes' shell / code-exec / browser / file tools are +# disabled in config.yaml (see ./hermes-data/config.yaml) so it can ONLY use the +# braindb_* memory tools. +# +# Intended for an ISOLATED, THROWAWAY host (the Raspberry Pi) — NOT a work laptop. +# Bring BrainDB up FIRST (its own compose, COMPOSE_PROFILES=internal-db + +# LLM_PROFILE=deepinfra), then this. See README.md. + +services: + hermes: + image: nousresearch/hermes-agent:latest # multi-arch (incl. arm64 for the Pi) + container_name: hermes_test + restart: unless-stopped + networks: + - local-network + environment: + # The braindb memory provider reads this. braindb_api is BrainDB's + # container on local-network (its api listens on 8000). + BRAINDB_BASE_URL: http://braindb_api:8000 + # Hermes' OWN brain — deepinfra via the custom OpenAI-compatible provider. + # Key set in ./hermes-data/.env (CUSTOM_API_KEY); mirrored here for safety. + CUSTOM_API_KEY: ${CUSTOM_API_KEY:-} + volumes: + # Throwaway profile dir (/opt/data == ~/.hermes inside the image): holds + # config.yaml, .env and state. NOT a real ~/.hermes. + - ./hermes-data:/opt/data + # The BrainDB memory-provider plugin (read-only), from the sibling folder. + - ../braindb:/opt/data/plugins/memory/braindb:ro + # Hermes itself needs no host capabilities — drop them all (defense in depth + # on top of the tool-stripping; the real isolation is the throwaway Pi host). + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + # Keeps the agent process alive; interact with: + # docker exec -it hermes_test hermes chat + command: gateway run + +networks: + # BrainDB already declares this as an external network; we join the same one. + local-network: + external: true diff --git a/tests/test_integrations.py b/tests/test_integrations.py new file mode 100644 index 0000000..a631afb --- /dev/null +++ b/tests/test_integrations.py @@ -0,0 +1,66 @@ +""" +External-integration endpoints (the Hermes memory-provider surface): + GET /api/v1/skill/{name} + POST /api/v1/entities/datasources/upload + +Runs against the isolated test stack (the `api` fixture). That stack has NO +watcher and uses the throwaway ./data_test mount, so uploads here never trigger +ingestion (no LLM) and never touch the personal stack. +""" +import requests + +SKILL = "/api/v1/skill" +UPLOAD = "/api/v1/entities/datasources/upload" + + +def test_skill_served(api): + r = requests.get(f"{api}{SKILL}/braindb-agent", timeout=10) + assert r.status_code == 200 + assert r.text.strip() # non-empty markdown body + + +def test_skill_unknown_404(api): + r = requests.get(f"{api}{SKILL}/no-such-skill", timeout=10) + assert r.status_code == 404 + + +def test_skill_unsafe_name_400(api): + # leading dot -> rejected by _safe_basename before any disk access + r = requests.get(f"{api}{SKILL}/.hidden", timeout=10) + assert r.status_code == 400 + + +def test_upload_accepted(api, test_tag): + name = f"{test_tag}.md" + r = requests.post( + f"{api}{UPLOAD}", + params={"filename": name}, + data=b"# note\n\nbody text", + headers={"Content-Type": "application/octet-stream"}, + timeout=10, + ) + assert r.status_code == 202, r.text + assert r.json()["filename"] == name + + +def test_upload_bad_extension_400(api, test_tag): + r = requests.post( + f"{api}{UPLOAD}", params={"filename": f"{test_tag}.exe"}, data=b"x", timeout=10 + ) + assert r.status_code == 400 + + +def test_upload_empty_400(api, test_tag): + r = requests.post( + f"{api}{UPLOAD}", params={"filename": f"{test_tag}.md"}, data=b"", timeout=10 + ) + assert r.status_code == 400 + + +def test_upload_collision_no_overwrite(api, test_tag): + name = f"{test_tag}_c.md" + first = requests.post(f"{api}{UPLOAD}", params={"filename": name}, data=b"one", timeout=10) + second = requests.post(f"{api}{UPLOAD}", params={"filename": name}, data=b"two", timeout=10) + assert first.status_code == 202 and second.status_code == 202 + assert first.json()["filename"] == name + assert second.json()["filename"] == f"{test_tag}_c.1.md" From 01195467c41668c0b6dbbb044ba6dac28c243e9b Mon Sep 17 00:00:00 2001 From: dimknaf <136385722+dimknaf@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:23:16 +0100 Subject: [PATCH 2/4] fix(integrations): drop Llama default; fix toolset name + flat plugin path - Remove all Llama-3.3-70B references. The sandbox template no longer hardcodes any model: model.default + model.api_key are blank placeholders, filled only in the gitignored Pi copy (hermes-data/config.yaml). - Disable the correct toolset name 'code_execution' (was 'execute_code', which left code execution enabled). - User memory-plugin path is FLAT (~/.hermes/plugins//): fix the compose mount target, the plugin + sandbox READMEs, and the plugin docstring. - Sync the sandbox compose to the working form: default caps + non-root UID (the image's s6 init needs CHOWN/SETUID/SETGID, so cap_drop ALL broke startup). - Drop the vestigial .env.example (the key now lives in config.yaml model.api_key). --- integrations/hermes/braindb/README.md | 4 +- integrations/hermes/braindb/__init__.py | 4 +- integrations/hermes/sandbox/.env.example | 9 --- integrations/hermes/sandbox/README.md | 80 ++++++++----------- integrations/hermes/sandbox/config.yaml | 32 ++++---- .../sandbox/docker-compose.hermes-test.yml | 44 ++++------ 6 files changed, 69 insertions(+), 104 deletions(-) delete mode 100644 integrations/hermes/sandbox/.env.example diff --git a/integrations/hermes/braindb/README.md b/integrations/hermes/braindb/README.md index 0acca46..fef941f 100644 --- a/integrations/hermes/braindb/README.md +++ b/integrations/hermes/braindb/README.md @@ -10,10 +10,10 @@ full toolset. ## Install -**Option A — drop-in folder** +**Option A — drop-in folder** (user plugins live FLAT under `plugins//`) ```bash -cp -r braindb ~/.hermes/plugins/memory/braindb +cp -r braindb ~/.hermes/plugins/braindb ``` **Option B — pip** (if packaged): `pip install hermes-plugins-braindb`. diff --git a/integrations/hermes/braindb/__init__.py b/integrations/hermes/braindb/__init__.py index 9904ce7..4a89cec 100644 --- a/integrations/hermes/braindb/__init__.py +++ b/integrations/hermes/braindb/__init__.py @@ -1,8 +1,8 @@ """ BrainDB memory provider for Hermes Agent (https://github.com/NousResearch/hermes-agent). -Install: drop this `braindb/` folder into `~/.hermes/plugins/memory/braindb/` -(or pip-install it), then `hermes memory setup` and pick "braindb". +Install: drop this `braindb/` folder into `~/.hermes/plugins/braindb/` (user +plugins live FLAT under plugins//), then `hermes memory setup` -> "braindb". Design (deliberately small): - AGENT-ONLY gateway. `braindb_ask` forwards a natural-language request to diff --git a/integrations/hermes/sandbox/.env.example b/integrations/hermes/sandbox/.env.example deleted file mode 100644 index c777bd6..0000000 --- a/integrations/hermes/sandbox/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Copy to ./hermes-data/.env (Hermes reads /opt/data/.env at startup). -# NEVER commit a filled-in copy — it holds your API key. - -# Hermes' OWN brain — DeepInfra key (custom OpenAI-compatible provider in config.yaml). -CUSTOM_API_KEY= - -# Where the braindb memory provider reaches BrainDB (its container on local-network). -# Also set in the compose `environment:`; kept here so Hermes loads it into the env too. -BRAINDB_BASE_URL=http://braindb_api:8000 diff --git a/integrations/hermes/sandbox/README.md b/integrations/hermes/sandbox/README.md index 130964b..29cea7b 100644 --- a/integrations/hermes/sandbox/README.md +++ b/integrations/hermes/sandbox/README.md @@ -1,41 +1,39 @@ # Hermes ↔ BrainDB sandbox (run it on an isolated throwaway host) -A self-contained way to actually run **Hermes Agent** against BrainDB to test the +A self-contained way to actually run **Hermes Agent** against BrainDB and test the [`braindb` memory provider](../braindb/) — **safely**. ## ⚠ Why a throwaway host -Hermes is an autonomous agent: by default its `terminal` / `execute_code` / browser tools +Hermes is an autonomous agent: by default its `terminal` / `code_execution` / browser tools run **on the host**, and its docs say plainly *"the only security boundary against an -adversarial LLM is the OS."* So: +adversarial LLM is the OS."* So, defense in depth: -1. We run Hermes **in a container** with `cap_drop: ALL` + `no-new-privileges`. -2. We **strip** the dangerous toolsets in `config.yaml` (it can only use `braindb_*`). -3. Most importantly, we run the whole thing on a **throwaway, low-trust host** (a - Raspberry Pi) — never a work laptop. If Hermes misbehaves, the blast radius is the Pi. +1. We **strip** the dangerous toolsets in `config.yaml` — the agent can only use `braindb_*`. +2. We run Hermes **in a container**, as a non-root UID. (We don't `cap_drop` everything — the + image's s6 init needs `CHOWN`/`SETUID`/`SETGID`; that's why the host isolation matters most.) +3. Most importantly, we run the whole thing on a **throwaway, low-trust host** (a Raspberry + Pi) — never a work laptop. If Hermes misbehaves, the blast radius is the Pi. -We also run a **separate, throwaway BrainDB** here — not your real personal BrainDB — so the -test never pollutes real memory or re-exposes an unauthenticated service. +We also run a **separate, throwaway BrainDB** here — not your real personal one — so the test +never pollutes real memory or re-exposes an unauthenticated service. ## What runs - **BrainDB** (api + bundled Postgres + watcher) via BrainDB's own compose, `internal-db` - profile, `LLM_PROFILE=deepinfra`. On the `local-network` docker network. -- **Hermes** (`nousresearch/hermes-agent`, this folder's compose) joined to `local-network`, - reaching BrainDB at `http://braindb_api:8000`, brain on **deepinfra** (a ≥64k model). + profile, `LLM_PROFILE=deepinfra`, on the `local-network` docker network. +- **Hermes** (`nousresearch/hermes-agent`, this folder's compose) on the same network, + reaching BrainDB at `http://braindb_api:8000`, brain on **deepinfra**. -Both LLMs are on deepinfra — **the workstation/bench is never touched.** +Both LLMs are on deepinfra — **the workstation is never touched.** ## Deploy (on the Pi) ```bash -# 0) Get the repo onto the Pi (e.g. rsync from your machine) and cd into it. cd ~/braindb - -# 1) Network BrainDB expects docker network create local-network 2>/dev/null || true -# 2) BrainDB env (repo root .env) — bundled DB + deepinfra + wiki off +# 1) BrainDB env (repo-root .env): bundled DB + deepinfra + wiki off cat > .env <<'EOF' COMPOSE_PROFILES=internal-db LLM_PROFILE=deepinfra @@ -43,56 +41,46 @@ DEEPINFRA_API_KEY= WIKI_ENABLED=false EOF -# 3) Build + start BrainDB (first build on arm64 takes a while; downloads the -# embedding model on first /health) -docker compose up -d --build -curl -s http://localhost:8000/health # {"status":"ok","embeddings":true} +# 2) Build + start BrainDB (first arm64 build is slow; downloads the embedding model) +docker compose up -d --build api watcher braindb_db +curl -s http://localhost:8000/health # {"status":"ok","embeddings":true} -# 4) Hermes profile dir: config + .env + (plugin is bind-mounted by the compose) +# 3) Hermes profile: copy the template, then set YOUR model + key in it (gitignored) cd integrations/hermes/sandbox mkdir -p hermes-data cp config.yaml hermes-data/config.yaml -cp .env.example hermes-data/.env -# edit hermes-data/.env -> set CUSTOM_API_KEY= +# edit hermes-data/config.yaml -> model.default: =64k-context model id> +# model.api_key: -# 5) Start Hermes +# 4) Start Hermes docker compose -f docker-compose.hermes-test.yml up -d ``` ## Verify ```bash -# Hermes can reach BrainDB -docker exec hermes_test curl -s http://braindb_api:8000/health - -# Only the braindb tools are present; shell/code/browser are OFF -docker exec -it hermes_test hermes tools - -# braindb is the active memory provider -docker exec hermes_test hermes memory status +docker exec hermes_test curl -s http://braindb_api:8000/health # Hermes can reach BrainDB +docker exec hermes_test hermes memory status # Provider: braindb / installed + active -# Talk to it — it should call braindb_ask -> BrainDB answers -docker exec -it hermes_test hermes chat -m "What do you remember about me? Save: my favourite colour is teal." +# Talk to it (-q = single non-interactive query). It should call braindb_ask. +docker exec hermes_test hermes chat -q "Remember that my favourite colour is teal, then tell me what you stored." -# Ingest: a file INSIDE the container -> braindb_ingest -> BrainDB watcher extracts (async) +# Ingest a local file -> braindb_ingest -> BrainDB watcher extracts (async) docker exec hermes_test sh -c 'echo "Project Falcon ships on Friday." > /tmp/note.md' -docker exec -it hermes_test hermes chat -m "Ingest the file at /tmp/note.md into memory." +docker exec hermes_test hermes chat -q "Ingest the file at /tmp/note.md into memory." ``` ## Teardown ```bash -docker compose -f docker-compose.hermes-test.yml down -v -rm -rf hermes-data -# (and `docker compose down -v` at the repo root to drop the throwaway BrainDB) +docker compose -f docker-compose.hermes-test.yml down -v && rm -rf hermes-data +docker compose down -v # at the repo root, to drop the throwaway BrainDB ``` ## Notes -- Hermes' brain needs a **≥64k-context** model (it refuses smaller). The default - `meta-llama/Llama-3.3-70B-Instruct` (128k) qualifies; change it in `config.yaml`. -- If `hermes memory status` doesn't show `braindb`, the plugin may need enabling - (`docker exec -it hermes_test hermes plugins`); the provider is at - `/opt/data/plugins/memory/braindb` (bind-mounted read-only). +- **Model:** Hermes refuses models under 64k context; set a ≥64k model in `config.yaml`. +- **Plugin path is FLAT:** the provider is mounted at `/opt/data/plugins/braindb` (= user + plugins live at `~/.hermes/plugins//`, not under a `memory/` subdir). - Remote BrainDB would need auth in front of it; here everything is co-located on one - isolated host, so loopback/internal-network access is fine. + isolated host, so loopback / internal-network access is fine. diff --git a/integrations/hermes/sandbox/config.yaml b/integrations/hermes/sandbox/config.yaml index 7a17275..ddebf24 100644 --- a/integrations/hermes/sandbox/config.yaml +++ b/integrations/hermes/sandbox/config.yaml @@ -1,29 +1,29 @@ -# Hermes Agent config for the BrainDB sandbox test (throwaway profile). -# Copied to ./hermes-data/config.yaml before `docker compose up`. +# Hermes Agent config for the BrainDB sandbox (TEMPLATE — throwaway profile). +# Copied to ./hermes-data/config.yaml at deploy; edit THAT copy (gitignored) with +# your real model id + deepinfra key. Nothing secret is committed here. # -# Brain : deepinfra via a custom OpenAI-compatible endpoint (model >= 64k ctx). +# Brain : deepinfra via a custom OpenAI-compatible endpoint. # Memory : the braindb provider (its braindb_ask / braindb_ingest tools). -# Safety : every dangerous toolset is STRIPPED below, so the agent can only -# reach BrainDB — it cannot run shell, code, the browser, or write files. +# Safety : every dangerous toolset is STRIPPED below, so the agent can only reach +# BrainDB — it cannot run shell, code, the browser, or write files. model: provider: custom - # DeepInfra-hosted, 128k context (well over Hermes' 64k floor) and strong at - # tool use. Swap for any >=64k deepinfra model you prefer. - default: meta-llama/Llama-3.3-70B-Instruct base_url: https://api.deepinfra.com/v1/openai - # API key is read from CUSTOM_API_KEY in ./hermes-data/.env (not hard-coded here). + # Fill these in your gitignored ./hermes-data/config.yaml (NEVER commit them): + default: "" # a model id with >=64k context (Hermes refuses anything smaller) + api_key: "" # your deepinfra key (the custom provider reads model.api_key) memory: provider: braindb agent: # Strip everything that could touch the host or the open internet. The braindb - # provider's tools are NOT part of these toolsets, so they remain available. + # provider's tools are NOT part of these toolsets, so they stay available. disabled_toolsets: - - terminal # arbitrary shell on the host - - execute_code # arbitrary Python (+ a known terminal-approval bypass, #4146) - - browser # web automation - - computer_use # desktop control - - web # web_search / web_extract (network egress) - - file # write_file / patch (filesystem writes) + - terminal # arbitrary shell on the host + - code_execution # arbitrary Python (toolset is 'code_execution', not 'execute_code') + - browser # web automation + - computer_use # desktop control + - web # web_search / web_extract (network egress) + - file # write_file / patch (filesystem writes) diff --git a/integrations/hermes/sandbox/docker-compose.hermes-test.yml b/integrations/hermes/sandbox/docker-compose.hermes-test.yml index 38f317d..7f56404 100644 --- a/integrations/hermes/sandbox/docker-compose.hermes-test.yml +++ b/integrations/hermes/sandbox/docker-compose.hermes-test.yml @@ -1,13 +1,14 @@ -# Hermes Agent sandbox — runs Hermes in a HARDENED, TOOL-STRIPPED container and -# joins it to BrainDB's docker network so it reaches the API at -# http://braindb_api:8000. Hermes' shell / code-exec / browser / file tools are -# disabled in config.yaml (see ./hermes-data/config.yaml) so it can ONLY use the -# braindb_* memory tools. +# Hermes Agent sandbox — runs Hermes in a TOOL-STRIPPED container (see +# ./hermes-data/config.yaml: agent.disabled_toolsets) joined to BrainDB's docker +# network so it reaches the API at http://braindb_api:8000. # -# Intended for an ISOLATED, THROWAWAY host (the Raspberry Pi) — NOT a work laptop. -# Bring BrainDB up FIRST (its own compose, COMPOSE_PROFILES=internal-db + -# LLM_PROFILE=deepinfra), then this. See README.md. - +# For an ISOLATED, THROWAWAY host (a Raspberry Pi) — NOT a work laptop. The host +# isolation + the tool-stripping ARE the security boundary. We do NOT cap_drop: +# the image's s6-overlay init needs CHOWN/SETUID/SETGID to set up its own +# unprivileged user and /opt/data (matches the official Hermes compose); instead +# we run as a non-root UID. +# +# Bring BrainDB up first (its own compose, COMPOSE_PROFILES=internal-db), then this. services: hermes: image: nousresearch/hermes-agent:latest # multi-arch (incl. arm64 for the Pi) @@ -16,28 +17,13 @@ services: networks: - local-network environment: - # The braindb memory provider reads this. braindb_api is BrainDB's - # container on local-network (its api listens on 8000). - BRAINDB_BASE_URL: http://braindb_api:8000 - # Hermes' OWN brain — deepinfra via the custom OpenAI-compatible provider. - # Key set in ./hermes-data/.env (CUSTOM_API_KEY); mirrored here for safety. - CUSTOM_API_KEY: ${CUSTOM_API_KEY:-} + BRAINDB_BASE_URL: http://braindb_api:8000 # the braindb memory provider reads this + HERMES_UID: "1000" # run as the host user that owns ./hermes-data + HERMES_GID: "1000" volumes: - # Throwaway profile dir (/opt/data == ~/.hermes inside the image): holds - # config.yaml, .env and state. NOT a real ~/.hermes. - - ./hermes-data:/opt/data - # The BrainDB memory-provider plugin (read-only), from the sibling folder. - - ../braindb:/opt/data/plugins/memory/braindb:ro - # Hermes itself needs no host capabilities — drop them all (defense in depth - # on top of the tool-stripping; the real isolation is the throwaway Pi host). - cap_drop: - - ALL - security_opt: - - no-new-privileges:true - # Keeps the agent process alive; interact with: - # docker exec -it hermes_test hermes chat + - ./hermes-data:/opt/data # throwaway profile (config.yaml with your model + key) + - ../braindb:/opt/data/plugins/braindb:ro # the BrainDB plugin (FLAT user-plugin path: plugins//) command: gateway run - networks: # BrainDB already declares this as an external network; we join the same one. local-network: From 0d5284d64929186ad1d5a6ad5acbf8b9e68c015a Mon Sep 17 00:00:00 2001 From: dimknaf <136385722+dimknaf@users.noreply.github.com> Date: Sun, 14 Jun 2026 01:16:25 +0100 Subject: [PATCH 3/4] docs: point to the Hermes integration from README + CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brief "Integrations" section in README.md linking to integrations/hermes/ (the provider plugin + the test sandbox), and note the folder in CLAUDE.md's project structure (+ list the new integrations.py router). Signpost only — the detailed docs live in integrations/hermes/. --- CLAUDE.md | 3 ++- README.md | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0e780fb..d4567dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ braindb/ │ ├── config.py # Settings (decay rates, graph params, agent config) │ ├── db.py # get_conn() — one psycopg2 connection per request │ ├── ingest_watcher.py # Always-on sidecar: polls data/sources/, triggers agent -│ ├── routers/ # entities.py, relations.py, memory.py, agent.py +│ ├── routers/ # entities.py, relations.py, memory.py, agent.py, wiki.py, integrations.py │ ├── schemas/ # Pydantic models (entities, relations, search) │ ├── services/ # search.py, graph.py, context.py, embedding_service.py, │ │ # keyword_service.py, activity_log.py @@ -88,6 +88,7 @@ braindb/ │ ├── braindb/SKILL.md # Direct curl-based skill │ └── braindb-agent/SKILL.md # Thin wrapper around the agent endpoint ├── frontend/ # Read-only browser UI — vanilla JS, no build (served at :8642) +├── integrations/hermes/ # BrainDB-as-memory for Hermes Agent (provider plugin + test sandbox) ├── docker-compose.yml # api + watcher + wiki_scheduler + frontend (+ optional bundled Postgres) ├── .env # Real credentials — DO NOT COMMIT └── BRAINDB_GUIDE.md # Full API reference with curl examples diff --git a/README.md b/README.md index 6eb48ce..434356e 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,10 @@ If you'd rather have BrainDB always running (even before any skill is invoked), Replace `/ABSOLUTE/PATH/TO/braindb` with your repo path. The hook is async (non-blocking). +## Integrations + +Use BrainDB as the memory backend for other agents. **Hermes Agent** (Nous Research) has a ready-made provider in [`integrations/hermes/`](integrations/hermes/): a native memory-provider plugin ([`braindb/`](integrations/hermes/braindb/)) that gives the agent a `braindb_ask` gateway plus a `braindb_ingest` tool, and a hardened, tool-stripped Docker sandbox ([`sandbox/`](integrations/hermes/sandbox/)) for safely trying it on a throwaway host. Each folder's README has the setup. + ## File Ingestion Drop a file in `data/sources/` — the always-on watcher sidecar picks it up within 7s, ingests it, and runs a chunked fact-extraction pipeline that saves atomic facts into the knowledge graph linked back to the source via `derived_from` relations. Processed files move to `data/sources/ingested/`, failures to `data/sources/failed/` with an `.error.txt` sidecar. From 44a4607d09faa875ff3fe1eff7ef123b75dc7aee Mon Sep 17 00:00:00 2001 From: dimknaf <136385722+dimknaf@users.noreply.github.com> Date: Sun, 14 Jun 2026 10:43:40 +0100 Subject: [PATCH 4/4] docs(hermes): drop unsupported pip install from plugin README Hermes discovers memory providers by scanning plugin folders, not via pip/entry points, so the listed pip package could never register the provider (and naming an unowned PyPI package invites a squat). Keep only the verified folder-copy install. --- integrations/hermes/braindb/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/integrations/hermes/braindb/README.md b/integrations/hermes/braindb/README.md index fef941f..cb33462 100644 --- a/integrations/hermes/braindb/README.md +++ b/integrations/hermes/braindb/README.md @@ -10,14 +10,13 @@ full toolset. ## Install -**Option A — drop-in folder** (user plugins live FLAT under `plugins//`) +Hermes discovers memory providers as folders under `~/.hermes/plugins/`. Copy this +plugin folder there (user plugins live FLAT under `plugins//`): ```bash -cp -r braindb ~/.hermes/plugins/braindb +cp -r integrations/hermes/braindb ~/.hermes/plugins/braindb ``` -**Option B — pip** (if packaged): `pip install hermes-plugins-braindb`. - Then activate it: ```bash