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
test — Dolibarr UI automation (Deno + Playwright)
A small Deno + Playwright POC that drives the Dolibarr admin UI in the fr-FR
locale. Playwright fills the same forms a human admin would, so the automation
works even where the REST API can't (e.g. generating an API key, which is
encrypted with the instance's own DOLI_INSTANCE_UNIQUE_ID).
Layout
main.ts— original entrypoint (first install, company/display/module setup).provisionSandbox.ts— entrypoint that provisions theerp-sandboxinstance for the AI agent (enable REST API, create a write-scoped user, generate its API key).scripts/login.ts— admin login / logout / whoami helpers.scripts/forms.ts—fillForm,toggleOnOff, CKEditor/ACE helpers.scripts/admin/moduleSetup.ts—configureModule,enableApiModule.scripts/admin/userSetup.ts—createUser,assignRights,generateApiKey.
Configure
Copy .env.example to .env and fill it in. .env, *.key, and
.ai_agent_sandbox.key are gitignored — never commit secrets.
cp .env.example .env
Lock the installer (after a fresh install via main.ts)
Dolibarr keeps its web installer reachable until an install.lock file exists.
After a fresh install (the main.ts flow), create it in the target pod — for the
sandbox:
kubectl -n erp-sandbox exec \
"$(kubectl get pod -n erp-sandbox -l app.kubernetes.io/instance=erp-sandbox -o name)" -- \
/bin/sh -c 'touch /var/www/documents/install.lock && chown www-data:www-data /var/www/documents/install.lock'
The path is the Dolibarr data root (/var/www/documents, a PVC) — that's where
Dolibarr checks, and being on the PVC the lock persists across pod restarts. For
prod, swap to -n erp -l app.kubernetes.io/instance=erp. A sandbox seeded from
prod still needs this: the seed (see ../ops/sandbox/) copies the DB +
documents/mycompany, not install.lock.
Provision the sandbox
Provisions erp-sandbox.arcodange.lab: enables the REST API module, creates the
write-scoped ai_agent_sandbox user, grants it its write rights, and has
Dolibarr generate the user's API key. The key is written to
test/.ai_agent_sandbox.key (gitignored) — it is never printed.
cd test
deno run --allow-all provisionSandbox.ts
Populate .env from the erp-sandbox namespace secrets first. secretkv
carries the app env (including DOLI_ADMIN_PASSWORD); vso-db-credentials
carries the database password:
# Admin password (key DOLI_ADMIN_PASSWORD inside the secretkv secret)
kubectl get secret secretkv -n erp-sandbox \
-o jsonpath='{.data.DOLI_ADMIN_PASSWORD}' | base64 -d
# Database password (key `password` inside vso-db-credentials)
kubectl get secret vso-db-credentials -n erp-sandbox \
-o jsonpath='{.data.password}' | base64 -d
Set in .env:
DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab
DOLI_ADMIN_LOGIN=admin
DOLI_ADMIN_PASSWORD="<from secretkv above>"
DOLI_DB_PASSWORD="<from vso-db-credentials above>"
# Optional — otherwise a random password is generated and only the API key emitted:
# AI_AGENT_SANDBOX_PASSWORD="<choose one>"
After it runs
The generated API key lands in test/.ai_agent_sandbox.key. Next step (not
automated by this POC): load it into the dolibarr skill's sandbox config /
Vault at kvv2/erp-sandbox/ai_agent.
Important
The sandbox Dolibarr is not installed/provisioned yet (empty DB, fresh install wizard). Until the install wizard has been completed against the sandbox,
provisionSandbox.tswill not have a UI to drive, and the selectors inmoduleSetup.ts/userSetup.tsare best-effort (Dolibarr 22 conventions, not verified live). Confirm them on the first real run.
Write rights granted
The ai_agent_sandbox user is created non-admin and granted (the authoritative
list is WRITE_IDS in provisionSandbox.ts):
| Module | rights ids |
|---|---|
| facture | lire=11, creer=12 |
| societe | lire=121, creer=122, client voir=262 |
| societe contact | lire=281, creer=282 |
| fournisseur | lire=1181, facture lire=1231, facture creer=1232 |
| produit | lire=31, creer=32 |
| banque | lire=111 |
| user | lire=251 — requis par le probe armé GET /users/info (checkpoint status/relink-env) |