bastion agent 0.1.16
ci / test (push) Successful in 1m1s
ci / personal-data (push) Successful in 3s
ci / reproducible (push) Successful in 4s

This commit is contained in:
Privatae
2026-10-25 23:16:50 +00:00
parent 53094a6940
commit e3074f50b0
34 changed files with 1345 additions and 156 deletions
+6 -3
View File
@@ -122,9 +122,12 @@ checks each edge's bytes against the manifest it serves. Where it publishes
is not written into the script: the edges, the ssh key, the web root and the
backup directory come from its environment, and it stops if one is missing.
The manifest's `notes` link to the tag `v<version>` on the public repository,
which marks the source of that release. `publish.sh` does not make the tag; it
is made when that release's source is published to the public repository.
The manifest's `notes` link to the GitHub release of the tag `v<version>`,
which marks the source of that release and carries its files. `publish.sh`
does not make the tag; it is made when that release's source is published to
the public repository, and pushing it starts `.github/workflows/release.yml`,
which rebuilds the `.pyz` from the tag and attaches it, with its `.sha256`, the
signed manifest and its signature, once its sha256 matches the manifest.
### The legacy name
+19 -6
View File
@@ -15,7 +15,9 @@ personal data is replaced with stable placeholder tokens, the real values stay
in a local vault, and `/rehydrate` puts them back, locally, on demand. A cloud
model can then work over the text without the values in it. Patterns alone
cannot do this: "my daughter goes to Lincoln Elementary" contains no pattern,
only meaning.
only meaning. The agent the bastion deploys runs the rules pass only (`flag`
mode), so names, street addresses and dates of birth stay in the text it
returns ([sanitize.md](sanitize.md#modes-manifest-sanitizemode)).
## Pipeline (`pii/pipeline.py`)
@@ -116,7 +118,7 @@ Configuration, from the environment:
| Variable | Default | Meaning |
|---|---|---|
| `PII_API_KEY` | (none) | Required. Without it the service refuses to start: `/rehydrate` returns real values, so there is no unauthenticated mode. (`PII_ALLOW_NOAUTH=1` exists for the unit tests only.) |
| `PII_BIND` | `0.0.0.0:8091` | The address the service listens on; this is its exposure control. |
| `PII_BIND` | `127.0.0.1:8091` | The address the service listens on; this is its exposure control. Another address only when set. |
| `PII_DB` | `/data/vault.db` | The vault. |
| `PII_RECALL` | `llm` | The recall backend (below). |
| `LLM_URL` | `http://llm:8080` | The model server for `llm`. |
@@ -125,7 +127,9 @@ Configuration, from the environment:
Two ways to run it, the same code:
- **Container**: `pii/Dockerfile` (a pinned `python:*-slim` image, no pip
install; the vault on a mounted `/data`).
install; the vault on a mounted `/data`, writable by uid 10001, the
unprivileged user it runs as). Inside a container the default loopback bind
reaches nothing else, so set `PII_BIND` and publish the port deliberately.
- **systemd**: `pii/pii.service`, with install steps in its header. It runs
with `DynamicUser`, a private 0700 state directory for the vault
(`/var/lib/privatae-pii/vault.db`), loopback bind (`127.0.0.1:8091`), and
@@ -167,9 +171,18 @@ the machine.
Every detector is anchored by a checksum, a prefix or a fixed shape; none
depends on a model.
- **E-mail, phone, SSN, IPv4/IPv6, MAC.**
- **Cards** → `CREDIT_CARD`, checked with Luhn.
- **IBAN** → `BANK_ACCOUNT`: ISO 13616 mod-97 plus a per-country length
- **E-mail, IPv4/IPv6, MAC.** A dotted number right after a version word
(`version 1.2.3.4`, `v1.2.3.4`) is not an IP address.
- **Phone**: North American and other grouped national forms; international
numbers with `+` and a country code, any grouping, an optional `(0)` trunk
prefix; national numbers with a trunk `0` (`07700 900123`); a seven-digit
local number (`555-0142`) only after a word such as call, phone or tel.
- **SSN**: dashed or spaced; nine bare digits only after `SSN` or `social
security` within 40 characters. Numbers never issued (area 000, 666, 900+;
group 00; serial 0000) are left alone.
- **Cards** → `CREDIT_CARD`, with spaces, dots or dashes, checked with Luhn.
An expiry date or security code beside the card is not detected.
- **IBAN** → `BANK_ACCOUNT`, in either case: ISO 13616 mod-97 plus a per-country length
table. It runs before the card detector, so an IBAN's digits cannot be
claimed as a card number.
- **US ABA routing number** → `BANK_ACCOUNT`: the weighted checksum and a
+6 -3
View File
@@ -51,9 +51,12 @@ stages, there is no extract index, or the stage **stopped**; `2` completed with
- **`flag`**: the rules pass **only**: deterministic, no model, milliseconds.
It replaces the anchors a pattern can find: e-mail addresses, phone
numbers, SSNs, cards, IBANs, routing numbers, crypto addresses, IP and MAC
addresses, API keys, private-key blocks, JWTs. **It does not find a name.**
A flag-mode catalog is not free of personal data, and the manifest doc says
so too.
addresses, API keys, private-key blocks, JWTs. It can also replace an
ordinary number that looks like a phone or ID number. **It does not find names,
street addresses, dates of birth**, or anything else that takes reading for
meaning (an employer, a school, a medical fact). It is the mode the bastion
writes into every manifest it deploys. A flag-mode catalog is not free of
personal data, and the manifest doc says so too.
## Where the sterilizer runs
+19 -3
View File
@@ -2,7 +2,9 @@
`serve` is how a host reads what the agent prepared without the data moving:
a host (the owner's, or a grantee's) sends a query and gets **sanitized**
chunks and catalog cards back. Nothing else has a route: not the extract
chunks and catalog cards back (sanitized by the manifest's mode: in `flag`
mode, which the bastion deploys, names, street addresses and dates of birth
remain; [sanitize.md](sanitize.md#modes-manifest-sanitizemode)). Nothing else has a route: not the extract
store, not the vault, not the vectors table. It is `bastion_agent/serve.py`,
mutual TLS with the standard library's `ssl`.
@@ -31,12 +33,26 @@ There is no other credential: no token, no password, no header.
| Route | Body | Returns |
|---|---|---|
| `POST /search` | `{query, k?}` (k defaults to 5 and is held to 1–50) | `{hits, k}`: the top k sanitized windows with score, document id, chunk id, index and offsets. The same search as the CLI, through the same `EMBED_URL` model ([embed.md](embed.md)); 502 if the model cannot be reached. |
| `POST /read_range` | `{doc, start?, end}` | Up to 65,536 characters of the **sanitized** text of that document, with `total`. |
| `POST /search` | `{query, k?}` (k defaults to 5 and is held to 1–50) | `{hits, k, sanitize_mode}`: the top k sanitized windows with score, document id, chunk id, index and offsets, and the sanitize mode that wrote them. The same search as the CLI, through the same `EMBED_URL` model ([embed.md](embed.md)); 502 if the model cannot be reached. |
| `POST /read_range` | `{doc, start?, end}` | Up to 65,536 characters of the **sanitized** text of that document, with `total` and `sanitize_mode`. |
| `GET /catalog`, `GET /catalog/dirs` | query `prefix`, `offset`, `limit` (default 500, at most 2,000) | The file cards or the directory cards (owner only), a page at a time: `{cards, kind, total, offset, limit, next_offset, prefix}`, `next_offset` being `null` on the last page. 404 until the first sweep has written a catalog. |
| `GET /status` | | `{state}`: `run.state.json` (owner only). |
| `GET /health` | | `{ok, id, serving}` for any client that chained. |
`sanitize_mode` says what was done to the text in that reply: the mode the
sanitize stage recorded for that document in `sanitize.jsonl`, `flag` or
`sterilize`. A search names the weakest mode among its hits. It is `unknown`
when that record does not vouch for the text: the last sanitize run did not
write the document (it stopped, or the file is an older run's), the file was
rewritten after the record (a sweep is running), or a search hit's text is no
longer what the store holds (the chunks predate the latest sanitize run). A
search with no hits names the manifest's `sanitize.mode`, or `none` when the
manifest has no sanitize stage. In `flag` mode, the one the bastion deploys,
names, street addresses and dates of birth remain in the text; only
`sterilize` looks for them. Changing the manifest's mode does not change the
field: after a change of mode the next sweep sanitizes every file again, and
until it has, the text served is the previous mode's and the reply says so.
`doc` is the document's content hash (64 lowercase hex), the same id the
extract and sanitized stores share. The route reads only the `sanitize/`
directory, so the plaintext for that id, which exists on disk, is never what