Yarlis CorpusOS¶
Universal data preparation platform for AI agents. macOS · Windows · Linux · open source (Apache-2.0) · cloud-agnostic.
CorpusOS discovers, classifies, extracts, normalizes, validates, versions and indexes your files, whatever their format, and turns them into trusted, searchable, permission-aware, traceable knowledge that any authorized LLM or agent can consume.
It is not a "convert everything to Markdown" tool. It produces three separate data products:
| Product | What it is for | Where it lives |
|---|---|---|
| Knowledge corpus (RAG) | Answering questions from source-grounded passages with citations and access control | canonical/, chunks/, lexical + vector index |
| Structured data (SQL) | Exact numbers: typed tables with units and formulas kept | structured/<dataset>/<version>/data.parquet |
| Model datasets (SFT / eval) | Measuring and improving model behaviour, not storing changing facts | evaluations/, exports/training/, exports/evaluation/ |
A vector database is an index, not the source of truth. Fine-tuning is not a replacement for a governed knowledge repository.
Quick start¶
uv sync # core + SQL + API + MCP + Office readers + tests
uv run corpusos inventory ~/Documents/projects # Phase 0: read-only, writes nothing
uv run corpusos init corpus --name "My Knowledge" && cd corpus
uv run corpusos ingest ~/Documents/projects --source projects
uv run corpusos review list # documents held before indexing
uv run corpusos search "leave policy" # routed search with an evidence verdict
uv run corpusos sql 'SELECT Item, "Total (USD)" FROM personal_projects_finance_budget_budget'
uv run corpusos scorecard # release gates
uv run corpusos export knowledge -t chatgpt -a owner # upload-ready files for an agent platform
uv run corpusos web # the web app on http://127.0.0.1:8080
Running ingest again over unchanged files is a no-op. A changed file gets a new revision, and the earlier raw bytes stay where they were. A deleted file leaves the index, SQL, document lookups and exports immediately, together with the archive members and attachments inside it.
What happens to a file¶
discover → store raw bytes (immutable, SHA-256) → detect type from bytes → parse with the right adapter
→ PII scan / redact → screen for injected instructions and hidden text → classify → quality gates
→ canonical document (schema 1.0.0) → typed datasets → [human review if high risk] → chunks → embeddings → index
- Quarantine. A file that fails a quality gate or has no installed parser goes to
quarantine/with the reason. Files that had no parser are retried on every run, so installing the suggested extra is enough to recover them. - Review. A document that is restricted (SSNs, card numbers, IBANs), contains instruction-like text ("ignore previous instructions…"), hides text from readers, or carries invisible characters is stored but not indexed until a person approves it. Configure the triggers in
quality-gates.yamlunderreview.required_for.
| Input | Treatment | Output |
|---|---|---|
| Markdown / text | Heading hierarchy, code fences, pipe tables, line provenance; legacy encodings detected | Structured JSON + Markdown |
| CSV / TSV | Typed columns (leading-zero IDs stay strings, NA is not null), units from headers |
Parquet + schema.json + table description |
Excel (.xlsx) |
One dataset per sheet, cached values, column formulas, units, sheet/row provenance | Parquet per sheet |
PowerPoint (.pptx) |
Slide order, titles, bullets, tables, charts, speaker notes, slide provenance | Slide-level records |
| JSON / JSONL / logs | Field types and nesting kept; logs become timestamp/level/message events | Records + Parquet |
| HTML / XML | Hierarchy, lists, tables, links; scripts, nav and hidden text removed (hidden text reported); XML entities rejected | Structural records |
Email (.eml) |
Sender, date (as effective date), thread ID; attachments processed as child documents | Thread-aware messages |
| Source code | Python by AST symbol + imports; other languages by symbol boundaries | Symbol-aware chunks |
| ZIP / TAR | Safe extraction (no traversal, links or bombs), nested discovery | Manifest + child documents |
| PDF / Word / images | Docling (layout, tables, page + bbox provenance), Tika fallback, Tesseract OCR with a confidence gate | Structured JSON + Markdown |
| Audio / video | faster-whisper, timestamped segments | Timestamped transcript |
corpusos parsers shows which engines are installed. Add the heavier ones with extras: uv sync --extra docling --extra ocr --extra media --extra tika --extra pii --extra tokenizers --extra embed.
Canonical metadata contract¶
Every document, whatever its format, is one document.json that conforms to a versioned, strict schema: schemas/document.schema.json (plus chunk.schema.json). Unknown fields are rejected, and corpusos validate re-checks every stored record.
| Section | Contents |
|---|---|
| identity | schema_version, document_id, revision_id, corpus_id |
source |
connector, locator_ref, display path, original filename, MIME type, size, SHA-256, parent document |
security |
tenant, owner, classification, acl_ref, training_allowed, retention_policy_id, PII counts, risk flags |
processing |
parser and version, pipeline version, config digest, run, status |
content |
title, language, domain, type, blocks with section_path and provenance (page / slide / sheet / line / time), tables, media |
quality |
status, warnings, failures, checks, extraction confidence (which says nothing about whether the content is true) |
lifecycle |
active · superseded · pending_review · quarantined · rejected · deleted |
time |
source modified, ingested, effective date (only when the source states it) |
retrieval |
chunk profile and version, tokenizer, embedding model, index version |
Agent-facing outputs never contain absolute source locators or ACL principal lists, only locator_ref and acl_ref. The absolute path stays in the catalog and in metadata.json for operators.
Retrieval for agents¶
search (CLI), GET /search (HTTP) and search_corpus (MCP) route each question:
| Question | Route |
|---|---|
| "Explain our authentication architecture" | hybrid: BM25 + vectors fused with reciprocal rank fusion |
| "Find error code AUTH-1042" | keyword; the evidence is insufficient unless a passage contains AUTH-1042 |
| "What was revenue in Q2?" | sql: lists the readable typed tables to query |
| "What changed between revisions?" | compare_revisions: block-level diff |
| "Show the architecture diagram" | assets: media references of matching documents |
| "Which source supports this?" | provenance: lineage of the top passage |
Each response carries evidence_status: sufficient | insufficient, so an agent can answer "insufficient evidence" instead of inventing an answer. Passages are marked trust: untrusted_source_data.
Chunks are structure-aware and tokenizer-aware (default 512 tokens, minimum 64). They never mix sibling sections, and a short parent introduction is merged into its first subsection. Each chunk records its tokenizer and its section_id, so get_evidence can expand a precise hit to its parent section or its neighbours.
| Interface | Tools |
|---|---|
| CLI | search, show, evidence, compare, lineage, datasets, sql |
HTTP (corpusos serve) |
/search, /documents/{id}, /evidence/{chunk}, /documents/{id}/compare, /lineage/{id}, /tables, POST /sql |
MCP (corpusos mcp -p alice, or --transport http for remote clients) |
search_corpus, get_document, get_evidence, list_tables, query_table, compare_revisions, get_lineage, plus search and fetch in the shape ChatGPT connectors expect |
Every read interface is read-only; only admins write, through the web app's storage, sync and review endpoints. The caller's principal is checked against the live ACL on every call, and every call is audited. The stdio MCP principal is fixed when the server starts; the remote MCP principal comes from a verified OAuth token or API key on each request (see Agent platforms); the web app's principal comes from its sign-in (see Web app); and corpusos serve takes the principal from the X-CorpusOS-Principal header, so put an authenticating proxy in front of it before exposing it. SQL runs in an in-memory DuckDB that holds only the tables the principal may read, with file and network access disabled.
{ "mcpServers": { "corpusos": { "command": "uv", "args": ["run", "--directory", "/path/to/CorpusOS", "corpusos", "mcp", "-C", "/path/to/corpus", "-p", "owner"] } } }
Embeddings are off by default, which leaves search lexical only. In corpus.yaml, set embeddings.backend to sentence-transformers (needs the embed extra) or ollama (a local server), then run corpusos reindex. The hash backend is an offline, non-semantic baseline for testing.
Web app¶
corpusos web serves a browser UI at /, the HTTP API at /api (interactive reference at /api/reference) and the docs site at /docs/. People sign in, search with citations, browse the documents they may read, and run SQL over typed tables. Admins also upload files, sync storage, watch ingest jobs and work the review queue.
- Sign-in (
web.authinsecurity.yaml):firebaseuses Firebase Auth or Identity Platform, with HttpOnly session cookies verified on every request, including the revocation check.jwtuses an authenticating proxy such as Google IAP or oauth2-proxy.headeris for loopback development only. API keys (web.api_keys, hashes only) are for scripts. - Storage: files reach the corpus from one folder (
web.storage_dir, default<corpus>/storage/<source>/). They get there directly (copy them, or mount a bucket) or throughPOST /api/storage/<source>. A sync, from the UI, fromPOST /api/syncor on a timer, ingests every source folder. A file removed from storage is deleted from the corpus. - Hosting: the
Dockerfilebuilds the app with the docs site. The corpus tree can sit on a Cloud Storage or NFS mount, with the catalog on local disk (CORPUSOS_CATALOG_PATH); it is restored on start and snapshotted after each write.
The docs site covers the web app and API, storage and self-hosting.
Agent platforms¶
There are two ways to give ChatGPT, Claude, Copilot Studio, Bedrock or Vertex AI the corpus. Use the first wherever the platform allows it.
| Remote MCP server | Knowledge export (uploaded files) | |
|---|---|---|
| Platforms | ChatGPT (connectors, deep research), Claude.ai and Claude Code (custom connectors), Copilot Studio (MCP tools), any MCP client | ChatGPT GPTs and Projects, Claude Projects, Copilot Studio file knowledge, Bedrock Knowledge Bases, Vertex AI Search / RAG Engine, OpenAI vector stores |
| Access control | the live ACL, per user, on every call | none: everyone who can use the agent reads every file |
| Deletions and revocations | effective on the next call | the copy stays until someone deletes it |
| Review gate, audit, exact SQL | yes | no |
Remote MCP server¶
corpusos mcp --transport http serves streamable HTTP (stateless, JSON responses) at /mcp. Every request carries an OAuth 2.1 access token or an API key, which resolves to corpus principals; each tool call is then checked against the live ACL and audited, exactly as over stdio. Configure it in config/security.yaml under mcp:
mcp:
public_url: https://corpus.example.com/mcp # what clients connect to; required beyond loopback
oauth: # JWT access tokens from your identity provider
issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
audience: api://corpusos # default: public_url
required_scopes: [corpus.read]
principal_claim: preferred_username # or sub, email, oid
group_claims: [groups, roles]
principal_map: {"<entra-group-id>": finance} # claim value -> corpus principal(s)
api_keys: # for clients that only send a key
- {label: copilot-studio, sha256: <from corpusos mcp-key create>, principals: [copilot-agent]}
- OAuth (ChatGPT, Claude.ai, Copilot Studio): the server is a resource server. It verifies the signature against the issuer's JWKS (discovered from its metadata), plus issuer, audience, expiry and scopes, and publishes RFC 9728 Protected Resource Metadata at
/.well-known/oauth-protected-resource/mcp. A request without a valid token gets401with aWWW-Authenticateheader pointing at it, and a token without the required scope gets403. Sign-in, client registration and consent stay with the identity provider (Entra ID, Okta, Auth0, Keycloak, Google). - API keys (Copilot Studio's API key option):
corpusos mcp-key create -p copilot-agent --label copilot-studioprints a key once. Only its SHA-256 goes intosecurity.yaml. Clients send it asAuthorization: Bearer <key>orX-API-Key: <key>, never in the URL. - Refusals: the server will not start beyond loopback without
public_urland authentication, andpublic_urlmust be https. TheHostandOriginheaders are checked againstpublic_url, loopback andallowed_hosts/allowed_origins. Run it behind a TLS-terminating proxy. On loopback without authentication it serves as--principal, for local testing only. searchandfetchreturn{results: [{id, title, url}]}and{id, title, text, url, metadata}, which ChatGPT needs for company knowledge and deep research.urlcomes frommcp.citation_url(defaultcorpusos://<corpus>/<document>/<revision>); point it at your document viewer if you have one.
| Client | Where to add it |
|---|---|
| ChatGPT | Apps & Connectors, in developer mode: create a connector with the public_url and OAuth |
| Claude.ai | Settings > Connectors > Add custom connector. Allow the callback https://claude.ai/api/mcp/auth_callback at your identity provider |
| Copilot Studio | Agent > Tools > Add a tool > Model Context Protocol, with OAuth 2.0 or an API key header X-API-Key |
Knowledge exports¶
corpusos export knowledge --target chatgpt --audience sales-team
corpusos export knowledge -t bedrock -a '*' --max-classification public
corpusos export knowledge -t vertex -a finance --base-uri gs://my-bucket/finance-kb
--audience names the people who will use the agent. A document is exported only if all of these hold:
- it is current, accepted and indexed;
- its live ACL lets the audience read it (
*in an ACL means everyone); - it is at or below
--max-classification(defaultinternal, so documents that PII madeconfidentialstay out); - it has no risk flags (
--allow-flaggedlets in reviewed ones).
Each export goes to exports/knowledge/<target>/<date>__<audience>__v<NNN>/:
| File | Contents |
|---|---|
READ-ME-FIRST.md |
what the platform will not enforce, upload steps, platform notes, limits applied, what was left out |
upload/ |
exactly the files to upload, named <date>__<domain>__<type>__<slug>__v<revision>.<ext> |
export.json |
each file with the documents and revisions it holds, its SHA-256 and size |
excluded.json |
each document left out, and why |
changes.json |
files to add, upload in place of older copies, and delete on the platform, compared with the previous export for the same audience |
withdrawn.json |
files removed after a later deletion or revocation, which must also be deleted on the platform |
Every file starts with the document title and a source line: path, document_id, revision_id, classification and effective date. Platforms without metadata still cite the right revision. Tables are written as Markdown with column types, units and formulas, and also as CSV where the platform reads CSV.
| Target | Format | Limits applied | Metadata |
|---|---|---|---|
chatgpt |
.md |
20 files per GPT, 2M tokens and 512 MB per file: documents are merged into per-domain bundles when they do not fit | in the text |
claude |
.md + .csv |
30 MB per file | in the text |
copilot |
.md + .csv |
500 files, 512 MB per file | in the text |
bedrock |
.md |
50 MB per file | <file>.metadata.json sidecars with typed, filterable attributes (at most 10 KB) |
vertex |
.txt (Markdown inside) |
10 MB per file (RAG Engine's limit for text) | upload/metadata.jsonl for Vertex AI Search, with gs:// URIs from --base-uri |
openai |
.md |
5M tokens and 512 MB per file | attributes.jsonl for vector_stores.files.create (16 keys, 512 characters) |
The limits are the ones each platform documented on 2026-10-27. OpenAI's help pages could not be read directly, so the ChatGPT figures come from quotes of its knowledge article, and .md support in ChatGPT GPTs and Claude Projects is widely used but not listed in their help pages. A document too large for one file is split at headings, then paragraphs, then lines, into -part-NN files; table parts repeat the column header. Platforms change their limits, so --max-files, --max-file-mb and --max-file-tokens override them; each export records the limits it applied and their source pages.
Security, lifecycle and retention¶
- Policies per source (
security.yaml→defaults,sources["<tenant>/<source>"]): ACL, owner,training_allowed, retention policy. Policies apply live: revoking access takes effect on the next query, with no reprocessing. - Deletion propagation: chunks, embeddings, dataset registrations,
chunks.jsonland every file underexports/are updated as soon as a deletion is detected. Each ingest also refreshes theacl_refof exported records to the current policy. In knowledge exports, a file whose document was deleted, or whose audience lost access, is removed fromupload/and listed inwithdrawn.json, so that the copy on the platform can be deleted too. Copies already taken outside the corpus folder (including exports written with--output) cannot be recalled. - Policy reaches nested files: a policy change also applies to the archive members and email attachments of an unchanged archive or email.
- Retention:
corpusos purge [--dry-run]removes the derived artifacts of deleted and superseded revisions afterdeleted_days/superseded_days, and the original bytes too whenpurge_raw: true. - PII: detected (regex by default, or Microsoft Presidio) and optionally redacted in derived artifacts; originals are never modified. Automated detection cannot guarantee that every sensitive value is found.
Quality and continuous improvement¶
ingest → sample → inspect → evaluate → classify failures → correct → reprocess → regression test
| Command | Purpose |
|---|---|
corpusos inventory PATH |
Read-only Phase 0 inventory: kinds, sizes, parser coverage, duplicates, unportable names, collisions |
corpusos sample -n 10 |
Documents to compare with their originals, plus a checklist in evaluations/inspections/ |
corpusos failures add --category retrieval_miss --query … --expected … |
Classify what went wrong |
corpusos failures to-regression |
Turn reviewed failures into golden queries |
corpusos eval -k 10 |
recall@k, precision@k, MRR, citation correctness, permission leakage, abstention accuracy |
corpusos scorecard |
The release gates below; exits non-zero until all pass |
corpusos export training |
Verified, training-allowed SFT pairs with a stable held-out split |
The scorecard measures the proposed release gates. A gate without data is reported as not measured, never as passed.
| Gate | Target | How it is measured |
|---|---|---|
| Inventoried files accounted for | 100% | every discovered file has a recorded outcome in its run |
| Chunks with source provenance | 100% | chunk → blocks → revision → raw bytes on disk |
| Unauthorized cross-tenant retrieval | 0 | ACL probes with every known principal + golden-query leakage probes |
| Silent ingestion failures | 0 | files without an outcome, runs that never finished |
| Parser correctness on golden fixtures | ≥ 98% | evaluations/parser-fixtures.jsonl |
| Retrieval Recall@10 | ≥ 90% | evaluations/golden-queries.jsonl |
| High-risk documents reviewed before publication | 100% | published revisions with risk flags all carry a reviewer |
| Repeated unchanged ingestion | no duplicates | duplicate revisions/chunks and needless reprocessing |
Before processing a whole disk or NAS, pilot on about 100 representative files: clean documents, scans, nested spreadsheets, email attachments, corrupted files, multilingual documents and permission-restricted material.
Storage layout¶
corpus/
├── config/ corpus.yaml parsers.yaml security.yaml naming.yaml quality-gates.yaml
├── raw/ <tenant>/<source>/<document-id>/<revision-id>/original.<ext>
├── canonical/ <tenant>/<document-id>/<revision-id>/{document.json, content.md, metadata.json, assets/}
├── structured/ <dataset>/<version>/{schema.json, data.parquet}
├── chunks/ <corpus>/<profile>/chunks.jsonl
├── manifests/ sources / documents / revisions / runs / datasets .jsonl
├── evaluations/ golden-queries.jsonl, parser-fixtures.jsonl, curated-pairs.jsonl, inspections/, reports/
├── quarantine/ <run-id>/<document-id>.json
├── exports/ rag/ training/ evaluation/ knowledge/<target>/<date>__<audience>__v<NNN>/
├── storage/ <source>/...: files the web app and API ingest (default web.storage_dir)
└── .corpusos/ catalog.db: the transactional source of truth
| ID | Meaning | Changes when |
|---|---|---|
document_id (doc_<ULID>) |
Logical document | Never: stable across revisions |
revision_id (rev_…) |
Exact bytes of one version | The source bytes change |
chunk_id (chk_…) |
One segment under one chunking profile | The revision, the profile version or the text changes |
Generated artifacts follow <date>__<domain>__<document-type>__<short-slug>__v<revision>.<ext> (corpusos naming build|check). Originals are never renamed.
Development¶
uv sync && uv run pytest
uv run corpusos schema # refresh schemas/ after changing models.py (a test checks they match)
uv run python tests/fixtures/make_fixtures.py # regenerate the .xlsx/.pptx fixtures
Architecture, design decisions and the roadmap are in docs/architecture.md. Build the docs site with uvx --from mkdocs-material==9.5.39 sh deploy/build-docs.sh site. See CONTRIBUTING.md and SECURITY.md.
License¶
Apache License 2.0. See LICENSE.