5.8 KiB
bastion agent catalog stage: the curated dataset
The catalog is the small, structured thing a host reasons over and the thing
seal ships: one card per file and one per directory, built only from the
sanitized text. It is bastion_agent/catalog.py, standard library only.
python3 -m bastion_agent catalog <manifest> [--out DIR] [--require-signed]
needs the walk, extract and sanitize outputs, and reads chunks.jsonl if it
is there. It writes:
catalog.jsonl: one card per file the walk found, sorted by(root, path).catalog.dirs.jsonl: one card per directory that holds files (every ancestor of every file; the root is""), sorted the same way.catalog.summary.json: counts, the summarizer that ran (ornull),stopped, andbytes_catalog(the length of all the cards, in characters).
The file card
| Field | From | Notes |
|---|---|---|
root, path, name, kind, size, mtime, mime, sha256 |
walk | path and name after the name mask (below). |
text |
extract | ok, empty, unsupported, error, skipped or not_extracted. |
extractor |
extract | Which reader, e.g. pdftotext, pdf-builtin, docx, text:utf-8. |
sanitized |
sanitize | ok, skipped, error, not_run, or null when the sanitize stage has no record for it. |
title |
sanitized text | The first line of at least three characters, list and heading markers stripped, at most 120 characters. |
head |
sanitized text | The first 300 characters, whitespace collapsed. |
chars |
sanitized text | Length. |
keywords |
sanitized text | The top 8 by TF-IDF across the catalog: words of three or more letters, stopwords and placeholders removed, ties broken alphabetically (so the result is deterministic). |
entities |
sanitize | Token counts per type minted for this file, e.g. {"EMAIL": 2, "PERSON": 1}. Counts, never values. |
chunks |
chunk | How many windows the document produced (0 without the chunk stage). |
summary |
summarizer | Present only when the manifest asks for file summaries; null when there was no text. |
v |
The card format version. |
Only files with sanitized text get the text fields. A file without text still gets a card: the host can see that it exists, its type and size, and why it has no text.
The directory card
files, with_text, bytes, latest (newest mtime), types (the top 6
MIME types), keywords (the top 12 over the merged term counts), entities
(summed over the files), and summary when asked for.
What never enters the catalog
- Values. Every field derived from text comes from the sanitized store. A test searches the whole catalog for the fixture's personal data and fails on any hit.
- Placeholders as keywords.
[PII_*_n]tokens are removed before terms are counted. - Anchors in names. File and directory names are plaintext by nature, so
every segment of every path (parent directories included) goes through the
rules pass; an e-mail address, phone number, card or key in a name is masked
to
[PII_TYPE]. The masking is conservative: the e-mail rule spans dots and hyphens, soinvoice-m@example.com.pdfbecomes[PII_EMAIL];mimeandsizestill say what it was. The summary counts masked segments asnames_masked. A name likeMarisol_medical.pdfis not caught: that is contextual personal data, the same limit as the rules-onlyflagmode. The mask is the catalog's: the bastion's own listing, for a grant withread, returns names as they are. - Not hidden: content hashes. Each file card's
sha256, also thedocid in search hits and excerpts, is the hash of the original file, so whoever holds a copy of a file can confirm it is in the folder.
The summarizer (a sensor)
A manifest section summarize: {model, per: [file, directory]} turns it on
(it needs summarize in the mask). The model is reached through
SUMMARIZE_URL (default http://127.0.0.1:11434, a local Ollama),
SUMMARIZE_API (ollama or openai; unset means try Ollama's /api/chat,
then /v1/chat/completions) and SUMMARIZE_TIMEOUT (300 s): the
environment, never the manifest.
It sees only sanitized text (the first 12,000 characters), is told that
placeholders are redactions to keep as they are, and runs at temperature 0.
Its reply is cleaned (<think> blocks and surrounding quotes removed,
whitespace collapsed, at most 600 characters) into the summary string, the
only field it can touch. A directory's summary is built from its direct
children's summaries or heads (at most 60, 200 characters each). Summaries are
cached in catalog.cache.json by document, model and version, so a re-run
costs nothing for unchanged documents.
The first failure stops summarizing: every card is still written, one card
carries summary_error, the summary says stopped, and the exit code is 1.
Tests
python3 -m unittest bastion_agent.test_catalog covers title, head and
keywords and their determinism; name masking (e-mail, phone, plain names,
parent directories); summary cleaning; the whole stage with and without a
summarizer (card fields, entities, keywords free of placeholders, a card for
a file with no text, directory totals including the root, masked names
counted); the catalog containing none of the fixture's personal data; file and
directory summaries with the cache reused on the second run and the model
seeing only sanitized text; OpenAI-style detection; a summarizer failure
stopping after one call with every card still written; an unreachable
summarizer; and the guard that the earlier stages ran first.
The stopword list is for prose: on source code, identifiers such as self
and def count as keywords.
Not in this stage
Sealing (the next stage: the catalog encrypted to the owner and the
manifest's seal_to recipients) and any rendering of the cards on the host.