Story-2846

Versioned channel consent + current-version enrollment gate

This story builds the channel data-transit consent substrate that every conversational-channel enrollment will pass through (RFD-AGT-013, D21 / CC-51): a versioned consent registry, per-firm acceptance evidence, a fail-closed enrollment gate, and an audit trail. The Meta subprocessor gate is built now; the legal text is deferred to the first real firm (zero clients, staging only), so the seeded v1 row carries placeholder copy.

What problem this solves

When counsel talks to Mira over WhatsApp, matter content transits Meta — a subprocessor the firm must affirmatively consent to before anyone binds a number. CC-51: enrollment is refused until the firm accepts the consent. Consent must be versioned (a text change re-gates every firm) and auditable (who accepted which version, when). Channel consent deliberately does not overload the existing inference-egress consent (business.firm_inference_policies, pramaan-docs ADR-116): different subject, different tables, different gate.

The two tables (migration 0206)

TableScopeHolds
ops.channel_consent_versions Platform-global, no RLS (app role: SELECT only) provider / purpose / version + is_current; exactly one current version per (provider, purpose), enforced by a partial unique index. Seeded: (meta, channel_data_transit, v1).
business.channel_consent_acceptances Firm-scoped RLS (firm isolation + staff metadata + support app) Which version the firm accepted, accepted_by_user_id, accepted_at. Append-only: SELECT + INSERT grants, plus a mutation-blocking trigger — consent evidence never changes.

The gate (src/domains/channel_consent/service.py)

Version bumps re-close the gate

When legal text lands (or changes), a migration mints v2, flips is_current, and every firm's v1 acceptance instantly stops satisfying the gate — enrollment is refused again until the firm re-accepts. Both evidence rows are kept (append-only), so the audit trail shows the full consent history.

How to verify

# Static migration pins (single head, RLS, append-only, seed, downgrade)
uv run pytest tests/unit/test_channel_consent_migration.py

# CC-51 lifecycle + version-bump re-gate + audit trail (Docker test DB)
make test-db
bash scripts/run-with-test-db-env.sh uv run pytest \
  tests/integration/test_channel_consent_db.py \
  tests/integration/test_channel_consent_rls.py

# Migration round-trip + model drift (the CI gate)
make test-db-alembic-roundtrip

Scope boundaries (TICKET-MAP v2)