Plan: ADR-004 Amendment 1 — authorize canopy-reporting PHI tenancy (T-MSIS / CMS-416) (#1250, epic &73)

On this page
NOTE

Implements the ADR-004 Amendment 1 contract (A1–A9) as modified by Amendment 2 (ADR-041, 2026-08-03): A6’s audit rows are delivered via the epic &74 facility adoption (no bespoke reporting audit log), A7’s chain-v2 attachment is withdrawn (no hash chain — the original ADR-014 chain-v2 pointer here predates the supersession), and A8 storage controls are unchanged. The ADR pins the tenancy contract + the normative controls; children own the byte-level (encryption, restricted role, facility audit adoption). Origin: ADR-001 Amendment 1 §B8 (#1235) surfaced the gap and named this the hard-prerequisite blocker. Access path is ADR-001 A1 §B4 (projection) + §B7 (report_runs); encryption via ADR-036.

Status

Step Description Status

0

Claim #1250; file the follow-up children (#1256 storage-controls/audit-log impl, #1257 FTI-provenance determination, #1258 QC IEVS-touchpoint) + /relate; commit this plan + nav.

Done (2026-07-27) — #1250 (this MR)

1

#1250 ADR-004 Amendment 1 — the reporting restricted-data tenancy contract (A1–A9) + settled decisions + consequences.

Done (2026-07-27) — this MR

2a

#1256 A8(a) sealing — encryption at rest for the T-MSIS extract ATTRIBUTES under the storage classification rule, via ADR-036 context-bound envelopes (per-generation DEK); ADR-004 Amendment 3 rides the MR. Scope narrowed 2026-08-11 to the extract OUTPUT table: the shared run substrate (report_run_universe/report_runs.progress, transient drain scratch) is not sealed — one accepted residual (report_run_universe.aux determination status) tracked as a follow-up. Prereq: the #1454/#1455 export-path fixes (merged 2026-08-11, !1124).

Done (2026-08-11) — #1256

2b

#1456 A8(b) — least-privilege restricted DB role + credential cutover. Canonical spec: the child plan reporting-least-privilege-role-a8b (2026-08-12) — the shipped chain-v2 owner/app pattern (NOT fleet-first). Implemented 2026-08-12 (all prerequisites #1256/#1463/#1464 + the #1465 residual cleared en route): owner/app split, catalog-driven ownership-transfer loop (REASSIGN OWNED probe-proven unusable on the pinned devstack superuser — see the child plan), per-object grant matrix, SECURITY DEFINER janitor, superuser-rejecting boot guard, devstack real-login cutover. Child plan archived.

Done (2026-08-12) — #1456

2c

A6 audit rows + A8(c) audited export — moved to the epic &74 facility-adoption child #1457 (blocked by #1300/#1301) per the 2026-08-03 scope note + ADR-004 Amendment 2; the chain-v2 attachment (old A7) is withdrawn outright.

N/A

3

#1257 FTI-provenance determination — trace income_as_pct_fpl to its income source(s); confirm FTI-derived vs PHI-only and relax A3 if cleared.

Done (2026-08-25) — not FTI-derived as-built; A3 relaxed to PHI-only, conditionally (ADR-004 Amendment 4)

4

#1258 snap_qc_universe IEVS-touchpoint classification — verify whether the QC universe (financials + ievs_match_completed) is correctly non-restricted or a secondary tenancy gap.

Done (2026-08-25) — non-restricted as held (contested columns NULL-by-construction; populated set = §B8 classes; sourcing boundary pinned in ADR-004 Amendment 4)

Epic: &73
Issue: #1250 (priority::critical)
Branch: feature/1250-adr004-reporting-phi-tenancy

Context

ADR-001 Amendment 1 §B8 (#1235) surfaced a shipped compliance defect: canopy-reporting persists person-level T-MSIS PHI at rest (services/canopy-reporting/migrations/20260409000000_tanf_medicaid_reporting_tables.sql, medicaid_tmsis_eligibility_extracts.person_id), yet ADR-004’s isolation map does not name reporting at all — and ADR-004 forbids restricted data reaching a non-authorized consumer. The ADR-001 A1 mandate to build the caseload-wide T-MSIS/CMS-416 universes deepens the exposure. This amendment authorizes reporting as a mapped restricted-data consumer (strictly for those two federal extracts, minimum-necessary), governed under the stronger Pub 1075 §4 control set, with (as amended by Amendment 2) facility-adopted audit rows, encryption at rest, and a restricted role — the original bespoke-audit-log + chain-v2-retention controls were re-homed/withdrawn by ADR-041. It is the hard-prerequisite blocker for the T-MSIS/CMS-416 PHI-extract children.

The reporting restricted-data tenancy contract (summary — full text in the ADR)

The authoritative contract is ADR-004 Amendment 1 A1–A9; a contextless implementer reads that first.

  • A1 — Isolation-map entry. canopy-reporting authorized for the T-MSIS/CMS-416 extracts (mirror row; the Decision-section map is byte-immutable).

  • A2 — HIPAA PHI explicit. The person-level T-MSIS extract is HIPAA PHI (ADR-001 A1 §B8); CMS-416 as held is de-identified aggregate (45 CFR 164.514), named for scope completeness, not a PHI holding.

  • A3 — Pub 1075 §4 governance. FTI-derived-or-PHI: income_as_pct_fpl is MAGI-methodology-based and potentially FTI-derived, so — fail-safe pending #1257 — governed under the superset control set (Medicaid §6103(l)(12)); relaxes to PHI-only only if #1257 clears it.

  • A4 — Minimum-necessary. Exactly the T-MSIS/CMS-416 layout fields (no SSN, no raw FTI); CMS-416 aggregate-only; projection (ADR-001 A1 §B4) enforces.

  • A5 — Access path. HTTP-only via the ADR-001 A1 §B7 report_runs job model + §B1/§B2/§B3 completeness + §B4 projection.

  • A6 — Independent Pub 1075 §4 / HIPAA audit log (own DB, per-access, scrubbed from canopy.events) — normative, not as-built. Amendment 2: delivered by the epic &74 facility adoption (#1457 — reporting-owned rows as system-of-record + the #1300/#1301 mechanisms); no bespoke reporting audit log.

  • A7 — Chain-v2 retentionwithdrawn by Amendment 2 (ADR-041: no hash chain). Retention/legal-hold generalizes in #1303; the Pub 1075 floor was corrected to 7 years (AU-11).

  • A8 — Storage controls — encryption at rest (ADR-036), least-privilege restricted DB role, audited export — normative, not as-built; unchanged by Amendment 2. Sealing = #1256 (2a), role = #1456 (2b), audited export rides the #1457 adoption (2c).

  • A9 — Scope boundary. Implementation is #1256; non-PHI extracts (FNS-388/ACF-199/QC) unchanged; ADR-002 untouched; unblocks the #1250 PHI-extract children.

Decisions surfaced (fail-safe calls adopted from the understand-phase synthesis)

  • FTI-derived vs PHI-only (A3). Govern under the stronger Pub 1075 §4 control set (subsumes HIPAA), because income_as_pct_fpl is plausibly FTI-derived and under-classifying leaves shipped FTI unauthorized. The mandated controls are identical either way; #1257 settles the provenance and may relax to PHI-only.

  • Persist vs read-live (A1). Authorize the persisted extract snapshot (the shipped reality; federal submissions must be reproducible; the B7 job model is snapshot-based), bounded by the mandatory A6–A8 compensating controls.

  • Controls inline vs deferred (A6–A8). Both — normative MUST/SHALL clauses in the ADR AND a filed implementation child (#1256), keeping the tenancy contract self-contained while the code stays out of a docs-only ADR.

  • Retention floor (A7). Per-jurisdiction ruleset value bounded below by max(Pub 1075 §4 5yr, HIPAA 6yr) — belt-and-suspenders against whichever framework binds. (Historical: Amendment 2 withdrew A7, corrected the Pub 1075 floor to 7 years (AU-11), and re-homed retention to #1303.)

  • Scope (A9). Strictly T-MSIS/CMS-416. The snap_qc_universe IEVS-touchpoint (financials + ievs_match_completed) is a plausible secondary gap, filed separately as #1258 rather than folded in.

Step 2a/2b design (ratified 2026-08-11; canonical for #1256 — the #1456/2b spec moved to the A8b child plan 2026-08-12)

The storage classification rule (becomes ADR-004 Amendment 3, riding #1256’s MR)

Plaintext is permitted only for values the database engine itself must evaluate (filter/join/order/unique/group keys), and every such column is enumerated with its justifying query; all other restricted-table content is sealed. Amendment 3 records the rule and corrects the Amendment-1 premise "CMS-416 as held is aggregate-only" — factually wrong for the shared report_run_universe working state, which durably holds person UUIDs for the tmsis/cms_416 drains.

Scope (narrowed 2026-08-11, extracts-only): the rule seals the ATTRIBUTE content of the one restricted-as-held OUTPUT table, medicaid_tmsis_eligibility_extracts. The shared run substrate (report_run_universe, report_runs.progress) stays plaintext — it is transient, janitor-bounded drain scratch (not a published holding), and sealing it would burden the cross-kind (SNAP/TANF/QC-shared) substrate with per-kind branching. Honest residual: report_run_universe.aux for the tmsis drain (TmsisDetAux) holds person_id/household_id (pseudonymous keys), assigned_coa/assigned_coa_track (identical to the plaintext-by-design coverage_group/chip_indicator engine keys), AND the raw determination status — the one field of the same class as the sealed eligibility_status that this scope leaves plaintext. Accepted for #1256 and tracked as follow-up #1459; a threat review decides whether to seal aux or narrow what the drain persists.

Table Plaintext (engine-evaluated — justifying query) Sealed

medicaid_tmsis_eligibility_extracts

person_id (CMS-416 universe DISTINCT/keyset/COUNT + medicaid_tmsis_person index), enrollment_id (unique index), report_month (window predicates), generation_id, chip_indicator (universe WHERE), coverage_group (CMS-64 GROUP BY), timestamps

The 11 attribute fields (eligibility status/dates, income_as_pct_fpl, citizenship, disability/dual/managed-care/restricted-benefit fields) → ONE TmsisRestrictedPayload envelope per row (restricted_payload)

report_run_universe, report_runs

Unchanged — pseudonymous engine-key UUIDs (item_id, progress cursors) stay plaintext, consistent with the extracts keys

— (not in scope; see the scope note above)

Threat-model note for the plaintext person/enrollment UUIDs: bare UUIDs are pseudonymous references; the linkage data lives sealed in other legally-scoped databases — ADR-004 tenancy isolation is the linkage control, envelope encryption the content control. Derived-data redaction unit = the generation (person-level redaction happens at source, then regenerate) — per-generation DEKs are therefore correct, not a compromise.

Sealing mechanics (#1256 / 2a)

  • DEK: per-generation ("report_generation", generation_id) sealing that generation’s extract attribute payloads.

  • canopy-crypto-shred gains additive context-bound seal/open variants (AAD = envelope identity + caller context ["tmsis_extract", generation_id, row_id]) so a ciphertext relocated between rows under the shared per-generation DEK fails the tag; KAT + relocation tests ride the crate.

  • Versioned TmsisRestrictedPayload (v: 1, version-dispatched decode, algorithm validation, a golden serialized fixture + a serde-roundtrip test).

  • Typed RestrictedStoreError::{Transient, Permanent} — crypto/key failures terminalize the run as crypto_failure, never lease-retried. Sealing happens PURE (before the token-fenced chunk tx), so the tx stays sqlx::Error.

  • redaction_keys (persons DDL + tombstone trigger) plus a partial unique live-subject index; writer get-or-create is atomic (INSERT … ON CONFLICT DO NOTHING + re-SELECT); readers load-only (never mint/resurrect). Shared-crate mint_dek hardening is #1458.

  • require_kek (reporting-contextualized) runs BEFORE migrations at boot.

  • Quiesced fresh-start reset migration (pre-1.0, derived data, loss authorized): terminalize in-flight tmsis/cms_416 runs, supersede tmsis generations (honest 404 until re-published), clear their universe rows + derived CMS-64/416 rows, TRUNCATE + reshape extracts.

Role + credential architecture (#1456 / 2b — summary; canonical spec in the A8b child plan)

  • canopy_reporting_owner (NOLOGIN; owns schema objects) + canopy_reporting_app (runtime), fail-closed attribute reconcile; full grant surface enumerated from actual SQL; REVOKE ALL FROM PUBLIC; explicit per-object grants (never ALTER DEFAULT PRIVILEGES — the fleet rule from the chain-v2 substrate) + search_path pinned. (Corrected 2026-08-12: the original "default ACLs pinned" wording contradicted the fleet’s enumerate-per-object directive; full spec in the A8b child plan.)

  • Published-snapshot immutability: direct DELETE on output tables revoked; the janitor calls a SECURITY DEFINER reap function that verifies the generation is superseded/abandoned.

  • Migrator/runtime split: additive CANOPY_{SVC}__MIGRATION_DATABASE_URL bootstrap support; devstack provisions the app login and the runtime URL switches to it — the battery/e2e run reporting AS the restricted login.

  • Boot guard (#1006 pattern): outside development, reject sessions with rolsuper/rolcreatedb/rolcreaterole/rolreplication/rolbypassrls and require canopy_reporting_app membership unless CANOPY_REPORTING__ALLOW_BROAD_DB_ROLE=true (loud WARN naming the cutover runbook).

Files touched (this MR — docs only)

File Change

adrs/adr-004-legally-scoped-data-tenancy.adoc

[#amendment-1] — A1–A9 + settled decisions + consequences + the mirror isolation-map row; Status-section NOTE. The Decision section + Context source table left byte-immutable.

architecture.adoc

ADR-004 index line gains the Amendment 1 parenthetical.

CHANGELOG.adoc

== Unreleased › Changed (Closes #1250).

plans/scale-audit-adr004-reporting-phi-tenancy.adoc, nav.adoc

this plan + nav entry (Scale Readiness, epic &73).

The control implementations land in the child MRs — sealing #1256 (2a), role #1456 (2b), facility audit adoption #1457 (2c) — not here. (The original sentence’s chain-v2 family attachment was withdrawn by Amendment 2.)

Verification

  1. cargo xtask plan-lint + check-docs clean; the Antora build resolves the #amendment-1 xref + all issue/ADR refs.

  2. CHANGELOG.adoc == Unreleased carries Closes #1250; architecture.adoc ADR-index updated.

  3. Fidelity re-read: every A1–A9 clause maps to a real shipped shape/path or a named child; the §Decision section (isolation map + FTI-audit bullets) and the §Context source table are byte-immutable; the not-yet-built controls are phrased normatively (MUST/SHALL) and explicitly labelled not-as-built (no false runtime guarantee); the minimum-necessary field list matches medicaid_tmsis_eligibility_extracts exactly.

  4. Docs-only ⇒ no functional battery; the children carry the code + tests. docs: MR to main.

Documentation updates

  • ADR-004 Amendment 1; architecture.adoc ADR-index; CHANGELOG.adoc.

  • Follow-up children filed + related: #1256 (storage-controls/audit-log impl), #1257 (FTI-provenance), #1258 (QC IEVS-touchpoint).

  • Plan → Archive on completion (final MR of the stream) — archived 2026-08-25 with the #1257/#1258 determinations (ADR-004 Amendment 4), the last open steps.

Open decisions

All decisions resolved (the five fail-safe calls above). The last open runtime question — whether income_as_pct_fpl is FTI-derived — was settled 2026-08-25 by the #1257 provenance trace: not FTI-derived as-built (reporting recomputes FPL from persons facts; the FTI readers are dead-code-gated per #785/#810; the IEVS estate has no IRS source). A3 relaxed to PHI-only with both tripwires recorded in ADR-004 Amendment 4: the IEVS estate staying IRS-free, and #785/#810 not writing FTI back into the persons fact corpus.

Edit this page · default