324 lines
20 KiB
Markdown
324 lines
20 KiB
Markdown
# Companion Firefox Session Restore V2
|
||
|
||
Candidat local **1.1.1 non signé**, même ID `session-v2@labfy.org`, même clé
|
||
`labfy.session.window_uuid`, mêmes permissions `sessions`, `nativeMessaging`.
|
||
La version signée 1.0.0 installée n'est pas modifiée par ces sources.
|
||
Aucun accès aux profils, au runtime utilisateur ou au socket Sway réel n'est
|
||
nécessaire pour les validations décrites ici. Aucun déploiement réalisé.
|
||
|
||
L'expérience visée est **installation seule, puis Firefox normal, Ctrl+N compris**.
|
||
Le bouton et le panneau provisoires de 1.1.0 sont supprimés. Le background ne
|
||
crée aucune fenêtre. Une fenêtre ambiguë peut rester sans identité et faire
|
||
échouer la capture ; aucune action quotidienne de remplacement n'est imposée.
|
||
|
||
## Preuves disponibles et limite fondamentale
|
||
|
||
La 1.0.0 pouvait attribuer après `undefined` et seulement 200 ms, donc écraser
|
||
une identité arrivée plus tard. L'incident historique (onglets conservés, UUID
|
||
modifié entre deux logins) est compatible avec cette course, sans en prouver la
|
||
cause précise. L'extension ne répare ni cet UUID perdu ni les checkpoints.
|
||
|
||
Sources primaires consultées pour cette révision :
|
||
|
||
- [getWindowValue](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sessions/getWindowValue)
|
||
conserve les valeurs lors de close/restore, avec un autre windowId runtime.
|
||
Son exemple inclut les fenêtres restaurées dans `onCreated`. `undefined` et
|
||
rejet sont deux résultats distincts ; aucun délai maximal d'extData n'est fourni.
|
||
- [onChanged](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sessions/onChanged)
|
||
signale des changements de liste fermée, sans provenance de fenêtre.
|
||
- [Window.sessionId](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/windows/Window)
|
||
identifie une fenêtre obtenue via l'API sessions. Sa présence est utilisée
|
||
uniquement pour protéger ; son absence ne prouve jamais une création neuve.
|
||
- [sessions.restore](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/sessions/restore)
|
||
renvoie la session restaurée lorsque l'extension appelle elle-même cette API.
|
||
Cela n'identifie pas directement une restauration effectuée par l'utilisateur.
|
||
`getRecentlyClosed` fournit une liste bornée, pas une correspondance certaine
|
||
entre ses éléments et chaque événement `windows.onCreated`. Nous ne l'utilisons
|
||
pas comme preuve, et n'inspectons aucun contenu d'onglet.
|
||
- [Événements SessionStore Gecko](https://firefox-source-docs.mozilla.org/browser/components/sessionstore/docs/events.html)
|
||
documente `sessionstore-windows-restored` comme notification interne de fin du
|
||
startup. Ce n'est pas un événement de WebExtension standard. Les
|
||
[API privilégiées/Experiments](https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html)
|
||
ne constituent pas une voie AMO ordinaire pour ce candidat Firefox Release.
|
||
- [runtime.onInstalled](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled)
|
||
distingue explicitement `install`, `update`, `browser_update`.
|
||
|
||
**Conclusion de conception :** les observations répétées et une période calme
|
||
réduisent le risque ; elles ne prouvent pas la fin de toutes les restaurations.
|
||
Une restauration manuelle/tardive sans signal exploitable, dont l'ancienne valeur
|
||
reste absente jusqu'après la dernière lecture, est indiscernable d'un Ctrl+N.
|
||
Elle peut recevoir un nouvel UUID dans ce compromis automatique. Une valeur
|
||
ancienne arrivant entre la dernière lecture et l'écriture peut encore être
|
||
écrasée : `setWindowValue` n'offre pas de compare-and-set. Les contrôles après
|
||
écriture ne suppriment pas ce risque. **Aucune garantie absolue de classification
|
||
ou d'absence d'écrasement de données jamais encore observées n'est revendiquée.**
|
||
Garantir cela pour toutes les restaurations exigerait une preuve supplémentaire
|
||
fournie par Firefox ; refuser tout mint sur `onCreated` supprimerait l'auto Ctrl+N.
|
||
La 1.1.1 retient l'automatisme demandé avec refus des ambiguïtés observables.
|
||
|
||
## Initialisation : BOOT, READY, PROTECTED
|
||
|
||
Les listeners sont enregistrés avant tout appel asynchrone. Le premier `getAll`,
|
||
sans populate, capture les IDs de la baseline. Une fenêtre émise **avant la
|
||
résolution** de cette photographie reste ambiguë, même si `getAll` la contient.
|
||
Un événement reçu ensuite, y compris pendant BOOT, devient candidat potentiel :
|
||
`type` normal ou absent, sans `sessionId` positif. Sa présence dans
|
||
`getAll({windowTypes:["normal"]})` sera exigée avant toute écriture. Les types
|
||
explicitement différents de normal sont ignorés.
|
||
|
||
**Auto-baseline sans clic, sous attestation native.** L'événement
|
||
`onInstalled(reason=install)` est mémorisé qu'il précède ou suive le premier
|
||
`getAll`. Il autorise seulement une tentative de lot après stabilisation.
|
||
`update`, `browser_update` et restart ne l'autorisent jamais. Avant **chaque**
|
||
écriture de baseline, l'extension envoie exactement :
|
||
|
||
```json
|
||
{"op":"bootstrap-eligibility"}
|
||
```
|
||
|
||
Le host répond avec exactement deux champs, sans chemin ni contenu utilisateur :
|
||
|
||
```json
|
||
{"result":"OK","baseline_enrollment_allowed":true}
|
||
```
|
||
|
||
`true` signifie que le checkpoint automatique V2 canonique
|
||
`automatic/last.json` est absent **et** que `sessions` est absent ou vide
|
||
(aucune sauvegarde nommée ni entrée inattendue), à l'instant du contrôle, sous l'autorité
|
||
`XDG_STATE_HOME` ou son fallback HOME habituel. Le chemin est commun au store et
|
||
au host ; l'extension ne le reçoit pas. Le parcours utilise des descripteurs de
|
||
répertoires et `NOFOLLOW`, sans créer le store, ouvrir le checkpoint ou suivre un
|
||
lien. Toute présence, même fichier corrompu, illisible, lien ou FIFO, refuse.
|
||
Le host examine les deux branches même si l'une est absente. `sessions` est
|
||
énuméré par `os.listdir(dirfd)` sans ouvrir ses entrées ; tout fichier, lien,
|
||
FIFO ou sous-répertoire refuse. Le chemin est limité à 4096 octets ; un
|
||
répertoire `sessions` dont la taille de métadonnées dépasse 64 Kio refuse
|
||
avant énumération. Ce budget conservateur ne certifie aucune absence.
|
||
Une erreur, un parent inaccessible, un namespace non privé ou un chemin invalide
|
||
refuse aussi. Le host revalide le socket et l'UID du pair Sway par `GET_VERSION`,
|
||
en lecture seule, sans demander l'arbre des fenêtres pour cette opération.
|
||
|
||
Seules les fenêtres de la photographie initiale encore sans valeur, sans
|
||
identité déjà observée ni tentative d'écriture et sans signal protecteur sont
|
||
éligibles. Deux fenêtres initiales sans UUID reçoivent ainsi automatiquement
|
||
deux UUID distincts si le host atteste l'absence à chaque contrôle et si les
|
||
relectures Firefox restent stables. Les fenêtres signalées avant la photographie
|
||
ou avec `sessionId` positif restent protégées. Après réponse positive : relecture
|
||
intégrale du lot, révision inchangée et dernière absence exigées avant écriture.
|
||
|
||
**Une installation seule ne garantit donc pas l'enrôlement.** Host indisponible,
|
||
réponse non exacte, refus, déconnexion ou timeout de 5 secondes : aucune
|
||
attribution de baseline concernée ; elle reste en quarantaine. Une activité
|
||
pendant le lot révoque la tentative. Aucun retry d'autorisation baseline dans
|
||
cette instance, même après reconnexion ou nouvel événement install. Si le
|
||
checkpoint apparaît entre deux écritures, la seconde est refusée ; aucune
|
||
annulation de l'UUID déjà écrit. Les futurs Ctrl+N, y compris ceux reçus pendant
|
||
BOOT après photographie, conservent leur automatisme indépendamment du refus.
|
||
|
||
Cette attestation concerne les checkpoints V2 automatiques et nommés à leurs
|
||
emplacements canoniques. Elle ne prouve ni l'absence historique de sauvegardes,
|
||
ni l'absence de sauvegardes déplacées, ni la provenance des restaurations tardives
|
||
Firefox. Elle n'est ni un verrou
|
||
persistant, ni un CAS ACID, ni un observer sessionstore ; un checkpoint peut
|
||
apparaître après la dernière réponse. Aucun checkpoint ou navigateur n'est
|
||
modifié par cette observation native.
|
||
|
||
BOOT attend au moins 30 secondes depuis le chargement, 2 secondes sans activité
|
||
et l'expiration du guard de 5 secondes suivant `sessions.onChanged`. Ensuite la
|
||
phase passe READY même si des anciennes fenêtres restent ambiguës. Celles-ci
|
||
bloquent **la capture exacte**, mais ne condamnent plus les futurs Ctrl+N, y
|
||
compris après 60 secondes et après fermeture de l'ancienne fenêtre. PROTECTED
|
||
reste un refus opérationnel, notamment si la photographie initiale échoue ; un
|
||
timeout API exige un redémarrage. Ces délais sont des budgets, pas une barrière
|
||
sessionstore ni une preuve de nouveauté.
|
||
|
||
Un Ctrl+N à 10 secondes, après photographie initiale, reste en attente puis reçoit
|
||
automatiquement son UUID après stabilisation et observation. Une restauration
|
||
sans signal dans ce même intervalle est indiscernable : le timing ne la rend pas
|
||
« prouvée nouvelle ». Une identité reçue pendant l'attente est préservée.
|
||
|
||
## Fonctionnement automatique après READY
|
||
|
||
Un passage périodique unique par seconde observe le lot complet. Chaque absence
|
||
candidate doit persister durant 2 secondes de lectures espacées de 250 ms. Dans
|
||
le cas normal, l'UUID apparaît environ 2 à 3 secondes après Ctrl+N, sans action sur
|
||
l'extension. Les anciennes fenêtres ambiguës restent exclues, sauf la baseline initiale
|
||
explicitement admissible sous l'attestation décrite ci-dessus.
|
||
|
||
Avant chaque écriture : relecture intégrale, ensemble inchangé, identités connues
|
||
inchangées, UUID v4 valide, absence de collisions visibles, révision inchangée.
|
||
Deux Ctrl+N simultanés ou espacés d'une seconde reçoivent des identités distinctes
|
||
après réobservation du lot stable. L'ordre numérique des IDs sert seulement à
|
||
sérialiser les opérations. Aucune unicité sur toutes les fenêtres fermées de
|
||
l'histoire n'est prouvée ; la génération repose sur `crypto.randomUUID`.
|
||
|
||
`sessions.onChanged` peut signaler une simple fermeture d'onglet. Il invalide le
|
||
passage en lecture et diffère les candidats pendant le guard, sans les mettre
|
||
en quarantaine définitive. Une modification de révision ou d'ensemble avant
|
||
écriture abandonne le passage ; les mêmes candidats sont réobservés après calme.
|
||
Les rejets de lecture, identités invalides ou divergentes et écritures incertaines
|
||
restent des refus conservateurs. La présence de `sessionId` ou un événement avant
|
||
la photographie initiale protège individuellement la fenêtre concernée.
|
||
|
||
Une valeur observée, même dans un lot partiellement rejeté ou invalide, ne peut
|
||
jamais devenir une autorisation de mint. Changement,
|
||
disparition ou collision refuse le lot. Aucune suppression de valeur et aucun
|
||
rollback d'identité. Une seule tentative d'écriture par fenêtre/instance ; un
|
||
rejet peut cacher un effet appliqué et n'est jamais réessayé. Vérification par
|
||
relecture avant de libérer l'opération. Un restart perd toute autorité de mint en
|
||
attente et ne conserve comme vérité que les valeurs de session.
|
||
|
||
Ni onglets, ni URL, ni titres, ni géométrie, ni ordre, ni PID n'identifient une
|
||
fenêtre. Aucune lecture/écriture de tab data ; déplacer des onglets n'entraîne
|
||
aucun transfert d'identité ni fusion de fenêtres.
|
||
|
||
## Capture native et bornes
|
||
|
||
La capture native reste **non-mutante pour l'identité et les onglets**. Elle relit
|
||
un lot incomplet pendant 2 secondes puis refuse sans publication. Elle peut
|
||
récupérer une valeur arrivée plus tard lors d'une nouvelle capture. Lot complet
|
||
revalidé avant publication et UUID relu avant chaque message. Aucun protocole
|
||
atomique avec Firefox ou transaction native de lot n'est prétendu.
|
||
Le préfixe `titlePreface` éphémère de corrélation, déjà présent dans 1.1.0, reste
|
||
la seule mutation de présentation ; nettoyage en `finally` et au démarrage.
|
||
Si l'appel de marquage expire puis se règle tardivement, un seul effacement de
|
||
secours est lancé après son règlement, sans relancer le marquage. Si l'appel ne
|
||
se règle jamais, ou si l'effacement bloque/rejette, le préfixe peut rester visible
|
||
jusqu'au redémarrage/nettoyage réussi. Aucune API WebExtension ne permet d'assurer
|
||
une borne de disparition face à une API définitivement bloquée ; le préfixe ne
|
||
peut donc pas être qualifié d'inconditionnellement éphémère.
|
||
Les titres eux-mêmes ne sont jamais lus.
|
||
|
||
Une seule opération active (initialisation/nettoyage, auto-mint ou capture) ;
|
||
captures concurrentes coalescées sans queue, sans accumuler des opérations.
|
||
Une demande native en vol ; timeout 5 secondes ; reconnexion au plus une fois par
|
||
seconde, sans map en idle. Le backend existant garde son timeout de 6 secondes :
|
||
un checkpoint trop tôt ou durant un mint peut échouer, il ne reçoit pas de faux
|
||
succès ni d'identité inventée par la capture.
|
||
|
||
Chaque appel Firefox a une borne de 2 secondes. Un timeout arrête les appels
|
||
Firefox ordinaires de cette instance jusqu'au redémarrage (seul l'effacement
|
||
de secours après règlement du marquage peut encore être tenté) ; les promesses ne sont pas
|
||
annulables et un effet tardif reste possible. Les lectures sœurs sont toutes
|
||
attendues même si l'une rejette. Limite locale conservée : 256 fenêtres.
|
||
Les ensembles en mémoire sont bornés aux fenêtres suivies et nettoyés sur close.
|
||
Le provider Python exact, le format et le contenu des checkpoints et les
|
||
applications SwayFX restent inchangés. Le host ajoute uniquement l’opération
|
||
d’observation bootstrap au protocole existant. Une capture refusée ne publie pas un message d'erreur incompatible :
|
||
le provider existant échoue faute du lot demandé. Le diagnostic facultatif
|
||
`runtime.sendMessage({op: "identity-status"})`, réservé à cette extension, retourne
|
||
phase, résultat WAIT/READY/REFUSED, dernier motif et compteurs ; il n'attribue rien.
|
||
Le résultat READY exige la phase READY, aucune erreur, aucune quarantaine et
|
||
aucun candidat en attente. La phase READY seule peut coexister avec un résultat
|
||
REFUSED pour ambiguïté historique. `prefixCleanupPending` expose un effacement
|
||
de secours non confirmé. Aucun panneau ni clic quotidien n'est ajouté.
|
||
|
||
## Validation et build local
|
||
|
||
Depuis la racine de cette copie :
|
||
|
||
```sh
|
||
node --test tests/firefox_session_v2/*.test.js
|
||
node --check firefox-extension/session-v2/background.js
|
||
python3 -B -m unittest discover -s tests/session_v2 -p 'test_*.py'
|
||
python3 -B tools/build-session-v2.py --extension-only
|
||
sha256sum build/session-v2/labfy-session-v2-unsigned.xpi
|
||
```
|
||
|
||
Le XPI déterministe ne contient que `manifest.json` et `background.js`.
|
||
`extension-manifest.json` indique SHA-256, `signed: false`, `installed: false`.
|
||
Deux builds locaux sont comparés octet pour octet. Le build complet historique
|
||
sans `--extension-only` reste disponible ; aucun helper n'est installé ici.
|
||
|
||
Lint ESLint 9.39.1 épinglé, outil isolé dans la copie (pas le validateur AMO) :
|
||
|
||
```sh
|
||
mkdir -p build/session-v2
|
||
touch build/session-v2/npm-user.conf build/session-v2/npm-global.conf
|
||
npm --userconfig "$PWD/build/session-v2/npm-user.conf" \
|
||
--globalconfig "$PWD/build/session-v2/npm-global.conf" \
|
||
exec --cache ./build/session-v2-npm-cache --yes --package eslint@9.39.1 -- \
|
||
eslint --no-config-lookup --no-ignore \
|
||
--rule 'no-unused-vars:error' --rule 'no-undef:error' \
|
||
--global browser --global crypto --global performance \
|
||
--global setTimeout --global clearTimeout --global setInterval \
|
||
firefox-extension/session-v2/background.js
|
||
```
|
||
|
||
Les tests Node exécutent le vrai background dans une VM : APIs WebExtension,
|
||
ports natifs et temps synthétiques. Des getters interdits détectent toute lecture
|
||
d'URL, titre, onglets, dimensions ou API tabs, même si le background attrape
|
||
l'exception. Ils couvrent installation avant/après start, Ctrl+N, boot ambigu,
|
||
extData tardives, restauration signalée, absence de signal (limite exposée),
|
||
erreurs et timeout, collisions, close, déplacements d'onglets, restart/update,
|
||
concurrence et reconnexion. Les 46 tests Node présents dans la copie initiale sont conservés avec leur
|
||
comportement protecteur lorsque le host n'atteste pas l'absence. Le nouveau P0
|
||
échoue contre le background pré-protocole conservé en preuve, puis passe contre
|
||
le correctif. La suite compte **58 tests Node** et **63 tests Python** (les
|
||
43 backend préexistants et 20 tests bootstrap). Les nouveaux tests Python passent par
|
||
le framing stdio réel du host avec HOME/XDG privés et un faux socket Sway
|
||
possédé, y compris aller-retour bootstrap/map/bootstrap, checkpoint présent,
|
||
valide, corrompu ou inaccessible, sauvegarde nommée seule sans `last.json`,
|
||
répertoires vides, entrées inattendues dans `sessions`, liens, FIFO, permissions,
|
||
erreur d'énumération, enveloppes invalides et allowlist.
|
||
Les scénarios VM ajoutés couvrent les deux ordres install/start, le refus,
|
||
l'expiration, les réponses non exactes, la capture concurrente, le rollback,
|
||
la relecture avant écriture, le checkpoint apparu entre écritures et les
|
||
reconnexions sans retry baseline. Le risque de restauration indiscernable est
|
||
exposé par un test ; il ne constitue pas une preuve de classification parfaite.
|
||
|
||
Cette validation ne prouve pas la persistance physique au logout/login ni le
|
||
comportement réel de sessionstore. Aucun Firefox réel n'a été lancé.
|
||
Le minimum de manifeste reste Firefox 157. Le déploiement ultérieur de 1.1.1
|
||
avec le même ID demanderait une signature Mozilla et une installation autorisée
|
||
séparément ; aucune signature, publication AMO ni installation n'a lieu ici.
|
||
|
||
## Native host
|
||
|
||
Après déploiement Stow, rendre le template `native-host.json.in` en remplaçant
|
||
`@ABSOLUTE_NATIVE_HOST_LAUNCHER@` par le chemin absolu de
|
||
`$HOME/.config/quickshell/labfy-sway/session-v2-native-host.py`.
|
||
Conserver le nom du host, le type `stdio` et l’allowlist exacte de l’extension.
|
||
Le wrapper doit rester exécutable.
|
||
|
||
Sur l’installation Linux validée, le manifeste est
|
||
`${XDG_CONFIG_HOME:-$HOME/.config}/mozilla/native-messaging-hosts/org.labfy.session_v2_identity.json`.
|
||
Le chemin historique `$HOME/.mozilla` pointe vers cette arborescence. Sur un
|
||
autre packaging Firefox, vérifier le répertoire Native Messaging utilisé avant
|
||
installation. Créer le manifeste avec permissions 0600 ; ne pas écraser un
|
||
host actif lors d’une simple mise à jour de documentation.
|
||
|
||
Le host accepte `map-window` avec UUID et jeton runtime, ainsi que
|
||
`bootstrap-eligibility` sans autre champ. Les deux schémas sont fermés.
|
||
Les messages sont bornés à 4096 octets ; la sortie standard est réservée au
|
||
framing Native Messaging. Le host lit l’arbre Sway uniquement pour corréler le préfixe
|
||
éphémère, sans exécuter de commande Sway mutatrice. L’extension retire ce
|
||
préfixe après corrélation. Le provider exige des observations récentes et un
|
||
publisher vivant ; une identité absente ou ambiguë produit un échec explicite.
|
||
|
||
Les UUID persistants appartiennent aux fenêtres Firefox ; les observations et
|
||
identifiants de processus restent privés dans le runtime. Ne pas publier ces
|
||
fichiers. Le provider demeure le candidat validé pour le profil supporté ; le
|
||
multi-profil n’est pas pris en charge. Les fenêtres et onglets sont restaurés
|
||
par Firefox, pas par une lecture de sessionstore côté Labfy.
|
||
|
||
Voir [Session Restore V2](../../docs/session-restore-v2.md) pour le cycle complet,
|
||
les tests et les limites.
|
||
|
||
## Verdict du correctif local
|
||
|
||
**SOURCE_TESTED — AUTO_BOOTSTRAP_CONDITIONNEL** : dans la VM, première
|
||
installation avec deux fenêtres sans UUID et absence attestée de checkpoint :
|
||
deux identités stables attribuées automatiquement. Réinstallation avec checkpoint
|
||
présent, contrôle inaccessible ou erreur : refus conservateur. Les Ctrl+N restent
|
||
automatiques. Les preuves, empreintes, logs et patch non commité sont conservés
|
||
dans `build/bootstrap-evidence/` de cette copie privée.
|
||
|
||
**Firefox Release non installé et non testé ici.** Aucun PASS de login réel,
|
||
de persistance physique ou de déploiement n'est revendiqué. Le XPI 1.1.1 reste
|
||
non signé ; signature AMO et installation autorisée seront nécessaires
|
||
ultérieurement. La provenance d'une restauration tardive sans signal reste
|
||
inconnue ; une ancienne valeur jamais encore visible peut toujours arriver entre
|
||
la dernière lecture et l'écriture. Aucune garantie absolue de CAS ou de protection
|
||
d'une identité jamais observée n'est annoncée. Aucune réparation d'UUID perdu,
|
||
réécriture de checkpoint ou modification du PC réel n'a été effectuée.
|