From 67ee61dfe9bd276a22c94275a40d80dba54b49db Mon Sep 17 00:00:00 2001 From: thierryvm Date: Mon, 27 Jul 2026 15:11:53 +0200 Subject: [PATCH] =?UTF-8?q?docs(handoff):=20six=20PR,=20z=C3=A9ro=20ligne?= =?UTF-8?q?=20de=20la=20feature=20vis=C3=A9e=20=E2=80=94=20et=20pourquoi?= =?UTF-8?q?=20c'=C3=A9tait=20juste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'étape 3 devait brancher un cron sur la file de suppression de compte. Aucune ligne de ce cron n'existe encore. Ce qui a été livré, c'est ce sur quoi il devait reposer, et qui ne tenait pas : un journal d'audit qui n'enregistrait rien depuis avril, trois affirmations publiques inexactes, un préflight qui lisait des fichiers au lieu d'interroger les comptes actifs, et des agents QA dont la checklist affirmait elle-même les deux faussetés corrigées le jour même. Le plan de la file a été rejeté trois fois. Chaque rejet a empêché quelque chose de réel : une suppression pouvant écrire zéro ligne d'audit sans erreur, puis une conception reposant sur un privilège que service_role n'a pas, puis un verrou anti-concurrence qui testait la mauvaise colonne et aurait supprimé le même compte deux fois. Le handoff consigne aussi mes erreurs plutôt que de les lisser : un « prouvé en production » qui ne l'était pas, un inventaire annoncé exhaustif qui comptait 2 sites sur 8, et une réinterprétation complaisante d'un ADR qui s'était trompé. Double redondance appliquée : vault Obsidian + ce miroir. Co-Authored-By: Claude Opus 5 --- ...7-1930-etape-3-audit-mort-agents-adr024.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/handoffs/2026-07-27-1930-etape-3-audit-mort-agents-adr024.md diff --git a/docs/handoffs/2026-07-27-1930-etape-3-audit-mort-agents-adr024.md b/docs/handoffs/2026-07-27-1930-etape-3-audit-mort-agents-adr024.md new file mode 100644 index 0000000..ccf9904 --- /dev/null +++ b/docs/handoffs/2026-07-27-1930-etape-3-audit-mort-agents-adr024.md @@ -0,0 +1,177 @@ +# Handoff — 27 juillet 2026, 19h30 · Étape 3 : le journal mort, les agents complices, et deux conceptions rejetées + +**Agent** : @cc-ankora (Opus 5) +**Branches mergées** : `fix/h3-service-role-client` (#273), `fix/claims-publiques` (#274), +`docs/adr-023` (#275), `ci/preflight-comptes-actifs` (#276), +`chore/agents-silent-failure-coverage` (#277), `docs/adr-024-deletion-queue` (#279), +`docs/roadmap-etape-3-etat-reel` (#280) +**`main`** : `72f9fa6` + +--- + +## 1. Ce qui a été livré + +L'étape 3 devait brancher un cron sur la file de suppression de compte. **Aucune ligne de +ce cron n'existe encore.** Ce qui a été livré, c'est ce sur quoi il devait reposer — et qui +ne tenait pas. + +**#273 — le journal d'audit n'enregistrait rien depuis le 16 avril.** `createAdminClient()` +associait la clé `service_role` à un adaptateur de cookies `@supabase/ssr` : en présence +d'une session, le SDK envoyait le JWT de l'utilisateur à la place de la clé, et le client +retombait en rôle `authenticated`, à qui `audit_log` est révoquée. Trois mois d'écritures +refusées en silence — `logAuditEvent()` avale ses propres erreurs par conception. Client +reconstruit et **scellé** par l'option `accessToken` (l'espace `.auth` devient +inaccessible), `error_code` ajouté au journal d'échec, spec e2e de non-régression prouvée +rouge-avant / verte-après. + +**#274 — trois affirmations publiques inexactes**, corrigées dans les 5 locales : l'export +était annoncé « complet » (7 tables sur 14 — ni les soldes, ni les dettes), et la base +légale du journal d'audit était déclarée « obligation légale » alors qu'aucun texte ne +l'impose (intérêt légitime, art. 6(1)(f)). + +**#275 — ADR-023** : délai de grâce 30 → 14 jours. À 30 jours, l'effacement tombait au bord +exact du délai légal d'un mois (art. 12(3)) : un cron en échec, et on est hors délai. + +**#276 — le préflight comptes vérifie enfin les comptes actifs.** Il lisait deux fichiers +sur disque ; un fichier de lien peut nommer le bon projet pendant que la CLI est +authentifiée ailleurs. Il interroge maintenant chaque CLI (`supabase projects list`, +`vercel whoami`). + +**#277 — un agent QA créé, trois corrigés.** Cf. §3. + +**#279 — ADR-024 + plan d'exécution** de l'étape 3b. Cf. §4. + +**#280 — ROADMAP remis à l'état réel** : l'étape 3 était annoncée « suivante » alors +qu'elle tourne depuis deux jours ; une dette fermée le matin y figurait encore. + +## 2. Chiffres + +| | Avant | Après | +| ---------------------------------------------------- | ----------------------- | --------------------------------- | +| Types d'événements présents en prod dans `audit_log` | 4 (tous pré-session) | correctif livré, **à re-mesurer** | +| Tables couvertes par l'export RGPD | 7 / 14, dit « complet » | 7 / 14, **dit honnêtement** | +| Agents QA | 16 | **17** | +| Plan 3b rejeté par `plan-reviewer` | — | **3 tours** | +| Sites annonçant « 30 jours » (hors i18n) | annoncés 2 | **mesurés 8**, dont 2 publics | + +## 3. Les agents validaient les bugs qu'ils devaient attraper + +La checklist de `gdpr-compliance-auditor` affirmait elle-même +« `exportUserData()` returns a **complete** JSON bundle » et « `executeDeletion()` **wipes** ». +Les deux étaient faux. **Il aurait approuvé les deux bugs corrigés le jour même.** + +Cause commune : les 16 agents posaient tous la même question sous des angles différents — +_est-ce présent ?_ Aucun ne demandait _est-ce que ça marche, et le saurait-on si ça +s'arrêtait ?_. Trois incidents en trois mois sont sortis par ce trou, tous **verts** +pendant qu'ils échouaient : H3 (3 mois), la fonction de purge `SECURITY DEFINER` sur table +`FORCE RLS` (jamais appelée depuis avril), et le job e2e à 173 specs sautées. + +Réponse : **un** agent créé (`silent-failure-auditor`, Opus) plutôt qu'un par symptôme, +et trois corrigés — `rls-flow-tester` gagne le **sens privilégié** (l'écriture légitime +atterrit-elle vraiment ?), `security-auditor` gagne les endpoints non authentifiés et le +principe « la sous-permission est une vulnérabilité », `gdpr-compliance-auditor` gagne un +chapitre « déclaré vs implémenté ». + +## 4. Décisions verrouillées — ADR-024 + +**Deux conceptions écrites puis abandonnées, chacune tuée par une mesure.** Elles sont +consignées dans l'ADR parce que rien n'invite plus sûrement à les réécrire que leur +absence du dossier. + +1. **Fonction `SECURITY DEFINER` atomique — rejetée.** `FORCE ROW LEVEL SECURITY` + s'applique **au propriétaire de la table**. Possédée par `postgres`, elle écrirait + **zéro ligne sans erreur** si le `postgres` hébergé n'a pas `BYPASSRLS` — invérifiable. + Compte supprimé, IP et user-agent conservés, plus aucune clé de jointure. +2. **La même en `SECURITY INVOKER` appelée par `service_role` — rejetée aussi.** Mesuré : + `service_role` n'a **aucun privilège** sur `auth.users` ni `auth.audit_log_entries`. + +→ **L'atomicité SQL de bout en bout est impossible par toute conception** : c'est la +frontière entre PostgREST et GoTrue, pas une limite de notre code. + +**La garantie devient l'idempotence et la reprise.** Une seule instruction est +irréversible ; la précéder d'un nettoyage rejouable suffit, et **aucun privilège nouveau +n'est demandé** — c'est ce qui rend cette conception défendable. + +Autres décisions : verrou anti-double-suppression sur une colonne `claimed_at` (jamais +`requested_at`) ; `deletion_self_insert` **supprimée** plutôt que durcie (aucune insertion +client n'existe) ; orchestration extraite de l'enveloppe `server-only` pour que le chemin +destructeur soit enfin testable de bout en bout ; livraison en **deux PR** (A inerte, +B armement). + +## 5. Erreurs commises, et ce qu'elles ont appris + +- **Un verrou qui ne mesurait rien.** Ma reprise des lignes bloquées testait `requested_at` + — la date de la _demande_. Une ligne n'étant traitable que 14 jours plus tard, la + condition était **toujours vraie** : chaque exécution remettait en file les lignes qu'une + autre traitait. **Le même compte supprimé deux fois.** Trouvé par la revue, corrigé, et + le comportement voulu **mesuré** avant d'y croire. +- **« Prouvé en production » alors que ça ne l'était pas.** La mesure était locale ; la + vérification prod n'a jamais été refaite après #273. Une inférence habillée en mesure — + exactement ce que ce chantier corrige ailleurs. Devenu un NO-GO écrit. +- **Un inventaire annoncé exhaustif qui ne l'était pas.** 2 sites « 30 jours » annoncés, + 8 réels, dont `README.md` et `SECURITY.md` — dépôt public. +- **Une réinterprétation complaisante** : ADR-023 annonce « sept chaînes », il y en a cinq. + J'avais écrit « imprécise, pas fausse ». Elle est fausse ; corrigé dans ADR-024. +- **`delete from auth.users` n'efface pas tout le graphe.** Compter les clés étrangères rend + aveugle à ce qui n'en a pas — cf. §6. + +## 6. Écart art. 17 mesuré — issue #278 + +`auth.audit_log_entries` conserve **l'email en clair** (`payload.actor_username`, +`traits.user_email`) **et l'adresse IP**, n'a **aucune clé étrangère** vers `auth.users` +(18 FK dans le schéma `auth`, aucune depuis cette table), et **survit intégralement** à la +suppression du compte. `service_role` ne peut même pas la lire. + +Aucune rétention : ni GoTrue ni Supabase ne la purgent. Hors de portée du code applicatif — +la corriger demande d'atteindre le schéma `auth`, donc un ADR et une session dédiés. +**L'écart devient effectif le jour où le cron s'arme, pas avant.** + +## 7. Ce qui reste à faire + +**Bloqué par doctrine** : le code de 3b ne peut pas partir dans la session qui a produit +sa décision de schéma (banned list #2). **Reprendre en session fraîche**, plan prêt dans +[`docs/plans/step-3b-deletion-queue.md`](../plans/step-3b-deletion-queue.md). + +**PR-A** (inerte) : migration (statut `processing`, `claimed_at`, index unique partiel, +`claim_pending_deletions`, deux politiques, purge en `INVOKER`), `deletion-core.ts` +extrait, annulation honnête, branche `processing` de l'UI, 3 clés i18n × 5, tests. +Attention : régénérer `src/lib/supabase/types.ts` **après** le push de migration, mettre à +jour `e2e/authenticated-specs.json` dans le même commit, et refondre les tests existants +de `deletion.test.ts` (un meurt avec la suppression du `delete from workspaces`). + +**PR-B** (armement) : route de cron, `vercel.json`, `CRON_SECRET` optionnel, 30 → 14 jours +sur **8 sites + 25 chaînes**, vérification d'armement post-merge. + +**Reste ouvert** : alerte sur échec d'écriture d'audit (art. 32(1)(d)), +`gdpr-compliance-auditor` sur l'app entière (jamais fait), advisors Supabase (0028, 0029), +export élargi aux 14 tables, Zod sur `recordCookieConsentAction`, `CardTitle` sans rôle +heading, 6 specs e2e en quarantaine, registre art. 30 + DPA, THI-206 (échéance 30 octobre). + +## 8. Ce qui revient à @thierry + +**Trois lectures SQL sur `ankora-prod`, aucune écriture.** Rappel : **deuxième** compte +Supabase ; le signe distinctif est la colonne `event_type` sur `audit_log`. + +```sql +-- 1. Un workspace a-t-il plus d'un membre ? (rayon de destruction) +select workspace_id, count(*) from public.workspace_members group by 1 having count(*) > 1; + +-- 2. Des demandes `pending` en double ? (verrou du push de PR-A) +select user_id, count(*) from public.deletion_requests + where status='pending' group by 1 having count(*) > 1; + +-- 3. Le journal d'audit enregistre-t-il depuis #273 ? +select event_type, count(*) from public.audit_log group by 1 order by 2 desc; +``` + +1 et 2 doivent renvoyer **zéro ligne**. **La 3 est un NO-GO de PR-B** si `auth.login` et +les `gdpr.*` restent absents après le 27 juillet. + +**À trancher** : issue #278 (art. 17), et la réserve de fond signalée en #277 — j'écris les +agents qui auditent mon propre travail. Les incidents cités sont mesurés, mais le choix de +_ce qu'on cherche_ reste le mien. + +**Note DoD** : Sourcery n'a relu **aucune** des PR de cet après-midi — plafond hebdomadaire +de 500 000 caractères atteint. Absence de relecture, pas silence approbateur. Sur du +markdown le coût est nul ; sur PR-A et PR-B, il faudra soit attendre le reset, soit +l'assumer par écrit.