diff --git a/fleet/harness/promote/pipeline.py b/fleet/harness/promote/pipeline.py index 612d31e..3fbad61 100644 --- a/fleet/harness/promote/pipeline.py +++ b/fleet/harness/promote/pipeline.py @@ -229,8 +229,21 @@ def stage_apply(args) -> None: if os.environ.get("ARCO_PROD_CONFIRM") != "I-UNDERSTAND-THIS-WRITES-PROD": die("set ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD to apply to production") + # The production writer is its OWN user with the narrow `prod-write` scope + # (test/scopes.ts) — not the read-only `ai_agent` whose key every read skill + # holds. A read credential must never be able to write the ledger, and the + # writer must not be lying around in a file a dozen skills load. cfg = env_from(os.path.join(SKILL, "dolibarr", ".env")) - base, key = cfg["DOLIBARR_URL"], cfg["DOLIBARR_API_KEY"] + base = cfg["DOLIBARR_URL"] + key_file = os.path.join(REPO, "test", ".ai_agent_prod_prod_write.key") + if not os.path.exists(key_file): + die("no production writer key.\n" + f" expected: {key_file}\n" + " provision it: DOLIBARR_ADDRESS=https://erp.arcodange.lab " + "ARCO_ALLOW_PRODUCTION=erp.arcodange.lab " + "ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD \\\n" + " deno run -A test/provisionAiUser.ts --scope prod-write --env production") + key = open(key_file).read().strip() if SANDBOX_HOST in base: die(f"the prod .env points at the sandbox ({base}) — nothing to promote to") print(f"*** PRODUCTION: {base} — approved by {gate['by']} at {gate['at']} ***") diff --git a/test/scopes.ts b/test/scopes.ts index 99eab78..b8ffd2d 100644 --- a/test/scopes.ts +++ b/test/scopes.ts @@ -41,27 +41,79 @@ export interface Scope { * `provisionAiUser.ts --audit` prints the live labels next to each granted id. */ export const PERMISSION_LABELS: Readonly> = { + // --- lecture / export (le socle read) --- 11: "Lire les factures clients", + 16: "Exporter les factures clients", + 21: "Consulter les propositions commerciales", + 28: "Exporter les propositions commerciales", + 31: "Lire les comptes bancaires", + 41: "Lire les projets et les tâches", + 45: "Exporter les projets", + 91: "Lire les charges fiscales ou sociales et paiement de TVA", + 94: "Exporter les charges fiscales ou sociales", + 111: "Lire les factures fournisseurs", + 121: "Consulter les tiers", + 126: "Exporter les tiers", + 141: "Lire tous les projets et tâches", + 161: "Lire les contrats/abonnements", + 167: "Exporter les contrats", + 251: "Lire les utilisateurs", + 262: "Étendre l'accès à TOUS les tiers et leurs objets (voir_tous)", + 281: "Consulter les contacts", + 358: "Exporter les utilisateurs", + 531: "Consulter les services", + 771: "Lire les notes de frais", + 779: "Exporter les notes de frais", + 1181: "Consulter les fournisseurs", + 1182: "Consulter les commandes fournisseurs", + 1191: "Exporter les commandes fournisseurs", + 1201: "Récupérer le résultat d'un export", + 1231: "Lire les factures (et paiements) fournisseurs", + 1236: "Exporter les factures fournisseur", + 1321: "Exporter les factures clients, attributs et règlements", + 2401: "Lire ses propres actions/événements", + 2411: "Lire les actions/événements des autres", + 2414: "Exporter les événements des autres", + 2501: "Lire/récupérer les documents", + 3201: "Lire les événements archivés et leurs empreintes", + 50411: "Lire les opérations du Grand livre", + + // --- écriture (jamais dans le scope read) --- 12: "Créer/modifier les factures clients", 14: "Émettre des paiements sur les factures clients", - // 15 = "Supprimer les factures clients" — DELIBERATELY NEVER GRANTED - 16: "Exporter les factures clients", - 121: "Consulter les tiers", - 122: "Créer/modifier les tiers", - // 125 = "Supprimer les tiers" — DELIBERATELY NEVER GRANTED - 126: "Exporter les tiers", - 241: "Consulter les propositions commerciales", - 242: "Créer/modifier les propositions commerciales", - // 244 = "Supprimer les propositions commerciales" — DELIBERATELY NEVER GRANTED - 261: "Consulter les produits/services", - 262: "Créer/modifier les produits/services", - 31: "Lire les comptes bancaires", + 22: "Créer/modifier les propositions commerciales", 32: "Créer/modifier les comptes bancaires", - 111: "Lire les factures fournisseurs", - 251: "Lire les utilisateurs", + 122: "Créer/modifier les tiers", + 130: "Créer/modifier les informations de paiement des tiers", + 282: "Créer/modifier les contacts", + 1202: "Créer/modifier un export", + 1232: "Créer les factures fournisseur", + 2402: "Créer/modifier des actions/événements", + 2503: "Soumettre ou supprimer des documents", + 50401: "Lier les produits et factures avec des comptes comptables", + + // --- suppression : JAMAIS accordée par aucun scope --- + // 15 Supprimer les factures clients + // 27 Supprimer les propositions commerciales + // 125 Supprimer les tiers + // 2403 Supprimer des actions/événements } as const; -const READ_ONLY: ReadonlyArray = [11, 111, 121, 126, 241, 261, 31, 251, 16]; +/** + * Le socle de lecture — établi par audit de l'instance de production, pas + * deviné : c'est l'ensemble exact des droits de lecture/export dont les skills + * se servent aujourd'hui (factures clients et fournisseurs, tiers, contacts, + * documents, contrats, projets, banque, grand livre, TVA). + * + * `262` (voir_tous) est indispensable : sans lui, les endpoints de liste + * renvoient des tableaux vides plutôt qu'un 403 — le piège documenté par la + * skill `dolibarr`. + */ +const READ_ONLY: ReadonlyArray = [ + 11, 16, 21, 28, 31, 41, 45, 91, 94, 111, 121, 126, 141, 161, 167, 251, 262, + 281, 358, 531, 771, 779, 1181, 1182, 1191, 1201, 1231, 1236, 1321, 2401, + 2411, 2414, 2501, 3201, 50411, +]; export const SCOPES: Readonly> = { /** @@ -83,7 +135,7 @@ export const SCOPES: Readonly> = { "sandbox-write": { purpose: "Répétition d'un change-set sur la sandbox (factures, tiers, produits, propositions, règlements)", environments: ["sandbox"], - rights: [...READ_ONLY, 12, 122, 262, 242, 14, 32], + rights: [...READ_ONLY, 12, 14, 22, 32, 122, 130, 282, 1232, 2503], }, /** @@ -98,7 +150,11 @@ export const SCOPES: Readonly> = { "prod-write": { purpose: "Écriture de production, gated — création/modification de factures et rattachement de règlements", environments: ["production"], - rights: [...READ_ONLY, 12, 14], + // 2503 est nécessaire à builddoc (régénérer le PDF d'une facture modifiée). + // Dolibarr le livre en bundle « soumettre OU supprimer » : on ne peut pas + // avoir l'un sans l'autre. C'est la seule capacité de suppression du modèle, + // et elle est confinée au writer de production, gated par le promote. + rights: [...READ_ONLY, 12, 14, 2503], }, } as const;