root 76cb5acb03 phase 1.6: add consent-record endpoint POST /biometric/subject/{id}/consent (Gate 2 backend)
Backs the candidate-facing consent flow specified in
docs/policies/consent/biometric_consent_template_v1.md §5. Without
this endpoint, no production code path could flip
consent.biometric.status from NeverCollected/Pending to Given —
meaning every photo upload would fail-closed at the consent gate
(403 consent_required) forever, even after counsel signs the
template. This unblocks the post-cutover intake flow.

Endpoint shape:
- POST /biometric/subject/{candidate_id}/consent
- Auth: legal-tier (X-Lakehouse-Legal-Token header)
- Body (JSON): {consent_version_hash, consent_collection_method,
  consent_collection_evidence_path?, operator_of_record}
- consent_collection_method constrained to closed set:
  electronic_signature / paper / click_acceptance — operator
  typo would silently weaken evidentiary defensibility
- consent_version_hash recorded but not gated against allowlist
  at runtime (avoids config-deployment dependency for v2 template
  rotation; counsel validates retroactively against
  consent_versions table)

State-machine semantics (mirrors upload's double-upload guard):
- NeverCollected / Pending → Given (happy path)
- Given → 409 consent_already_given (re-collect requires explicit
  erase + fresh grant under new template version)
- Withdrawn / Expired → 409 consent_post_withdrawal_requires_erase
  (explicit erase preserves audit-chain ordering of the cycle)
- Subject status Withdrawn / Erased / RetentionExpired → 403

Server-side authoritative timestamps:
- given_at = Utc::now() (operator-supplied would be tamperable)
- retention_until = given_at + 540 days (18 months per retention
  schedule v1 §4). If counsel changes the cap, schedule doc + code
  constant CONSENT_RETENTION_DAYS change in the same PR.

Audit row:
- accessor.kind = "biometric_consent_grant"
- accessor.purpose = "version=<hash>;method=<m>;operator=<op>;evidence=<path>"
- fields_accessed = ["consent.biometric.status",
                     "consent.biometric.retention_until"]
- result = "given"
- Transactional: manifest commit before audit append; rollback
  manifest if audit fails.

Tests added (12 new):
- consent_happy_path_flips_status_and_records_audit (full happy
  path + audit row inspection + retention_until math)
- consent_missing_token_rejected
- consent_wrong_token_rejected
- consent_subject_not_found_returns_404
- consent_missing_version_hash_400
- consent_missing_method_400
- consent_invalid_method_400
- consent_missing_operator_400
- consent_already_given_returns_409
- consent_post_withdrawal_requires_erase_returns_409
- consent_subject_inactive_returns_403
- consent_grant_then_upload_is_the_intended_intake_flow
  (end-to-end: grant → upload → verify chain has 2 rows
  consent_grant→biometric_collection in correct order, upload
  row chains off the grant's hmac)

71/71 catalogd lib tests + gateway crate compile clean.

Live verification post-restart:
- /audit/health + /biometric/health both 200
- Live POST returns 400 on missing/malformed body, 404 on ghost
  subject — auth + body parse + subject lookup ordering verified
- strings(target/release/gateway) contains: record_consent symbol,
  biometric_consent_response.v1, biometric_consent_grant,
  consent_already_given, consent_post_withdrawal_requires_erase,
  electronic_signature, /subject/{candidate_id}/consent route

Production posture: gateway running with the new endpoint live.
The candidate-facing consent UI is NOT yet built; that is a
separate session. This endpoint is the backend the UI will call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:21:12 -05:00
2026-04-22 02:41:15 -05:00
Description
Rust-first object storage system
6.3 GiB
Languages
TypeScript 38.4%
Rust 35.8%
HTML 13.9%
Python 7.8%
Shell 2.1%
Other 2%