| .. | ||
| background.js | ||
| manifest.json | ||
| native-host.json.in | ||
| README.md | ||
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
conserve les valeurs lors de close/restore, avec un autre windowId runtime.
Son exemple inclut les fenêtres restaurées dans
onCreated.undefinedet rejet sont deux résultats distincts ; aucun délai maximal d'extData n'est fourni. - onChanged signale des changements de liste fermée, sans provenance de fenêtre.
- Window.sessionId 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
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.
getRecentlyClosedfournit une liste bornée, pas une correspondance certaine entre ses éléments et chaque événementwindows.onCreated. Nous ne l'utilisons pas comme preuve, et n'inspectons aucun contenu d'onglet. - Événements SessionStore Gecko
documente
sessionstore-windows-restoredcomme notification interne de fin du startup. Ce n'est pas un événement de WebExtension standard. Les API privilégiées/Experiments ne constituent pas une voie AMO ordinaire pour ce candidat Firefox Release. - 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 :
{"op":"bootstrap-eligibility"}
Le host répond avec exactement deux champs, sans chemin ni contenu utilisateur :
{"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 :
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) :
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 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.