Live build ledger · overnight autonomous run · 2026-07-04 → 05

Platform Extensions — The Overnight Build Ledger

The execution of Beyond the Wrapper. Overnight, unattended, the dark factory built the document-intelligence platform — five reusable extensions on top of Core + Model, each held to the same rigor bar Core and the Model gateway met, each landing as a green-CI feat/-branch PR that stays UNMERGED for Sam to test and deploy. Built one service at a time, dev-tested (build + run + curl + docker), documented. Nothing was deployed or merged unattended. Plan pivot (Jul 5): the three doc-intelligence services were consolidated into ONE LlamaIndex-based service, oll-memory (PR #80); the sovereign tier and MCP gateway are parked. This page is the living design doc for each.

⇄ Plan update · 2026-07-05 · consolidated → oll-memory

The three doc-intelligence services — #75 extract · #76 ingest · #77 rag — are superseded by ONE service, oll-memory (PR #80, green + unmerged), which replaces the hand-rolled pgvector-SQL / RRF / chunker with the mature LlamaIndex library. #78 (sovereign tier) and #79 (oll-mcp) are parked for later (branches stay open). Rationale: don't reinvent RAG — use a mature library; fewer deploys, one DB, one contract. Detail: the oll-memory design doc ↓.

✅ Consolidated build — oll-memory green + unmerged

oll-memory has landed as a green-CI, unmerged PR (#80) — one LlamaIndex-based stateful service that does ingest + hybrid retrieval + structured extraction, dev-tested to Core/Model rigor (65 pytest vs real pgvector, docker boot+health, 26/26 gh pr checks), with the real semantic win proven on our own stack — a synonym-only query ranked #9→#1 the moment a real embedder was switched on. Next: Sam tests locally + deploys deliberately (a tag or deploy-all.yml, never a merge). See the deploy card ↓ for the ordered runbook.

What this run delivers

  1. Five platform extensions, not five products. oll-extract (schema-locked structured extraction), oll-ingest (document/vision ingestion), oll-rag (hybrid retrieval), an oll-model sovereign tier (EU / on-device routing default), and oll-mcp (an MCP gateway that exposes the above as auth'd, Core-metered servers). Together they turn Core + Model into a document-intelligence platform other products — and other agents — call over HTTP.
  2. Each built to Core/Model rigor. Contract-first OpenAPI → DTOs, thin routes over a service boundary behind a boot-gate factory, per-endpoint tests that pass keyless in CI, a 12-factor Dockerfile with health probes, the unified error envelope, internal service-token auth, and integration wiring. No shortcuts because it's a "helper" service.
  3. Sequenced by dependency, landed as unmerged PRs. The factory builds in order (extraction first, then ingestion → retrieval → sovereign tier → the MCP gateway), dev-tests each one, and opens a feat/-branch PR with green CI. Sam tests locally and deploys deliberately (tag / deploy-all.yml) tomorrow. Phase 2 — a POC product in a separate repo consuming the extended platform — is Sam's follow-up, after 1–5 land green.

1 · The sequenced build

One row per service · statuses update live. Plan pivot (Jul 5): the three doc-intelligence rows (oll-extract · oll-ingest · oll-rag) are superseded by the single oll-memory row at the top; oll-mcp and the sovereign tier are parked. Superseded / parked rows are kept for provenance, not deleted.
ServiceWhat it isDepends onStatusPR
oll-memory Consolidated document intelligence — one stateful service = ingest + hybrid semantic retrieval + structured extraction, built on LlamaIndex (pgvector store, sentence splitter, Ollama embeddings). Replaces #75/#76/#77's hand-rolled internals. Design doc ↓ oll-model · pgvector ✅ Built · dev-tested · CI-green · consolidates #75/#76/#77 #80 (unmerged)
oll-extract Schema-locked structured extraction — text → typed JSON validated against a caller-supplied schema, on the Model gateway. The primitive every document product needs. Design doc ↓ oll-model ⤳ superseded by #80 #75 (→ #80)
oll-ingest Document / vision ingestion — messy PDFs, scans and images → clean, chunked, RAG-ready text with paragraph-aware boundaries + page metadata. Design doc ↓ ⤳ superseded by #80 #76 (→ #80)
oll-rag Hybrid retrieval — vector + keyword + rerank with chunk-level ACL, on pgvector / Neon. Turns ingested corpora into grounded answers. Design doc ↓ oll-ingest ⤳ superseded by #80 #77 (→ #80)
oll-model sovereign tier An additive EU / on-device routing default on the existing gateway — the honest "your data never leaves the EU, or your device" claim, made routable. Design doc ↓ oll-model ⏸ parked #78 (open, later)
oll-mcp MCP gateway exposing the above as authenticated, Core-metered MCP servers — the thing no beginner ships: a billing spine wrapped around agent-callable tools. Design doc ↓ oll-extract · oll-ingest · oll-rag ⏸ parked #79 (open, later)
Core + Model
live spine — auth · billing · email · model switch
oll-extract
text → typed JSON
oll-ingest
docs → clean chunks
oll-rag
hybrid retrieval
oll-mcp
metered MCP gateway

The sovereign tier is an additive default on the Model gateway, so it sits under the whole chain rather than in it — every service above inherits EU / on-device routing for free. As of Jul 5 the extract → ingest → rag arc is collapsed into oll-memory (below); the sovereign tier and MCP gateway are parked.

1½ · oll-memory — the consolidated service (LlamaIndex), in detail

The plan pivot in one place. Instead of three hand-rolled services, one stateful service built on a mature RAG library — LANDED, dev-tested, CI-green, open as PR #80 (unmerged; deploy is Sam's). This section is the living design doc, so the service can be understood without reading the code.

Purpose & the pivot's why

One stateful service = document ingest + hybrid semantic retrieval + structured extraction, built on LlamaIndex (PGVectorStore on pgvector, SentenceSplitter, OllamaEmbedding). Only a thin house-style shell is hand-written — auth, the unified error envelope, health probes, the boot-gate factory — and extraction generation is routed through the oll-model gateway. It owns a private Neon oll_memory DB. The pivot's logic: don't reinvent RAG. The original #75/#76/#77 hand-rolled a chunker, pgvector SQL, and Reciprocal Rank Fusion; a mature library does that better, so the win is fewer deploys, one DB, one contract instead of three services to wire, provision and keep in sync.

Unified API

One surface for the whole pipeline. Thin routes over one service boundary; every response DTO comes from the OpenAPI slice.
EndpointWhat it does
POST /api/memoryIngest + embed + store — chunk a document, embed each chunk, persist to the pgvector-backed index.
POST /api/memory/queryHybrid retrieve — dense (vector) + sparse (keyword) retrieval over the caller's corpus, ACL-scoped.
DELETE /api/memory/{collection}/documents/{document_id}Remove a document and all its nodes from a collection.
POST /api/extractSchema-locked structured extraction — text → typed JSON, generation routed through the oll-model gateway.
GET /api/health · /api/health/db · /api/health/modelLiveness · DB readiness (SELECT 1 + vector extension present) · upstream gateway reachability.

The SOTA semantic win — the headline result

This is why you use a real embedder, proven on our own stack rather than asserted.

With the deterministic hash embedder (the keyless CI default), a synonym-only query — "mountain gold vault custodian", where keyword_hits=0 so retrieval is pure dense — ranked the correct document #9 of 9: no signal. Switching only EMBED_PROVIDER to Ollama nomic-embed-text (768-dim) ranked the same document #1.

hash embedder (CI default)
synonym-only query → correct doc ranked #9 / 9 (no signal)
nomic-embed-text (768-dim)
same query, only EMBED_PROVIDER changed → correct doc ranked #1

Real semantic retrieval — meaning matched with zero shared keywords. Embeddings default to a keyless deterministic local-hash provider (so CI runs real cosine maths, no keys), with nomic (self-hosted / EU) behind an env flag for real use.

Engineering notes — honest, verified by running

  • LlamaIndex, pinned. llama-index-core==0.14.16 · llama-index-vector-stores-postgres==0.7.1 · llama-index-embeddings-ollama==0.9.0 — pinned so a library bump can't silently change retrieval behaviour.
  • EMBED_DIM=768 (the old oll-rag was 384) — nomic's native dimension; the pgvector column and index are sized to match.
  • ACL "public-or-intersecting" preserved via a stored public∈{0,1} flag + FilterOperator.ANY. LlamaIndex's IS_EMPTY does not match an empty JSONB array, so a plain "no tags = public" check would have failed — the explicit flag restores the private-by-default semantics the hand-rolled version had.
  • document_id stored as document — LlamaIndex reserves the document_id metadata key, so ours is remapped to avoid a collision.
  • Binary / PDF ingestion deferred — text / markdown only for now (other MIME → 415); a real parser can slot behind the same seam later without a contract change.
  • One transitive nltk advisory --ignore-vuln'd per the house pip-audit pattern (documented, not silently suppressed).

Rigor evidence — the rung actually reached

  • LlamaIndex engine (not hand-rolled RAG)
  • 65 pytest vs real pgvector
  • ruff clean
  • docker boot + health against pgvector
  • Keyless CI-equivalent verified — ingest / query / ACL / delete / extract
  • nomic semantic win #9→#1 reproduced
  • Dedicated test-memory pgvector CI job + integration ingest→query assertion
  • All 26/26 gh pr checks green
  • PR #80 UNMERGED — deploy is Sam's

What this supersedes / parks

#75 extract, #76 ingest and #77 rag are superseded by #80 and will be closed when it merges — their design docs below are kept for provenance and rationale. #78 (sovereign tier) and #79 (oll-mcp) are parked — branches stay open, to revisit once oll-memory is deployed and earning its keep.

2 · The rigor bar applied to each

The same checklist Core and the Model gateway passed. A service is not "done" — and its PR is not opened — until every box is checked. This is what keeps a "helper" service from becoming technical debt.
  • Contract-first OpenAPI slice → generated DTOs (spec is the source of truth)
  • Thin routes + a single service boundary + boot-gate factory
  • Per-endpoint testsmock-default, keyless in CI
  • 12-factor Dockerfile ($PORT) + health probes
  • Unified error envelope {code,message,request_id}
  • Internal service-token auth between services
  • CI matrix job + integration wiring (real cross-service call)
  • Dev-tested: build + run + curl + docker, not just diff-read
  • HTML design doc + architecture diagram + timeline entry
  • feat/-branch PR, green CI, UNMERGED

3 · Provider & infra defaults

Every external dependency ships mock-default (so CI stays keyless) with the real provider behind an env flag — the same pattern Core and the Model gateway use. Filled in as each service lands; rows marked TBD as built are decided at build time.
ServiceDependencyDefault choiceReal provider behind env
oll-extract Model call oll-model gateway; default mock — synthesizes a deterministic schema-conforming stub with zero gateway calls, so CI is fully keyless Real providers behind EXTRACT_PROVIDER + gateway env (provider-aware boot gate — no silent fallback)
oll-rag Embeddings Default local — a deterministic hashed-feature, L2-normalized vector (dim 384), keyless, so CI runs real cosine maths (not a stub) Real embedder behind EMBED_PROVIDER=api + EMBED_BASE_URL, isolated to embed_client.py; optional cross-encoder rerank behind RERANK_PROVIDER (off by default)
oll-rag Vector store pgvector on Neon — its own private oll_rag DB in prod; pgvector/pgvector:pg16 in CI Same store — Sam must provision a private Neon oll_rag DB with the vector extension at deploy
oll-ingest Parser Default local — real, dependency-free normalize + paragraph-aware chunking of text / markdown with zero external calls, so CI is fully keyless (not a stub — it does real work) Real PDF / scan / image parser behind INGEST_PROVIDER=parser + PARSER_BASE_URL / PARSER_API_KEY (provider-aware boot gate), isolated to parser_client.py
oll-mcp Auth / metering Core JWT (Bearer) → GET /api/auth/me → plan-gate (MCP_REQUIRED_PLAN, default pro); server-side service-token custody for every downstream hop (client never sees a token); per-user identity scopes the default RAG collection / ACL; a per-call usage audit line for future metering. Keyless CI via MCP_MOCK (canned pro user + in-process fakes) Live Core (core.oll.am) for identity + plan; real siblings behind their X-Service-Token + *_BASE_URL. No Core change.
oll-model sovereign tier Routing policy Default MODEL_DEFAULT_PRIVACY=anyexisting behaviour, zero change. privacy:eu → EU / on-device providers only (self-hosted ollama on our EU / Frankfurt VPS, mock); privacy:on_device → on-device only. US providers (groq, claude) excluded from both. Off-by-default EU-managed-provider toggle (metadata only — no backend wired). Honesty note: EU data residency only — never "Swiss-hosted".

4 · oll-extract — the first extension, in detail

Service 1 has LANDED — built, dev-tested, CI-green, open as PR #75 (unmerged; deploy is Sam's). This section is the living design doc, so the service can be understood without reading the code.

Purpose

Schema-locked structured extraction. Give it text (or input) plus a JSON Schema; it returns validated, typed JSON that conforms to that schema — the messy-language-in, clean-fields-out primitive every document product needs (invoice fields, résumé parsing, contract terms). It calls the Model gateway for inference and never imports a model SDK of its own. It is stateless — no database: a pure request → typed-JSON transform.

Contract

Three endpoints. Thin routes over one service boundary; every response DTO comes from the OpenAPI slice.
EndpointRequest → ResponseNotes
POST /api/extract { input, schema, instructions?, provider?, max_tokens? }{ data, provider, model, tokens_in, tokens_out, latency_ms } data is the schema-validated typed object; the rest is call provenance for metering & debugging.
GET /api/health → liveness {status} Process-up probe — the Docker HEALTHCHECK target.
GET /api/health/model → upstream readiness {status} Proves the hop to the oll-model gateway is reachable (deep health).

Error taxonomy

One unified envelope {code,message,request_id} across every failure — the same shape Core and the Model gateway return.
StatusCodeWhen
400INVALID_SCHEMAThe caller-supplied JSON Schema is malformed or not a valid schema.
401 / 403authMissing or wrong X-Service-Token (internal service-token auth).
422EXTRACTION_PARSE_ERRORThe model output could not be parsed into schema-valid JSON, even after the one corrective retry.
429 / 502 / 504propagated gateway errorsRate-limit / upstream / timeout from oll-model, propagated with cause rather than masked.

Key design decisions

  • The llm-call-contract pattern. Prompts are named constants with the target schema embedded and an explicit "return ONLY JSON" instruction; the response goes through a defensive fence-strip parse (tolerates ```json wrappers), then jsonschema validation. On a validation miss it runs ONE corrective retry (feeding the error back) before returning 422. Input is truncated to a safe budget so a huge document can't blow the context.
  • Mock-default, guarded real provider. Default mock synthesizes a deterministic schema-conforming stub with zero gateway calls, so tests and CI need no keys; real providers sit behind EXTRACT_PROVIDER + gateway env.
  • Internal service-token auth. Callers present X-Service-Token; this is a private platform service, not a public endpoint.
  • Provider-aware boot gate. If a real provider is selected, the required env is asserted at boot — no silent fallback to mock in production.
  • Sole gateway HTTP boundary. All model I/O lives in extract_client.py; routes and the service layer never touch HTTP directly — one seam to test and swap.
  • Unified error envelope. Every path returns {code,message,request_id}, so callers handle failures uniformly.

Rigor evidence — the rung actually reached

  • Contract-first OpenAPI slice → DTOs
  • 33 pytest, keyless
  • ruff clean
  • docker build 270MB, non-root + HEALTHCHECK HEALTHY
  • Real keyless hop to a live oll-model container proven
  • Booted inside the CI integration compose
  • All gh pr checks green — lint+test, pip-audit, docker boot+health, integration smoke
  • PR #75 UNMERGED — deploy is Sam's

One deploy note to carry

OLL_MODEL_BASE_URL defaults to http://oll-model:5000, but oll-write points at the gateway on :5003. Set the real gateway port per deploy — this is flagged in .env.example so it isn't missed at wiring time.

5 · oll-ingest — the second extension, in detail

Service 2 has LANDED — built, dev-tested, CI-green, open as PR #76 (unmerged; deploy is Sam's). This section is the living design doc, so the service can be understood without reading the code.

Purpose

Document / vision ingestion — the front-half of the pipeline. Give it a document (text, markdown, PDF or image); it returns clean, normalized text split into overlapping, paragraph-aware chunks with character offsets and metadata. It is the step that turns a messy source into retrieval-ready pieces — its chunks are what oll-rag embeds and searches. Like the other extensions it is stateless — no database: a pure document → chunks transform.

Contract

Three endpoints. A thin route over one service boundary; every response DTO comes from the OpenAPI slice.
EndpointRequest → ResponseNotes
POST /api/ingest { source.{text | content_base64, mime_type, filename}, options.{chunk_size, chunk_overlap, ocr} }{ document_id, mime_type, char_count, page_count, chunk_count, chunks[], provider, latency_ms } Either inline text or base64 bytes; options tune chunking; the response carries the chunks plus call provenance for metering & debugging.
GET /api/health → liveness {status} Process-up probe — the Docker HEALTHCHECK target.
GET /api/health/parser → upstream readiness {status} Proves the hop to the real parser is reachable when INGEST_PROVIDER=parser (deep health).

Error taxonomy

One unified envelope {code,message,request_id} across every failure — the same shape Core, the Model gateway and oll-extract return.
StatusCodeWhen
400INVALID_SOURCENeither text nor decodable content_base64 present, or the source is empty / malformed.
413PAYLOAD_TOO_LARGEInput exceeds MAX_INPUT_BYTES — checked both pre- and post-decode so base64 can't smuggle a bomb.
415UNSUPPORTED_MIMEThe mime_type isn't one the selected provider can parse.
401 / 403authMissing or wrong X-Service-Token (internal service-token auth).
502PARSER_UNREACHABLE / PARSER_ERRORThe real parser hop failed or returned an error — propagated with cause rather than masked.

The chunk shape it emits

This is the contract oll-rag consumes, so it is worth reading exactly. Each chunk is a self-describing slice of the source that can be embedded and, later, cited back to its exact character span.
FieldMeaning
index0-based position of the chunk in reading order.
textThe chunk's normalized text.
char_start · char_endHalf-open character span into the full normalized document — the anchor for citation & highlight.
pageSource page number, or null for formats without pages (plain text / markdown).
metadataPer-chunk context — notably heading, the nearest markdown heading the chunk falls under.

Guarantees that make it safe to consume:

  • Contiguous coveragechunks[0].char_start == 0 and chunks[-1].char_end == char_count: every character is accounted for, nothing dropped.
  • Deterministic overlap — adjacent chunks overlap by exactly chunk_overlap characters, so a retrieval hit never straddles a lost boundary.
  • Stable identitydocument_id is a deterministic sha256 prefix of the normalized content: the same document always yields the same id, giving oll-rag a free dedup key.

Key design decisions

  • The local chunker does REAL work, not a stub. The keyless default genuinely normalizes and chunks text / markdown — so CI proves the actual behaviour, not a placeholder that a real provider would later contradict.
  • Paragraph-aware boundaries. Chunk cuts snap to \n\n paragraph breaks rather than slicing mid-sentence, so each chunk is a coherent unit — better embeddings, cleaner citations.
  • Markdown structure is preserved AND recorded. Headings are kept in the text and written into metadata.heading, so retrieval can weight or filter by section.
  • Size enforced twice. MAX_INPUT_BYTES is checked before and after base64 decode — a small encoded payload can't expand into an oversized document past the guard.
  • Real parser isolated to parser_client.py. All PDF / scan / image I/O lives behind one seam — arch-test-enforced so routes and the service layer can never reach it directly. One place to test and swap.
  • Provider-aware boot gate. If INGEST_PROVIDER=parser is selected, the required parser env is asserted at boot — no silent fallback to local in production.
  • Unified error envelope + service-token auth. Every path returns {code,message,request_id}; callers present X-Service-Token — a private platform service, not a public endpoint.

Rigor evidence — the rung actually reached

  • Contract-first OpenAPI slice → DTOs
  • 40 pytest, keyless
  • ruff clean
  • docker build 266MB, non-root + HEALTHCHECK HEALTHY
  • Verified curl: 3 chunks, contiguous offsets 0..529, exact 40-char overlap, heading metadata present
  • Booted inside the CI integration compose
  • All 26/26 gh pr checks green — lint+test, pip-audit, docker boot+health, integration smoke
  • PR #76 UNMERGED — deploy is Sam's

One CI note to carry

oll-ingest shares services.yml / integration.yml / docker-compose.ci-integration.yml matrix lines with its sibling extension PRs. If a sibling (e.g. oll-extract #75) merges first, this branch needs a trivial rebase of those shared workflow files — no code change, just re-add the matrix line.

6 · oll-rag — the third extension, in detail

Service 3 has LANDED — built, dev-tested, CI-green, open as PR #77 (unmerged; deploy is Sam's). The first stateful extension — it owns its own database. This section is the living design doc, so the service can be understood without reading the code.

Purpose

Hybrid retrieval over a corpus — the retrieval back-half of the pipeline. Store embedded, chunked documents (oll-ingest's output) and answer a query by fusing vector + keyword search with chunk-level ACL, backed by Postgres + pgvector. Where the other extensions are stateless transforms, this is the first extension that owns its own DB — a corpus persists between an ingest and a query.

Contract

Five endpoints. Thin routes over one service boundary; every response DTO comes from the OpenAPI slice.
EndpointRequest → ResponseNotes
POST /api/collections/{collection}/documents { document_id, chunks[], acl_tags? }{ chunk_count, embedded } Upsert a document's chunks into a collection — each chunk is embedded and indexed; re-upserting the same document_id replaces its chunks.
POST /api/collections/{collection}/query { query, top_k?, acl?, filter? }{ results[], retrieval{ vector_hits, keyword_hits, fused } } Fuses vector + keyword retrieval; retrieval reports how many hits each arm returned and the fused count, so the ranking is auditable.
DELETE …/documents/{document_id} → removal {status} Drops a document and all its chunks from the collection.
GET /api/health → liveness {status} Process-up probe — the Docker HEALTHCHECK target.
GET /api/health/db → DB readiness {status} SELECT 1 and asserts the vector extension is present — a shallow liveness probe can't miss a mis-provisioned DB (deep health).

Errors follow the unified envelope {code,message,request_id}: 400 on an oversize query, 401 / 403 on missing / wrong X-Service-Token, plus standard propagated failures — the same shape Core, the Model gateway, oll-extract and oll-ingest return.

How retrieval actually works

Worth teaching, because "hybrid" is the whole point — a chunk found by both retrievers should beat one found by either alone.
  • Vector search — chunks are embedded and compared by pgvector cosine distance, accelerated by an HNSW index. Good at meaning: "revenue" matches "turnover" even with no shared word.
  • Keyword search — a GENERATED ALWAYS … STORED tsvector column with a GIN index does full-text ranking. Good at exactness: an id, a name, a rare token the embedder blurs.
  • Reciprocal Rank Fusion merges the two ranked lists — for each chunk, score += 1 / (RRF_K + rank) in whichever list it appears. A chunk that ranks in both lists accumulates from both and rises to the top, without needing to tune vector-vs-keyword weights. An optional cross-encoder rerank (behind RERANK_PROVIDER, off by default) can re-score the fused shortlist.
  • Chunk-level ACL enforced IN SQL, not in app code: the query carries the caller's tags and the WHERE clause is cardinality(acl_tags)=0 OR acl_tags && caller_tagspublic-or-intersecting. A chunk the caller may not see is never even fetched, so there is no app-layer path that could leak it.

Where it sits in the pipeline

It consumes oll-ingest's chunks verbatim — the shapes match, by design.
ingest_document
doc → chunks
rag_upsert_document
same document_id / chunks → embedded + indexed
rag_query
fused results, carry a score

The canonical flow: oll-ingest produces chunks → rag_upsert_document stores them under the same document_id and chunk shape → rag_query returns fused results. Each result carries a fused score (higher = better), not a raw distance — the RRF fusion has already normalized the two retrievers into one comparable ranking.

Key design decisions

  • HNSW over ivfflat. HNSW needs no training step and works incrementally as chunks are inserted — ivfflat would need a populated table to build its lists, which fights an upsert-as-you-go corpus.
  • Boot-time best-effort schema-ensure + lazy ensure. entrypoint.sh ensures the schema before gunicorn (mirroring oll-core's alembic-before-gunicorn), and the service also ensures lazily — so the keyless docker-boot gate still boots against a dummy DB, while a real deploy gets its tables and indexes created idempotently.
  • Deterministic local embeddings. The keyless default is a hashed-feature, L2-normalized vector — so tests are stable AND the cosine maths is real, not stubbed. CI proves the actual ranking behaviour.
  • Provider-aware boot gate. If a real embedder / reranker is selected, its env is asserted at boot — no silent fallback to local in production.
  • Own private DB (database-per-service). oll-rag owns oll_rag; it integrates with siblings over HTTP, never by reaching into another service's DB.
  • Service-token auth + unified error envelope. Callers present X-Service-Token; every path returns {code,message,request_id} — a private platform service, not a public endpoint.

Rigor evidence — the rung actually reached

  • Contract-first OpenAPI slice → DTOs
  • 46 pytest incl. 14 DB-backed against real pgvector
  • ruff clean
  • docker non-root + HEALTHY two ways — real pgvector and keyless dummy DB
  • Verified curl: RRF top-1 score 0.0328 (found by both retrievers) vs 0.0161 vector-only
  • ACL hides then reveals the tagged chunk; delete works
  • Dedicated test-rag pgvector CI job + integration smoke boots pgvector + oll-rag
  • All 26/26 gh pr checks green — PR #77 UNMERGED

Deploy note to carry

oll-rag needs a new Coolify app and — because it is stateful — a private Neon oll_rag DB with the vector extension enabled (CREATE EXTENSION vector;). It shares the services.yml / integration.yml CI matrix files with its sibling extension PRs, so if a sibling merges first this branch needs a trivial rebase of those workflow files — no code change.

7 · oll-model sovereign tier — the fourth extension, in detail

Service 4 has LANDED — built, dev-tested, CI-green, open as PR #78 (unmerged; deploy is Sam's). Unlike the other three, this is not a new service — it is an additive tier on the FROZEN Model gateway. This section is the living design doc, so the change can be understood without reading the code.

Purpose

An additive, backward-compatible routing tier that lets a caller require inference to run only on EU-hosted or on-device providers — the honest, sellable "your data never leaves the EU, or your device" capability, made routable. It does NOT add a new model backend; it rides the existing provider switch. Because the Model gateway is a frozen service, the whole design turns on one promise: a caller who ignores the new field sees no change whatsoever.

What's additive

Every addition is opt-in. The any path is byte-for-byte the old behaviour.
SurfaceAddition
Request field Optional privacy: any(default) | eu | on_device on the text call. Absent ⇒ any ⇒ the identical pre-existing code path.
Response echo Additive echo fields privacy_applied, region, on_deviceEXCLUDED from the body on the any path (absent, not null), so an existing caller's parsed key set is unchanged.
New endpoint GET /api/text/policy — returns the provider policy matrix: per-provider region / zdr / on_device plus satisfies.eu and satisfies.on_device. This is what a UI reads to draw honest labels.
New errors POLICY_VIOLATION (422) when a caller pins a provider that violates the requested privacy; NO_COMPLIANT_PROVIDER (503) when nothing satisfies it — no silent US fallback, ever.

The backward-compat guarantee — why a frozen service can take this safely

This is the load-bearing decision, so it is worth stating exactly. A request with no privacy field:

  • hits the identical code path as before the change;
  • returns the identical key set {text, provider, model, tokens_in, tokens_out, latency_ms} — the echo fields are simply not emitted;
  • and the 54 pre-existing tests pass UNCHANGED. Total is now 84 = 54 + 30 new — the old suite is a regression net that proves the frozen behaviour is intact, and the 30 new tests cover only the opt-in tier.

That is what makes an additive tier legitimate on a service we promised never to touch: the old contract is not edited, it is extended past.

The honest posture — what may and may not be claimed

The provider policy metadata is the single source of truth for every claim the UI is allowed to make:

ProviderRegionOn-deviceZDRSatisfies
ollamaself-hosted EU (Frankfurt) · euyesyeseu + on_device
mocklocalyesyeseu + on_device
groqUSnono
claudeUSnono

Claims allowed: EU data residency · no US endpoint · on-device. NEVER claimed: "Swiss-hosted", CH residency, or a city as a selling point — the VPS being in Frankfurt is a fact of the region tag, not a marketing line. The matrix is env-overridable via MODEL_PROVIDER_REGIONS / MODEL_PROVIDER_ONDEVICE / MODEL_PROVIDER_ZDR, so a future managed EU provider can be added without a code change.

How oll-mcp will use it

The tier is not decoration — it is the mechanism behind the next service's headline feature.

The MCP gateway exposes a "Private / EU-only" toggle that simply sets privacy:"eu" or privacy:"on_device" on its model calls, and renders its honest labels by reading /api/text/policy rather than hard-coding provider claims. The label a user sees is therefore always derived from the same metadata the router enforces — the UI cannot drift from the truth.

Rigor evidence — the rung actually reached

  • Additive OpenAPI — optional field + new endpoint, no breaking change
  • 84 pytest54 unchanged + 30 new, keyless
  • ruff clean
  • docker builds + boots HEALTHY keyless
  • Verified curl: no-privacy call unchanged (no echo fields)
  • Verified curl: on_device routes + echoes region
  • Verified curl: eu + groq422 POLICY_VIOLATION
  • Verified curl: nothing compliant → 503 NO_COMPLIANT_PROVIDER
  • Verified curl: /api/text/policy matrix returns
  • Integration smoke: no-privacy cross-service proves backward-compat
  • All 20/20 gh pr checks green
  • PR #78 UNMERGED — deploy is Sam's

8 · oll-mcp — the fifth extension, in detail

Service 5 has LANDED — built, dev-tested, CI-green, open as PR #79 (unmerged; deploy is Sam's). It completes the platform build: the four extensions below become one authenticated, entitlement-gated surface for any agent. This section is the living design doc, so the service can be understood without reading the code.

Purpose

A single deployable Streamable-HTTP MCP server that exposes the whole platform as authenticated, entitlement-gated tools for Claude, ChatGPT and Cursor. This is the distribution + defensibility move from the research: the same primitives (extract / ingest / RAG / private inference) that any MCP builder can wire up, but wrapped in the Core billing + identity spine most of them lack — so a caller is a known, paying user, not an anonymous key holder. It is the one ASGI service in the platform (the MCP SDK brings its own uvicorn / Starlette); every other house convention is kept, and it is purely additive — no other service is touched.

Tools exposed

Seven tools plus one resource. Each is a thin, typed wrapper over a downstream platform call; the paid ones are gated, platform_health is free.
ToolWhat it doesGate
extract_structuredText → schema-validated typed JSON (via oll-extract).plan-gated
ingest_documentDocument / image → clean, chunked, RAG-ready text (via oll-ingest).plan-gated
rag_upsert_documentEmbed + index a document's chunks into the caller's collection (via oll-rag).plan-gated
rag_queryHybrid (vector + keyword) retrieval over the caller's corpus, ACL-scoped.plan-gated
rag_delete_documentRemove a document and its chunks from the collection.plan-gated
complete_textModel completion — with a Private / EU-only option that sets privacy:eu / on_device on the model call; labels read live from /api/text/policy.plan-gated
platform_healthRoll-up readiness of the downstream services.free
oll-mcp://policy/privacyResource — the provider policy matrix (region / on-device / ZDR) an agent reads to make honest privacy claims.free

Auth & entitlement model — the part that teaches

This is the whole point of the service, so it is worth reading exactly. It reuses the house pattern — identity in the token, plan from /me — end to end, without modifying frozen Core.
  • Bearer = a Core JWT. Every /mcp call must present Authorization: Bearer <core-jwt>. The token is validated live against Core GET /api/auth/meoll-mcp never verifies or trusts the JWT locally, so a revoked or expired identity fails immediately.
  • Plan from /me, not from the token. The plan field on the /me response gates the paid tools (MCP_REQUIRED_PLAN, default pro). A free or lapsed user gets a clean "upgrade required" tool error — not a stack trace, not a silent empty result. Mutable entitlement is read live, never baked into a token (the house rule).
  • Server-side token custody. Core is hit with the caller's Bearer; every other hop (extract / ingest / rag / model) carries that service's own X-Service-Token, minted server-side. The MCP client never sees a service token — it holds only its own Core JWT. One compromised agent can't reach a sibling directly.
  • Identity scopes the data. The authenticated user id scopes the default RAG collection and ACL (u{id}), so one user's corpus is never visible to another by construction — the ACL is enforced in SQL by oll-rag, and oll-mcp simply passes the right tags.
  • Per-call usage audit. Every tool call emits a structured audit line (user, tool, downstream, latency) — the seam future metering plugs into without touching the tool code.
  • No Core change. All of this rides Core's existing /api/auth/me; the frozen billing spine is used, not modified.

Key design decisions

  • Sole downstream HTTP boundary. Every call to a sibling service lives in downstream.pyarch-test-enforced so tools and the server layer can never reach a sibling directly. One seam to test, mock and swap.
  • Mock mode for keyless CI. MCP_MOCK supplies a canned pro user and in-process fakes for every downstream, so the full tool suite runs in CI with zero keys and zero network — real behaviour, not a stub that a live provider would later contradict.
  • Structured per-call usage audit. Emitted for every tool — the metering hook is present from day one, dark until Sam wires it.
  • Provider-aware boot gate. If a real downstream or Core URL is selected, its env (base URL + service token) is asserted at boot — no silent fallback to mock in production.

Rigor evidence — the rung actually reached

  • MCP Streamable-HTTP server ✓
  • 38 pytest, keyless (+5 cross-user scoping regression, hardening pass)
  • ruff clean · pip-audit clean
  • docker non-root + HEALTHY ~3s (283MB)
  • Real MCP client vs booted uvicorn: auth-reject 401
  • list_tools = 7
  • ingest → upsert → query chain — top chunk score 0.83
  • complete_text privacy=eu → provider ollama / region eu
  • free-plan caller → "upgrade required" tool error
  • All 26/26 gh pr checks green
  • PR #79 UNMERGED — deploy is Sam's

Deploy note to carry

oll-mcp needs a new Coolify app at mcp.oll.am, real per-service tokens for each downstream hop, and CORE_BASE_URL pointed at core.oll.am. Its integration.yml wiring is deferred — a real oll-mcp → core → services chain can be added to the integration compose once the sibling extension PRs land, so this branch doesn't depend on merge order.

§ · Overnight hardening pass (adversarial review)

After all five extensions landed, a fresh reviewer was pointed at the two security-sensitive seamsoll-rag's chunk-level ACL and oll-mcp's auth / entitlement — with one job: try to REFUTE them, not confirm them. This is the improve-loop closing on itself: assume the build is wrong and go looking for the hole. It found one.

Findings

SeamVerdictAction
oll-rag chunk-level ACL VERIFIED SOUND Every attack tried was refuted. No change needed — enforcement is in SQL, private-by-default, and holds on both retrieval arms.
oll-mcp auth / entitlement HOLE FOUND cross-user IDOR · High A real cross-user read/delete/overwrite was reproduced, then fixed (pin, don't default) with a dedicated regression suite. oll-mcp now 38 tests, PR #79 green.

That split is the point: the seam that enforced in the database survived; the seam that trusted a client override did not. A genuine bug was caught before deploy, not after a stranger hit it.

1 · oll-rag chunk-ACL — VERIFIED SOUND

The reviewer attacked the ACL from four angles. Each was refuted, and why teaches the design.
  • Tag-less / omitted acl cannot read private chunks. The enforcement is cardinality(acl_tags)=0 OR acl_tags && caller_tags — a chunk is public only when it carries no tags. A caller with an empty tag array overlaps nothing, so it sees only public chunks. Private-by-default: a tagged (private) chunk is invisible unless the caller presents an intersecting tag.
  • SQL injection via acl / filter / collection / query is inert. Every one of those inputs reaches Postgres as a bound parameter, never string-concatenated into SQL — the classic array-overlap and filter payloads do nothing.
  • No cross-collection leakage. Every read, write and delete pins collection in its WHERE clause, so a query against collection A can never surface a chunk stored under collection B, regardless of tags.
  • ACL enforced on BOTH branches. The same predicate guards the vector arm and the keyword arm before Reciprocal Rank Fusion — a chunk the caller may not see is filtered out of each retriever, so RRF can never resurrect it. A one-sided guard would have been the classic hybrid-search leak; it isn't here.

Scope note (by design, not a gap): within oll-rag a caller may present any tags — it is an internal service behind a service token, and binding tags → identity is the gateway's job, not the store's. That binding is exactly what the oll-mcp finding below is about.

2 · oll-mcp auth / entitlement — a real HOLE, found and FIXED

This is the seam where identity meets the corpus — and where the bug lived.

The bug — cross-user IDOR (High)

The gateway defaulted to the caller's own u{id} collection and user:{id} ACL tag — but then trusted client-supplied overrides. So any authenticated user could pass a victim's collection / acl on rag_query / rag_upsert_document / rag_delete_document and read, delete or overwrite the victim's corpus. Reproduced end-to-end. The oll-rag ACL was doing exactly what it was told — the tags it received were the attacker's chosen tags, because the gateway forwarded them. Default-but-trust-override is not enforcement.

The fix — pin, don't default

  • Collection is namespaced under the caller. A client-requested collection is forced under the caller's own u{id} prefix — it can name a sub-collection, never a sibling's.
  • ACL is forced to the caller's own tag. No membership / sharing model exists, so client ACLs are never trusted — the tag is set server-side from the authenticated identity, full stop.
  • Applied to all three data toolsrag_query, rag_upsert_document, rag_delete_document — with a test_cross_user_scoping.py regression proving cross-user read, delete and overwrite are all blocked. oll-mcp is now 38 tests, gh pr checks 79 green.

Also attacked, and refuted

  • Free / expired / garbage bearer reaching a paid tool. Every paid tool runs through the plan gate (/meMCP_REQUIRED_PLAN); only platform_health is free. A non-pro or invalid identity gets a clean tool error, never data.
  • Token-custody leak. Service tokens are added only inside the downstream boundary (downstream.py); errors surface code + message, not headers or URLs — a tool error can't exfiltrate a token or an internal endpoint.

Follow-up hardening also applied

Boot-gate refuses mock auth in production. Defense-in-depth: mock auth mode was opt-in only, so a mis-set env could in principle have booted a real deploy with fake identities. The boot gate now refuses to start in a production configuration with mock auth enabled — the safe default is enforced by the process, not by remembering to set a flag.

9 · Deploy the platform tomorrow — the ordered runbook

Post-pivot, there is ONE new app to deploy: oll-memory. #75/#76/#77 don't need separate deploys anymore (superseded by #80); #78/#79 are parked. Here is the whole sequence for Sam, in order.
  1. Merge PR #80 (oll-memory). It supersedes #75/#76/#77 — close those three when #80 merges. #78 (sovereign tier) and #79 (oll-mcp) stay open but parked; nothing to deploy for them now.
  2. One Coolify app for oll-memory. Base Directory services/oll-memory, matching Watch Paths, honour $PORT, set OLL_MEMORY_SERVICE_TOKEN and OLL_MODEL_* (base URL + token) so the /api/extract generation hop reaches the oll-model gateway.
  3. Provision oll-memory's database. It is stateful: create a private Neon oll_memory DB and enable the vector extension (CREATE EXTENSION vector;) before first boot.
  4. Wire embeddings. Real use runs on self-hosted Ollama nomic-embed-text — set EMBED_PROVIDER=ollama + OLLAMA_EMBED_BASE_URL (and EMBED_DIM=768). The keyless local-hash default stays for CI; the boot gate asserts the Ollama env when the real provider is selected — no silent fallback.
  5. Deploy stays deliberate. A tag or deploy-all.yml, health-gated (/api/health/db + /api/health/model), never a merge-to-main. Then curl an ingest → query round-trip against the live app to confirm the semantic win holds with the real embedder.

10 · Phase 2 — a POC product on the extended platform

The five have landed — Phase 2 (Vault Chat) is in progress

With all five extensions green, the payoff is a POC product in a SEPARATE repo that consumes the extended platform end-to-end — ingest a document, extract typed fields, retrieve grounded answers, complete text, meter it through Core, all EU / on-device by default. Tested end-to-end against the live platform, it proves the extensions are reusable, not just present. Phase 2 (Vault Chat) is now in progress — the deliberate next move now that the foundation is real and green.

11 · An honest note on this ledger

This page is a living ledger, written through the night as each service moved Building → Dev-tested → PR-open → CI-green. All five have now landed — but the platform is built, not yet deployed. Nothing here was deployed or merged unattended — every service lands as an unmerged feat/-branch PR with green CI, waiting for Sam to test locally and deploy deliberately (a tag or deploy-all.yml, never a merge-to-main). If a service stalls, its row will say so honestly rather than claim done. The rung each service has actually reached — built / tested / PR-open — is what the Status column reports.

● LIVE LEDGER   Source: Beyond the Wrapper — product & platform research · Platform: The Platform · Factory: The Factory · Control Room · Timeline

Built one service at a time, dev-tested, documented. Nothing deployed or merged unattended. Kicked off 2026-07-04.