docs: correct the backups-pull mount, the mounts' status and the rotation rule (#63)
All checks were successful
CI / verification (pull_request) Successful in 34s
All checks were successful
CI / verification (pull_request) Successful in 34s
The host side of the backups-pull mount is /srv/vps-health-api/backups-pull (docker inspect, 2026-10-19), and the three mounts marked "to set up" are in place. The test list gains agent-pg-backup-pull.test.js, missing until now. REVIEW.md stated the rotation was anchored on the workstation's date: it is anchored on the newest valid backup not after today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0b351e3937
commit
1d3be3ca49
2 changed files with 5 additions and 4 deletions
|
|
@ -57,9 +57,9 @@ Consequence voulue : un appelant non authentifie sur une route inconnue voit 404
|
|||
- `/data/defenseurs` (host) -> `/data/defenseurs:ro` : `status.json` + `findings-state.json` + `agents-map.json`, ecrits directement la par le Sergent. `/home/defenseur/defenseurs/status.json` est un leurre obsolete lu par personne. **Monte en lecture seule** depuis le 2026-09-30 (issue #36).
|
||||
- `/home/defenseur/defenseurs/reports` (host) -> `/data/defenseurs/reports:ro` : rapports de scan (+ sous-dir `archive/`), montage imbrique dans le precedent. Pose le 2026-07-15 (issue #10), recree le 2026-09-30 en Volume Mount avec Source Path pour recevoir le `:ro` (#36). **Monte en lecture seule**.
|
||||
- `/data/hosts` (host) -> `/data/hosts` : snapshots des postes. **Lecture-ecriture** — doit appartenir a l'uid 1000 (`node`, l'utilisateur du conteneur), sinon chaque ingestion repond 500.
|
||||
- `/data/claude-usage` (host) -> `/data/claude-usage` : snapshot de consommation Claude (issue #48). **Lecture-ecriture**, proprietaire uid 1000, meme regle que `/data/hosts` (sinon chaque `POST /claude-usage` repond 500). Monter le **dossier**, jamais le fichier seul : le snapshot est remplace par `rename`, un bind de fichier resterait accroche a l'ancien inode. A poser par Max avant le premier push de l'agent. Le sous-dossier `history/` (#55) vit sur ce meme montage : rien a poser, le serveur le cree au premier POST de plus d'un jour.
|
||||
- `/var/backups/postgres-status` (host) -> `/data/backups-status:ro` : `status.json` ecrit par `backup-postgres.sh` (#58), root, dossier 755, fichier 644, lisible par l'uid 1000. **Lecture seule**. Monter le **dossier** : le script remplace le fichier par `rename`. Sans ce montage, `GET /backups` repond avec `vps: null` (pas d'erreur). A poser par Max avant le premier deploy.
|
||||
- `/data/backups-pull` (host) -> `/data/backups-pull` : compte rendu du tirage (#58). **Lecture-ecriture**, proprietaire uid 1000, le dossier et non le fichier, meme regle que `/data/claude-usage` (sinon chaque `POST /backups/pull` repond 500, `GET /backups` continue de servir le bloc VPS). A poser par Max avant le premier deploy.
|
||||
- `/data/claude-usage` (host) -> `/data/claude-usage` : snapshot de consommation Claude (issue #48). **Lecture-ecriture**, proprietaire uid 1000, meme regle que `/data/hosts` (sinon chaque `POST /claude-usage` repond 500). Monter le **dossier**, jamais le fichier seul : le snapshot est remplace par `rename`, un bind de fichier resterait accroche a l'ancien inode. Pose (releve du 2026-10-19). Le sous-dossier `history/` (#55) vit sur ce meme montage : rien a poser, le serveur le cree au premier POST de plus d'un jour.
|
||||
- `/var/backups/postgres-status` (host) -> `/data/backups-status:ro` : `status.json` ecrit par `backup-postgres.sh` (#58), root, dossier 755, fichier 644, lisible par l'uid 1000. **Lecture seule**. Monter le **dossier** : le script remplace le fichier par `rename`. Sans ce montage, `GET /backups` repond avec `vps: null` (pas d'erreur). Pose (releve du 2026-10-19).
|
||||
- `/srv/vps-health-api/backups-pull` (host) -> `/data/backups-pull` : compte rendu du tirage (#58). **Lecture-ecriture**, proprietaire uid 1000, le dossier et non le fichier, meme regle que `/data/claude-usage` (sinon chaque `POST /backups/pull` repond 500, `GET /backups` continue de servir le bloc VPS). Pose (releve du 2026-10-19).
|
||||
- Deux couches tiennent l'API hors des fichiers Defenseurs. (1) Le mode du montage : `:ro` est un suffixe de la destination dans Coolify, recopie tel quel dans le compose genere (`'/data/defenseurs:/data/defenseurs:ro'`). (2) Les droits Unix : le contenu appartient a `defenseur:defenseur` (uid et gid 1001), dossiers en 755/775, fichiers en 664, et le conteneur tourne en uid 1000 (`node`, gid 1000, sans groupe supplementaire), donc `test -w` y echoue meme sans `:ro`. Ce sont les bits de groupe qui donnent l'ecriture : cette couche tient parce que le groupe proprietaire n'est pas le gid 1000 (groupe de `node` dans l'image, et de `ubuntu` sur l'hote). Un `chgrp ubuntu` ou un `chmod o+w` la ferait tomber sans bruit. Le `:ro` ne vaut que pour la vue du conteneur : le Sergent ecrit cote hote comme avant.
|
||||
- Verification apres tout redeploy, par trois vues : `docker inspect <conteneur> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}} RW={{.RW}}{{println}}{{end}}'` (`RW=false` sur les deux montages Defenseurs et sur `/data/backups-status`, `RW=true` sur `/data/hosts`, `/data/claude-usage` et `/data/backups-pull`) ; `/proc/mounts` dans le conteneur (vue du noyau) ; et le **contenu** des routes de lecture, pas seulement leur code, car un montage manquant repond 200 `no_data`.
|
||||
|
||||
|
|
@ -94,6 +94,7 @@ curl -H "Authorization: Bearer $(cat ~/.coolify-token)" \
|
|||
- `__tests__/hosts.test.js` — matrice exhaustive des endpoints postes (177 cas : regex de route comme controle anti-traversee, 401 avant 403, cloisonnement des deux tokens, 503 fail-closed, reconstruction par liste blanche, `loadAvg` a exactement 3 elements, 413, `neverSeen`, fraicheur serveur sous horloge figee, degradation de lecture (dont un `receivedAt` hors ISO complet, #54), temporaire supprime quand le `rename` echoue (#54), **balayage des `.tmp` orphelins** au demarrage, prouve sur le vrai point d'entree, symlink ni suivi ni supprime, ancre `^` de `TEMP_FILE_RE`)
|
||||
- `__tests__/agent.test.js` — agent poste (50 cas : contrat de payload verifie contre `sanitizeSnapshot()`, une seule tentative sans rejeu, timeout, codes de sortie, wrapper shell, et **le token absent de toute sortie d'echec, fragments de 5 caracteres compris**)
|
||||
- `__tests__/agent-claude-usage.test.js` — agent claude-usage du ThinkPad (136 cas). Agregation des transcripts, #49 (52 cas) : jours Toronto et changement d'heure, regles de projet, deduplication des requetes, 30 jours, top 8 + `(autres)`, 12 modeles, seuil de scan pris sur le plus ancien jour ouvert (jour fige futur, trou dans la serie figee, tout fige), noms `__proto__` et `constructor` sans ecriture sur `Object.prototype` ; #54 : requete a cheval sur minuit comptee une seule fois (jours figes ecartes apres la deduplication, egalite d'`output` gagnee par la ligne la plus ancienne quel que soit l'ordre de lecture), erreurs de lecture comptees (`readdir`, `ENOENT` d'un sous-dossier compris, `stat` y compris `ENOENT`, flux, ouverture), racine absente non comptee. Agent, #50 (74 cas) : reponse anonymisee de l'endpoint (`__tests__/fixtures/claude-usage/oauth-usage.json`) vers les items, repli `five_hour`/`seven_day`, tout `null` -> `unavailable` ; `fetch` (double) jamais appele sur jeton expire, `off`, credentials tronques, absents ou sans jeton, version absente ; 429 -> `http_error`/`http_429` avec les dernieres limites valides et leur `fetchedAt` ; corps hostiles -> `detail` dans l'enum ; bearer OAuth envoye a la seule URL Anthropic epinglee, aucune variable d'env ne la change (epingle sur le source), `redirect: "error"` ; **espions sur toutes les ecritures de `node:fs`** : le fichier de credentials n'est que lu ; aucun fragment de 5 caracteres des deux jetons dans le snapshot, l'etat ni la sortie (dont un vrai processus) ; deux passages autour d'un jour vide (fige, puis aucun fichier sous le seuil ouvert) ; `state.json` corrompu, elagage a 35 jours, ecriture atomique en 600, temporaire laisse en 644 ramene a 600 (#54) ; passage avec erreur de lecture : aucun jour fige, snapshot envoye, `read_errors=N` sans chemin, le passage suivant fige (#54) ; reponse de l'endpoint comptee en octets pendant la lecture, 256 Kio pile acceptes, + 1 octet en caracteres de 2 octets refuse, flux sans fin annule (#54) ; snapshot sur fixtures inchange par `sanitizeClaudeUsage` ; `http:` -> 1, > 64 Kio -> 2 sans envoi ; `run-claude-usage.sh` lance deux fois en parallele (faux `node`, HOME temporaire) : le second sort 0 sans travailler. Rattrapage ponctuel, #55 (10 cas) : fenetres passees contigues jusqu'a J-2, jours vides de tete retires, requete a cheval sur la frontiere de deux fenetres comptee une fois (meilleure ligne avant ou apres minuit), rien d'autre que `{ date, models }`, borne `--max-windows`, erreurs de lecture additionnees ; `main` n'ecrit que `--out` (mode 600, jamais ecrase), aucun `fetch`, aucun acces aux credentials, sortie en compteurs seulement, fichier accepte par le parseur d'import du serveur, erreurs d'usage
|
||||
- `__tests__/agent-pg-backup-pull.test.js` — agent de tirage des sauvegardes du ThinkPad (21 cas, la-compagnie-maximus#220, PR #59 : `ssh`, `rsync` et `curl` remplaces par de faux binaires en tete de `PATH` qui consignent leur argv ; tirage par le port 2222 avec cle d'hote epinglee, `.age` seulement, `--ignore-existing` et jamais `--delete` ; token d'ingestion dans un fichier d'en-tete, jamais en argv ni en sortie, fichier supprime meme sur un `TERM` ; codes de sortie 1, 2 et 3 ; rotation : 30 dossiers dates gardes, jamais la sauvegarde la plus recente, un dossier date futur ou un nom impossible ignores sans rien supprimer, `rotation_failed` ; aucun `set -x`)
|
||||
- `__tests__/ci.test.js` — epingles du workflow et des scripts de CI (49 cas : image du `container:` identique au `FROM` du Dockerfile, les quatre etapes presentes, aucun filtre `branches:` sous `pull_request`, `push` limite a `main`, ni `defaults:` ni `working-directory` sur une etape `uses:`, **aucun `continue-on-error`, sous toute ecriture litterale** (une etape rouge passerait au vert en silence ; detecteur sur le mot nu, sans ancrage ni deux-points exiges, lui-meme verifie sur la cle en bloc, en element de liste, en style flow, entre guillemets et via une ancre YAML ; seules les lignes de commentaire entieres en sont exemptees ; une cle ecrite avec une sequence d'echappement, `"continue\u002don-error"`, passe encore : l'epingle lit du texte, pas du YAML), ce que le boot check suppose du Dockerfile : `USER node`, `CMD` en forme exec, le **refus d'un port deja occupe** (voisin HTTP reel, script lance en `spawn` asynchrone), **aucune requete par un proxy de l'environnement** (boot complet derriere un faux proxy qui repond a tout, sa vivacite prouvee d'abord par un `curl` nu, avec un `no_proxy` qui laisse la boucle locale hors liste : exporter `NO_PROXY` seul ne suffit pas), **`test-curl.sh` lance en entier** (#41 et #46, 22 cas, chacun sur des ports libres qui lui sont propres, passes par `TEST_CURL_PORT` et `TEST_CURL_NOINGEST_PORT` : derriere le meme faux proxy avec `FORCE_COLOR=3`, avec `BASE_URL` et `NOINGEST_URL` pointes sur un port mort, et avec `TOKEN` et `INGEST_TOKEN` herites d'une meme valeur, rien n'atteint le proxy, les 57 cas passent, le nombre etant ecrit en dur dans le test, et les deux ports sont refuses des la premiere tentative a la sortie du script ; **deux passages au meme instant** sur quatre ports libres, 51 cas chacun ; les **refus de port** : valeur non numerique, port 0, port au-dela de 65535, nombre suivi de lettres (`80abc`), chacun sur chacune des deux variables, puis deux ports identiques, et les **deux bornes acceptees**, 1 et 65535, donnees aux deux variables a la fois pour ne rien faire ecouter sur un port fixe (atteindre le refus « must differ » montre que la valeur a passe le controle de plage), message sur stderr et code 1 avant tout lancement ; un **voisin HTTP reel** sur le port principal, puis sur le second : refus avant tout lancement, le voisin n'ayant vu que la sonde `/` ; un **occupant non HTTP** sur le port principal, qui ferme aussitot (code curl 52 ou 56) puis qui accepte sans repondre (28) : meme refus, la regle etant « tout code autre que 7 » ; un **serveur vivant qui n'ecoute jamais** : message d'echeance et code 1 apres les 5 s d'attente ; puis un faux `node` en tete de `PATH` qui sort en 1 pour le port principal, puis le second : message sur stderr, code 1 en moins de 3 s, aucun serveur laisse sur son port ; et deux **epingles sur le source** du script : defauts 3099 et 3098, seules occurrences de ces nombres hors commentaires, et URLs derivees des ports sans lecture de l'environnement. Plus aucun cas n'ecoute sur 3099 ni 3098 : les defauts ne sont exerces que par l'etape CI « Socket-level pass » et par un `bash test-curl.sh` a la main. Les ports viennent de `freePorts()`, qui ouvre ses sondes ensemble puis les ferme avant le lancement : un port repris dans l'intervalle fait echouer le cas avec le message de `require_free`, sans cas lance contre un mauvais serveur), et **des `require` en colonne 0 seulement, sans `import()`**, dans chaque `.js` du `COPY`. Cette derniere epingle est une heuristique sur les appels litteraux, espace avant la parenthese comprise (`require (`, `import (`). Son detecteur, `loadsLate()`, est lui-meme verifie sur six lignes : `require` indente et `import` a toute colonne, avec ou sans espace, signales ; `require` de premier niveau et ligne de commentaire laisses passer. Trous connus, entre autres, laisses ouverts (#35) : une fonction ecrite entiere sur une ligne en colonne 0 (`const lazy = () => require("x");`), un `require(` renvoye a la ligne en colonne 0 dans une fonction (de meme forme que le `} = require("node:fs");` legitime d'`index.js`), et toute indirection, comme un alias de `require`)
|
||||
- `__tests__/writes.test.js` — surfaces d'ecriture : `HOSTS_DIR`, et le dossier de `CLAUDE_USAGE_PATH` pour le seul `POST /claude-usage`, archive `history/` comprise (#55) (#48 : `violations()` prend le dossier autorise du cas, `HOSTS_DIR` par defaut ; deux cas dedies, `POST /claude-usage` n'ecrit que dans son dossier, et ses rejets 401/400/413 et la lecture d'un fichier corrompu n'ecrivent rien du tout ; `GET /claude-usage` entre dans les routes de lecture ; #55 : le cas POST exige le `rename` du fichier mensuel du jour clos, `GET /claude-usage/history` entre dans les routes de lecture, et un mois corrompu ou une plage refusee n'ecrivent rien ; #58 : le dossier de `BACKUPS_PULL_PATH` pour le seul `POST /backups/pull`, `GET /backups` entre dans les routes de lecture, ses rejets 401/400/413 et deux fichiers corrompus puis absents n'ecrivent rien) (18 cas : espions sur toutes les fonctions d'ecriture de `node:fs`, formes callback, Sync et `fs.promises`, et sur `child_process`, poses avant un `require` frais apres purge du cache de **tous** les modules runtime (un `metrics.js` reste en cache garderait les espions restaures d'un test precedent) ; demarrage par le balayage des `.tmp` ; les neuf routes GET n'ecrivent rien, meme une macrotache apres la reponse, contenu reel exige ; `POST /hosts/<id>` n'ecrit que sous `HOSTS_DIR` ; **chemins degrades** : rejets 401/403/404, entrees 400/413, `agents-map.json` corrompu, fichiers Defenseurs absents, snapshot corrompu, avec le contenu degrade exige (`no_data`, `neverSeen`) ; **commandes shell** figees sur `head -1 /proc/stat` et `df -k /`, `/health` servant de temoin ; chaque cas prouve que ses espions sont vivants ; **tout appel est compte, sans attribution par pile** (#40 : une fonction `node:fs` passee telle quelle a `process.nextTick`, a un ecouteur ou a un `.then` s'execute sans frame runtime sur sa pile, et un filtre par pile la laissait passer) ; les retouches de fixtures du cas degrade passent par les fonctions reelles capturees avant la pose des espions, jamais par un `rmSync` recursif, qui appelle en interne le `fs.rmdirSync` public, donc l'espion ; **epingle sur le bloc `require.main`**, ensemble exact des appels, puisqu'il ne tourne jamais dans le processus de test. Limites : un chemin qu'aucun cas n'emprunte, et une ecriture differee au-dela de 20 ms. Contrepartie assumee : une ecriture du lanceur de tests lui-meme pendant un cas le ferait rougir)
|
||||
- Runtime reste 0-dep ; vitest en devDep uniquement
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ Chaque règle cite sa source. Elles valent quel que soit le niveau : une PR norm
|
|||
- Aucun secret dans une sortie : tout ce qu'impriment les agents part dans syslog et journald (`logger -t …`). Le chemin d'erreur ne rend que `err.code` et le code HTTP, jamais un objet d'erreur, des options de requête, un en-tête ou un corps de réponse ; jamais de `set -x` dans un `run-*.sh`. Les tests exigent l'absence de tout fragment de 5 caractères des tokens. — `CLAUDE.md` « Gotchas », `agent/README.md` « No secret in the logs »
|
||||
- L'agent claude-usage ne rafraîchit jamais le jeton OAuth et n'écrit jamais `~/.claude/.credentials.json` (refresh token à usage unique : Claude Code serait déconnecté). L'URL Anthropic reste une constante de `limits.js`, sans variable d'environnement, avec `redirect: "error"`, pour que le bearer OAuth ne suive jamais l'URL de push configurable. — `CLAUDE.md` « Gotchas », `agent/README.md`, PR #53
|
||||
- Un passage qui a rencontré une erreur de lecture ne fige aucun jour ; les jours figés sont écartés après la déduplication. — `CLAUDE.md` « Gotchas », PR #51 (REQUEST_CHANGES : seuil de scan qui rendait vides des jours ouverts), #56
|
||||
- Rien de ce que livre le VPS ne détruit ni n'altère la copie hors site : la rotation s'ancre sur la date du poste, un fichier déjà rapatrié n'est jamais réécrit, un nom daté impossible ou futur est journalisé et ignoré. Le compte rendu se calcule après la rotation. — `agent/README.md` (pg-backup-pull), PR #59 (trois passes REQUEST_CHANGES : rotation qui vidait la copie, VPS qui pouvait l'effacer par un dossier futur ou l'écraser par `rsync`)
|
||||
- Rien de ce que livre le VPS ne détruit ni n'altère la copie hors site : la rotation s'ancre sur la sauvegarde valide la plus récente qui ne dépasse pas aujourd'hui, et garde ce dossier et les 29 jours qui le précèdent (ni sur aujourd'hui seul, qui ferait vieillir la dernière copie quand le VPS cesse de produire, ni sur le nom le plus récent seul, qu'un dossier daté dans le futur pousserait au-delà de toutes les vraies sauvegardes) ; un fichier déjà rapatrié n'est jamais réécrit (`--ignore-existing`, jamais `--delete`) ; un nom daté impossible ou futur est journalisé et ignoré. — `agent/README.md` (« Rotation is anchored… », « No overwrite »), PR #59 (trois passes REQUEST_CHANGES : rotation qui vidait la copie, VPS qui pouvait l'effacer par un dossier futur ou l'écraser par `rsync`)
|
||||
- Le token d'ingestion passe à `curl` par un fichier d'en-tête en 0600, jamais en argv ; fichiers d'environnement en 600. — `agent/README.md`
|
||||
- Le cron pointe vers une copie figée, jamais vers le clone. — `agent/README.md`, mémoire « ThinkPad = cette machine »
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue