Files
Privatae 3ae3a2841c
ci / personal-data (push) Successful in 3s
ci / test (push) Successful in 45s
ci / reproducible (push) Successful in 3s
bastion agent 0.1.17
2026-10-29 21:42:06 +00:00

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 (or null), stopped, and bytes_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, so invoice-m@example.com.pdf becomes [PII_EMAIL]; mime and size still say what it was. The summary counts masked segments as names_masked. A name like Marisol_medical.pdf is not caught: that is contextual personal data, the same limit as the rules-only flag mode. The mask is the catalog's: the bastion's own listing, for a grant with read, returns names as they are.
  • Not hidden: content hashes. Each file card's sha256, also the doc id 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.