fix(license): send a derived machine identifier, never the OS machine id (#350) #366

Merged
maximus merged 2 commits from issue-350-hash-machine-id into main 2026-10-02 01:25:09 +00:00
Owner

Refs #350

Revue du 2026-10-01 : APPROVE sur e5662ad, cinq suggestions appliquées dans 0a9c9f7 (docs, deux lignes de test, vérification manuelle ci-dessous). 0a9c9f7 ne change aucun comportement et n'a pas été relue.

Ce que la PR change

Rust (license_commands.rs)

  • Dérivation. derive_machine_id calcule HMAC-SHA256 de l'identifiant du système sous la clé figée com.simpl.resultat/license-machine-id/v1, rendu en 64 caractères hexadécimaux minuscules. Un identifiant vide est refusé (Internal), jamais haché. MachineIds porte les deux formes de l'identifiant de ce poste, la dérivée et la brute.
  • Activation. activate_and_store_in dérive juste avant l'appel, envoie le dérivé et exige que le jeton reçu le porte ; sinon InvalidResponse, et rien n'est écrit.
  • D1. Un jeton valide dont le claim vaut l'identifiant brut de ce poste se lit toujours « Activée ». Expiré, d'un autre poste sous l'une ou l'autre forme, ou signé par une autre clé : refusé, comme avant.
  • D3. list_activated_machines pose is_this_machine sur chaque ligne, pour le dérivé comme pour le brut. La valeur n'est jamais lue dans la réponse du serveur.
  • Désactivation. drop_token_of_deactivated_machine_in supprime le jeton local quand il représentait la place libérée.
  • ActivationStatus ne porte plus que is_activated. get_machine_id est retirée (commande et enregistrement dans lib.rs).

Front

  • licenseService.ts : getMachineId retirée, MachineInfo.is_this_machine ajouté, ActivationStatus sans identifiant.
  • LicenseCard.tsx : le marqueur « Cette machine » lit m.is_this_machine.

Docs : ADR 0020 (dérivation figée, D1 à D3, condition de retrait de la compatibilité), architecture.md, guide (« Ce que l'activation envoie ») et docs intégrées FR/EN, CHANGELOG FR/EN sous Sécurité.

Aucune dépendance ajoutée : hmac et sha2 étaient déjà déclarés, Cargo.toml et Cargo.lock sont intacts.

Écarts assumés par rapport au corps de l'issue

  • La règle de suppression du jeton couvre plus que la phrase de l'issue. L'issue disait : supprimé si l'identifiant désactivé est le dérivé de ce poste, ou celui du jeton stocké. Appliquée telle quelle, elle laissait sur le disque un jeton expiré ou illisible quand on désactive l'ancienne ligne brute de ce poste, là où le code d'avant le supprimait. La règle livrée : toute ligne de ce poste emporte le jeton, sauf l'ancienne ligne brute quand le jeton vaut pour la ligne dérivée, restée active ; la ligne d'un autre poste ne l'emporte que s'il a été émis pour elle. Les quatre cas du tableau de l'issue sont tenus. Relevé par le plan checker.
  • La condition de retrait de la compatibilité n'est plus celle de l'issue. L'issue comptait deux ans depuis la dernière version qui envoyait l'identifiant brut. Une v0.15.0 gratuite ne se met plus à jour et peut encore activer en brut des mois plus tard : l'ADR compte depuis la dernière émission d'un jeton brut, que seul le serveur connaît. Relevé par la revue.
  • activate_and_store_in reçoit l'identifiant brut, pas un MachineIds. La dérivation et la garde sur l'identifiant vide sont ainsi dans la fonction testée, et « aucun appel » est observé contre un mock, pas déduit. Relevé par le plan checker.
  • La liste et la désactivation n'échouent plus quand l'identifiant du système est illisible. Aucune ligne n'est alors marquée, et le jeton ne part que s'il a été émis pour la ligne désactivée. Avant, la désactivation renvoyait Internal après que le serveur avait déjà libéré la place.
  • L'état d'activation se lit « Non activée » au lieu d'échouer quand l'identifiant du système est illisible ou vide. « Activer » répond alors « Erreur interne de l'application », sans rien envoyer. Avant, un identifiant vide partait au serveur, qui répondait invalid_input.
  • store_activation_token, sans appelant côté front et laissée hors périmètre, refuse désormais un jeton émis pour l'identifiant brut : elle passe par la même fonction que l'activation.
  • Un conseil de plus dans les docs intégrées, plutôt qu'une phrase ajoutée au conseil sur l'activation, déjà long. Le guide gagne la puce correspondante.

Preuves

Suites sur 0a9c9f7 : cargo test 151 → 168 ; vitest 1311 → 1314 (68 fichiers) ; npm run build (tsc + vite) vert ; cargo check sans avertissement, avec et sans les tests.

Vecteurs de référence recalculés hors du code (Python hmac, puis openssl par le plan checker et par la revue) : identiques à ceux de l'issue et de maximus/maximus-api#82.

Mutations de l'auteur, 17 sur 17 rouges, mesurées sur e5662ad. Chaque mutation a été vérifiée comme appliquée (git diff non vide), puis le fichier restauré depuis HEAD et l'arbre vérifié propre. Rust : 71 tests du module ; front : 21 tests du fichier de rendu.

Mutation Rouges
l'activation envoie l'identifiant brut 1 / 71
MachineIds garde le brut à la place du dérivé 10 / 71
la forme brute ne désigne plus ce poste (D1 et D3 annulés) 6 / 71
un jeton tout juste émis peut porter le brut 2 / 71
la liste est renvoyée sans marquage 1 / 71
l'ancienne ligne brute emporte toujours le jeton 1 / 71
une ligne de ce poste n'emporte plus le jeton 2 / 71
la ligne d'un autre poste n'emporte jamais le jeton 2 / 71
la ligne d'un autre poste emporte toujours le jeton 3 / 71
un identifiant vide est haché 2 / 71
clé de dérivation modifiée 10 / 71
identifiant haché sans être rogné 1 / 71
is_this_machine non sérialisé vers le webview 1 / 71
un jeton expiré se lit « Activée » 7 / 71
marqueur « Cette machine » inversé 3 / 21
marqueur suivant l'état d'activation 3 / 21
marqueur jamais affiché 3 / 21

La première ligne est le point de décision demandé par l'issue : le seul mock déclaré exige le dérivé dans machine_id et refuse tout corps où l'identifiant du système apparaît, dans quelque champ que ce soit.

Mutation de la revue restée verte, maintenant rouge : « un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste ». Remesurée sur le fichier de e5662ad : 0 rouge sur 71. Sur 0a9c9f7, avec les deux lignes ajoutées au tableau de test : 1 rouge sur 71. Fichier restauré, 71 verts, arbre propre.

Ce que les tests ne gardent pas. Le câblage des commandes à l'identifiant réel du poste (local_machine_ids() et raw_machine_id(), qui appellent machine_uid::get()) : les tests n'appellent jamais cette lecture, comme le demande l'issue. Passer None à la liste ou à la désactivation laisse la suite verte, ce que la revue a confirmé. La vérification manuelle ci-dessous couvre l'état, la liste et l'activation ; rien ne couvre la suppression du jeton à la désactivation au-delà de son point 3.

Relevé en route, hors périmètre. architecture.md annonce 36 commandes (35 après cette PR) ; generate_handler! en enregistre 42 (41 après). L'écart est antérieur : six commandes ne sont pas listées (fetch_price, send_feedback, get_feedback_user_agent, ensure_backup_dir, get_file_size, file_exists).

Vérification manuelle (d'où Refs, sans mot-clé de fermeture)

À faire avant le tag de la release. La dérivation devient définitive à la première activation en production, et ce contrôle est le seul qui tienne le câblage de l'activation à l'identifiant réel du poste. Il demande l'application réelle, un poste activé par la v0.15.0 et le serveur de licences de production. Il se fait avec un build de main après le merge (npm run tauri dev, même dossier de données) ou avec le build de la release, avant de la publier.

  • 1. Sur un poste activé par la v0.15.0, lancer cette version : la carte Licence affiche toujours « Activée », la liste marque ce poste « Cette machine », et les cours du Bilan répondent (Premium).
  • 2. Lancer la commande ci-dessous. Attendu : 32 brut: True dérivé: False. C'est aussi la preuve, sur un vrai jeton, que la v0.15.0 envoyait bien l'identifiant du système tel quel.
  • 3. « Désactiver » ce poste puis « Activer » : la carte revient à « Activée », la nouvelle ligne est marquée « Cette machine », et la commande rend 64 brut: False dérivé: True. Une longueur de 64 ne suffit pas : une double dérivation ou une autre clé la donnent aussi, avec dérivé: False.
  • 4. #350 est fermée à la main par Max une fois les points 1 à 3 faits.

Commande des points 2 et 3, sous Linux. Elle compare le claim du jeton à l'identifiant du système et à son dérivé, recalculé hors de l'application, et n'affiche qu'une longueur et deux booléens. Essayée sur trois jetons synthétiques (brut, dérivé, double dérivation) par la revue puis par l'auteur.

python3 - <<'EOF'
import base64, hashlib, hmac, json, os
raw = next(open(p).read().strip() for p in ("/var/lib/dbus/machine-id", "/etc/machine-id") if os.path.exists(p))
p = open(os.path.expanduser("~/.local/share/com.simpl.resultat/activation.token")).read().split(".")[1]
claim = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["machine_id"]
want = hmac.new(b"com.simpl.resultat/license-machine-id/v1", raw.encode(), hashlib.sha256).hexdigest()
print(len(claim), "brut:", claim == raw, "dérivé:", claim == want)
EOF

Sous Windows, le jeton est dans %APPDATA%\com.simpl.resultat\activation.token et l'identifiant du système dans HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid ; l'équivalent de la commande reste à écrire si le point 3 se fait sous Windows.

🤖 Generated with Claude Code

Refs #350 > Revue du 2026-10-01 : APPROVE sur `e5662ad`, cinq suggestions appliquées dans `0a9c9f7` (docs, deux lignes de test, vérification manuelle ci-dessous). `0a9c9f7` ne change aucun comportement et n'a pas été relue. ## Ce que la PR change **Rust (`license_commands.rs`)** - **Dérivation.** `derive_machine_id` calcule `HMAC-SHA256` de l'identifiant du système sous la clé figée `com.simpl.resultat/license-machine-id/v1`, rendu en 64 caractères hexadécimaux minuscules. Un identifiant vide est refusé (`Internal`), jamais haché. `MachineIds` porte les deux formes de l'identifiant de ce poste, la dérivée et la brute. - **Activation.** `activate_and_store_in` dérive juste avant l'appel, envoie le dérivé et exige que le jeton reçu le porte ; sinon `InvalidResponse`, et rien n'est écrit. - **D1.** Un jeton valide dont le claim vaut l'identifiant brut de ce poste se lit toujours « Activée ». Expiré, d'un autre poste sous l'une ou l'autre forme, ou signé par une autre clé : refusé, comme avant. - **D3.** `list_activated_machines` pose `is_this_machine` sur chaque ligne, pour le dérivé comme pour le brut. La valeur n'est jamais lue dans la réponse du serveur. - **Désactivation.** `drop_token_of_deactivated_machine_in` supprime le jeton local quand il représentait la place libérée. - `ActivationStatus` ne porte plus que `is_activated`. `get_machine_id` est retirée (commande et enregistrement dans `lib.rs`). **Front** - `licenseService.ts` : `getMachineId` retirée, `MachineInfo.is_this_machine` ajouté, `ActivationStatus` sans identifiant. - `LicenseCard.tsx` : le marqueur « Cette machine » lit `m.is_this_machine`. **Docs** : ADR 0020 (dérivation figée, D1 à D3, condition de retrait de la compatibilité), `architecture.md`, guide (« Ce que l'activation envoie ») et docs intégrées FR/EN, CHANGELOG FR/EN sous Sécurité. Aucune dépendance ajoutée : `hmac` et `sha2` étaient déjà déclarés, `Cargo.toml` et `Cargo.lock` sont intacts. ## Écarts assumés par rapport au corps de l'issue - **La règle de suppression du jeton couvre plus que la phrase de l'issue.** L'issue disait : supprimé si l'identifiant désactivé est le dérivé de ce poste, ou celui du jeton stocké. Appliquée telle quelle, elle laissait sur le disque un jeton expiré ou illisible quand on désactive l'ancienne ligne brute de ce poste, là où le code d'avant le supprimait. La règle livrée : toute ligne de ce poste emporte le jeton, sauf l'ancienne ligne brute quand le jeton vaut pour la ligne dérivée, restée active ; la ligne d'un autre poste ne l'emporte que s'il a été émis pour elle. Les quatre cas du tableau de l'issue sont tenus. Relevé par le plan checker. - **La condition de retrait de la compatibilité n'est plus celle de l'issue.** L'issue comptait deux ans depuis la dernière version qui envoyait l'identifiant brut. Une v0.15.0 gratuite ne se met plus à jour et peut encore activer en brut des mois plus tard : l'ADR compte depuis la dernière émission d'un jeton brut, que seul le serveur connaît. Relevé par la revue. - **`activate_and_store_in` reçoit l'identifiant brut, pas un `MachineIds`.** La dérivation et la garde sur l'identifiant vide sont ainsi dans la fonction testée, et « aucun appel » est observé contre un mock, pas déduit. Relevé par le plan checker. - **La liste et la désactivation n'échouent plus quand l'identifiant du système est illisible.** Aucune ligne n'est alors marquée, et le jeton ne part que s'il a été émis pour la ligne désactivée. Avant, la désactivation renvoyait `Internal` après que le serveur avait déjà libéré la place. - **L'état d'activation se lit « Non activée » au lieu d'échouer** quand l'identifiant du système est illisible ou vide. « Activer » répond alors « Erreur interne de l'application », sans rien envoyer. Avant, un identifiant vide partait au serveur, qui répondait `invalid_input`. - **`store_activation_token`**, sans appelant côté front et laissée hors périmètre, refuse désormais un jeton émis pour l'identifiant brut : elle passe par la même fonction que l'activation. - **Un conseil de plus dans les docs intégrées**, plutôt qu'une phrase ajoutée au conseil sur l'activation, déjà long. Le guide gagne la puce correspondante. ## Preuves **Suites sur `0a9c9f7`** : `cargo test` 151 → **168** ; vitest 1311 → **1314** (68 fichiers) ; `npm run build` (tsc + vite) vert ; `cargo check` sans avertissement, avec et sans les tests. **Vecteurs de référence** recalculés hors du code (Python `hmac`, puis `openssl` par le plan checker et par la revue) : identiques à ceux de l'issue et de maximus/maximus-api#82. **Mutations de l'auteur, 17 sur 17 rouges**, mesurées sur `e5662ad`. Chaque mutation a été vérifiée comme appliquée (`git diff` non vide), puis le fichier restauré depuis `HEAD` et l'arbre vérifié propre. Rust : 71 tests du module ; front : 21 tests du fichier de rendu. | Mutation | Rouges | |---|---| | l'activation envoie l'identifiant brut | 1 / 71 | | `MachineIds` garde le brut à la place du dérivé | 10 / 71 | | la forme brute ne désigne plus ce poste (D1 et D3 annulés) | 6 / 71 | | un jeton tout juste émis peut porter le brut | 2 / 71 | | la liste est renvoyée sans marquage | 1 / 71 | | l'ancienne ligne brute emporte toujours le jeton | 1 / 71 | | une ligne de ce poste n'emporte plus le jeton | 2 / 71 | | la ligne d'un autre poste n'emporte jamais le jeton | 2 / 71 | | la ligne d'un autre poste emporte toujours le jeton | 3 / 71 | | un identifiant vide est haché | 2 / 71 | | clé de dérivation modifiée | 10 / 71 | | identifiant haché sans être rogné | 1 / 71 | | `is_this_machine` non sérialisé vers le webview | 1 / 71 | | un jeton expiré se lit « Activée » | 7 / 71 | | marqueur « Cette machine » inversé | 3 / 21 | | marqueur suivant l'état d'activation | 3 / 21 | | marqueur jamais affiché | 3 / 21 | La première ligne est le point de décision demandé par l'issue : le seul mock déclaré exige le dérivé dans `machine_id` et refuse tout corps où l'identifiant du système apparaît, dans quelque champ que ce soit. **Mutation de la revue restée verte, maintenant rouge** : « un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste ». Remesurée sur le fichier de `e5662ad` : 0 rouge sur 71. Sur `0a9c9f7`, avec les deux lignes ajoutées au tableau de test : **1 rouge sur 71**. Fichier restauré, 71 verts, arbre propre. **Ce que les tests ne gardent pas.** Le câblage des commandes à l'identifiant réel du poste (`local_machine_ids()` et `raw_machine_id()`, qui appellent `machine_uid::get()`) : les tests n'appellent jamais cette lecture, comme le demande l'issue. Passer `None` à la liste ou à la désactivation laisse la suite verte, ce que la revue a confirmé. La vérification manuelle ci-dessous couvre l'état, la liste et l'activation ; rien ne couvre la suppression du jeton à la désactivation au-delà de son point 3. **Relevé en route, hors périmètre.** `architecture.md` annonce 36 commandes (35 après cette PR) ; `generate_handler!` en enregistre 42 (41 après). L'écart est antérieur : six commandes ne sont pas listées (`fetch_price`, `send_feedback`, `get_feedback_user_agent`, `ensure_backup_dir`, `get_file_size`, `file_exists`). ## Vérification manuelle (d'où `Refs`, sans mot-clé de fermeture) **À faire avant le tag de la release.** La dérivation devient définitive à la première activation en production, et ce contrôle est le seul qui tienne le câblage de l'activation à l'identifiant réel du poste. Il demande l'application réelle, un poste activé par la v0.15.0 et le serveur de licences de production. Il se fait avec un build de `main` après le merge (`npm run tauri dev`, même dossier de données) ou avec le build de la release, avant de la publier. - [ ] 1. Sur un poste activé par la v0.15.0, lancer cette version : la carte Licence affiche toujours « Activée », la liste marque ce poste « Cette machine », et les cours du Bilan répondent (Premium). - [ ] 2. Lancer la commande ci-dessous. Attendu : `32 brut: True dérivé: False`. C'est aussi la preuve, sur un vrai jeton, que la v0.15.0 envoyait bien l'identifiant du système tel quel. - [ ] 3. « Désactiver » ce poste puis « Activer » : la carte revient à « Activée », la nouvelle ligne est marquée « Cette machine », et la commande rend `64 brut: False dérivé: True`. Une longueur de 64 ne suffit pas : une double dérivation ou une autre clé la donnent aussi, avec `dérivé: False`. - [ ] 4. #350 est fermée à la main par Max une fois les points 1 à 3 faits. Commande des points 2 et 3, sous Linux. Elle compare le claim du jeton à l'identifiant du système et à son dérivé, recalculé hors de l'application, et n'affiche qu'une longueur et deux booléens. Essayée sur trois jetons synthétiques (brut, dérivé, double dérivation) par la revue puis par l'auteur. ```bash python3 - <<'EOF' import base64, hashlib, hmac, json, os raw = next(open(p).read().strip() for p in ("/var/lib/dbus/machine-id", "/etc/machine-id") if os.path.exists(p)) p = open(os.path.expanduser("~/.local/share/com.simpl.resultat/activation.token")).read().split(".")[1] claim = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["machine_id"] want = hmac.new(b"com.simpl.resultat/license-machine-id/v1", raw.encode(), hashlib.sha256).hexdigest() print(len(claim), "brut:", claim == raw, "dérivé:", claim == want) EOF ``` Sous Windows, le jeton est dans `%APPDATA%\com.simpl.resultat\activation.token` et l'identifiant du système dans `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` ; l'équivalent de la commande reste à écrire si le point 3 se fait sous Windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-10-01 23:58:26 +00:00
fix(license): send a derived machine identifier, never the OS machine id (#350)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
PR Check — Rust / rust (pull_request) Successful in 9m19s
e5662ad018
Activation sent `machine_uid::get()` as is: /etc/machine-id on Linux,
MachineGuid on Windows. machine-id(5) calls that id confidential and asks
applications to send a keyed hash under a fixed, application-specific key
instead. The license server stores what it receives next to the license
email and the host name, lists it back and signs it into the activation
token.

- derive_machine_id: HMAC-SHA256 under a frozen key, 64 lowercase hex
  characters, pinned by two reference vectors. An empty OS id is refused,
  never hashed.
- Activation sends the derived identifier and requires the issued token to
  carry it. Nothing else originates a machine identifier.
- A machine activated before this change stays activated: a valid token
  whose claim is the raw id of this machine still reads "Activated" (D1).
- The machine list flags `is_this_machine` in Rust, for a row under either
  identifier (D3). `ActivationStatus` no longer carries an identifier and
  the unused `get_machine_id` command is removed: no command hands the OS
  id to the webview any more.
- After a deactivation the local token is dropped when it stood for the
  freed slot: any row of this machine takes it, except the old raw row
  while the token stands for the derived one.
- ADR 0020 records the derivation, the transition and the condition for
  removing the raw-id compatibility.

Behaviour changes beyond the issue text: the machine list and a
deactivation no longer fail on an unreadable OS id (best effort), the
activation status then reads "not activated" instead of failing, and the
unused `store_activation_token` command refuses a token issued for the
raw id.

No new dependency (hmac and sha2 were already declared); Cargo.lock is
untouched. Rust 151 -> 168 tests, vitest 1311 -> 1314.

Refs #350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

CI verte sur e5662ad, logs lus et pas seulement le statut :

  • check-frontend (run 441) : tsc && vite build vert, 68 fichiers, 1314 tests.
  • check-rust (run 442) : Suppression guard: 2 checks, exit 0, cargo test 168 verts. cargo audit relève 666 crates et 12 warnings autorisés, comme sur main : Cargo.lock n'a pas bougé.

Les deux runs ont bien extrait e5662ad (pull/366/head). La tête n'a pas encore été relue.

CI verte sur `e5662ad`, logs lus et pas seulement le statut : - **`check-frontend`** (run 441) : `tsc && vite build` vert, 68 fichiers, **1314** tests. - **`check-rust`** (run 442) : `Suppression guard: 2 checks, exit 0`, `cargo test` **168** verts. `cargo audit` relève 666 crates et 12 warnings autorisés, comme sur `main` : `Cargo.lock` n'a pas bougé. Les deux runs ont bien extrait `e5662ad` (`pull/366/head`). La tête n'a pas encore été relue.
Author
Owner

Revue adversariale — APPROVE

Verdict : APPROVE. Aucun point bloquant. Cinq suggestions ; la première porte sur la vérification manuelle qui reste à faire avant la fermeture à la main de #350.

Résumé. La PR fait ce que dit son titre : l'activation envoie HMAC-SHA256 de l'identifiant du système et plus cet identifiant, une machine activée par la v0.15.0 reste reconnue dans le jeton comme dans la liste, et la suppression du jeton à la désactivation est une règle écrite et testée cas par cas. Les écarts au corps de l'issue sont déclarés, et celui qui change une règle est gardé par un test (mutation 4 du tableau).

Ce qui a tourné

Sur une copie hors dépôt de e5662ad (git archive, dossier target neuf). Le dépôt n'a pas été touché.

  • cargo test --all-targets --locked : 168 verts, sans avertissement. --locked passe : Cargo.lock est intact et cohérent avec Cargo.toml.
  • npm run build vert ; vitest 1314 verts sur 68 fichiers.
  • CI de la tête, lue dans les logs : les runs 441 et 442 ont extrait e5662ad ; 1314 et 168 verts, Suppression guard: 2 checks, exit 0.
  • Vecteurs de référence recalculés hors du code (Python hmac, puis openssl dgst -hmac) : identiques aux deux littéraux.
  • Sonde ajoutée à la copie puis retirée, sur un poste Linux réel : local_machine_ids() rend le contenu rogné du fichier machine-id et le dérivé que donne la recette hors Rust. Comparaison booléenne, aucun identifiant affiché.

Mutations du relecteur

22 mutations Rust (71 tests du module) et 5 mutations front (21 tests du fichier de rendu). Chacune a été vérifiée comme appliquée (git diff --stat), puis le fichier restauré et l'arbre vérifié propre ; la suite est verte à la fin. Celles marquées « PR » rejouent une mutation de la description et rendent le même chiffre.

Mutation Rouges
1. l'activation envoie l'identifiant brut (PR) 1 / 71
2. un jeton tout juste émis peut porter le brut (PR) 2 / 71
3. la forme brute ne désigne plus ce poste (PR) 6 / 71
4. règle de suppression écrite comme la phrase de l'issue : dérivé de ce poste, ou identifiant du jeton stocké 1 / 71
5. machine_id porte le dérivé, mais le brut part dans un autre champ du corps 1 / 71
6. la liste ne marque que la ligne dérivée 2 / 71
7. le drapeau lu dans la réponse du serveur est conservé 1 / 71
8. un jeton expiré « vaut » encore pour sa ligne à la désactivation 1 / 71
9. tout jeton valide survit à la désactivation de la ligne brute 1 / 71
10. l'exception protège la ligne dérivée au lieu de la brute 1 / 71
11. la ligne d'un autre poste emporte tout jeton valide 2 / 71
12. le jeton reçu est écrit avant d'être vérifié 2 / 71
13. l'activation écrit la réponse sans vérifier son claim 1 / 71
14. le jeton est écrit sans être rogné 1 / 71
15. l'état d'activation accepte tout jeton signé, quel que soit le poste 2 / 71
16. MachineIds garde le brut sans le rogner 1 / 71
17. identifiant haché sans être rogné (PR) 1 / 71
18. dérivé rendu en hexadécimal majuscule 10 / 71
19. skip_deserializing remplacé par default, la réécriture du drapeau conservée verte
20. list_activated_machines_impl passe None verte
21. deactivate_machine_impl passe None verte
22. un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste verte
front : marqueur inversé (PR) 3 / 21
front : marqueur suivant l'état d'activation (PR) 3 / 21
front : marqueur jamais affiché (PR) 3 / 21
front : marqueur posé sur la première ligne dès qu'une ligne est marquée 2 / 21
front : marqueur affiché une seule fois par liste 1 / 21

Les quatre vertes :

  • 19 n'est pas un défaut. flag_this_machine réécrit le drapeau de chaque ligne, donc l'attribut est une seconde barrière ; lever les deux (mutation 7) rougit.
  • 20 et 21 confirment ce que la description annonce pour la liste, et l'étendent à la désactivation : le câblage des commandes à local_machine_ids() n'est gardé par aucun test. Le point 1 de la vérification manuelle couvre la liste et l'état. Rien à changer ici.
  • 22 est un cas que la règle livrée tranche et qu'aucun test ne tient : voir la suggestion 3.

La mutation 5 isole le match_request du test de décision : le corps satisfait le PartialJson, seul le refus de l'identifiant du système « dans quelque champ que ce soit » le fait tomber. La mutation 4 montre que l'écart déclaré sur la règle de suppression est bien gardé.

Vérifié à la source, hors de ce dépôt

  • maximus-api, commit déployé 3673d09. machine_id est validé par z.string().min(1) (borné à 255 sur main), stocké dans une colonne text, et le claim du jeton reprend input.machine_id tel que reçu. /v1/prices ne lit pas ce claim. 64 caractères passent, et l'exigence « le jeton reçu porte le dérivé » tient contre la production.
  • machine-uid 0.5.4, la version du Cargo.lock. La valeur rendue est déjà rognée sous Linux (read_file) et sous Windows (id.trim()). Le trim de MachineIds::from_raw ne change donc rien à ce que la v0.15.0 envoyait : la comparaison de D1 tient à l'octet près.
  • Lignes ajoutées relues (dépôt public) : ni courriel, ni adresse IP, ni jeton. Les seuls identifiants sont les deux vecteurs synthétiques.

Fermeture de l'issue

La timeline de #350 porte un pull_ref venant de cette PR avec ref_action: "none", et le message du commit dit Refs. Le merge laissera #350 ouverte, ce qui est voulu : la vérification manuelle reste due.

Suggestions

1. Vérification manuelle : comparer la valeur, pas la longueur. Le point 3 attend « 64 ». Une double dérivation, une autre clé ou un identifiant lu ailleurs donnent aussi 64. Or le câblage de activate_machine_impl à la lecture réelle de l'identifiant (raw_machine_id()) n'a que ce contrôle manuel pour lui, et la dérivation devient définitive à la première activation en production. La commande ci-dessous affiche une longueur et deux booléens, aucun identifiant. Essayée sur trois jetons synthétiques : brut 32 brut: True dérivé: False, dérivé 64 brut: False dérivé: True, double dérivation 64 brut: False dérivé: False.

python3 - <<'EOF'
import base64, hashlib, hmac, json, os
raw = next(open(p).read().strip() for p in ("/var/lib/dbus/machine-id", "/etc/machine-id") if os.path.exists(p))
p = open(os.path.expanduser("~/.local/share/com.simpl.resultat/activation.token")).read().split(".")[1]
claim = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["machine_id"]
want = hmac.new(b"com.simpl.resultat/license-machine-id/v1", raw.encode(), hashlib.sha256).hexdigest()
print(len(claim), "brut:", claim == raw, "dérivé:", claim == want)
EOF

Attendu : au point 2, 32 brut: True dérivé: False ; au point 3, 64 brut: False dérivé: True. La première ligne, sur un vrai jeton de la v0.15.0, est aussi la preuve de la prémisse de D1 que les tests ne peuvent pas donner. L'équivalent Windows reste à écrire si le point 3 se fait sous Windows.

2. « Plus aucune commande ne remet l'identifiant du système au webview » est plus large que le code. Pour une activation antérieure à la dérivation, list_activated_machines rend la ligne de ce poste avec machine_id égal à l'identifiant du système et is_this_machine: true. C'est le jeu de données même de the_webview_gets_the_flag_and_no_identifier_of_its_own (license_commands.rs:1685), et le commentaire du champ machine_id, dans la même interface de licenseService.ts, le dit. La phrase est vraie d'un poste activé sous le dérivé. Trois endroits à borner, comme D2 borne le critère réseau : ADR 0020 §2 (ligne 43), docs/architecture.md (paragraphe « Identifiant machine »), src/services/licenseService.ts:44-46. Formulation possible : « Plus aucune commande ne lit l'identifiant du système pour le remettre au webview. La liste du serveur porte encore celui d'une ligne antérieure à la dérivation, jusqu'à la réactivation. »

3. Deux lignes à ajouter au tableau de deactivation_drops_the_token_that_stood_for_the_freed_slot. La règle livrée dit que toute ligne de ce poste emporte le jeton. Pour un jeton valide émis pour un autre poste (données restaurées d'un autre ordinateur, le cas de #360), rien ne le tient : la mutation 22 reste verte. (OTHER_RAW, THIS_RAW, false, …) et (OTHER_DERIVED, THIS_DERIVED, false, …) épinglent le comportement actuel, qui est aussi celui d'avant la PR. La ligne correspondante manque au tableau du §4 de l'ADR.

4. ADR 0020, condition de retrait : compter depuis la dernière activation brute, pas depuis la dernière version. « Deux ans depuis la v0.15.0, plus aucun jeton brut n'est alors valide » ne tient pas. Une v0.15.0 en édition Gratuite ne se met plus à jour ; son utilisateur peut acheter une clé dans un an, la saisir et cliquer « Activer » avant toute mise à jour. Le serveur signe alors un jeton brut neuf, valable deux ans. La borne exacte est la dernière émission d'un jeton à claim brut, que seul le serveur connaît : aujourd'hui le last_seen_at des lignes de 32 ou 36 caractères, que seules l'activation et son rafraîchissement écrivent. Après la piste 2 de maximus/maximus-api#82 (hachage à l'arrivée, claim gardé tel qu'envoyé), les lignes ne le diront plus : la date devra être consignée à l'arrivée. La phrase vient du corps de l'issue, l'ADR la reprend fidèlement.

5. ADR 0020 §5 : deux précisions.

  • La seconde ligne. Quand une ligne brute de ce poste est encore active et que le jeton local manque ou n'est plus valide (expiration à deux ans, dossier de données réinitialisé, autre clé puis retour à la première), « Activer » envoie le dérivé. Le serveur n'a aucune ligne active sous cet identifiant : c'est une activation nouvelle, là où la v0.15.0 obtenait un rafraîchissement. Elle prend une place de plus, ou rend machine_limit_reached si la licence est pleine. Le code s'en sort bien : les deux lignes sont marquées « Cette machine », et désactiver l'ancienne garde le jeton. Le §5 ne nomme pas ce reste, alors que le rejet de la « coupure nette » s'appuie sur la même machine fantôme. À consigner, avec ce qui l'éteint : le rehachage en place des lignes actives (piste 1 de maximus/maximus-api#82), fait avant l'expiration des premiers jetons. Les points d'attention de cette issue décrivent le cas miroir, une ancienne version après le rehachage, pas celui-ci.
  • Ce que la clé publique ne couvre pas. Le dérivé ne permet pas de retrouver le brut. Un tiers qui détient déjà l'identifiant brut d'un poste peut en revanche recalculer son dérivé, puisque la clé est publique. La protection porte sur la remontée du dérivé vers le brut, pas sur ce rapprochement. Une phrase sous « La clé n'est pas un secret » rendrait le modèle de menace exact.

Confirmé au passage, hors périmètre

  • generate_handler! enregistre 41 commandes sur cette tête, architecture.md en annonce 35 : le décompte de la description est exact, et l'écart est antérieur à la PR.
## Revue adversariale — APPROVE **Verdict : APPROVE.** Aucun point bloquant. Cinq suggestions ; la première porte sur la vérification manuelle qui reste à faire avant la fermeture à la main de #350. **Résumé.** La PR fait ce que dit son titre : l'activation envoie `HMAC-SHA256` de l'identifiant du système et plus cet identifiant, une machine activée par la v0.15.0 reste reconnue dans le jeton comme dans la liste, et la suppression du jeton à la désactivation est une règle écrite et testée cas par cas. Les écarts au corps de l'issue sont déclarés, et celui qui change une règle est gardé par un test (mutation 4 du tableau). ### Ce qui a tourné Sur une copie hors dépôt de `e5662ad` (`git archive`, dossier `target` neuf). Le dépôt n'a pas été touché. - `cargo test --all-targets --locked` : **168** verts, sans avertissement. `--locked` passe : `Cargo.lock` est intact et cohérent avec `Cargo.toml`. - `npm run build` vert ; vitest **1314** verts sur 68 fichiers. - CI de la tête, lue dans les logs : les runs 441 et 442 ont extrait `e5662ad` ; 1314 et 168 verts, `Suppression guard: 2 checks, exit 0`. - Vecteurs de référence recalculés hors du code (Python `hmac`, puis `openssl dgst -hmac`) : identiques aux deux littéraux. - Sonde ajoutée à la copie puis retirée, sur un poste Linux réel : `local_machine_ids()` rend le contenu rogné du fichier `machine-id` et le dérivé que donne la recette hors Rust. Comparaison booléenne, aucun identifiant affiché. ### Mutations du relecteur 22 mutations Rust (71 tests du module) et 5 mutations front (21 tests du fichier de rendu). Chacune a été vérifiée comme appliquée (`git diff --stat`), puis le fichier restauré et l'arbre vérifié propre ; la suite est verte à la fin. Celles marquées « PR » rejouent une mutation de la description et rendent le même chiffre. | Mutation | Rouges | |---|---| | 1. l'activation envoie l'identifiant brut (PR) | 1 / 71 | | 2. un jeton tout juste émis peut porter le brut (PR) | 2 / 71 | | 3. la forme brute ne désigne plus ce poste (PR) | 6 / 71 | | 4. règle de suppression écrite comme la phrase de l'issue : dérivé de ce poste, ou identifiant du jeton stocké | 1 / 71 | | 5. `machine_id` porte le dérivé, mais le brut part dans un autre champ du corps | 1 / 71 | | 6. la liste ne marque que la ligne dérivée | 2 / 71 | | 7. le drapeau lu dans la réponse du serveur est conservé | 1 / 71 | | 8. un jeton expiré « vaut » encore pour sa ligne à la désactivation | 1 / 71 | | 9. tout jeton valide survit à la désactivation de la ligne brute | 1 / 71 | | 10. l'exception protège la ligne dérivée au lieu de la brute | 1 / 71 | | 11. la ligne d'un autre poste emporte tout jeton valide | 2 / 71 | | 12. le jeton reçu est écrit avant d'être vérifié | 2 / 71 | | 13. l'activation écrit la réponse sans vérifier son claim | 1 / 71 | | 14. le jeton est écrit sans être rogné | 1 / 71 | | 15. l'état d'activation accepte tout jeton signé, quel que soit le poste | 2 / 71 | | 16. `MachineIds` garde le brut sans le rogner | 1 / 71 | | 17. identifiant haché sans être rogné (PR) | 1 / 71 | | 18. dérivé rendu en hexadécimal majuscule | 10 / 71 | | 19. `skip_deserializing` remplacé par `default`, la réécriture du drapeau conservée | **verte** | | 20. `list_activated_machines_impl` passe `None` | **verte** | | 21. `deactivate_machine_impl` passe `None` | **verte** | | 22. un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste | **verte** | | front : marqueur inversé (PR) | 3 / 21 | | front : marqueur suivant l'état d'activation (PR) | 3 / 21 | | front : marqueur jamais affiché (PR) | 3 / 21 | | front : marqueur posé sur la première ligne dès qu'une ligne est marquée | 2 / 21 | | front : marqueur affiché une seule fois par liste | 1 / 21 | Les quatre vertes : - **19** n'est pas un défaut. `flag_this_machine` réécrit le drapeau de chaque ligne, donc l'attribut est une seconde barrière ; lever les deux (mutation 7) rougit. - **20 et 21** confirment ce que la description annonce pour la liste, et l'étendent à la désactivation : le câblage des commandes à `local_machine_ids()` n'est gardé par aucun test. Le point 1 de la vérification manuelle couvre la liste et l'état. Rien à changer ici. - **22** est un cas que la règle livrée tranche et qu'aucun test ne tient : voir la suggestion 3. La mutation 5 isole le `match_request` du test de décision : le corps satisfait le `PartialJson`, seul le refus de l'identifiant du système « dans quelque champ que ce soit » le fait tomber. La mutation 4 montre que l'écart déclaré sur la règle de suppression est bien gardé. ### Vérifié à la source, hors de ce dépôt - **maximus-api, commit déployé `3673d09`.** `machine_id` est validé par `z.string().min(1)` (borné à 255 sur `main`), stocké dans une colonne `text`, et le claim du jeton reprend `input.machine_id` tel que reçu. `/v1/prices` ne lit pas ce claim. 64 caractères passent, et l'exigence « le jeton reçu porte le dérivé » tient contre la production. - **`machine-uid` 0.5.4**, la version du `Cargo.lock`. La valeur rendue est déjà rognée sous Linux (`read_file`) et sous Windows (`id.trim()`). Le `trim` de `MachineIds::from_raw` ne change donc rien à ce que la v0.15.0 envoyait : la comparaison de D1 tient à l'octet près. - **Lignes ajoutées relues** (dépôt public) : ni courriel, ni adresse IP, ni jeton. Les seuls identifiants sont les deux vecteurs synthétiques. ### Fermeture de l'issue La timeline de #350 porte un `pull_ref` venant de cette PR avec `ref_action: "none"`, et le message du commit dit `Refs`. Le merge laissera #350 ouverte, ce qui est voulu : la vérification manuelle reste due. ### Suggestions **1. Vérification manuelle : comparer la valeur, pas la longueur.** Le point 3 attend « 64 ». Une double dérivation, une autre clé ou un identifiant lu ailleurs donnent aussi 64. Or le câblage de `activate_machine_impl` à la lecture réelle de l'identifiant (`raw_machine_id()`) n'a que ce contrôle manuel pour lui, et la dérivation devient définitive à la première activation en production. La commande ci-dessous affiche une longueur et deux booléens, aucun identifiant. Essayée sur trois jetons synthétiques : brut `32 brut: True dérivé: False`, dérivé `64 brut: False dérivé: True`, double dérivation `64 brut: False dérivé: False`. ```bash python3 - <<'EOF' import base64, hashlib, hmac, json, os raw = next(open(p).read().strip() for p in ("/var/lib/dbus/machine-id", "/etc/machine-id") if os.path.exists(p)) p = open(os.path.expanduser("~/.local/share/com.simpl.resultat/activation.token")).read().split(".")[1] claim = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))["machine_id"] want = hmac.new(b"com.simpl.resultat/license-machine-id/v1", raw.encode(), hashlib.sha256).hexdigest() print(len(claim), "brut:", claim == raw, "dérivé:", claim == want) EOF ``` Attendu : au point 2, `32 brut: True dérivé: False` ; au point 3, `64 brut: False dérivé: True`. La première ligne, sur un vrai jeton de la v0.15.0, est aussi la preuve de la prémisse de D1 que les tests ne peuvent pas donner. L'équivalent Windows reste à écrire si le point 3 se fait sous Windows. **2. « Plus aucune commande ne remet l'identifiant du système au webview » est plus large que le code.** Pour une activation antérieure à la dérivation, `list_activated_machines` rend la ligne de ce poste avec `machine_id` égal à l'identifiant du système et `is_this_machine: true`. C'est le jeu de données même de `the_webview_gets_the_flag_and_no_identifier_of_its_own` (`license_commands.rs:1685`), et le commentaire du champ `machine_id`, dans la même interface de `licenseService.ts`, le dit. La phrase est vraie d'un poste activé sous le dérivé. Trois endroits à borner, comme D2 borne le critère réseau : ADR 0020 §2 (ligne 43), `docs/architecture.md` (paragraphe « Identifiant machine »), `src/services/licenseService.ts:44-46`. Formulation possible : « Plus aucune commande ne lit l'identifiant du système pour le remettre au webview. La liste du serveur porte encore celui d'une ligne antérieure à la dérivation, jusqu'à la réactivation. » **3. Deux lignes à ajouter au tableau de `deactivation_drops_the_token_that_stood_for_the_freed_slot`.** La règle livrée dit que toute ligne de ce poste emporte le jeton. Pour un jeton valide émis pour un autre poste (données restaurées d'un autre ordinateur, le cas de #360), rien ne le tient : la mutation 22 reste verte. `(OTHER_RAW, THIS_RAW, false, …)` et `(OTHER_DERIVED, THIS_DERIVED, false, …)` épinglent le comportement actuel, qui est aussi celui d'avant la PR. La ligne correspondante manque au tableau du §4 de l'ADR. **4. ADR 0020, condition de retrait : compter depuis la dernière activation brute, pas depuis la dernière version.** « Deux ans depuis la v0.15.0, plus aucun jeton brut n'est alors valide » ne tient pas. Une v0.15.0 en édition Gratuite ne se met plus à jour ; son utilisateur peut acheter une clé dans un an, la saisir et cliquer « Activer » avant toute mise à jour. Le serveur signe alors un jeton brut neuf, valable deux ans. La borne exacte est la dernière émission d'un jeton à claim brut, que seul le serveur connaît : aujourd'hui le `last_seen_at` des lignes de 32 ou 36 caractères, que seules l'activation et son rafraîchissement écrivent. Après la piste 2 de maximus/maximus-api#82 (hachage à l'arrivée, claim gardé tel qu'envoyé), les lignes ne le diront plus : la date devra être consignée à l'arrivée. La phrase vient du corps de l'issue, l'ADR la reprend fidèlement. **5. ADR 0020 §5 : deux précisions.** - *La seconde ligne.* Quand une ligne brute de ce poste est encore active et que le jeton local manque ou n'est plus valide (expiration à deux ans, dossier de données réinitialisé, autre clé puis retour à la première), « Activer » envoie le dérivé. Le serveur n'a aucune ligne active sous cet identifiant : c'est une activation nouvelle, là où la v0.15.0 obtenait un rafraîchissement. Elle prend une place de plus, ou rend `machine_limit_reached` si la licence est pleine. Le code s'en sort bien : les deux lignes sont marquées « Cette machine », et désactiver l'ancienne garde le jeton. Le §5 ne nomme pas ce reste, alors que le rejet de la « coupure nette » s'appuie sur la même machine fantôme. À consigner, avec ce qui l'éteint : le rehachage en place des lignes actives (piste 1 de maximus/maximus-api#82), fait avant l'expiration des premiers jetons. Les points d'attention de cette issue décrivent le cas miroir, une ancienne version après le rehachage, pas celui-ci. - *Ce que la clé publique ne couvre pas.* Le dérivé ne permet pas de retrouver le brut. Un tiers qui détient déjà l'identifiant brut d'un poste peut en revanche recalculer son dérivé, puisque la clé est publique. La protection porte sur la remontée du dérivé vers le brut, pas sur ce rapprochement. Une phrase sous « La clé n'est pas un secret » rendrait le modèle de menace exact. ### Confirmé au passage, hors périmètre - `generate_handler!` enregistre **41** commandes sur cette tête, `architecture.md` en annonce 35 : le décompte de la description est exact, et l'écart est antérieur à la PR.
maximus added 1 commit 2026-10-02 01:13:43 +00:00
fix(license): apply the review of #366
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m47s
PR Check — Rust / rust (pull_request) Successful in 8m46s
0a9c9f71dd
The review approved the previous head with five suggestions; this commit
applies the four that touch the repository (the fifth is the manual check
in the pull request description).

- Tests: a valid token issued for another machine goes with any row of
  this machine. Two rows added to the deactivation table: the reviewer's
  mutation that kept such a token stayed green before them.
- ADR 0020, removal condition: counted from the last raw-claim token the
  server issued, not from a client version. A Free 0.15.0 no longer
  updates and can still activate with the raw id months from now.
- ADR 0020: names what the transition leaves behind when a machine is
  activated again while its raw row is still active (a second row, one
  more slot), and what a public key does not cover (whoever already holds
  the OS id can recompute the derived one).
- "No command hands the OS id to the webview" was wider than the code: the
  server's list still carries it for a row from before the derivation.
  Bounded in the ADR, architecture.md and the licenseService.ts comment.

No behaviour change. Rust 168, vitest 1314.

Refs #350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Les cinq suggestions de la revue sont appliquées dans 0a9c9f7. Cette tête n'a pas été relue : l'approbation portait sur e5662ad, et #350 repasse en status:review. Max a demandé d'appliquer puis de merger sans nouvelle revue ; le commit ne change aucun comportement (docs, commentaire, deux lignes de test).

  1. Vérification manuelle par la valeur. La description de la PR reprend la commande de la revue : 32 brut: True dérivé: False avant, 64 brut: False dérivé: True après. Je l'ai rejouée sur trois jetons synthétiques (brut, dérivé, double dérivation) : mêmes sorties que celles annoncées. La vérification est maintenant datée : avant le tag de la release.
  2. Phrase trop large. « Plus aucune commande ne remet l'identifiant du système au webview » devient « ne lit plus l'identifiant du système pour le remettre au webview », et nomme la ligne antérieure que la liste porte encore. Bornée dans l'ADR 0020 §2, docs/architecture.md et le commentaire de licenseService.ts.
  3. Deux lignes de test. (OTHER_RAW, THIS_RAW) et (OTHER_DERIVED, THIS_DERIVED) ajoutées à deactivation_drops_the_token_that_stood_for_the_freed_slot, et la ligne correspondante au tableau du §4 de l'ADR.
  4. Condition de retrait. L'ADR compte désormais deux ans depuis la dernière émission d'un jeton à claim brut, que seul le serveur connaît, et non depuis une version du client.
  5. ADR §5 et §1. La seconde ligne à la réactivation est nommée, avec ce qui l'éteint (le rehachage en place des lignes actives, maximus/maximus-api#82). Le §1 dit ce que la clé publique ne couvre pas.

Preuve du rouge, mutation 22 de la revue (« un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste ») :

Arbre Rouges
fichier de e5662ad, mutation appliquée 0 / 71
0a9c9f7, mutation appliquée 1 / 71 (deactivation_drops_the_token_that_stood_for_the_freed_slot)
0a9c9f7, fichier restauré 0 / 71, arbre propre

Suites sur 0a9c9f7 : cargo test 168, vitest 1314, npm run build vert. Les mutations 20 et 21 de la revue (câblage de la liste et de la désactivation à l'identifiant réel) restent vertes, comme annoncé : la description le dit maintenant pour les deux commandes.

Les cinq suggestions de la revue sont appliquées dans `0a9c9f7`. **Cette tête n'a pas été relue** : l'approbation portait sur `e5662ad`, et #350 repasse en `status:review`. Max a demandé d'appliquer puis de merger sans nouvelle revue ; le commit ne change aucun comportement (docs, commentaire, deux lignes de test). 1. **Vérification manuelle par la valeur.** La description de la PR reprend la commande de la revue : `32 brut: True dérivé: False` avant, `64 brut: False dérivé: True` après. Je l'ai rejouée sur trois jetons synthétiques (brut, dérivé, double dérivation) : mêmes sorties que celles annoncées. La vérification est maintenant datée : avant le tag de la release. 2. **Phrase trop large.** « Plus aucune commande ne remet l'identifiant du système au webview » devient « ne lit plus l'identifiant du système pour le remettre au webview », et nomme la ligne antérieure que la liste porte encore. Bornée dans l'ADR 0020 §2, `docs/architecture.md` et le commentaire de `licenseService.ts`. 3. **Deux lignes de test.** `(OTHER_RAW, THIS_RAW)` et `(OTHER_DERIVED, THIS_DERIVED)` ajoutées à `deactivation_drops_the_token_that_stood_for_the_freed_slot`, et la ligne correspondante au tableau du §4 de l'ADR. 4. **Condition de retrait.** L'ADR compte désormais deux ans depuis la dernière émission d'un jeton à claim brut, que seul le serveur connaît, et non depuis une version du client. 5. **ADR §5 et §1.** La seconde ligne à la réactivation est nommée, avec ce qui l'éteint (le rehachage en place des lignes actives, maximus/maximus-api#82). Le §1 dit ce que la clé publique ne couvre pas. **Preuve du rouge**, mutation 22 de la revue (« un jeton valide d'un autre poste survit à la désactivation d'une ligne de ce poste ») : | Arbre | Rouges | |---|---| | fichier de `e5662ad`, mutation appliquée | 0 / 71 | | `0a9c9f7`, mutation appliquée | **1 / 71** (`deactivation_drops_the_token_that_stood_for_the_freed_slot`) | | `0a9c9f7`, fichier restauré | 0 / 71, arbre propre | Suites sur `0a9c9f7` : `cargo test` 168, vitest 1314, `npm run build` vert. Les mutations 20 et 21 de la revue (câblage de la liste et de la désactivation à l'identifiant réel) restent vertes, comme annoncé : la description le dit maintenant pour les deux commandes.
maximus merged commit 0a9c9f71dd into main 2026-10-02 01:25:09 +00:00
maximus deleted branch issue-350-hash-machine-id 2026-10-02 01:25:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#366
No description provided.