Skip to content

Proposal

PCBZ edited this page Sep 17, 2026 · 1 revision

Proposal — RSP v0.2

Status: proposal, pre-implementation. Supersedes v0.1. Changes land here first, then in SPEC.md once a fixture exists for them.


1. Problem

Local RAG systems index whatever directory you point them at. That directory usually contains .env files, SSH keys, database URLs, and API tokens. All of it gets chunked, embedded, and stored — and any of it can be retrieved into an LLM's context later.

Tools to detect this already exist. Gitleaks ships 222 credential rules. Presidio does PII. rag-sanitizer catches prompt injection. But they run at the gateway or at prompt time, never at ingestion, and none of them talk to each other.

The gap isn't detection. It's the absence of a place to plug detection in.


2. Proposal

A protocol defining where security tools hook into a RAG pipeline and what they return.

  • Host — the RAG system. Calls plugins at defined points. Implements no detection itself.
  • Plugin — a security tool. Receives content, returns a verdict. Runs as a subprocess.

Design target: wrapping an existing tool costs ~50 lines of adapter, not a reimplementation.

The spec is the deliverable. Everything else is evidence that it works.


3. Hooks

Three hooks in v0.1. Two more are reserved but unimplemented.

chunked        ──→  on_chunk       ★ blocked here means it never enters the vector DB
                    ↓
               [ embedding ]
               [ storage ]
────────────────────────────────────
retrieval hit  ──→  on_retrieve    applies to already-stored content; injection scrubbing
response out   ──→  on_response    exfiltration checks (external links, MD images)

reserved: on_source (path-level), on_document (file-level)

on_chunk is the one that matters. Everything shipping today intercepts at the gateway or at prompt time — by then the data is in the vector store and recoverable via embedding inversion. Blocking pre-embedding means it was never there.

Three hooks are enough to prove the protocol works. The reserved two get added when someone needs them.


4. Verdict model

ALLOW
REDACT(spans, replacement)    byte ranges to mask
FLAG(severity, reason)        pass through, tagged
BLOCK(reason)                 reject — not indexed / not returned

Plugins run in configured order. REDACT spans accumulate. BLOCK short-circuits. Final verdict is the strictest returned.

Ordering, from least to most strict: ALLOW < FLAG < REDACT < BLOCK.

Composition details that prose cannot settle — span coalescing, whether a later plugin sees pre- or post-redaction content — are tracked in Open Questions and will be settled by fixtures, not by paragraphs.


5. Wire format

Subprocess, JSON over stdin/stdout.

// → plugin
{ "hook": "on_chunk",
  "content": "AWS_SECRET_ACCESS_KEY=wJalr...",
  "metadata": { "source": "notes/aws.md", "chunk_index": 3 } }

// ← plugin
{ "verdict": "REDACT",
  "spans": [{ "start": 24, "end": 64, "type": "aws-secret-key" }],
  "replacement": "[REDACTED:aws-secret-key]",
  "severity": "critical" }

Spans are byte offsets into the UTF-8 encoding of content, half-open [start, end). See Design Decisions → D6.

Why subprocess, not in-process:

  1. The tools worth wrapping are binaries in other languages. Gitleaks is Go, Presidio is Python. Wrapping beats reimplementing.
  2. Plugin authors use any language.
  3. Plugins are untrusted code. A security tool whose plugin system is itself an attack surface would be an embarrassing outcome.

6. Four decisions that shape everything

Full context and consequences for each in the Design Decisions log.

1. Host calls plugin. Not a proxy design — a proxy can't reach on_chunk, which is the whole point.

2. Large content by reference. Under a threshold, inline. Over it, pass a file path plus offsets. Threshold negotiated at handshake.

3. Fail closed. Crash, timeout, or malformed JSON → BLOCK. Per-plugin override exists (on_error: block | allow | skip) but the default is stated in the spec with its reasoning. This is where security protocols usually get it wrong.

4. Verdicts are cacheable. Key is hash(content + plugin_version + plugin_config). Plugins declare determinism at handshake. Without this, reindexing means tens of thousands of subprocess spawns.


7. Host: LlamaIndex

Not a hand-rolled RAG. LlamaIndex already has the hook points:

pipeline = IngestionPipeline(
    transformations=[
        SentenceSplitter(),
        RSPIngestGuard(),        # ← on_chunk
        embed_model,
    ]
)

TransformComponent maps to on_chunk. BaseNodePostprocessor maps to on_retrieve. Its ingestion cache already hashes node+transformation pairs, which is the caching design the protocol needs.

This choice is strategic, not just convenient. A protocol with one hand-written host looks like an API with extra steps. A protocol that embeds into somebody else's framework in ~60 lines is evidence the abstraction is right. Gitleaks proves the plugin side is cheap; LlamaIndex proves the host side is cheap. Both ends have third-party evidence.

Cost: the demo depends on LlamaIndex. Acceptable — it's a demo.


8. Repo layout

SPEC.md                    ← the deliverable
rsp/
  runtime.py               plugin runtime (~200 lines, the real work)
  guards.py                LlamaIndex adapters (~60 lines)
  server.py                MCP stdio (~80 lines)
  config.py                yaml → plugin list → per-path policy
plugins/
  rsp-gitleaks/            wraps the official binary
  rsp-chaos/               deliberately misbehaves
conformance/
  cases/                   input/expected fixtures
  runner.py
demo/
  rsp.yaml
  sample-docs/             innocuous-looking files with planted fake secrets
README.md

9. Config

plugins:
  - name: gitleaks
    command: ["rsp-gitleaks"]
    hooks: [on_chunk]
    on_match: block

  - name: injection
    command: ["rsp-injection"]
    hooks: [on_retrieve]
    on_match: redact

policies:
  - path: "~/projects/**"
    plugins: [gitleaks, injection]
  - path: "~/Documents/personal/**"
    plugins: [presidio]

Per-path policy matters: a code directory and a personal notes folder have different threat models.


10. The demo

$ rsp ingest demo/sample-docs/
  scanned 42 chunks
  ⚠ BLOCK  aws-access-key   sample-docs/deploy-notes.md:12
  ⚠ BLOCK  private-key      sample-docs/backup.txt:3
  40 chunks indexed, 2 blocked

Clone, install, run, understand. That's all the demo has to do.

All planted secrets in demo/sample-docs/ are fabricated and non-functional.


11. Success criteria

  1. The gitleaks adapter is under 80 lines and wraps an unmodified upstream binary.
  2. The LlamaIndex host adapter is under 80 lines.
  3. A plugin that crashes mid-index blocks the chunk and does not take down the host.
  4. Someone can write a conforming plugin from SPEC.md alone, without reading the runtime source.

Criterion 4 is the real one. Test it at the end by handing the spec and conformance kit to someone with no context. If they have to ask questions or read your code, the spec isn't finished.


12. Risks

A protocol with one implementation is just indirection. Mitigation: gitleaks and LlamaIndex are both third-party, both unmodified. Put that fact in the README's first screen.

Scope creep. No version negotiation beyond the handshake field, no dependency resolution, no hot reload, no plugin registry. Subprocess, JSON, one config file. Spec under three pages.

Reading as a RAG demo rather than a protocol. The README's first screen is the protocol, not installation steps. The demo is evidence, not the product.


13. Explicit non-goals for v0.1

  • Not a detection engine. RSP ships zero rules of its own.
  • Not a gateway or proxy. See D1.
  • Not a policy language. Config is a plugin list plus path globs, nothing more.
  • Not a plugin registry, marketplace, or dependency resolver.
  • Not a multi-tenant or networked service. One host process, local subprocesses.

RSP

Home


Status: v0.2 proposal Target v0.1 tag: 2026-09-29

Clone this wiki locally