Theme: the data-provenance / egress-control milestone.
One boundary, both directions. 0.5.12's headline is PII rehydration done safely — Airlock reverses its own redaction to make tool calls work, and it must do so without building a PII goldmine or an unaudited egress path. Its companion is the Phase-B indirect-injection / multi-turn input boundary, deferred here from 0.5.11 as a design-first pack. These are the same seam seen from two sides:
- Egress provenance (PII rehydration): real PII values flow out into tool-call arguments. Which sink may receive them is an egress-control decision.
- Ingress provenance (injection boundary): untrusted text flows in from tool outputs and prior turns. Which text is trusted at which boundary is the dual question.
Building the provenance concept once, for both directions, is why they belong in
one milestone. It is also the strongest form of the GTM "prove control over your
organization's data" positioning (dev/notes/gtm-buyers-gatekeepers-overview.md,
0.6.0 tree): not "we have guardrails" but "we can prove where your data went and
what reached your model."
Status: implementation complete for the startup/OOM resilience work, A-1
through A-4, and B-0. A-5 is intentionally pending a deployed dogfood window
and recorded human DECIDE; it cannot be completed from unit tests or an empty
telemetry population. The code commits are e10c842, 9d0e2c2, c9ca67c,
e281ec6, and 9579716. pyproject remains 0.5.10 until publication closes.
Publication (owner decision, at closeout): 0.5.12 is intended as the next
published release. Publishing it publishes everything since 0.5.10 — the
internal 0.5.11 FathomDB work and 0.5.12 — so the version bump is
0.5.10 → 0.5.12 and the CHANGELOG carries both trains (as 0.5.10 did for
0.5.8→0.5.10). Whether to publish at all remains the owner's call at closeout.
The PII design is already done and consolidated, and committed to this branch
alongside this plan (relocated from the 0.6.0 worktree where it was drafted):
dev/notes/design-pii-rehydration-primary.md— authoritative; the reconciled design.dev/notes/design-pii-rehydration-goldmine.md— Part-A detail (verified threat model, file:line; opaque-handle; graceful-degrade).dev/notes/design-rehydration-authorization-maintainability.md— Part-B detail (research survey; layered egress gate; Cedar/CaMeL/blocklist evidence).
Still DRAFT / owner-review-pending. The injection-boundary design does not exist yet — pack B-0 produces it.
| Decision | Consequence |
|---|---|
| PII containment is pack 1, folded into the milestone (owner decision) — not a separate hotfix | The live map-in-logs leak ships fixed in the first pack; the on-disk logs/airlock-2026-08-04.jsonl (cleartext maps) is purged as an ops action within that pack |
| Unified provenance milestone (owner decision) — PII and the injection boundary | TUI backlog (#25/#26/#30/#31/#32) is deferred to 0.5.14; this milestone is not TUI ballast |
| Global-first lists | Per-tenant PII allow/block lists are 0.6.0 territory (tenant identity); 0.5.12 ships one global policy, per-tenant-ready in shape |
| Streaming rehydration stays deferred | The non-streaming tool-call path is the deliverable; streaming inherits the invariants when built (own later pack) |
| Cedar / CaMeL are later tiers | v1 needs no external PDP and no dual-LLM rebuild; adopt only when their triggers fire (primary note §4) |
| Enforce is data-gated | The PII egress gate ships in observe mode; flipping to enforce is a HITL DECIDE on measured metrics, not a release-date decision |
On 2026-08-09, production was OOM-killed and systemd correctly attempted to
restart it. Recovery instead entered a restart storm: Airlock's GET /health
replacement only inspected top-level FastAPI routes, while the locked FastAPI
0.141.1 keeps LiteLLM's route inside an included router. Airlock therefore
raised its safety error even though the inherited, provider-fan-out route was
present.
- Find and remove
GET /healthfrom both top-level and included FastAPI routers, invalidate affected router caches, then register the safe aggregate route. Do not weaken the fatal check: an actually unreplaceable inherited route must still prevent startup rather than expose a billed liveness path. - Add a regression test using
FastAPI.include_router()and preserve the structural no-model-call coverage for every Airlock health endpoint. - Bound systemd restart storms with
StartLimitIntervalSec=5minandStartLimitBurst=3while retaining automatic retry for transient failures. - Done when: the health suite passes against the locked FastAPI/LiteLLM
environment, an isolated proxy completes startup, and the deployed service is
verified with
/livez(neverGET /healthas a liveness probe).
The OOM trigger remains a separate operational investigation; raising the memory limit alone is not an accepted fix.
From design-pii-rehydration-primary.md. Two axes kept distinct: stages =
no-goldmine data handling; layers = egress authorization.
The live leak, fixed first. Invariant: airlock_pii_map MUST NOT reach any
sink or serialized record.
- Denylist the map at the event-builder snapshot (
request_event.py:176) and inwrite_precall_block_record— no current or future sink can serialize it. - Ops action in this pack: purge or scrub
logs/airlock-2026-08-04.jsonl(and any other map-bearing log) —jq 'del(.airlock_pii_map)'. A code fix cannot clean data already on disk. (Owner runs this; the file's provenance — real vs. test canary PII — is unconfirmed, so treat as real.) - Done when: the T1/T2/T4 canary sweep passes (map + a canary PII string absent from every serialized sink and block record); the on-disk log is clean.
- Opaque handle (A1 stage): the map lives in a bounded, request-scoped,
process-local store keyed by a random handle; only the handle rides
metadata; deleted infinallyon success / exception / timeout / disconnect; TTL sweep for abandoned handles; store is bounded. Closes the LiteLLM-debug-dump surface the denylist cannot. - Bounded lifetime + ops rule (A2 stage): no LiteLLM detailed-debug with PII
on;
ulimit -c 0on the prod host. - Hydrate a copy (A3 stage): telemetry sees the pre-hydration (redacted) response; only the client sees the hydrated one — stop mutating the shared response object in place.
- Done when: T3/T5/T7/T8 pass (handle not map on the bus; store cleanup on every exit path + TTL + bounded; telemetry object stays redacted).
The layered, default-deny gate — built mode-aware and landing in observe.
- Evaluation order (primary note §3.1): known-bad blocklist (deny-override) → coarse taint → type-compatibility (asymmetric) → sink egress-trust band (round-trip auto-allow / exfil / unknown⇒deny) → residual allowlist.
- Required known-bad blocklist as the step-0 deny-override (distinct from the rejected allow-by-default fallback): globally-dangerous sinks, incident response, class vetoes. Default-deny handles unknown-bad; the blocklist handles known-bad; neither is exhaustive.
- Suppressed-hydration telemetry (value-free): every suppression emits an audit event that auto-populates the residual list (learn-then-enforce).
- v1 builds Layer 2 on the existing
mcp_tool_guardallowlist as the trust catalog, extended with an egress band. Layer 1 type-compatibility lands when MCP schemas are available (refinement). - Done when: T9 passes (mismatch→suppress; round-trip match→allow; exfil→deny-unless-listed; known-bad blocklist vetoes an otherwise-allowed sink; suppression events are value-free); the gate runs in observe without changing behavior (T11).
AIRLOCK_PII_FAIL_MODE = open | closedfor the redaction-unavailable case (defaultopenper directive; regulated deployments setclosed), with a loud startup warning and a per-requestairlock_pii_unavailableaudit marker that survives to the audit sink. Rehydration-unavailable always degrades to the placeholder (safe, no egress).- Done when: T10 passes (open serves + stamps the marker; closed blocks).
The falsifiable step. Run the gate observe → shadow → enforce in dogfood with synthetic + canary PII (prod PII is off — isolated). Six pre-registered metrics (primary note §5.2): zero-entry rate, false-deny on canary-legit, residual growth curve, false-allow into exfil, canary egress, unknown-tool rate. HITL DECIDE: enforce the layered allowlist if the criteria hold; fall back to the blocklist-interim only if the zero-entry claim fails for the real tool mix. The known-bad blocklist + telemetry run in every mode regardless.
- Done when: the metrics are collected, the DECIDE is recorded, and 0.5.12 ships in the decided mode (observe or enforce) — never enforce on faith.
Implementation status: the value-free event field and pii-egress mode of
scripts/measurement-report.py are ready. The required traffic matrix and
pre-registered thresholds are in
dev/plans/runs/0.5.12-pii-egress-measurement-protocol.md. The live Airlock
process predates this branch and must be deployed before it can emit the field;
do not treat its existing logs as an observe window.
Design-first, deferred here from 0.5.11 ("what text is untrusted, and at which boundary — building that provenance seam twice is the waste").
- Define the untrusted-text boundary: tool outputs and prior-turn content are untrusted; where does Airlock mark and carry that provenance through the request lifecycle? How does it compose with the semantic/injection classifiers already shipped (0.5.9) and with the PII egress gate (shared provenance concept — do not build a second provenance mechanism)?
- Produce a design memo + a DECIDE on the enforcement seam before any IMPL pack.
- Done when: the design memo exists, is reviewed, and a HITL DECIDE fixes the boundary + enforcement seam. No IMPL pack is authored before this.
Implementation status: design memo is complete at
dev/notes/design-indirect-injection-provenance.md. Semantic-classifier
selection/review and the enforcement contract remain explicit human gates before
any B-1 implementation.
Shape TBD by B-0. Provisional: mark untrusted spans at the ingress boundary;
carry provenance on the RequestEvent; let the injection classifiers weight
untrusted spans; enforce per the observe/shadow/enforce lifecycle.
- Streaming PII rehydration — deferred; inherits every invariant when built.
- Per-tenant PII policy — 0.6.0 (tenant identity).
- Cedar / CaMeL — later tiers (primary note §4).
- TUI backlog (#25/#26/#30/#31/#32) — 0.5.14.
- JSONL-half erasure (BL-2) — a 0.6.x backlog decision, though the provenance work here is adjacent.
Unchanged from 0.5.11, plus:
- The PII reverse map is a secret — never in any sink, log, or serialized record; enforced by the A-1 canary sweep at CI.
- Rehydration is an egress capability — default-deny; the gate fails closed-on-egress (an unavailable gate keeps the placeholder, never releases).
- Never enforce on faith — the egress gate ships in observe; enforcement is DECIDE-gated on measured canary + telemetry evidence, matching the project's observe→shadow→enforce posture.
config.local.yamlanddev/competitor-tracker.mdare owner-owned local artifacts — never commit them.