Commit Graph
29 Commits
Author SHA1 Message Date
arcodangeandClaude Opus 5 c96deb5ad6 feat(erp): les deux comptes bancaires tombent au centime — reprise FAC004/FAC006, réparation FAC009, cashbacks
Suite de la réconciliation ouverte par la question de l'opérateur. Après ce
passage, l'ERP et la banque disent le même chiffre sur les deux comptes :
Qonto 2 013,30 EUR et Wise 11 863,85 EUR, écart 0,00.

CE QUI A DÉBLOQUÉ LE RESTE. L'opérateur a établi que KissMetrics n'a JAMAIS reçu
la moindre facture — le client payait le forfait de 2 500 USD sans pièce. Rien
ne s'opposait donc à corriger des factures qu'on croyait entre ses mains.

FAC004 et FAC006 portaient toutes deux 2 145,92 EUR, le même montant repris tel
quel d'un cycle à l'autre, alors que la banque a reçu 2 147,00 EUR le 29/05 et
2 195,97 EUR le 25/06 : 2 500 USD à chaque fois, au taux du jour du paiement.
51,13 EUR d'encaissement manquaient aux livres. Leurs notes qualifiaient déjà
l'écart d'« écart de change » — défendable tant qu'on ne pouvait pas toucher aux
factures ; plus vrai maintenant qu'on le peut. Une facture, un règlement, une
ligne bancaire, le même chiffre.

RÉPARATION DE FAC009. `PUT /invoices/{id}/lines/{lid}` n'est PAS un PATCH : tout
champ absent du corps est remis à zéro. La correction appliquée ce matin ne
passait que subprice, pu_ht, qty et desc — `product_type` est donc retombé de 1
(service) à 0 (produit), faisant de FAC009 la seule ligne du registre typée
« produit » dans une société qui ne vend que des prestations et facture hors UE
sous l'art. 259-1° du CGI. Rétabli.

C'est le juge pré-gate qui l'a vu, sur la répétition de FAC004 et FAC006 — où le
même appel avait en plus effacé `desc` ENTIÈREMENT. Premier finding de la
journée qui apporte quelque chose de réel, et il aurait envoyé au client deux
factures sans désignation. Les charges de ligne sont désormais complètes.

test/deleteInvoicePayment.ts. Corriger une facture encaissée suppose de refaire
son règlement, et l'API REST ne sait pas le supprimer : `PUT
/invoices/{id}/payments` ne met à jour que son NUMÉRO. Deux verrous dans
l'interface, consignés dans le fichier : tant que la facture est marquée payée
le lien de suppression est simplement ABSENT — pas d'erreur, rien — il faut
d'abord la rouvrir ; et la confirmation est une boîte MODALE jQuery dont les
boutons n'ont ni name ni value, si bien qu'un sélecteur sur input[type=submit]
ne trouve rien et que la suppression n'a pas lieu, silencieusement. Sans
--payment, le script retrouve le règlement sur la fiche : coder en dur un
identifiant relevé en bac à sable est une façon commode de supprimer le mauvais.

ABONNEMENT ANTHROPIC D'AOÛT. 90,00 EUR du 19/08, reçu 2330-6710-2536 arrivé dans
books@ le 24/08. Second abonnement à usage interne, distinct du budget IA client.

CASHBACKS WISE — compte 768. Wise parle de « remise », mais le montant suit le
SOLDE et non les frais : 0,19 EUR sur ~510 EUR en mars, 4,97 EUR sur ~9 700 EUR
en août, soit ~0,5-0,6 % l'an, stable. C'est la rémunération d'un solde, donc un
produit financier. Le seul frais Wise de l'exercice est le forfait de 50 EUR du
26/01, sans rapport. Ni 763, qui vise les revenus de créances commerciales, ni
764, réservé aux valeurs mobilières de placement : 768.

RISTOURNE QONTO — compte 627 au crédit. L'opération est typée `qonto_fee` par la
banque elle-même, côté crédit : une remise sur un service reçu vient en
diminution de la charge, elle ne crée pas un produit.

PIÈGE CATALOGUÉ. bank-match.sh continuera d'afficher ces sept écritures en
BANK-ONLY : il ne rapproche que les RÈGLEMENTS de factures et ignore les
paiements divers. Les voir listées ne veut donc PAS dire qu'elles manquent —
c'est écrit dans known-patterns.json, sans quoi le prochain agent les
ressaisirait.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-24 13:10:34 +02:00
arcodangeandClaude Opus 5 b745fdcb43 feat(erp): encaissement KissMetrics du 17/08, deux échéances URSSAF, règlement des charges sociales
L'opérateur a demandé si le versement de la semaine passée avait été pris en
compte. Il ne l'était pas — et la réconciliation menée en réponse a fait sortir
trois autres trous.

L'ENCAISSEMENT. KissMetrics a viré 2 164,75 EUR le 17/08 (Wise, réf.
VENDOR:DEV), soit 2 500,00 USD au taux du jour : la part fixe du cycle M4. Le
mouvement n'était pas enregistré, si bien que FAC009 a été émise le matin même
au taux du 24/08 (2 140,45 EUR) alors que le contrat arrête le montant dû « au
taux du jour du règlement ». La facture était fausse de 24,30 EUR, et déjà
payée.

Le précédent commandait la méthode : FAC008 avait été libellée 2 185,00 EUR,
exactement la somme reçue le 20/07, même schéma de règlement anticipé. Sur
arbitrage de l'opérateur, FAC009 est repassée en brouillon, portée à
2 164,75 EUR, revalidée sous le même numéro et la même date, puis adossée au
virement. Sa note reprend la rédaction de FAC008.

Le juge a bloqué en relevant que `last_main_doc` était présent — donc, selon
lui, facture émise. Il confond PDF PRODUIT et facture TRANSMISE : le PDF avait
été produit deux heures plus tôt par l'agent lui-même pour assembler le dossier,
et le message d'envoi est un brouillon jamais envoyé. Mais il a raison sur la
conséquence, et c'est le seul de ses findings de la journée qui apporte quelque
chose : le PDF sur disque devenait faux. Il a été régénéré aussitôt. Son second
finding — `emetteur` null — tombe : le champ est null sur les huit lignes
d'encaissement Wise antérieures ; le renseigner sur la seule ligne d'août
romprait la permanence des méthodes au lieu de la servir.

LES DEUX ÉCHÉANCES URSSAF. 493,00 EUR prélevés le 22/05 et 1 215,00 EUR le
17/08 n'étaient enregistrés ni l'un ni l'autre. Créés comme charges sociales —
pas comme factures fournisseur : l'URSSAF n'est pas un fournisseur — puis
réglés depuis Qonto.

test/paySocialCharge.ts comble un trou du chemin gated : recordSocialCharge.ts
crée la charge et la laisse impayée, et rien n'enregistrait le règlement. Tant
qu'il manque, le mouvement bancaire reste orphelin et le solde de l'ERP diverge
de celui de la banque — c'est ce qui laissait ces deux échéances invisibles.
Dolibarr n'expose aucune route REST pour les charges sociales, donc le pipeline
ne peut pas porter l'opération ; le script garde ce qu'il peut de sa discipline.

Trois pièges consignés dans le fichier, dont un qui a coûté une fausse alerte :
NE PAS chercher « payée » dans la page — « ImPAYÉE » contient « payée ». Une
première version déclarait ÉCHEC sur un règlement qui venait d'aboutir, ce qui
pousse à rejouer, donc à créer un doublon. On lit le montant restant dû.

LE RUNBOOK disait 646. C'est faux depuis adc-009 : dans une SARL à l'IS, ce que
la société verse à l'URSSAF pour son gérant majoritaire est une charge de
personnel, donc 641. Corrigé, avec la mention explicite que toute version
portant 646 est périmée. Ajouté aussi ce que la fiche de charge confirme à
l'écran — « Code comptable: Inconnu » : le type de charge ne pilote PAS le
compte sur ce déploiement, contrairement à ce que le runbook laissait croire.

Enfin l'exemple `--period 2026` du runbook était invalide : le script découpe la
valeur comme une date ISO et produisait `undefined/undefined/2026`, ce qui fait
échouer la création sans message.

Reste au tableau, documenté et non traité ici : six cashbacks Wise (11,13 EUR),
un remboursement Qonto (5,22 EUR), l'abonnement Anthropic du 19/08 (90,00 EUR,
reçu pas encore arrivé), et deux encaissements client sous-enregistrés — FAC004
et FAC006, 51,13 EUR reçus de plus que ce que l'ERP porte.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-24 11:23:35 +02:00
arcodangeandClaude Opus 5 4d26790504 feat(erp): corriger l'adresse électronique de la fiche société
L'en-tête de chaque facture portait `[email protected]` alors que
`static/config/company.json` porte `[email protected]`. La divergence
ne se voit nulle part dans l'ERP : elle ne s'est découverte qu'en relisant un PDF
destiné au client. Arbitrage de l'opérateur : l'adresse au nom de domaine de la
société est la bonne.

`GET /setup/company` répond 403 à tout scope agent (« open to admin users only »)
et aucune route REST n'écrit les constantes MAIN_INFO_SOCIETE_*. La fiche n'est
donc modifiable que par l'interface.

Deux pièges consignés dans le script. Le champ s'appelle simplement `mail` — pas
`MAIN_INFO_SOCIETE_MAIL`, malgré le nom de la constante qu'il alimente et malgré
la convention que suivent tous ses voisins du même formulaire. Et la page de
retour réaffiche la valeur soumise qu'elle ait été enregistrée ou non : le script
RECHARGE la page avant de vérifier.

Répété en bac à sable, puis appliqué en production. FAC009 et FAC010 régénérées.

FAC005, FAC007 et FAC008 gardent l'ancienne adresse, délibérément : elles ont été
émises sous celle-ci, et on ne réécrit pas un document daté pour le faire coller
à un état postérieur.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-24 10:47:41 +02:00
arcodangeandClaude Opus 5 bcea21f96b feat(erp): cycle M4 KissMetrics, comblement des écarts bancaires, clause de pénalités rétablie
Deux passages gated en production, et l'outil qui manquait pour clore le dossier
client.

CYCLE M4 (2026-08-24-m4-et-ecarts). Cinq opérations : le tiers Anthropic PBC —
entité américaine, distincte d'Anthropic Ireland Limited — et sa facture de
90 EUR réglée le 19/07 ; le règlement Darnis de 262,20 EUR ; les deux factures
du cycle M4 (part fixe 2 500 USD → 2 140,45 EUR échéance 22/09, part différée
3 000 USD → 2 568,54 EUR échéance 23/11). Les trois écarts bancaires relevés
sont comblés ; Qonto tombe à 3 806,08 EUR.

Le juge pré-gate a bloqué : le règlement Darnis du 14/08 précède la facture
datée du 31/08. C'est un fait bancaire, pas une erreur de saisie — Darnis
facture en fin de mois — et adc-008 avait EXPRESSÉMENT prévu ce blocage et
autorisé le passage outre tracé. L'art. 289 du CGI qu'invoque le juge régit la
numérotation des factures ÉMISES, non l'ordre entre un paiement et sa facture ;
c'est la troisième fois de la session qu'il l'étend hors de son champ.

CLAUSE DE PÉNALITÉS (2026-08-24-clause-bilingue). Les factures M4 sont sorties
avec une clause AMPUTÉE : sans la traduction anglaise, sans la mention « Ces
stipulations sont des minima légaux d'ordre public auxquels il ne peut être
renoncé ». Détecté par le contrôle des mentions obligatoires sur les PDF —
avant tout envoi. La note publique est rétablie dans sa rédaction de référence,
à droit constant : aucun montant, aucune date, aucune ligne touchés.

Deux raisons de corriger plutôt que de laisser courir. La permanence des
méthodes (PCG art. 121-5, adc-011) : une clause légale identique doit être
rédigée identiquement, sans quoi la variation se lit comme une intention. Et le
fond : le destinataire est américain, c'est la version anglaise qui lui rend la
clause opposable en fait — et c'est sur elle que s'appuie la relance.

Le juge a bloqué là aussi, en supposant que la référence portait « ce montant ».
Il ne l'avait pas vérifié, et son propre residual risk demandait de le faire.
Vérification faite et consignée (02b-preuve-reference.txt) : FAC005 à FAC008
portent toutes « ce forfait », et la clause proposée leur est identique au
caractère près — 1283 car. Le finding était inversé.

test/buildInvoicePdf.ts. Valider une facture par l'API ne produit AUCUN PDF : le
fichier n'existe que si quelqu'un l'a demandé. `PUT /documents/builddoc` le
ferait mais répond 403 — ce droit n'est accordé à aucun scope agent. Le script
emprunte donc l'interface, puis relit le fichier par l'API : on ne croit pas la
page de retour, on vérifie que le PDF est déposé et lisible par un tiers.

Les deux juges post-gate passent sans dérive.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-24 10:04:33 +02:00
arcodange c00cf948ed feat(erp): manifeste d'exercice, rejeu à blanc, gardes manquantes, trunk réconcilié (#92)
Co-authored-by: Gabriel Radureau <[email protected]>
2026-08-14 22:05:44 +02:00
arcodange 7c7d41cda7 feat(erp): annoter un objet figé — la date URSSAF portée par une pièce jointe (#90)
Co-authored-by: Gabriel Radureau <[email protected]>
2026-08-13 23:58:03 +02:00
arcodange 480888937f feat(erp): indemnité d'occupation janv→juil 2026 (1 483,23 €) — paiements divers, sans tiers (#89)
Co-authored-by: Gabriel Radureau <[email protected]>
2026-08-13 21:09:10 +02:00
arcodange 216dc0213b feat(erp): verser les pièces juridiques de 1_DOCUMENTS dans la GED (#88)
Co-authored-by: Gabriel Radureau <[email protected]>
2026-08-13 19:15:01 +02:00
arcodangeandClaude Opus 5 81cc3df13c feat(erp): enregistrer les charges sociales — script + runbook rationalisés
Dolibarr n'expose AUCUNE API REST pour les charges sociales (/taxes,
/socialcontributions, /chargesociales répondent tous « API not found »). Le
module est actif, seule l'API manque : le pipeline de promotion, qui parle REST,
ne peut pas porter cette opération. D'où un script UI, gardé par guard.ts.

La sandbox a joué son rôle : quatre doublons y ont été créés pendant la
découverte, sans conséquence, et les quatre pièges du formulaire sont désormais
documentés au lieu d'être redécouverts.

- recordSocialCharge.ts : idempotent (cherche la charge avant de créer),
  vérifie par LECTURE de la liste, type TNS par défaut, --dry-run.
- RUNBOOK_charges_sociales.md : écrit pour être suivi par un agent moins
  performant ou un harness limité — la commande, les garanties, les quatre
  pièges, et ce que le script ne garantit PAS (ni juge, ni artefact de gate).

Les quatre pièges, tous rencontrés :
1. La date visible est décorative : le backend ne lit que les champs CACHÉS
   echday/echmonth/echyear. Remplir le champ texte crée l'enregistrement avec
   une période aberrante (20/06/2000 observé) au lieu d'échouer.
2. Le bouton de soumission n'a pas d'attribut name — le cibler par value.
3. L'URL après soumission ne porte pas d'id : vérifier par l'URL fait conclure
   à un échec sur une création réussie. C'est ce qui a produit les doublons.
4. Les milliers portent une espace insécable (« 1 215,00 ») : une comparaison
   littérale casse au-delà de 999 € et l'idempotence saute en silence.

Correction comptable : 645x → 646 dans known-patterns.json. Les cotisations d'un
gérant TNS sont des cotisations personnelles du dirigeant (646), pas des
cotisations patronales sur salaires (645) — Arcodange n'a aucun salarié.

Appliqué en production : les trois échéances URSSAF 2026, toutes IMPAYÉES.
Reste à vérifier dans le dictionnaire Dolibarr que le type « indépendants »
porte bien le code comptable 646.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-08-13 17:27:17 +02:00
arcodangeandClaude Opus 5 4d1e3ecb23 fix(email-ingest): extraction testable et pinnée au golden set + adc-008
L'extraction de champs vivait dans un heredoc à l'intérieur d'email-inspect.sh :
impossible à exécuter isolément, donc jamais mesurée, donc fausse sans que
personne puisse le voir. Sur la facture Darnis F1048 elle renvoyait le numéro de
TVA d'Arcodange comme référence de facture, et aucune date.

- extract_fields.py : l'extraction sort du shell et devient un module.
- test_extract.py : régression contre les 16 factures hand-vérifiées de
  fleet/golden/invoice-extract/. Score par champ, et une valeur FAUSSE pèse plus
  qu'une valeur absente — un humain recopie ce qui s'affiche.

Valeurs fausses : 4 → 0. Exactitude ref 62,5 → 75 %, date 62,5 → 75 %,
HT 68,8 → 75 %, TTC 81,2 → 93,8 %.

Cinq bugs réels, dont trois invisibles sans test :
- « Nº » sur les factures françaises est U+00BA (ordinal masculin), pas le signe
  degré. La classe [°o] le rate, le motif principal échoue, et le repli attrape
  le premier jeton ref-shaped du document — très souvent un numéro de TVA.
- Le filtre anti-TVA rejetait « FR73261832 », qui est la vraie référence OVH : un
  numéro FR fait exactement 11 caractères après le préfixe.
- « Montant total (HT) » était lu comme un TTC.
- Une référence coupée par la colonne (« 06-01-26- » / « payment-366753 ») était
  renvoyée amputée : le recollage doit précéder le scan, sinon la queue seule est
  trouvée en premier.
- Un `\b` après `€` ne peut jamais matcher en fin de ligne (€ n'est pas un
  caractère de mot) — la TVA n'était jamais extraite.

adc-008 : une facture fournisseur s'enregistre à SA date, même future, tant que
l'exercice (année civile) ne bascule pas. Le document fait foi ; altérer sa date
ferait diverger l'écriture de sa pièce justificative (CGI art. 289 VII).
Registre validé : 8 règles, 8 ADC, 0 erreur.

scopes.ts : 1232 (factures fournisseur) ajouté à prod-write — oubli initial,
révélé par un 403 en production sur F1048. Le pipeline s'est arrêté sans écrire.

Appliqué en production via le pipeline gated : FAF2026014 (Darnis F1048),
218,50 HT + 43,70 TVA = 262,20 TTC, validée, non réglée.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-08-13 10:47:30 +02:00
arcodangeandClaude Opus 5 2e699e1fc6 feat(sandbox): scoped agents provisioned by the checkpoint cycle
Completes the role model on the sandbox side, and closes the third failure of
the 2026-07/08 sessions: a refresh wiped hand-granted rights and nothing
recorded them, so the sandbox silently lost capabilities nobody had written down.

- Provisioned `ai_agent_sandbox_read` (36 rights) and
  `ai_agent_sandbox_sandbox_write` (44 rights) from test/scopes.ts.
  Verified functionally on the live sandbox: the reader reads and gets
  403 on invoice creation; the writer creates a draft and gets 403 on DELETE.
- checkpoint-provision.sh now re-creates both scoped agents after every
  refresh, so their rights come from code rather than from someone's memory.
  Failure to provision a scope warns instead of aborting the whole checkpoint.
- checkpoint-relink-env.sh points the write skill at the scoped writer key,
  falling back to the legacy single-user key so an older checkout still works.
  The write skill now operates as ai_agent_sandbox_sandbox_write (id 6).

Smoke-tested end to end after the credential swap: the promote pipeline
rehearses on the sandbox under the scoped writer, and `apply` still refuses
without a recorded human gate.

The redundant `ai_agent_prod_prod_write` login is documented as deliberate:
renaming a provisioned production credential means creating a second privileged
user and repointing the promote flow — churn for cosmetics.

Left behind in the sandbox: draft invoice id=19, a scope probe. It cannot be
deleted (no scope grants DELETE, which is the point) and the next refresh
reclaims it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-08-09 19:41:37 +02:00
arcodangeandClaude Opus 5 1cba032512 fix(security): production read agent is now actually read-only
Migration applied to production, in the only order that does not break the
promote flow:

1. Created `ai_agent_prod_prod_write` (id=5) with the narrow `prod-write` scope —
   invoices, payments, and document submission (needed by builddoc to regenerate
   a modified invoice's PDF). Verified functionally: reads pass, DELETE on an
   invoice returns 403.
2. Repointed the promote pipeline at that user's key. It no longer borrows the
   read skills' credential; if the key is absent it dies with the provisioning
   command rather than silently falling back.
3. Revoked 12 write/delete rights from `ai_agent` (id=3), the credential every
   read skill holds: create/modify on customer AND supplier invoices,
   thirdparties, contacts, thirdparty payment details, proposals, exports,
   accounting links — and delete on proposals, events, and GED documents.

Verified after: invoices, thirdparties, contacts, products, proposals, supplier
invoices and bank accounts all still read; creating an invoice returns
`403 Forbidden: Insuffisant rights`. The documented posture and the real one
finally agree.

scopes.ts corrected against the live instance: 262 is NOT "créer/modifier les
produits" as the first catalogue guessed but the `voir_tous` ACL extension — a
READ right the skills depend on (without it, list endpoints return empty arrays
instead of 403). Revoking it would have silently blinded every read skill. This
is why the audit reads labels off /user/perms.php rather than trusting ids in
code. The READ_ONLY baseline is now the audited read surface (35 rights), not a
guess.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-08-09 19:33:00 +02:00
arcodangeandClaude Opus 5 11c8c65d45 feat(test): scoped AI users — one user per (environment × scope), provisioned by script
provisionSandbox.ts granted `[12, 122, 262, 32, 111, 251]`: opaque numeric ids,
one fixed scope, one agent per environment. Three failures in the 2026-07/08
sessions came straight from that:

1. Production `ai_agent` — documented as READ-ONLY in AGENTS.md, ADR-0003 and
   every SKILL.md — actually holds 46 rights against 9 declared, including
   create/modify on customer AND supplier invoices, thirdparties, contacts,
   thirdparty payment details, proposals, plus THREE delete rights (proposals,
   events, and submit/delete documents in the GED).
2. Writing a payment, then a product, to production each required granting a
   right by hand and revoking it after. A privilege granted ad hoc under time
   pressure is worse than one nobody holds.
3. A sandbox refresh wiped the agent's proposal rights, because they had been
   granted manually and lived nowhere in code.

- scopes.ts declares three scopes with their purpose and allowed environments:
  `read` (both envs), `sandbox-write` (sandbox only), `prod-write` (production
  only, narrow: invoices + payments, what the gated promote apply actually
  does). resolveScope() refuses a scope on an environment it does not belong to.
  No scope grants DELETE — the ledger is append-only, deletion stays human.
- provisionAiUser.ts creates or aligns one user per (environment × scope), emits
  its API key to a gitignored 600 file, and has an --audit mode that diffs what a
  user HOLDS against what its scope DECLARES. Production writes require the
  guard.ts opt-in.
- The audit reads permission labels LIVE off /user/perms.php rather than trusting
  a catalogue in code: ids are stable per Dolibarr version, not across them, and
  an audit that cannot name what it found is not actionable.

findUserId is implemented locally rather than imported: the trunk's userSetup.ts
has one, but it is uncommitted WIP and a provisioning script must not depend on
someone's working tree.

Tooling only — no production rights were changed. The migration (create the
scoped users, repoint the promote pipeline, then strip the over-grants from
`ai_agent`) rotates credentials used by every read skill and is the operator's
call.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-08-09 18:13:21 +02:00
arcodangeandClaude Opus 5 dbe4b36c62 feat(write-skill): chronology guard + explicit production opt-in
Two guards, both from real incidents in the same session.

1. invoice-create.sh — chronology (CGI art. 289). Dolibarr assigns the number at
   validation, in creation order, so issuing a document dated BEFORE the last one
   already issued gives a higher number to an earlier date. The July plan walked
   straight into it: the M3 deferred part is due 2026-10-23 and must be issued at
   D-60 (24/08) to stay under the L.441-10 I ceiling, while the M4 fixed part is
   dated 23/08 — issue them in the wrong order and the numbering breaks. The
   guard reads the last issued document of the same kind and refuses an earlier
   date, with ARCO_ALLOW_BACKDATE as a loud, documented override.
   Verified: refuses a 01/07 invoice against FAC008 (23/07), accepts 23/08.

2. test/scripts/guard.ts — production opt-in. The sandbox-only guard had no way
   to express a deliberate production run, so any prod work meant bypassing it
   entirely (which is how guards die). Production now requires BOTH
   ARCO_ALLOW_PRODUCTION=<exact host> and
   ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD, and prints a banner. Nothing
   reaches prod by inheriting an ambient variable.
   Also fixes a misleading "(sandbox verified)" log that printed even on prod.

grantAgentRight.ts joins the repo (it was never committed) and gains --revoke,
so a temporarily elevated right can be handed back — used today to attach a
payment in production and revoked immediately after.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-26 01:26:12 +02:00
arcodangeandClaude Opus 5 aa43862076 feat(test): host guard for UI admin scripts + sandbox legal-mentions setup
test/.env ships DOLIBARR_ADDRESS pointing at PRODUCTION and test/main.ts
defaults to it, so any Playwright admin script run with the ambient
environment drives the real ERP. The REST path has been structurally safe
since ADR-0003 (dol-write.sh refuses non-sandbox hosts); the UI path had no
equivalent. scripts/guard.ts closes that gap — assertSandbox() resolves the
target and refuses anything that is not erp-sandbox.*, with the override
spelled out in the error. Verified: an unqualified run now dies instead of
reaching prod.

Two settings the write agent cannot reach (non-admin by design, 403 on
/setup/conf), rehearsed on the sandbox:

- sandboxLegalSetup.ts — capital social + multi-currency module. Finding:
  the capital was NOT missing, it was stored as "1000€"; the symbol made the
  value unusable by the PDF template, which is why "Capital de 1 000 €" was
  absent from every invoice since January (C. com. R.123-238). Normalised to
  "1000" → the mention now renders.
- sandboxCurrencySetup.ts — registers the USD reference rate. Enabling the
  module is not enough: a currency absent from the rate table makes Dolibarr
  silently fall back to EUR (observed on a probe invoice). With the rate
  registered, an invoice carries USD 3,000.00 with its EUR counter-value,
  i.e. the contractual obligation itself rather than a drifting equivalent.

Both scripts are report-only when they cannot recognise a form, and screenshot
what they did.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-25 23:14:15 +02:00
arcodangeandClaude Fable 5 0240c519b1 fix(provision): grant user lire (251) to ai_agent_sandbox so the armed probe passes
The checkpoint status/relink-env armed probe calls GET /users/info, which
requires Dolibarr right 251 (user->user->lire). WRITE_IDS didn't include it,
so a freshly provisioned agent answered 403 on the probe — reported NOT armed
— while its key actually authenticates (GET /thirdparties -> 200). Right 251
was granted live in SQL on the sandbox (fk_user=4) today; this persists it in
WRITE_IDS so every future provision grants it.

Also teach both probes to tell the failure modes apart instead of one opaque
message: 401 = key rejected (stale/instance-encrypted -> re-provision),
403 = key OK but right 251 missing (-> grant it / re-provision), 200 = armed.
Docs updated accordingly (checkpoint SKILL.md probe outcomes, sandbox-write
SKILL.md gotcha, test/README.md rights table synced to WRITE_IDS incl. 262/111).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 17:38:30 +02:00
arcodangeandClaude Opus 4.7 7dcd982448 fix(sandbox-poc): regenerate api_key when the existing one is garbage; note platform backup gaps
After an iso-prod refresh the instance unique-id changes, so an api_key encrypted
with the OLD id can't be decrypted — Dolibarr renders non-UTF-8 bytes in the field.
The POC's generateApiKey reused any non-empty value, so it copied that garbage into
test/.ai_agent_sandbox.key (corrupt key, 401s). Now it reuses ONLY a clean key
(^[A-Za-z0-9_-]{24,}$); otherwise it clears the field and regenerates. So
`checkpoint provision` after a refresh yields a fresh, working key.

Also documents the open PLATFORM follow-ups in ops/backup/README.md (easy to find
when revisiting ERP backups): the orphaned Longhorn `default` recurring-job group
(other cluster volumes have no offsite backup), and verifying the factory
pg_dumpall host cron.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 22:44:40 +02:00
arcodangeandClaude Opus 4.7 64d2cb4237 feat(skills,cli): supplier avoirs + banque lire (bank-account discovery)
The two V9 follow-ups, both proven live on the sandbox.

- creditnote-create.sh: `kind:"supplier"` makes an avoir fournisseur on
  /supplierinvoices (type=2 + fk_facture_source, carries ref_supplier); default
  customer path unchanged. Proven: customer AVC002 (-240) + supplier AVF2026001
  (-144, ref_supplier carried, linked to source, validated).
- bank-accounts.sh + `arcodange sandbox accounts`: list bank accounts (id/label/
  bank) so a payment can pick its account_id. Needs `banque lire` (rights 111),
  now added to the provisioner's WRITE_IDS so fresh runs include it; the existing
  ai_agent_sandbox user was granted it live. GET /bankaccounts now returns the 3
  accounts (QONTO, WISE EURO, Compte Courant Asso).
- SKILL.md: supplier-avoir example + accounts helper + updated banque-lire note.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:07:48 +02:00
arcodange c269751422 Merge pull request 'fix(test): grant societe client voir (262) so /thirdparties list works' (#20) from claude/sandbox-provision-run into main 2026-06-29 20:17:20 +02:00
arcodangeandClaude Opus 4.7 7619a4f358 fix(test): grant societe client voir (262) so /thirdparties list works
Validating ai_agent_sandbox's key against the sandbox API, /thirdparties
returned 404 (the voir_tous ACL trap) while /invoices, /products,
/supplierinvoices returned 200. The missing right is `societe client voir`
(id 262, "see all thirdparties") — prod's ai_agent has it. Added it to
WRITE_IDS so the list endpoint works; other modules' lists are fine with plain
`lire`.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 20:16:59 +02:00
arcodange 37865c55c5 Merge pull request 'fix(test): persist API key + anchor rights selector + idempotent createUser' (#19) from claude/sandbox-provision-run into main 2026-06-29 14:31:24 +02:00
arcodangeandClaude Opus 4.7 18c5d0ebda fix(test): persist generated API key + anchor rights selector + idempotent createUser
First real run against the sandbox revealed three issues in userSetup.ts:

1. generateApiKey generated the key client-side and read it into the file but
   never submitted the edit form, so Dolibarr never persisted api_key (DB stayed
   NULL → the key could not authenticate). Now it clicks Save after generating.

2. assignRights matched `rights=<id>` as an href substring, so a short id like
   12 (facture creer) also matched rights=121 / rights=1232 and .first() clicked
   the wrong link — facture creer was never granted. Anchored with a trailing
   "&" (rights=<id>&) for an exact match.

3. createUser was not idempotent: a re-run hit the existing login and failed to
   parse a new id. Added findUserId (look up by login via the user list) and
   return the existing id instead of creating a duplicate.

Verified the symptoms live: ai_agent_sandbox (rowid 4) had api_key NULL and was
missing only facture/creer among the 11 intended rights.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 14:30:36 +02:00
arcodangeandClaude Opus 4.7 416cd807a3 fix(sandbox): run Dolibarr in dev mode (DOLI_PROD=0) + correct install.lock path
After seeding erp-sandbox from prod, the home dashboard rendered a generic
"technical error" banner per box: prod mode ($dolibarr_main_prod=1, the image
default via DOLI_PROD) escalates the seed's minor non-fatal warnings into that
banner. Setting DOLI_PROD=0 for non-prod environments makes Dolibarr render
real errors inline (correct for a rehearsal env) and clears the banners.

config.yaml adds `DOLI_PROD: "0"` only when env != prod, so the prod configmap
is byte-identical (prod keeps the image default DOLI_PROD=1) — verified via
helm template diff. ArgoCD rolls only the sandbox pod.

Also corrects the test/README install.lock path: Dolibarr checks the DATA root
(/var/www/documents, a PVC — persists across restarts), not /var/www/html. And
notes that a prod-seeded sandbox still needs install.lock created (the seed +
documents/mycompany sync don't include it).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 14:09:59 +02:00
arcodangeandClaude Opus 4.7 e4a7f99333 feat(test): split env config — .env (prod) vs .env.sandbox (sandbox)
provisionSandbox.ts now loads its own .env.sandbox (via @std/dotenv loadSync)
instead of the shared .env, so prod (main.ts → .env) and sandbox
(provisionSandbox.ts → .env.sandbox) configs don't collide. .gitignore widened
to .env* (keeping .env.example tracked). .env.example rewritten to document the
two-file convention + the per-env kubectl secret sources, including the caveat
that a prod-seeded sandbox uses PROD's admin password.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 11:26:05 +02:00
arcodangeandClaude Opus 4.7 c010099dae docs(test): preserve the install.lock step in test/README
The pre-existing (untracked) test/README documented creating Dolibarr's
install.lock after a fresh install — a non-obvious operational step missing from
the rewritten README. Preserve it (generalized to the per-env namespace/label,
with a note that a prod-seeded instance doesn't need it).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 07:54:01 +02:00
arcodangeandClaude Opus 4.7 523f0cf001 feat(test): provision erp-sandbox via Playwright (REST API + write-scoped ai_agent_sandbox user)
Extend the Deno + Playwright UI-automation POC to provision the erp-sandbox
Dolibarr for the AI agent:

- moduleSetup.ts: add enableApiModule(ctx) — toggles the REST API / Web services
  module on /admin/modules.php (kanban). Resilient: tries the fr_FR card label
  "API/Web services REST (serveur)" first, falls back to a /API.*REST|REST.*API/i
  title match if the exact label is absent.
- userSetup.ts (new): createUser (returns the new numeric id), assignRights
  (clicks each addrights link on /user/perms.php, idempotent), generateApiKey
  (triggers Dolibarr's generate control on the user card and reads the value back).
- provisionSandbox.ts (new entrypoint, main.ts untouched): login → enable API →
  create ai_agent_sandbox (non-admin) → grant write rights → generate API key,
  then write the key to test/.ai_agent_sandbox.key (gitignored) instead of
  printing it.
- .gitignore (new), .env.example + README.md: sandbox vars, the
  deno run --allow-all provisionSandbox.ts command, and kubectl one-liners to
  pull DOLI_ADMIN_PASSWORD (secretkv) / DOLI_DB_PASSWORD (vso-db-credentials)
  from the erp-sandbox namespace.

Why UI not SQL: API keys are encrypted with the instance's DOLI_INSTANCE_UNIQUE_ID,
so the key must be generated by the sandbox itself, not INSERTed raw.

deno check passes for provisionSandbox.ts and scripts/admin/userSetup.ts.
NOT run end-to-end: the sandbox Dolibarr is not installed yet (empty DB / fresh
install wizard), so the selectors are best-effort Dolibarr 22 conventions and
must be confirmed on the first real run.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 07:34:21 +02:00
arcodange afba67c68c .duckdns.org to internal dns .lab 2026-01-03 18:50:39 +01:00
arcodange f4d450c75a work in progress with deno 2025-08-08 17:57:56 +02:00
arcodange 9d4d33ef45 begin setup automation with deno and playwright 2024-11-15 16:08:41 +01:00