Aller au contenu

PD-253 — Confrontation Gate 5

Reviewer : Claude (second reviewer — confrontation indépendante) Date : 2026-03-12 Rôle : Validation ou infirmation des 3 écarts identifiés dans la review Gate 5


1. Sources confrontées

Document Version Sections clés examinées
PD-253-specification.md v2 §4 (invariants), §5.2 (paramètres), §5.4 (machine à états), §5.5 flux 5, §9 (hypothèses)
PD-253-plan.md v1.0 §2/ECT-05, §4/CC-253-06, §4/CC-253-11, §6 (hypothèses), §7.1 (stack), §7.4 (temporaires)
PD-253-plan-review.md v1 §E-01, §E-02, §E-03 (analyse feasibility, coherence)

2. Validation des écarts

E Libellé Statut Justification
E-01 INV-253-11 — protection fichiers temporaires non précisée CONFIRMÉ MAJEUR Voir détail ci-dessous
E-02 Contradiction DOWNLOADED terminal vs DOWNLOADED → EXPIRED dans plan §2/ECT-05 CONFIRMÉ BLOQUANT Voir détail ci-dessous
E-03 S3 multipart 100 GB — support non confirmé dans S3Service.putObject() CONFIRMÉ MAJEUR Voir détail ci-dessous

E-01 — CONFIRMÉ MAJEUR : INV-253-11 chiffrement artefacts temporaires

Textes sources :

  • Spec §4 INV-253-11 : "Tout artefact cryptographique temporaire (clé, fragment, DEK, ReKey, package temporaire) est chiffré au repos; aucun secret en clair persistant."
  • Plan §7.4 : "Répertoire temporaire : /tmp/bulk-export-{exportId}/. Création : fs.mkdir() au démarrage du processor. Nettoyage : finally { await cleanup.purgeStaleTemp(exportId) } + purgeStaleTemp() au démarrage."
  • Plan §4/CC-253-06 listing INV couverts : INV-253-11 listé, mais mécanisme décrit uniquement comme "pas de secret en clair sur disque" (§8 mapping).
  • Plan §8 mapping INV-253-11 : "T-253-17 (processor — pas de secret en clair sur disque), T-253-24 (TC-INV-11)".

Analyse :

L'invariant INV-253-11 est clair : chiffrement au repos de tout artefact temporaire. Or le plan ne contractualise aucun mécanisme concret. Le répertoire /tmp/bulk-export-{exportId}/ contient : - content.enc : binaire chiffré WORM copié depuis S3 — déjà chiffré, pas de secret en clair ici. - metadata.json par document : contient plaintext_hash, ciphertext_hash, la ProofEnvelope complète (hash Merkle, preuve TSA, signature serveur). Ces données sont des empreintes cryptographiques, pas des secrets (pas de clé, pas de contenu en clair). - manifest-sha256.txt, manifest-sha3.txt : hashes de fichiers — données publiques du package. - destruction-log.json : destruction_act_hash, bordereau_id — hashes et UUID. - export.sig (optionnel) : signature HSM du package.

Constat : aucun des fichiers temporaires listés ne contient de secret stricto sensu (clé, DEK, fragment de clé). La spec cite explicitement "clé, fragment, DEK, ReKey" comme cas primaires de l'invariant — PD-253 ne manipule aucun de ces éléments (Zero-Knowledge : le contenu S3 est copié chiffré sans déchiffrement, cf. CC-253-06 interdit). Les données de metadata.json sont des empreintes non réversibles.

Nuance critique : la review Gate 5 qualifie E-01 en MINEUR (pas MAJEUR), non BLOQUANT. La qualification originale est pertinente car le risque réel est faible : les fichiers temporaires ne contiennent pas de secret au sens cryptographique. L'écart est réel (mécanisme non précisé) mais son impact sur la sécurité opérationnelle est limité.

Verdict E-01 : CONFIRMÉ MAJEUR. Le mécanisme de protection n'est pas contractualisé dans le plan. La correction attendue est simple : documenter dans CC-253-06 que la protection repose sur le chiffrement disque de l'hôte (hypothèse H-253-10 à créer), ou confirmer explicitement que les fichiers temporaires ne contiennent aucun secret au sens de l'INV (distinction "secret" vs "empreinte"). La qualification MAJEUR (non BLOQUANT) de la review est confirmée.


E-02 — CONFIRMÉ BLOQUANT : DOWNLOADED → EXPIRED vs état terminal

Textes sources :

  • Spec §5.4 : "DOWNLOADED (terminal fonctionnel) — DOWNLOADED -> * : INTERDITE (état terminal, résolution manuelle uniquement)"
  • Spec §5.4 transitions retour : "EXPIRED/FAILED/CANCELLED/DOWNLOADED -> état actif : INTERDITE (nouvelle demande d'export requise)"
  • Plan §2/ECT-05 décision : "État DOWNLOADED : accès URL signée toujours possible jusqu'à expiration TTL. À expiration : ExportExpiryScheduler passe DOWNLOADED → EXPIRED et purge l'objet S3."
  • Plan §4/CC-253-11 Scheduler : "toutes les 15 min, sélectionne les bulk_exports avec status = 'READY_FOR_DOWNLOAD' ET expires_at <= NOW(), appelle expireExport() pour chacun."

Analyse :

La contradiction est réelle et précise. Le plan §2/ECT-05 dit explicitement que l'ExportExpiryScheduler fait passer DOWNLOADED → EXPIRED. La spec §5.4 dit que DOWNLOADED → * : INTERDITE. Ce sont deux affirmations incompatibles sur le même comportement.

Cependant, la lecture du §4/CC-253-11 révèle une incohérence interne au plan lui-même : la description du scheduler dans CC-253-11 ne sélectionne que status = 'READY_FOR_DOWNLOAD' — pas DOWNLOADED. Il y a donc une contradiction entre §2/ECT-05 (qui promet DOWNLOADED → EXPIRED) et CC-253-11 (qui n'implémente le scheduler que sur READY_FOR_DOWNLOAD).

Conséquences : 1. Si le comportement ECT-05 est retenu (DOWNLOADED → EXPIRED autorisée) : la spec §5.4 doit être amendée pour retirer DOWNLOADED de la liste des états terminaux ou ajouter explicitement DOWNLOADED → EXPIRED comme transition autorisée. La machine à états dans CC-253-04 doit être mise à jour. Sans cet amendement, tout agent implementant CC-253-04 (isTerminal guard) bloquera la transition. 2. Si le comportement CC-253-11 est retenu (scheduler sur READY_FOR_DOWNLOAD uniquement) : §2/ECT-05 doit être corrigé, et la politique de purge des exports DOWNLOADED après TTL doit être explicitée (l'objet S3 est-il conservé indéfiniment ? purgé sans transition d'état ?).

La double incohérence (spec vs plan ET interne au plan) en fait un écart BLOQUANT car les agents d'implémentation recevront des instructions contradictoires sur le comportement du scheduler.

Verdict E-02 : CONFIRMÉ BLOQUANT. La qualification de la review Gate 5 est confirmée et renforcée : l'incohérence est non seulement spec vs plan, mais aussi interne au plan (ECT-05 vs CC-253-11). Une décision univoque doit être prise et propagée dans les deux documents avant ouverture de l'étape 6.


E-03 — CONFIRMÉ MAJEUR : S3 multipart 100 GB non contractualisé

Textes sources :

  • Spec §5.2 : "Taille max package : 100 GB", "H-253-02 : Le stockage staging supporte des objets jusqu'à 100 GB par export."
  • Plan §7.1 stack : "S3/OVH Object Storage : S3Service existant (src/modules/storage/s3.service.ts)"
  • Plan §4/CC-253-06 : "Upload S3 staging via S3Service.putObject(key, stream)."
  • Plan §6 H-253-02 : "Le stockage staging S3/OVH supporte des objets jusqu'à 100 GB par export. Limite à revoir ; risque non-conformité volumétrique sur gros coffres."
  • Plan §4/CC-253-11 (aucune mention du multipart dans ExportCleanupService).

Analyse :

AWS S3 et OVH Object Storage ont une limite de 5 GB par PutObject simple. Au-delà, l'upload doit utiliser l'API Multipart Upload (initiée par CreateMultipartUpload, puis UploadPart par tranches de 5–1000 MB, finalisée par CompleteMultipartUpload). Un putObject() de 100 GB échouerait silencieusement ou lèverait une erreur S3 EntityTooLarge au moment de l'upload.

Le plan assume S3Service.putObject(key, stream) sans vérifier si l'implémentation existante de S3Service utilise @aws-sdk/client-s3 avec Upload (multipart automatique) ou PutObjectCommand (limité à 5 GB). La distinction est critique : @aws-sdk/lib-storage gère le multipart automatiquement pour les streams de grande taille, PutObjectCommand ne le fait pas.

La spec H-253-02 reconnaît le risque mais le plan ne le résout pas : il reproduit l'hypothèse sans la vérifier ni prévoir d'extension si S3Service ne supporte pas le multipart.

Un export GLOBAL d'un utilisateur avec 1000 documents de 100 MB chacun atteint exactement 100 GB — cas tout à fait réaliste pour un coffre usage intensif. L'échec à l'upload se traduirait par STORAGE_UPLOAD_FAILED en état FAILED, contournant silencieusement INV-253-01 (exhaustivité).

Verdict E-03 : CONFIRMÉ MAJEUR. La qualification de la review Gate 5 est confirmée. L'action requise est : vérifier l'implémentation de S3Service.putObject() dans src/modules/storage/s3.service.ts. Si elle utilise PutObjectCommand (pas de multipart), documenter l'extension requise dans CC-253-11 (ou un CC dédié). Si elle utilise @aws-sdk/lib-storage/Upload, lever l'hypothèse H-253-02 par confirmation explicite et fermer l'écart.


3. Zones d'ombre supplémentaires

ZA-S1 — Cohérence index partiel PostgreSQL sur expires_at

Plan §3.1 :

CREATE INDEX idx_bulk_exports_expires_at ON vault_secure.bulk_exports (expires_at)
  WHERE status = 'READY_FOR_DOWNLOAD';

Si la décision E-02 retient DOWNLOADED → EXPIRED (scheduler couvre aussi DOWNLOADED), cet index partiel ne couvre pas les exports DOWNLOADED avec expires_at <= NOW(). Le scheduler CC-253-11 serait inefficace sur les exports DOWNLOADED sans index. Non bloquant à ce stade, mais dépendant de la décision E-02.

ZA-S2 — expiredAt : colonne présente, downloadedAt aussi, mais pas de colonne failedTimeoutAt

Plan §3.2 : la table contient failed_at pour FAILED mais pas de colonne distincte failed_timeout_at pour FAILED_TIMEOUT. L'entité TypeORM ne définit pas cette colonne. Conséquence : la distinction FAILED vs FAILED_TIMEOUT dans les métriques repose uniquement sur la colonne status (VARCHAR). Acceptable architecturalement, mais à noter pour le monitoring.

ZA-S3 — Reprise crash post-upload S3 partiel

Plan §4/CC-253-06 décrit purgeStaleTemp() au démarrage pour les fichiers locaux. Mais si le worker crash après un CreateMultipartUpload non finalisé, un objet S3 multipart incomplet persiste (coût de stockage + risque de confusion). Le plan ne prévoit pas de AbortMultipartUpload dans la logique de reprise. Non bloquant mais risque opérationnel à faible volumétrie.


4. Recommandation

Scores de confirmation

Écart original Qualification review Qualification second reviewer Delta
E-01 MAJEUR Confirmé CONFIRMÉ MAJEUR Stable
E-02 BLOQUANT Confirmé CONFIRMÉ BLOQUANT (renforcé : double incohérence interne) Renforcé
E-03 MAJEUR Confirmé CONFIRMÉ MAJEUR Stable

Recommandation finale : RESERVE

Les trois écarts de la review Gate 5 sont tous confirmés. La qualification de la review est correcte. La recommandation reste RESERVE avec les mêmes conditions de levée.

Conditions de levée pour GO Gate 5 (ordre de priorité) :

  1. (E-02 — BLOQUANT) Trancher univoquement : DOWNLOADED → EXPIRED autorisée ou non ?
  2. Option A : amender spec §5.4 pour ajouter DOWNLOADED → EXPIRED comme transition autorisée (retirer DOWNLOADED des états terminaux stricts ou créer une catégorie "terminal fonctionnel avec expiration"). Mettre à jour CC-253-04 (isTerminal guard), CC-253-11 (scheduler couvre READY_FOR_DOWNLOAD ET DOWNLOADED), index partiel §3.1.
  3. Option B : corriger plan §2/ECT-05 pour affirmer que DOWNLOADED ne transite jamais en EXPIRED ; préciser le sort de l'objet S3 (purgé après TTL sans transition d'état, ou conservé indéfiniment).

  4. (E-03 — MAJEUR) Vérifier src/modules/storage/s3.service.ts : utilise-t-il @aws-sdk/lib-storage/Upload (multipart automatique) ou PutObjectCommand (5 GB max) ?

  5. Si Upload : documenter la confirmation dans H-253-02 (fermer l'hypothèse ouverte).
  6. Si PutObjectCommand : documenter l'extension requise dans CC-253-06 ou CC-253-11 (utilisation de Upload pour les streams > 5 GB).

  7. (E-01 — MAJEUR) Préciser dans CC-253-06 le mécanisme de protection des temporaires :

  8. Option A : créer hypothèse H-253-10 "le disque hôte est chiffré (LUKS/équivalent) — protection des fichiers temporaires déléguée à l'infrastructure".
  9. Option B : confirmer explicitement dans CC-253-06 que les fichiers du répertoire /tmp/bulk-export-{exportId}/ ne contiennent aucun secret au sens de INV-253-11 (content.enc déjà chiffré, metadata.json contient des empreintes non réversibles, pas de DEK ni de clé en clair).

Les points ZA-S1, ZA-S2, ZA-S3 sont non bloquants et peuvent être adressés en note de bas de contrat ou en story de suivi.