PD-248 — Décomposition multi-agents¶
Vue d'ensemble¶
| Attribut | Valeur |
|---|---|
| Story | PD-248 — Protection screenshot native iOS |
| Branche | feature/PD-248-screenshot-protection |
| Tâches | 6 |
| Agents | 3 (agent-sre, agent-developer, agent-qa) |
| Complexité | Low-Medium |
Séquencement des tâches¶
TASK-1 (agent-sre)
↓
TASK-2 (agent-developer)
├→ TASK-3 (agent-developer)
└→ TASK-4 (agent-developer)
↓
TASK-5 (agent-qa)
↓
TASK-6 (agent-qa-e2e)
Détail des tâches¶
TASK-1 : Installation expo-screen-capture¶
| Attribut | Valeur |
|---|---|
| Agent | agent-sre |
| Contract | N/A |
| Invariants | INV-248-07 |
| Fichiers | package.json, app.json |
| Dépendances | Aucune |
Instructions : 1. Installer expo-screen-capture via npx expo install expo-screen-capture 2. Vérifier que le plugin est ajouté dans app.json si nécessaire 3. Pas de dépendance native additionnelle (INV-248-07)
Livrable :
TASK-2 : Hook useScreenshotProtection¶
| Attribut | Valeur |
|---|---|
| Agent | agent-developer |
| Contract | CC-248-01 |
| Invariants | INV-248-01, INV-248-02, INV-248-03, INV-248-04, INV-248-08 |
| Fichiers | src/hooks/useScreenshotProtection.ts |
| Dépendances | TASK-1 |
Instructions : 1. Créer le hook avec signature export function useScreenshotProtection(): void 2. Utiliser useFocusEffect de @react-navigation/native 3. Appeler preventScreenCapture() sur focus 4. Appeler allowScreenCapture() sur blur (vers écran non sensible) 5. Maintenir protection sur transition sensible→sensible (INV-248-03) 6. Aucune UI visible (INV-248-04) 7. Activation < 10ms (INV-248-08)
Livrable :
// src/hooks/useScreenshotProtection.ts
import { useCallback } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import * as ScreenCapture from 'expo-screen-capture';
export function useScreenshotProtection(): void {
useFocusEffect(
useCallback(() => {
// Activation immédiate (< 10ms)
ScreenCapture.preventScreenCaptureAsync();
return () => {
// Désactivation sur blur
ScreenCapture.allowScreenCaptureAsync();
};
}, [])
);
}
TASK-3 : Intégration écrans sensibles¶
| Attribut | Valeur |
|---|---|
| Agent | agent-developer |
| Contract | CC-248-02 |
| Invariants | INV-248-02, INV-248-05 |
| Fichiers | 4 fichiers écrans |
| Dépendances | TASK-2 |
Fichiers cibles : - src/screens/recovery/MnemonicDisplay.tsx (SCR-248-01) - src/screens/settings/MFASetupScreen.tsx (SCR-248-02) - src/components/mfa/TOTPCodeDisplay.tsx (SCR-248-03) - src/screens/settings/APIKeyDisplay.tsx (SCR-248-04)
Instructions : 1. Ajouter import { useScreenshotProtection } from '@/hooks/useScreenshotProtection'; 2. Appeler useScreenshotProtection(); en première ligne du composant 3. Pas de configuration, pas de props 4. Si APIKeyDisplay.tsx n'existe pas, le créer (HYP-248-01)
Pattern :
export function MnemonicDisplay() {
useScreenshotProtection(); // OBLIGATOIRE - première ligne
// ... reste du composant
}
TASK-4 : Service de logging¶
| Attribut | Valeur |
|---|---|
| Agent | agent-developer |
| Contract | CC-248-03 |
| Invariants | INV-248-04 |
| Fichiers | src/services/screenshotProtection.ts |
| Dépendances | TASK-2 |
Instructions : 1. Créer l'interface ScreenshotProtectionLogger 2. Implémenter les 4 méthodes de logging (ERR-248-001..004) 3. Logging technique uniquement (console, Sentry) — pas d'UI 4. Pas d'alerte utilisateur (INV-248-04)
Livrable :
// src/services/screenshotProtection.ts
type ScreenId = 'SCR-248-01' | 'SCR-248-02' | 'SCR-248-03' | 'SCR-248-04';
interface ScreenshotProtectionLogger {
logActivationError(screen: ScreenId, error: Error): void;
logMaintainError(from: ScreenId, to: ScreenId, error: Error): void;
logUnregisteredComponent(component: string): void;
logIncompatibleEnvironment(): void;
}
export const screenshotProtectionLogger: ScreenshotProtectionLogger = {
logActivationError(screen, error) {
console.error(`[ERR-248-001] Screenshot protection activation failed`, {
screen,
error: error.message,
});
},
logMaintainError(from, to, error) {
console.error(`[ERR-248-002] Screenshot protection maintain failed`, {
from,
to,
error: error.message,
});
},
logUnregisteredComponent(component) {
console.warn(`[ERR-248-003] Unregistered sensitive component`, { component });
},
logIncompatibleEnvironment() {
console.warn(`[ERR-248-004] Incompatible environment for screenshot protection`);
},
};
TASK-5 : Tests unitaires¶
| Attribut | Valeur |
|---|---|
| Agent | agent-qa-unit-integration |
| Contract | CC-248-04 |
| Tests | TC-248-NOM-01..07, TC-248-SIL-01 |
| Fichiers | src/__tests__/hooks/useScreenshotProtection.test.ts |
| Dépendances | TASK-2, TASK-3, TASK-4 |
Instructions : 1. Mocker expo-screen-capture 2. Mocker useFocusEffect de React Navigation 3. Tester activation sur focus 4. Tester désactivation sur blur 5. Tester maintien en transition sensible→sensible 6. Vérifier absence d'UI (INV-248-04) 7. Tester temps d'activation < 10ms
Livrable :
// src/__tests__/hooks/useScreenshotProtection.test.ts
import { renderHook } from '@testing-library/react-native';
import { useScreenshotProtection } from '@/hooks/useScreenshotProtection';
import * as ScreenCapture from 'expo-screen-capture';
jest.mock('expo-screen-capture');
jest.mock('@react-navigation/native', () => ({
useFocusEffect: (callback: () => void) => callback(),
}));
describe('useScreenshotProtection', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call preventScreenCapture on focus', () => {
renderHook(() => useScreenshotProtection());
expect(ScreenCapture.preventScreenCaptureAsync).toHaveBeenCalled();
});
it('should not show any UI alert', () => {
// Verify no Alert, Toast, or Modal is triggered
const alertSpy = jest.spyOn(require('react-native').Alert, 'alert');
renderHook(() => useScreenshotProtection());
expect(alertSpy).not.toHaveBeenCalled();
});
// ... autres tests
});
TASK-6 : Tests E2E avec OCR¶
| Attribut | Valeur |
|---|---|
| Agent | agent-qa-e2e |
| Contract | CC-248-05 |
| Tests | CA-248-01..08, TC-248-PERF-01 |
| Fichiers | src/__tests__/e2e/screenshot-protection.e2e.ts |
| Dépendances | TASK-5 |
| Invariants | INV-248-06 (note Gate 5) |
Note Gate 5 (RESERVE) : Ajouter stratégie robustesse OCR
Instructions : 1. Capturer screenshot de chaque écran sensible (SCR-248-01..04) 2. Exécuter OCR Tesseract sur les captures 3. Vérifier absence des données sensibles 4. Test de performance (< 10ms activation, p95/max sur 100 itérations) 5. Valider en build EAS production (INV-248-06)
Stratégie robustesse OCR : - Seuil de confiance OCR : 70% - Retry : 2 tentatives max - Timeout : 5s par capture - Artefacts CI : conserver screenshots pour debug
Livrable :
// src/__tests__/e2e/screenshot-protection.e2e.ts
import { execSync } from 'child_process';
describe('Screenshot protection E2E', () => {
const OCR_CONFIDENCE_THRESHOLD = 0.7;
const MAX_RETRIES = 2;
async function captureAndOCR(screenName: string): Promise<string> {
// Capture via xcrun simctl
const screenshotPath = `/tmp/${screenName}-screenshot.png`;
execSync(`xcrun simctl io booted screenshot ${screenshotPath}`);
// OCR via Tesseract
const ocrResult = execSync(`tesseract ${screenshotPath} stdout --oem 1`);
return ocrResult.toString();
}
it('should block screenshot on MnemonicDisplay (CA-248-01)', async () => {
// Navigate to MnemonicDisplay
// Take screenshot
const ocrText = await captureAndOCR('mnemonic-display');
// Verify no mnemonic word detected
const mnemonicWords = ['abandon', 'ability', /* ... BIP-39 words */];
mnemonicWords.forEach(word => {
expect(ocrText.toLowerCase()).not.toContain(word);
});
});
// ... tests pour SCR-248-02, 03, 04
});
Mapping récapitulatif¶
| Tâche | Agent | Contract | Fichiers | INV couverts |
|---|---|---|---|---|
| TASK-1 | agent-sre | - | 2 | INV-248-07 |
| TASK-2 | agent-developer | CC-248-01 | 1 | INV-248-01,02,03,04,08 |
| TASK-3 | agent-developer | CC-248-02 | 4 | INV-248-02,05 |
| TASK-4 | agent-developer | CC-248-03 | 1 | INV-248-04 |
| TASK-5 | agent-qa | CC-248-04 | 1 | - |
| TASK-6 | agent-qa-e2e | CC-248-05 | 1 | INV-248-06 |
Total : 6 tâches, 10 fichiers
Références¶
- Plan : PD-248-plan.md
- Code Contracts : code-contracts.yaml
- Verdict Gate 5 : PD-248-verdict-step5-v1.yaml