> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modusbrain.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Guardrails

# 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 by `test/guardrails.test.ts`:

* **Observe-only.** `runGuardrails()` returns `void`. 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 call `runGuardrails(&#123; hook, content, metadata &#125;)` from
`src/core/guardrails.ts`.

| `hook`                  | Location                               | Fires                                                                                                 |
| ----------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `file_storage.markdown` | `import-file.ts` → `importFromContent` | After `parseMarkdown` + size guard, **before** content-sanity, hashing, chunking, embedding, DB write |
| `file_storage.code`     | `import-file.ts` → `importCodeFile`    | After code size guard, **before** hashing, code-chunking, embedding, DB write                         |
| `ai_gateway.chat`       | `ai/gateway.ts` → `chat`               | On the **latest user message only**, before provider inference                                        |
| `ai_gateway.expand`     | `ai/gateway.ts` → `expand`             | On the query, before the expansion model call                                                         |
| `ai_gateway.tool_input` | `ai/gateway.ts` → `toolLoop`           | On `&#123;toolName, input&#125;`, before pending-persist and before tool execution                    |

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

```ts theme={null}
import { registerGuardrailProvider, type GuardrailInput } from 'modusbrain/core/guardrails';

registerGuardrailProvider({
  id: 'my-firewall',
  async classify(input: GuardrailInput) {
    // input.hook      — which boundary ('file_storage.markdown', etc.)
    // input.content   — the raw text to classify
    // input.metadata  — provider-opaque context (slug, source_kind, tool_name, model, ...)
    //
    // Do your own timeout/retry/logging here. The return value is IGNORED by
    // ModusBrain — return a typed verdict only if your own audit code consumes it.
    await fetch(MY_API, { method: 'POST', body: JSON.stringify({ text: input.content }) });
  },
});
```

Register once at process init (e.g. from a plugin entry or an operator boot
hook). Registration is idempotent by `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 `runGuardrails`
  so you can tune per-deployment latency. Use an `AbortController`.
* **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 `classify` and 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 complete
`classify` implementation that:

1. resolves `&lt;base&gt;/classify` from an env URL,
2. posts `&#123; text, hook, metadata &#125;` with an `x-api-key` header,
3. parses a `&#123; prediction, blocked, score, threshold &#125;` response,
4. emits one redacted stderr line (`status=… prediction=… content_sha256=…`),
5. fails open on every error path.

Because the verdict is ignored by ModusBrain, "shadow mode" requires *no* special
ModusBrain flag — it is the only mode this interface supports. Enforcement would be
a separate, future, RFC-gated seam.
