Content Guardrail Seams
ModusBrain exposes vendor-neutral guardrail seams at the boundaries where external content enters the retrieval layer and where queries/tool-inputs enter the LLM gateway. A guardrail is any external classifier — a content firewall, a prompt-injection detector, a PII scrubber — that wants to observe content at those boundaries. The OSS distribution ships inert: zero guardrails are registered by default, and every seam is a no-op until an operator registers a provider.Design contract (hard invariants)
These hold for every seam and are enforced bytest/guardrails.test.ts:
- Observe-only.
runGuardrails()returnsvoid. Callers never branch on a provider verdict. A guardrail registered through this interface cannot block, rewrite, drop, retry, or reorder ModusBrain behavior. Enforcement, if ever added, will get its own explicitly-named seam and its own RFC — it will not silently reuse this one. - Fail open. Missing config, provider throw/reject, timeout, and network error are all swallowed. A broken guardrail never breaks an ingest, a query, or a tool call.
- Inline await. Hooks await the provider before proceeding, so the classifier sees content at the exact pre-persist / pre-inference moment.
- No verdict persistence. ModusBrain writes no guardrail rows. Providers own their own audit trail.
- Content boundaries. Hooks pass only the ingest/user-facing payload — the markdown/code body, the last user message, the expansion query, the tool input. They never pass system prompts, full chat history, tool output, LLM output, embeddings, or multimodal/OCR/rerank payloads.
The five seams
All seams callrunGuardrails({ hook, content, metadata }) from
src/core/guardrails.ts.
The two
file_storage.* hooks cover every natural ingest caller that routes
through importFromContent / importCodeFile: modusbrain import, sync, capture,
put_page, subagent brain_put_page, trusted-workspace writes,
ingest_capture, inbox daemon dispatch, reindex, code reindex, and the public
import APIs.
Writing a guardrail provider
id, so a re-init won’t double-fire.
Provider responsibilities
ModusBrain deliberately keeps the seam minimal. The provider owns:- Timeout discipline. ModusBrain does not impose a timeout in
runGuardrailsso you can tune per-deployment latency. Use anAbortController. - Secret handling. Read API keys from env at call time. Never log the key.
- Redacted logging. Don’t log raw classified content (it may itself be the payload you’re trying to protect). Log a hash + verdict, not the body.
- Async fan-out. If you don’t want to block ingest on your classifier,
enqueue inside
classifyand return immediately. The seam awaits your function; what it does is up to you.
Example: shadow-mode firewall provider
A typical “shadow mode” provider (classify, log a redacted verdict, change nothing) is ~80 lines and lives entirely in the provider’s own package. See the reference provider doc shipped to integration partners for a completeclassify implementation that:
- resolves
<base>/classifyfrom an env URL, - posts
{ text, hook, metadata }with anx-api-keyheader, - parses a
{ prediction, blocked, score, threshold }response, - emits one redacted stderr line (
status=… prediction=… content_sha256=…), - fails open on every error path.