ADR-0061: Finance document storage & retention policy¶
Status: Accepted (2026-06-17)
Status pass (2026-10-18): the code this record describes is in the private domain tree, not in this repository. retention moved to filemanager_retention under ADR-0063 (config/heartbeats.yaml says finance_retention was retired)
Context: Downloaded statement PDFs and the extracted finance.db are high-sensitivity. Today they're gitignored (never committed) and bank-password-protected PDFs stay encrypted at rest, but protection is otherwise inherited from the source PDF + the OS: files are written world-readable (0644), the extracted DB is plaintext, unprotected source PDFs (e.g. a margin report) sit in the clear, and there is no retention. This ADR makes at-rest handling policy-driven and configurable in the finance agent.
Context¶
iris_harness.finance.ingest writes raw attachment bytes to
data/finance/statements/<institution>/<YYYY>/<sha12>_<file> and parses them
into data/finance.db. Audit of the on-disk state:
- ✅
data/finance/anddata/*.dbare gitignored; secrets live in the OS keychain / an optional Fernet vault (ADR-0032). Extraction is Tier-3 local only (ADR-0002/0006) — documents never leave the machine. - ⚠️ Files are
0644(readable by other local users); the extracted DB is plaintext; an unencrypted source PDF stays in the clear; documents are kept forever.
The weak points are local at-rest exposure and unbounded retention. The user wants these governed by a configurable policy, with 1-year doc retention.
Decision¶
A single declarative policy, config/finance/storage_policy.yaml, loaded into a
StoragePolicy and enforced deterministically:
storage:
file_mode: "0600" # owner-only on statement files + finance.db
dir_mode: "0700" # owner-only on the statements directories
retention_days: 365 # delete raw statement PDFs this long after download (0 = keep forever)
purge_after_extraction: false # if true, delete the raw PDF once extraction completes (keep only the DB)
db_encryption: "none" # future knob: "sqlcipher" (see Consequences)
-
Owner-only permissions (enforced). On write, statement files get
file_modeand their directoriesdir_mode;FinanceStore.ensure_schemarestrictsfinance.dbto owner-only. Closes the0644local-read gap. -
Retention (enforced, default 1 year).
enforce_retention(store, policy, now)deletes the raw PDF for any statement older thanretention_days(bycreated_at= download time), and — ifpurge_after_extraction— for any statement whose extraction iscompleted. The Statement row and the extracted data are kept; only the raw document is removed and itsfile_pathcleared (so the system knows the source is gone). Runs daily via afinance_retentionheartbeat and on demand viairis finance documents purge. -
purge_after_extractiondefaults OFF. Keeping the raw PDF for the retention window preserves re-extraction (parser improvements re-run against the original) — something this build relied on repeatedly. Users who want minimal residency can switch it on. -
Encryption at rest = OS disk encryption + owner-only perms, today. App-level crypto (SQLCipher for the DB, Fernet for PDFs, reusing the ADR-0032 vault key) is a declared but not-yet-enforced policy knob (
db_encryption) — called out so the policy surface is honest about what is and isn't enforced.
Consequences¶
- At-rest exposure drops from "world-readable, forever, plaintext DB" to "owner-only, 1-year retention, raw docs purgeable" — configurable per install.
- Re-extraction stays possible within the retention window (default on).
- Not yet enforced: DB/PDF app-level encryption.
db_encryption: "sqlcipher"is reserved; until implemented, the DB relies on OS disk encryption (FileVault etc.) +0600. This is the main residual and the natural next ADR. - Open-source friendly: a self-host can tighten (
purge_after_extraction: true, shorter retention) or loosen (retention_days: 0) without code changes.