Plan: ADR-004 Amendment 1 — authorize canopy-reporting PHI tenancy (T-MSIS / CMS-416) (#1250, epic &73)
On this page
- Status
- Context
- The reporting restricted-data tenancy contract (summary — full text in the ADR)
- Decisions surfaced (fail-safe calls adopted from the understand-phase synthesis)
- Step 2a/2b design (ratified 2026-08-11; canonical for #1256 — the #1456/2b spec moved to the A8b child plan 2026-08-12)
- Files touched (this MR — docs only)
- Verification
- Documentation updates
- Open decisions
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) + |
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 ( |
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 ( |
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 |
Done (2026-08-25) — not FTI-derived as-built; A3 relaxed to PHI-only, conditionally (ADR-004 Amendment 4) |
4 |
#1258 |
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-reportingauthorized 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_fplis 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_runsjob 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 retention — withdrawn 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_fplis 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_universeIEVS-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 |
|---|---|---|
|
|
The 11 attribute fields (eligibility status/dates, |
|
Unchanged — pseudonymous engine-key UUIDs ( |
— (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 ascrypto_failure, never lease-retried. Sealing happens PURE (before the token-fenced chunk tx), so the tx stayssqlx::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-cratemint_dekhardening 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 (neverALTER DEFAULT PRIVILEGES— the fleet rule from the chain-v2 substrate) +search_pathpinned. (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
DELETEon output tables revoked; the janitor calls aSECURITY DEFINERreap function that verifies the generation is superseded/abandoned. -
Migrator/runtime split: additive
CANOPY_{SVC}__MIGRATION_DATABASE_URLbootstrap 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/rolbypassrlsand requirecanopy_reporting_appmembership unlessCANOPY_REPORTING__ALLOW_BROAD_DB_ROLE=true(loud WARN naming the cutover runbook).
Files touched (this MR — docs only)
| File | Change |
|---|---|
|
|
|
ADR-004 index line gains the Amendment 1 parenthetical. |
|
|
|
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
-
cargo xtask plan-lint+check-docsclean; the Antora build resolves the#amendment-1xref + all issue/ADR refs. -
CHANGELOG.adoc == UnreleasedcarriesCloses #1250;architecture.adocADR-index updated. -
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_extractsexactly. -
Docs-only ⇒ no functional battery; the children carry the code + tests.
docs:MR tomain.
Documentation updates
-
ADR-004 Amendment 1;
architecture.adocADR-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.