feat(claude-usage): POST et GET /claude-usage (#48) #52
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/vps-health-api#52
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-48-claude-usage-routes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Resume
Ajoute
POST /claude-usage(ingestion du snapshot de consommation Claude pousse par l'agent du ThinkPad) etGET /claude-usage(lecture pour le dashboard/admin), contratschemaVersion: 1.checkAuth()lit le token attendu dans une tableTOKEN_KINDS(read->HEALTH_TOKEN,ingest->HOSTS_INGEST_TOKEN,claude-ingest->CLAUDE_USAGE_INGEST_TOKEN). Toujours un seul point de controle, un seul appeltokenMatches(), un seulreturn null. Chaque token d'ingestion absent -> 503 nommant sa propre variable. UntokenKindinconnu dansROUTESfait echouer le chargement du module.readBody(req, res, maxBytes): 4 096 pour/hosts/<id>, 65 536 pour/claude-usage, compte sur les octets recus.claude-usage.js(nouveau, pur, ajoute auCOPY) :sanitizeClaudeUsage()+ bornes exportees. Liste blanche ; valeur invalide jetee (cle omise), jamais forcee ; seuls « pas un objet » etschemaVersion !== 1refusent le corps (400).receivedAtserveur, ecriture atomique (temporaire suffixe du pid, mode 600,rename), 204.ageSeconds,stale(> CLAUDE_USAGE_STALE_SECONDS, defaut 2700),staleAfterSeconds;{status:"no_data"}si absent ou illisible.README.md,CLAUDE.md(endpoints, routage/auth, config, montage, tests, gotchas),.env.example.Decisions (autopilot)
__tests__/auth.test.jsmodifiee — ecart au critere « suite existante sans modification autre que la liste COPY ». L'epingle de source decheckAuth()autorisait exactement les egalitesheader === "string"etroute.tokenKind === "ingest". Le refactor en table demande par l'issue supprime la seconde par construction ; la garder aurait impose une comparaison morte dans le gate. L'allowlist passe a['header === "string"']seule : l'epingle ne fait que se resserrer. Toutes les autres epingles (untokenMatches, unreturn null, espiontimingSafeEqual, hachage) sont intactes.__tests__/writes.test.jsetendu (exige par CLAUDE.md pour tout nouveau chemin d'ecriture) :violations(writes, allowedDir = HOSTS_DIR)(appels existants inchanges), fixture + token Claude,/claude-usagedansREAD_ROUTES, deux cas :POST /claude-usagen'ecrit que dans le dossier deCLAUDE_USAGE_PATH; ses rejets 401/400/413 et la lecture d'un fichier corrompu n'ecrivent rien. Aucune assertion existante affaiblie.__tests__/agent.test.jsnon modifie : son test interditCOPY . .et l'agent mais ne fige pas la liste ; un pin positif « le COPY embarqueclaude-usage.js» est dans le nouveau fichier de test.daysgarde les 31 plus recents (ordre du contrat : plus ancien -> aujourd'hui),models/items/projectsgardent les N premiers.percentdoit etre un entier 0-100 (lecture litterale de « nombresNumber.isSafeInteger») : l'agent (issue suivante) doit arrondir, sinon le pourcentage est jete..tmporphelins pour/data/claude-usage: inoffensifs (GET ne lit que le fichier final), et l'ajout toucherait le blocrequire.mainepingle. Documente dans les gotchas.test-curl.shnon modifie (45 cas figes dansci.test.js).Tests
npm test: 500 cas (377 avant ; +119claude-usage.test.js, +3writes.test.js, +1ci.test.jspar l'epinglerequireen colonne 0 sur le nouveau module)bash test-curl.sh: 45/45sh scripts/boot-from-copy.sh: PASSActions requises de Max
CLAUDE_USAGE_INGEST_TOKEN(secret fort, distinct des deux autres) enis_runtime=true, is_buildtime=false. Sans elle,POST /claude-usagerepond 503 (fail-closed)./data/claude-usage(hote) ->/data/claude-usage(conteneur), lecture-ecriture, sans:ro. Monter le dossier, jamais le fichier.sudo mkdir -p /data/claude-usage && sudo chown 1000:1000 /data/claude-usage(sinon chaque POST repond 500).CLAUDE_USAGE_STALE_SECONDS(defaut 2700) etCLAUDE_USAGE_PATH(defaut/data/claude-usage/claude-usage.json).docker inspect(RW=truesur/data/claude-usage) etGET /claude-usage->{"status":"no_data"}avant le premier push de l'agent.Generated autonomously by /autopilot run of 2026-10-04
🤖 Generated with Claude Code
Auto-revue (autopilot) — verdict : pret pour revue humaine, un ecart assume
Relecture de
git diff main...HEADcritere par critere :/claude-usage-> 401 ; token Claude surPOST /hosts/<id>-> 401claude-usage.test.js« POST /claude-usage — authentication » (6 cas + croisement/hosts/thinkpad), plus token Claude refuse sur 4 routes GETCLAUDE_USAGE_INGEST_TOKENabsent -> 503 nommant la variable ;/hostsgarde son 503HOSTS_INGEST_TOKENhosts.test.jsinchange et vert/hosts/<id>413 a 4 097detailhors regex,statushors enum jetesreceivedAt,ageSeconds,staleno_dataauth.test.jsresserree (allowlist d'egalites decheckAuth():route.tokenKind === "ingest"disparait avec la table). Cas ajoutes awrites.test.js(exige par CLAUDE.md). Voir decisions dans la description.Points verifies en plus :
tokenMatches(), un seulreturn null, aucune egalite sur un secret ; espiontimingSafeEqualvert pour la nouvelle route ; ordre routage -> auth intact (404 avant 401 sur PUT/DELETE/sous-chemins).requireen colonne 0 (epingleci.test.jspasse aussi surclaude-usage.js),claude-usage.jsdans leCOPY,boot-from-copy.shPASS..tmplaisse ; 500 propre si le dossier est inutilisable.writes.test.js+claude-usage.test.jsrelances 5 fois, 133/133 a chaque fois.Points a l'attention de Max :
percentdoit etre entier : l'agent doit arrondir..tmporphelins dans/data/claude-usage(documente).CLAUDE_USAGE_INGEST_TOKENetHOSTS_INGEST_TOKEN: l'isolation suppose deux secrets distincts.Validation :
npm test500/500,bash test-curl.sh45/45,sh scripts/boot-from-copy.shPASS.Verdict : APPROVE
Resume : les deux routes
/claude-usagepassent par le gate unique (tableTOKEN_KINDS, un seultokenMatches(), un seulreturn null), le sanitizer par liste blanche est idempotent et la nouvelle surface d'ecriture entre danswrites.test.js. Suite, passe socket et boot check rejoues sur la teteef8558e, plus 11 mutations du relecteur.Execute (copie hors depot de
ef8558e)npm test: 500/500 ;bash test-curl.sh: 45/45 ;sh scripts/boot-from-copy.sh: PASS (ports 3097-3099 libres avant lancement)CI / verification (pull_request)= successMutations
POST /claude-usagesurtokenKind: "ingest"(token partage avec/hosts)staleen>=au lieu de>daysgarde les 31 premiers au lieu des 31 derniersreceivedAtlu sans controleisFinite)CLAUDE_USAGE_PATHwrites.test.js)unlinkSync(tmpPath)de nettoyageSuggestions (non bloquantes)
__tests__/claude-usage.test.js:739— le cas « 500 when the snapshot cannot be written, and no temp file is left » echoue aumkdirSync(parent = fichier), avant la creation du temporaire : le nettoyage ducatchdewriteClaudeUsage()n'est jamais emprunte, et le cas n'affirme pas l'absence de.tmp. La mutation qui retireunlinkSync(tmpPath)reste verte. Pour l'exercer : faire deCLAUDE_USAGE_PATHun dossier existant non vide (lewriteFileSyncdu temporaire passe, lerenameSyncechoue), puis exiger 500 et aucun.claude-usage.json.*.tmpdans le parent. Ou retirer « no temp file is left » du titre.index.jsreadClaudeUsage()— lereceivedAtrelu est valide parnew Date(...), permissif ("1"passe et repart tel quel vers le dashboard). Meme comportement quereadHostSnapshot(), et seul le serveur ecrit ce champ : passer par uncleanTimestampexporte declaude-usage.jsalignerait la relecture sur la liste blanche du reste du snapshot. A traiter dans une issue de suivi commune aux deux lecteurs plutot qu'ici.Resolves #48vit dans le message du commitef8558e, pas dans le corps de la PR (timeline actuelle :ref_action: none). L'issue se fermera quand ce commit atteindramain. Si #48 doit rester ouverte jusqu'aux actions Coolify (token, montage,chown), reformuler avant le merge.Les ecarts declares (une ligne d'
auth.test.jsqui ne fait que resserrer l'allowlist, extension dewrites.test.js) sont justifies et verifies.