From 5955ea37a2a8015e29495d7490a30dee1954c2aa Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 29 Jul 2026 15:57:37 +0200 Subject: [PATCH 1/2] =?UTF-8?q?chore(tickets):=20synchronise=20l'=C3=A9tat?= =?UTF-8?q?=20ticketing=20courant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Met à jour les carnets/issues existants et ajoute les tickets #108, #109, #111, #112 créés durant le cycle. État runtime sans rapport avec le lot de code #82, isolé dans son propre commit. Co-Authored-By: Claude Sonnet 5 --- .ideai/agents/git.md | 19 +- .ideai/agents/qa.md | 6 +- .ideai/tickets/103/carnet.md | 6 +- .ideai/tickets/103/issue.md | 8 +- .ideai/tickets/107/carnet.md | 4 +- .ideai/tickets/107/issue.md | 6 +- .ideai/tickets/108/carnet.md | 6 + .ideai/tickets/108/issue.md | 17 ++ .ideai/tickets/109/carnet.md | 6 + .ideai/tickets/109/issue.md | 17 ++ .ideai/tickets/111/carnet.md | 6 + .ideai/tickets/111/issue.md | 17 ++ .ideai/tickets/112/carnet.md | 6 + .ideai/tickets/112/issue.md | 17 ++ .ideai/tickets/15/carnet.md | 6 +- .ideai/tickets/15/issue.md | 8 +- .ideai/tickets/58/carnet.md | 6 +- .ideai/tickets/58/issue.md | 8 +- .ideai/tickets/67/carnet.md | 4 +- .ideai/tickets/67/issue.md | 6 +- .ideai/tickets/71/carnet.md | 6 +- .ideai/tickets/71/issue.md | 8 +- .ideai/tickets/73/carnet.md | 6 +- .ideai/tickets/73/issue.md | 8 +- .ideai/tickets/80/carnet.md | 6 +- .ideai/tickets/80/issue.md | 8 +- .ideai/tickets/84/carnet.md | 6 +- .ideai/tickets/84/issue.md | 8 +- .ideai/tickets/85/carnet.md | 6 +- .ideai/tickets/85/issue.md | 8 +- .ideai/tickets/91/carnet.md | 4 +- .ideai/tickets/91/issue.md | 6 +- .ideai/tickets/96/carnet.md | 4 +- .ideai/tickets/96/issue.md | 6 +- .ideai/tickets/99/carnet.md | 6 +- .ideai/tickets/99/issue.md | 8 +- .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 451 +++++++++++++++++++++++++++++++++-- 38 files changed, 621 insertions(+), 115 deletions(-) create mode 100644 .ideai/tickets/108/carnet.md create mode 100644 .ideai/tickets/108/issue.md create mode 100644 .ideai/tickets/109/carnet.md create mode 100644 .ideai/tickets/109/issue.md create mode 100644 .ideai/tickets/111/carnet.md create mode 100644 .ideai/tickets/111/issue.md create mode 100644 .ideai/tickets/112/carnet.md create mode 100644 .ideai/tickets/112/issue.md diff --git a/.ideai/agents/git.md b/.ideai/agents/git.md index 5763bc6..9c482b0 100644 --- a/.ideai/agents/git.md +++ b/.ideai/agents/git.md @@ -37,7 +37,7 @@ Tu t'occupes **du repo git local** : Le dépôt s'articule autour de trois niveaux : -``` +```text main ← branche de RELEASE. Stable, livrable. On n'y commite jamais en direct. │ develop ← branche d'INTÉGRATION. On y merge chaque feature une fois TERMINÉE et VERTE. @@ -62,7 +62,7 @@ feature/* ← une branche PAR nouvelle feature. C'est là que le dev se fait. Tu interviens à **deux moments** du cycle de dev (cf. CLAUDE.md §3), encadré par Main : -``` +```text 1. Main : « nouvelle feature X » (architecture cadrée par Architect) → TOI : décider de la branche. - nouvelle feature indépendante → créer feature/X depuis develop, switch dessus @@ -107,7 +107,18 @@ tu le dis. --- -## 5. Délégation & collaboration +## 5. Sous-repos Git imbriqués + +- Un dossier du projet pouvant contenir son propre `.git` fait **pleinement partie de ton périmètre** de gestion du dépôt local. +- Tu **ne redemandes pas à l'utilisateur** quoi faire pour un sous-repo/sous-module/sous-dépôt : tu examines l'état réel et tu **tranches**. +- Si un sous-repo est un **vrai sous-module voulu**, tu le traites comme tel (gitlink, état détaché, commit du pointeur dans le repo parent si pertinent). +- Si un sous-repo est un **dépôt imbriqué accidentel ou non initialisé** qui bloque l'intégration locale, tu prends la décision locale appropriée pour permettre le commit correct du lot dans le repo principal, puis tu la rapportes clairement à Main. +- Si le lot porte sur des fichiers d'un sous-repo imbriqué, tu dois décider comment les versionner proprement au lieu de déclarer un blocage par défaut. +- Tu ne considères pas la simple présence d'un `.git` imbriqué comme un motif suffisant pour t'arrêter ou renvoyer la décision à l'utilisateur. + +--- + +## 6. Délégation & collaboration - Quand Main te délègue une tâche via IdeA, tu la traites puis tu termines ton tour avec ta réponse normale. IdeA capture automatiquement ta réponse finale ; tu ne gères pas @@ -116,4 +127,4 @@ tu le dis. court), ce que tu as mergé/rebasé, et **ta décision** (pourquoi cette branche, pourquoi ce merge ou ce non-merge). - En cas de conflit de merge/rebase, tu le signales à Main avec le détail ; tu ne forces - pas une résolution hasardeuse. + pas une résolution hasardeuse. \ No newline at end of file diff --git a/.ideai/agents/qa.md b/.ideai/agents/qa.md index 70acc68..40f7ec0 100644 --- a/.ideai/agents/qa.md +++ b/.ideai/agents/qa.md @@ -10,7 +10,7 @@ ## 1. Ta mission (le cycle, §3 de la méthode) -``` +```text DevBackend/DevFrontend écrit le code → TOI : tu écris les tests unitaires + tu les exécutes → vert : feature validée @@ -28,6 +28,7 @@ tu le signales tel quel. - `crates/application` : use cases avec **fakes** des ports (jamais d'adapter concret). - `crates/infrastructure` : adapters concrets (peuvent toucher FS temporaire), tests d'intégration ciblés. - `crates/app-tauri` : DTO (round-trip serde), wiring. +- `crates/web-server` : handlers / Web API / mapping requête-réponse / erreurs. - Commandes : `cargo test -p ` ciblé, `cargo test --workspace` global. **Frontend (TS/React)** : @@ -42,6 +43,7 @@ tu le signales tel quel. - **Round-trip de sérialisation** (DTO ↔ domaine, fichiers `.ideai/*.json`). - **Régressions** : avant de valider un lot, relance la suite complète des crates touchées. - **Pas de faux vert** : un test tautologique ou qui ne s'exécute pas n'est pas un test. +- **Tests fonctionnels Web API** : dès qu'une feature passe par `web-server` ou une surface serveur/HTTP/JSON-RPC analogue, tu dois chercher une preuve fonctionnelle réelle sur les requêtes/réponses du serveur, pas seulement des tests de store/use case. Si le câblage serveur existe, ton objectif par défaut est d'avoir au moins un test qui exerce la requête publique correspondante et qui aurait échoué si le handler/DTO/route était cassé. ## 4. Format du rapport d'erreurs @@ -75,4 +77,4 @@ Trois chantiers (cadence **A+B ensemble, puis C**). Points de vigilance test : fichier quand un profil ne supporte pas MCP, la non-régression du protocole `.ideai/requests`. Tu interviens **après** le cadrage d'`Architect`, en binôme avec le dev du lot concerné, jusqu'au -vert. +vert. \ No newline at end of file diff --git a/.ideai/tickets/103/carnet.md b/.ideai/tickets/103/carnet.md index c74429c..dcd02f7 100644 --- a/.ideai/tickets/103/carnet.md +++ b/.ideai/tickets/103/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#103" -version: 4 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1785013507979 +version: 5 +updatedBy: {"kind":"user"} +updatedAt: 1785271085635 --- # Carnet #103 — permission réseau exposée dans IdeA diff --git a/.ideai/tickets/103/issue.md b/.ideai/tickets/103/issue.md index 8232181..4d4db5a 100644 --- a/.ideai/tickets/103/issue.md +++ b/.ideai/tickets/103/issue.md @@ -2,16 +2,16 @@ id: "3d9021da-26c3-439d-9463-8d206bd06f1b" number: 103 title: "Exposer et piloter la permission réseau des agents/commandes dans IdeA" -status: "qa" +status: "closed" priority: "high" sprint: null links: [] agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1785011668081 -updatedAt: 1785013507979 -version: 4 +updatedAt: 1785271085635 +version: 5 --- ## Problème diff --git a/.ideai/tickets/107/carnet.md b/.ideai/tickets/107/carnet.md index 47158a8..985d95d 100644 --- a/.ideai/tickets/107/carnet.md +++ b/.ideai/tickets/107/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#107" -version: 3 +version: 4 updatedBy: {"kind":"user"} -updatedAt: 1785136945441 +updatedAt: 1785271098557 --- diff --git a/.ideai/tickets/107/issue.md b/.ideai/tickets/107/issue.md index ab84b6e..0f826dd 100644 --- a/.ideai/tickets/107/issue.md +++ b/.ideai/tickets/107/issue.md @@ -2,7 +2,7 @@ id: "6a79006b-0201-4176-ae54-39a05cc3baa6" number: 107 title: "[Bug] croisement entre les projet des retours des agents" -status: "open" +status: "closed" priority: "critical" sprint: null links: [] @@ -10,8 +10,8 @@ agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"} createdBy: {"kind":"user"} updatedBy: {"kind":"user"} createdAt: 1785136727824 -updatedAt: 1785136945441 -version: 3 +updatedAt: 1785271098557 +version: 4 --- Il y a un soucis très important que j'ai constatés. J'ai actuellement 2 projets ouverts: IdeA et GameTime. Les deux projets travaillaient en même temps et j'ai vu GameTime qui semblait récupérer une requete du projet IdeA. Pour plus de précision, sur mes deux projets, j'ai un agent Git qui utilise OpenCode et un modelle local llamacpp, et j'ia eu l'impression qu'ils ont tous les deux appelé leur agent Git mais GameTime à recus la réponse de IdeA car la réponse parlait d'une branche du projet IdeA. Je ne suis pas totalement sur de ce que j'avance, la seule chose dont je suis sur, c'est que GameTime s'est vu adressé une réponse qui était déstinée au projet IdeA. \ No newline at end of file diff --git a/.ideai/tickets/108/carnet.md b/.ideai/tickets/108/carnet.md new file mode 100644 index 0000000..23e47be --- /dev/null +++ b/.ideai/tickets/108/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#108" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1785328001206 +--- diff --git a/.ideai/tickets/108/issue.md b/.ideai/tickets/108/issue.md new file mode 100644 index 0000000..399863d --- /dev/null +++ b/.ideai/tickets/108/issue.md @@ -0,0 +1,17 @@ +--- +id: "d5953745-406f-406f-9008-916de0527cbf" +number: 108 +title: "Ajouter la possibilité de joindre des fichiers aux tickets" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +attachments: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1785311525586 +updatedAt: 1785328001206 +version: 3 +--- +J'aimerais pouvoir ajouter des fichiers (photo, texte, xml etc...) lisible par les agents AI a mes tickets. Dans le cas ou un fichier a déjà été traité par un agent, il faudrait que le fichier soit résumé dans le carnet et flag par les agents de façon a ce que si plusieurs agents lisent le même tickets, ils ne grillent pas tous leurs tokens a lire le fichier \ No newline at end of file diff --git a/.ideai/tickets/109/carnet.md b/.ideai/tickets/109/carnet.md new file mode 100644 index 0000000..008bede --- /dev/null +++ b/.ideai/tickets/109/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#109" +version: 4 +updatedBy: {"kind":"user"} +updatedAt: 1785328001167 +--- diff --git a/.ideai/tickets/109/issue.md b/.ideai/tickets/109/issue.md new file mode 100644 index 0000000..ec49e22 --- /dev/null +++ b/.ideai/tickets/109/issue.md @@ -0,0 +1,17 @@ +--- +id: "1c6440f1-806f-41e4-92c9-6cef30d3023e" +number: 109 +title: "Ajouter le nom du créateur de ticket" +status: "closed" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +attachments: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1785311822279 +updatedAt: 1785328001167 +version: 4 +--- +J'aiemrais que le nom de celui qui a créé le ticket soit ajouté au ticket (nom de l'agent agent ou utilisateur). et qu'un filtre soit ajouté dans la liste des tickets \ No newline at end of file diff --git a/.ideai/tickets/111/carnet.md b/.ideai/tickets/111/carnet.md new file mode 100644 index 0000000..8a630b0 --- /dev/null +++ b/.ideai/tickets/111/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#111" +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785331658707 +--- diff --git a/.ideai/tickets/111/issue.md b/.ideai/tickets/111/issue.md new file mode 100644 index 0000000..465470e --- /dev/null +++ b/.ideai/tickets/111/issue.md @@ -0,0 +1,17 @@ +--- +id: "19adb66b-34db-4c44-98b7-d6383759e90c" +number: 111 +title: "Gestion tmp des outils idea" +status: "open" +priority: "medium" +sprint: null +links: [] +agentRefs: [] +attachments: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1785331481527 +updatedAt: 1785331658707 +version: 2 +--- + dans le dossier /tmp, je vois enormement de dossier du genre : idea-structured-session-factory-openai-e5bbdcac-0405-4ce3-be82-00d9639ee648 ou encore idea-web-root-4fbc4144-065c-4915-9e05-9f6fec20aa3b ou idea-server-test-863efbe5-5098-4f34-aab8-1ba5047f4209 ou idea-openai-compat-unreachable-f713f5fa-5469-4875-96f9-242f7913441b ou idea-openai-compat-tools-rejected-d000d319-8c11-4585-8a16-6ae8a3b45c49 ou idea-openai-compat-status-422-9fd4b27f-e0c1-4043-9fb9-72dcf8d2c2e1 ou idea-openai-compat-single-final-940b1d63-6d4a-40b0-953a-0f3279ec2bdc etc qui sont visiblement créé par IdeA. J'aiemrais que pour un maximum d'entre eux, on puisse clean ça au bon momment. Il faut donc identifier ce qui créé ce cache, puis le supprimer quand ce cache n'est plus utile. \ No newline at end of file diff --git a/.ideai/tickets/112/carnet.md b/.ideai/tickets/112/carnet.md new file mode 100644 index 0000000..dd83c46 --- /dev/null +++ b/.ideai/tickets/112/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#112" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1785332227989 +--- diff --git a/.ideai/tickets/112/issue.md b/.ideai/tickets/112/issue.md new file mode 100644 index 0000000..f876d51 --- /dev/null +++ b/.ideai/tickets/112/issue.md @@ -0,0 +1,17 @@ +--- +id: "ff8e11d1-98f8-4c6c-b5d0-a8a087c1dbbc" +number: 112 +title: "Pouvoir ajouter plusieurs tickets a la fois a un sprint" +status: "open" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}] +attachments: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1785332192429 +updatedAt: 1785332227989 +version: 3 +--- +Je veux qu'on ajoute la possibilité de set le sprint des tickets selectionnés grace a la selection multiple de ticket dans la liste des tickets. \ No newline at end of file diff --git a/.ideai/tickets/15/carnet.md b/.ideai/tickets/15/carnet.md index 58b55bd..b0a106d 100644 --- a/.ideai/tickets/15/carnet.md +++ b/.ideai/tickets/15/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#15" -version: 5 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784093861343 +version: 6 +updatedBy: {"kind":"user"} +updatedAt: 1785271075239 --- ## Cadrage Architect — DIFFÉRÉ (2026-07-13) diff --git a/.ideai/tickets/15/issue.md b/.ideai/tickets/15/issue.md index 5178c2c..8ad4d4b 100644 --- a/.ideai/tickets/15/issue.md +++ b/.ideai/tickets/15/issue.md @@ -2,16 +2,16 @@ id: "5de121f3-1cd1-4a73-bcab-b0a43a7c257f" number: 15 title: "[Bloqué par #7 — design durable] Limites de session — re-livraison auto parquée au reset (stretch B5)" -status: "open" +status: "closed" priority: "low" sprint: "e28a4d53-8bd2-446a-b0ac-2a017373b8b2" links: [{"target":"#7","kind":"dependsOn"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1783208599385 -updatedAt: 1784093861343 -version: 5 +updatedAt: 1785271075239 +version: 6 --- Extrait du cadrage Architect du ticket #7 (baseline livrée : propagation inter-agent B1→B3 + F1/F2). Stretch non retenu dans #7. diff --git a/.ideai/tickets/58/carnet.md b/.ideai/tickets/58/carnet.md index 7e0fa2e..226a4de 100644 --- a/.ideai/tickets/58/carnet.md +++ b/.ideai/tickets/58/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#58" -version: 1 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784064521992 +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785271075234 --- diff --git a/.ideai/tickets/58/issue.md b/.ideai/tickets/58/issue.md index 110d1b0..5cc2a7b 100644 --- a/.ideai/tickets/58/issue.md +++ b/.ideai/tickets/58/issue.md @@ -2,16 +2,16 @@ id: "495699aa-d5d1-409f-8830-cdfaba7a4be3" number: 58 title: "[B/F] Rendu live des tâches de fond — subscriber UI + canal IPC attachable au task output" -status: "open" +status: "closed" priority: "low" sprint: null links: [{"target":"#2","kind":"dependsOn"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784064521992 -updatedAt: 1784064521992 -version: 1 +updatedAt: 1785271075234 +version: 2 --- Sorti du ticket #2 lors de sa requalification (2026-07-14). diff --git a/.ideai/tickets/67/carnet.md b/.ideai/tickets/67/carnet.md index 42d8577..eb066d6 100644 --- a/.ideai/tickets/67/carnet.md +++ b/.ideai/tickets/67/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#67" -version: 2 +version: 3 updatedBy: {"kind":"user"} -updatedAt: 1784193467784 +updatedAt: 1785271075247 --- diff --git a/.ideai/tickets/67/issue.md b/.ideai/tickets/67/issue.md index b914898..3628584 100644 --- a/.ideai/tickets/67/issue.md +++ b/.ideai/tickets/67/issue.md @@ -2,7 +2,7 @@ id: "e3d16c70-01c5-448e-9b5e-37ba2ccbbb58" number: 67 title: "Lock inter-process de l'app-data-dir (desktop ↔ idea --serve)" -status: "open" +status: "closed" priority: "low" sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" links: [{"target":"#13","kind":"dependsOn"}] @@ -10,8 +10,8 @@ agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedBy: {"kind":"user"} createdAt: 1784187618710 -updatedAt: 1784193467784 -version: 2 +updatedAt: 1785271075247 +version: 3 --- Issu de la validation live #13. `idea --serve` et l'app desktop peuvent écrire le même app-data-dir (`~/.local/share/app.idea.ide`) simultanément — pas de lock inter-process → risque de corruption d'état (projects.json/profiles.json/tasks). Aujourd'hui contourné par une simple consigne de doc (« ne pas lancer les deux en même temps »). diff --git a/.ideai/tickets/71/carnet.md b/.ideai/tickets/71/carnet.md index 0318e68..31fd96c 100644 --- a/.ideai/tickets/71/carnet.md +++ b/.ideai/tickets/71/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#71" -version: 1 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784210443885 +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785271075230 --- diff --git a/.ideai/tickets/71/issue.md b/.ideai/tickets/71/issue.md index 619b635..7a60667 100644 --- a/.ideai/tickets/71/issue.md +++ b/.ideai/tickets/71/issue.md @@ -2,16 +2,16 @@ id: "4d78a0e9-b54c-4db6-92e5-1461c17d7c84" number: 71 title: "Diagnostic d'accessibilité du serveur : dire pourquoi ça ne marche pas, pas seulement que ça tourne" -status: "open" +status: "closed" priority: "medium" sprint: null links: [{"target":"#68","kind":"relatesTo"},{"target":"#65","kind":"relatesTo"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784210443885 -updatedAt: 1784210443885 -version: 1 +updatedAt: 1785271075230 +version: 2 --- Issu d'une session de debug live réelle (2026-07-16) où un serveur `idea-serve` CORRECTEMENT configuré est resté injoignable, sans que rien dans le produit n'indique pourquoi. Le serveur tournait, se croyait bon, affichait « listening on … ». Deux boucles de debug successives ont été nécessaires, dont aucune n'était diagnosticable depuis le produit. diff --git a/.ideai/tickets/73/carnet.md b/.ideai/tickets/73/carnet.md index f2aae01..60b0a7b 100644 --- a/.ideai/tickets/73/carnet.md +++ b/.ideai/tickets/73/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#73" -version: 1 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784223044517 +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785271075224 --- diff --git a/.ideai/tickets/73/issue.md b/.ideai/tickets/73/issue.md index c93bb13..65b6bbc 100644 --- a/.ideai/tickets/73/issue.md +++ b/.ideai/tickets/73/issue.md @@ -2,16 +2,16 @@ id: "a43a07d0-573f-41e9-9b7b-2f88beb4e660" number: 73 title: "TLS intégré à idea-serve : supprimer la cause racine de la cérémonie reverse proxy" -status: "open" +status: "closed" priority: "high" sprint: null links: [{"target":"#72","kind":"relatesTo"},{"target":"#66","kind":"relatesTo"},{"target":"#68","kind":"relatesTo"},{"target":"#71","kind":"relatesTo"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784223044517 -updatedAt: 1784223044517 -version: 1 +updatedAt: 1785271075224 +version: 2 --- **Décision utilisateur (2026-07-16) : IdeA gagne le TLS intégré.** Contrainte posée : prendre la technologie la plus légère. diff --git a/.ideai/tickets/80/carnet.md b/.ideai/tickets/80/carnet.md index 42e9d46..99f0114 100644 --- a/.ideai/tickets/80/carnet.md +++ b/.ideai/tickets/80/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#80" -version: 1 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784287697560 +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785271075219 --- diff --git a/.ideai/tickets/80/issue.md b/.ideai/tickets/80/issue.md index bb4bcec..59e9714 100644 --- a/.ideai/tickets/80/issue.md +++ b/.ideai/tickets/80/issue.md @@ -2,16 +2,16 @@ id: "f36ada98-0ca1-41e6-94c1-24eb81731fee" number: 80 title: "L'environnement de QA ne peut pas ouvrir de socket — il ne peut pas valider les features réseau" -status: "open" +status: "closed" priority: "medium" sprint: null links: [{"target":"#77","kind":"relatesTo"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784287697560 -updatedAt: 1784287697560 -version: 1 +updatedAt: 1785271075219 +version: 2 --- ## Constat diff --git a/.ideai/tickets/84/carnet.md b/.ideai/tickets/84/carnet.md index 703b2c2..7e14343 100644 --- a/.ideai/tickets/84/carnet.md +++ b/.ideai/tickets/84/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#84" -version: 1 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784377833773 +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785271075213 --- diff --git a/.ideai/tickets/84/issue.md b/.ideai/tickets/84/issue.md index ab9f99d..dbb9c7f 100644 --- a/.ideai/tickets/84/issue.md +++ b/.ideai/tickets/84/issue.md @@ -2,16 +2,16 @@ id: "beab1811-2fd7-4563-ac39-6a354fbd1feb" number: 84 title: "[Bug] Reprise auto après limite de session ne se déclenche pas pour l'orchestrator (Main, profil Claude)" -status: "open" +status: "closed" priority: "high" sprint: null links: [{"target":"#7","kind":"relatesTo"},{"target":"#15","kind":"relatesTo"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784377833773 -updatedAt: 1784377833773 -version: 1 +updatedAt: 1785271075213 +version: 2 --- Rapporté par l'utilisateur (2026-07-18) sur l'AppImage courante (buildée depuis develop, contient #7 baseline + #30 fix chemin direct Main) : quand l'orchestrator (Main) atteint une limite de session sur un profil Claude, la reprise automatique au reset n'a PAS lieu en pratique, alors que la feature #7 (mergée d7041c5, 2026-06-17) et le fix #30 dédié précisément au cas "Main qui limite session" (commit 9430c65, 2026-07-13, "brancher le handle de limite sur le chemin direct") sont censés couvrir ce cas. diff --git a/.ideai/tickets/85/carnet.md b/.ideai/tickets/85/carnet.md index e4aad97..98e75f8 100644 --- a/.ideai/tickets/85/carnet.md +++ b/.ideai/tickets/85/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#85" -version: 1 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784379436323 +version: 2 +updatedBy: {"kind":"user"} +updatedAt: 1785271075206 --- diff --git a/.ideai/tickets/85/issue.md b/.ideai/tickets/85/issue.md index 3e4dd56..7dcf9c7 100644 --- a/.ideai/tickets/85/issue.md +++ b/.ideai/tickets/85/issue.md @@ -2,16 +2,16 @@ id: "34b23044-4b82-4e7e-b999-cca7cbef7861" number: 85 title: "Test flaky : setCellAgent « changing the agent kills the previous PTY (Bug #3) » échoue en exécution shuffled" -status: "open" +status: "closed" priority: "low" sprint: null links: [{"target":"#79","kind":"relatesTo"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784379436323 -updatedAt: 1784379436323 -version: 1 +updatedAt: 1785271075206 +version: 2 --- Découvert en marge de #79 (DevFrontend puis confirmé par QA sur `npx vitest run --sequence.shuffle`, ~2 échecs sur 3 passages) : `src/features/layout/setCellAgent.test.tsx` → « changing the agent kills the previous PTY (Bug #3) ». diff --git a/.ideai/tickets/91/carnet.md b/.ideai/tickets/91/carnet.md index 1d4d9a4..0769da9 100644 --- a/.ideai/tickets/91/carnet.md +++ b/.ideai/tickets/91/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#91" -version: 4 +version: 5 updatedBy: {"kind":"user"} -updatedAt: 1784994105390 +updatedAt: 1785271075172 --- diff --git a/.ideai/tickets/91/issue.md b/.ideai/tickets/91/issue.md index 3d88ef8..e8c8069 100644 --- a/.ideai/tickets/91/issue.md +++ b/.ideai/tickets/91/issue.md @@ -2,7 +2,7 @@ id: "2d54c254-d2e8-44b3-ac12-3bf20f818d23" number: 91 title: "Sur la notification de fin de tache backend, mettre plutot les deux agents en conversation" -status: "open" +status: "closed" priority: "medium" sprint: "5afd6780-0f76-40d7-a10f-32ee52469d74" links: [] @@ -10,7 +10,7 @@ agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"} createdBy: {"kind":"user"} updatedBy: {"kind":"user"} createdAt: 1784652125081 -updatedAt: 1784994105390 -version: 4 +updatedAt: 1785271075172 +version: 5 --- Sur la notification de fin de tache backend, mettre plutot les deux agents en conveersation et qui a lancé l'appel (par exemple Main->DevBackend) pour que ça soit un peu plus explicite \ No newline at end of file diff --git a/.ideai/tickets/96/carnet.md b/.ideai/tickets/96/carnet.md index 057471b..4a0d932 100644 --- a/.ideai/tickets/96/carnet.md +++ b/.ideai/tickets/96/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#96" -version: 3 +version: 4 updatedBy: {"kind":"user"} -updatedAt: 1784994119004 +updatedAt: 1785271075243 --- ## Décision (2026-07-24) : différée — documentée, non codée diff --git a/.ideai/tickets/96/issue.md b/.ideai/tickets/96/issue.md index d7834cb..a9b401c 100644 --- a/.ideai/tickets/96/issue.md +++ b/.ideai/tickets/96/issue.md @@ -2,7 +2,7 @@ id: "d5993d21-307b-4fc8-93f9-2bfbf44223d8" number: 96 title: "Les assistants de ticket (tous adaptateurs) n'ont aucune résolution EffectivePermissions — permission toujours native/absente" -status: "open" +status: "closed" priority: "low" sprint: "883534aa-7fc1-4d83-a9c0-17cac4a4eea5" links: [] @@ -10,8 +10,8 @@ agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedBy: {"kind":"user"} createdAt: 1784823281794 -updatedAt: 1784994119004 -version: 3 +updatedAt: 1785271075243 +version: 4 --- Trouvé en implémentant #94 (projection des permissions dans opencode.json). Le call site des assistants de ticket dans `crates/infrastructure/src/assistant/mod.rs` n'a **aucun** `PermissionStore`/`EffectivePermissions` câblé, pour aucun adaptateur (Claude, Codex, OpenCode) — vérifié par grep sur la composition root `crates/backend/src/lib.rs`. Le fix #94 y passe donc `eff: None` (préserve le comportement natif existant, pas de régression), mais ce n'est pas un vrai fix de fond : les permissions IdeA configurées pour un agent n'ont jamais été appliquées aux assistants de ticket, quel que soit l'adaptateur. diff --git a/.ideai/tickets/99/carnet.md b/.ideai/tickets/99/carnet.md index 55c1de3..d73da13 100644 --- a/.ideai/tickets/99/carnet.md +++ b/.ideai/tickets/99/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#99" -version: 3 -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1785059758742 +version: 4 +updatedBy: {"kind":"user"} +updatedAt: 1785271085627 --- # Carnet #99 — Cadrage (prêt pour le cycle) diff --git a/.ideai/tickets/99/issue.md b/.ideai/tickets/99/issue.md index 018a47d..0169d15 100644 --- a/.ideai/tickets/99/issue.md +++ b/.ideai/tickets/99/issue.md @@ -2,16 +2,16 @@ id: "45733f3f-5ef5-4f33-96e7-25afddcd1ce6" number: 99 title: "Modèle par agent contrôlable pour Codex & Claude (headless + TUI) — égaler le pattern OpenCode" -status: "qa" +status: "closed" priority: "high" sprint: null links: [{"target":"#98","kind":"relatesTo"}] agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"user"} createdAt: 1784987973464 -updatedAt: 1785059758742 -version: 3 +updatedAt: 1785271085627 +version: 4 --- ## Problème diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index 5c590ed..a5ca793 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 107 + "nextNumber": 113 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index be21241..fa211b8 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -11,6 +11,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783175265022 }, { @@ -23,6 +26,10 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784065385002 }, { @@ -35,6 +42,10 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784377533777 }, { @@ -47,6 +58,10 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1783437430509 }, { @@ -59,6 +74,10 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1783328740886 }, { @@ -71,6 +90,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783312054217 }, { @@ -83,6 +105,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784718581033 }, { @@ -95,6 +120,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783310831918 }, { @@ -107,6 +135,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783210849647 }, { @@ -119,6 +150,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783241883970 }, { @@ -131,6 +165,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783278726067 }, { @@ -143,6 +180,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783312900907 }, { @@ -155,6 +195,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784193634690 }, { @@ -167,17 +210,24 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784449099987 }, { "issueRef": "#15", "path": "15", "title": "[Bloqué par #7 — design durable] Limites de session — re-livraison auto parquée au reset (stretch B5)", - "status": "open", + "status": "closed", "priority": "low", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], - "updatedAt": 1784093861343 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075239 }, { "issueRef": "#16", @@ -189,6 +239,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783405334263 }, { @@ -199,6 +252,9 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783405334977 }, { @@ -211,6 +267,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783405335941 }, { @@ -223,6 +282,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783405337651 }, { @@ -233,6 +295,10 @@ "priority": "low", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784049665491 }, { @@ -243,6 +309,9 @@ "priority": "low", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783437364397 }, { @@ -253,6 +322,9 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783437364871 }, { @@ -263,6 +335,9 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783437365276 }, { @@ -275,6 +350,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783491435654 }, { @@ -285,6 +363,9 @@ "priority": "medium", "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783861758866 }, { @@ -297,6 +378,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783926649550 }, { @@ -307,6 +391,10 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1783491458775 }, { @@ -317,6 +405,9 @@ "priority": "low", "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783921595742 }, { @@ -327,6 +418,9 @@ "priority": "medium", "sprint": "d8f3f37b-87ca-4509-9116-45a99bc711df", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783941648176 }, { @@ -337,6 +431,10 @@ "priority": "low", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784064442657 }, { @@ -349,6 +447,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783723378815 }, { @@ -359,6 +460,10 @@ "priority": "high", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784048928225 }, { @@ -369,6 +474,10 @@ "priority": "medium", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784050010085 }, { @@ -381,6 +490,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783777898736 }, { @@ -393,6 +505,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783777899737 }, { @@ -405,6 +520,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783878976369 }, { @@ -415,6 +533,9 @@ "priority": "low", "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783879443381 }, { @@ -425,6 +546,9 @@ "priority": "low", "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783921595316 }, { @@ -437,6 +561,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783922451268 }, { @@ -449,6 +576,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783878363957 }, { @@ -459,6 +589,10 @@ "priority": "low", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1783877612067 }, { @@ -469,6 +603,9 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784707900405 }, { @@ -481,6 +618,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783976073932 }, { @@ -493,6 +633,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783925599109 }, { @@ -505,6 +648,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783939380889 }, { @@ -517,6 +663,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783939089770 }, { @@ -529,6 +678,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783924961311 }, { @@ -541,6 +693,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783940643502 }, { @@ -553,6 +708,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783959345112 }, { @@ -563,6 +721,9 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783941336721 }, { @@ -575,6 +736,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1783958689416 }, { @@ -585,6 +749,9 @@ "priority": "medium", "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784018300921 }, { @@ -597,6 +764,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784649948611 }, { @@ -609,17 +779,24 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784048212123 }, { "issueRef": "#58", "path": "58", "title": "[B/F] Rendu live des tâches de fond — subscriber UI + canal IPC attachable au task output", - "status": "open", + "status": "closed", "priority": "low", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784064521992 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075234 }, { "issueRef": "#60", @@ -629,6 +806,10 @@ "priority": "high", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784360139966 }, { @@ -641,6 +822,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784661823324 }, { @@ -651,6 +835,10 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784406936659 }, { @@ -661,6 +849,9 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784098438671 }, { @@ -671,6 +862,10 @@ "priority": "medium", "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784718418070 }, { @@ -681,6 +876,10 @@ "priority": "high", "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784210420516 }, { @@ -691,17 +890,25 @@ "priority": "medium", "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784193460805 }, { "issueRef": "#67", "path": "67", "title": "Lock inter-process de l'app-data-dir (desktop ↔ idea --serve)", - "status": "open", + "status": "closed", "priority": "low", "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", "assignedAgentIds": [], - "updatedAt": 1784193467784 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075247 }, { "issueRef": "#68", @@ -713,6 +920,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784277313706 }, { @@ -725,6 +935,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784449061397 }, { @@ -737,17 +950,24 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1785083912470 }, { "issueRef": "#71", "path": "71", "title": "Diagnostic d'accessibilité du serveur : dire pourquoi ça ne marche pas, pas seulement que ça tourne", - "status": "open", + "status": "closed", "priority": "medium", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784210443885 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075230 }, { "issueRef": "#72", @@ -757,17 +977,25 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784236365445 }, { "issueRef": "#73", "path": "73", "title": "TLS intégré à idea-serve : supprimer la cause racine de la cérémonie reverse proxy", - "status": "open", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784223044517 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075224 }, { "issueRef": "#74", @@ -777,6 +1005,10 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784277308704 }, { @@ -787,6 +1019,10 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784449082160 }, { @@ -797,6 +1033,10 @@ "priority": "low", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784287679986 }, { @@ -807,6 +1047,10 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784449075576 }, { @@ -817,6 +1061,10 @@ "priority": "low", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784379475877 }, { @@ -827,17 +1075,25 @@ "priority": "low", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784379476483 }, { "issueRef": "#80", "path": "80", "title": "L'environnement de QA ne peut pas ouvrir de socket — il ne peut pas valider les features réseau", - "status": "open", + "status": "closed", "priority": "medium", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784287697560 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075219 }, { "issueRef": "#81", @@ -849,6 +1105,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784565918809 }, { @@ -859,6 +1118,9 @@ "priority": "critical", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784410760329 }, { @@ -869,27 +1131,38 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784567952845 }, { "issueRef": "#84", "path": "84", "title": "[Bug] Reprise auto après limite de session ne se déclenche pas pour l'orchestrator (Main, profil Claude)", - "status": "open", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784377833773 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075213 }, { "issueRef": "#85", "path": "85", "title": "Test flaky : setCellAgent « changing the agent kills the previous PTY (Bug #3) » échoue en exécution shuffled", - "status": "open", + "status": "closed", "priority": "low", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784379436323 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075206 }, { "issueRef": "#86", @@ -901,6 +1174,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784612705899 }, { @@ -913,6 +1189,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784650196560 }, { @@ -925,6 +1204,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784661583231 }, { @@ -937,19 +1219,25 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784661583085 }, { "issueRef": "#91", "path": "91", "title": "Sur la notification de fin de tache backend, mettre plutot les deux agents en conversation", - "status": "open", + "status": "closed", "priority": "medium", "sprint": "5afd6780-0f76-40d7-a10f-32ee52469d74", "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], - "updatedAt": 1784994105390 + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785271075172 }, { "issueRef": "#92", @@ -959,6 +1247,9 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1784993956910 }, { @@ -969,6 +1260,10 @@ "priority": "medium", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784934179780 }, { @@ -979,6 +1274,10 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784823278030 }, { @@ -989,17 +1288,25 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784934174326 }, { "issueRef": "#96", "path": "96", "title": "Les assistants de ticket (tous adaptateurs) n'ont aucune résolution EffectivePermissions — permission toujours native/absente", - "status": "open", + "status": "closed", "priority": "low", "sprint": "883534aa-7fc1-4d83-a9c0-17cac4a4eea5", "assignedAgentIds": [], - "updatedAt": 1784994119004 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271075243 }, { "issueRef": "#97", @@ -1009,6 +1316,10 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784915626417 }, { @@ -1019,17 +1330,25 @@ "priority": "high", "sprint": null, "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, "updatedAt": 1784983186248 }, { "issueRef": "#99", "path": "99", "title": "Modèle par agent contrôlable pour Codex & Claude (headless + TUI) — égaler le pattern OpenCode", - "status": "qa", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1785059758742 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271085627 }, { "issueRef": "#100", @@ -1039,6 +1358,9 @@ "priority": "high", "sprint": "e28a4d53-8bd2-446a-b0ac-2a017373b8b2", "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, "updatedAt": 1785083912470 }, { @@ -1051,6 +1373,9 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1785011116365 }, { @@ -1063,19 +1388,95 @@ "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], + "createdBy": { + "kind": "user" + }, "updatedAt": 1785083912470 }, { "issueRef": "#103", "path": "103", "title": "Exposer et piloter la permission réseau des agents/commandes dans IdeA", - "status": "qa", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], - "updatedAt": 1785013507979 + "createdBy": { + "kind": "agent", + "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" + }, + "updatedAt": 1785271085635 + }, + { + "issueRef": "#107", + "path": "107", + "title": "[Bug] croisement entre les projet des retours des agents", + "status": "closed", + "priority": "critical", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785271098557 + }, + { + "issueRef": "#108", + "path": "108", + "title": "Ajouter la possibilité de joindre des fichiers aux tickets", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785328001206 + }, + { + "issueRef": "#109", + "path": "109", + "title": "Ajouter le nom du créateur de ticket", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785328001167 + }, + { + "issueRef": "#111", + "path": "111", + "title": "Gestion tmp des outils idea", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785331658707 + }, + { + "issueRef": "#112", + "path": "112", + "title": "Pouvoir ajouter plusieurs tickets a la fois a un sprint", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6ced819-b893-4213-b003-9e9dc79b9641" + ], + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785332227989 } ] -} +} \ No newline at end of file From 21a84ab8f233a71a8c06d83e27a3298d6308e31b Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 29 Jul 2026 15:57:45 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(mcp):=20synchronise=20les=20permission?= =?UTF-8?q?s=20MCP=20avec=20les=20tools=20r=C3=A9ellement=20expos=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les permissions déclarées ne reflétaient pas toujours les tools effectivement exposés aux agents (contexte requester/projet lié après coup). Ajoute ToolInvoker::tools_for_bound_context/tools_for_context pour exposer la liste effective au moment de l'injection dans une requête OpenAI-compatible, et propage le calcul dans la policy MCP, le serveur, la factory de session et l'adapter openai_compat. Co-Authored-By: Claude Sonnet 5 --- crates/app-tauri/src/openai_tools.rs | 172 +++++++++++++----- crates/backend/src/lib.rs | 57 +----- crates/backend/src/openai_tools.rs | 168 ++++++++++++----- crates/domain/src/ports.rs | 23 +++ .../src/orchestrator/mcp/policy.rs | 108 ++++++++++- .../src/orchestrator/mcp/server.rs | 98 ++++------ crates/infrastructure/src/session/factory.rs | 43 +++++ .../src/session/openai_compat.rs | 16 +- 8 files changed, 462 insertions(+), 223 deletions(-) diff --git a/crates/app-tauri/src/openai_tools.rs b/crates/app-tauri/src/openai_tools.rs index 443c5da..438afa6 100644 --- a/crates/app-tauri/src/openai_tools.rs +++ b/crates/app-tauri/src/openai_tools.rs @@ -13,7 +13,7 @@ use domain::ports::{ AgentToolPolicyStore, McpToolPermissionStore, ProjectStore, ToolInvocationError, ToolInvoker, ToolSpec, }; -use domain::{AgentId, AgentToolPolicy, IssueRef, McpToolPolicy, Project}; +use domain::{AgentToolPolicy, IssueRef, McpToolPolicy, Project}; use infrastructure::{TemplateToolProvider, TicketToolProvider}; use serde_json::Value; @@ -62,6 +62,22 @@ impl ToolInvoker for LateBoundOpenAiToolInvoker { .map_or_else(Vec::new, |inner| inner.tools()) } + async fn tools_for_context( + &self, + project_root: &str, + requester: &str, + ) -> Result, ToolInvocationError> { + let inner = self + .inner + .lock() + .expect("mutex sain") + .clone() + .ok_or_else(|| { + ToolInvocationError::Execution("ToolInvoker OpenAI non initialisé".to_owned()) + })?; + inner.tools_for_context(project_root, requester).await + } + async fn call(&self, name: &str, args_json: &str) -> Result { let inner = self .inner @@ -102,14 +118,38 @@ impl ToolInvoker for AppOpenAiToolInvoker { fn tools(&self) -> Vec { infrastructure::orchestrator::mcp::catalogue() .into_iter() - .map(|tool| ToolSpec { - name: tool.name.to_owned(), - description: tool.description.to_owned(), - input_schema: tool.input_schema, - }) + .map(tool_def_to_spec) .collect() } + async fn tools_for_context( + &self, + project_root: &str, + requester: &str, + ) -> Result, ToolInvocationError> { + let ephemeral_policy = self.policies.get_policy(requester); + let project = self.find_project(project_root).await?; + let surface = infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface( + Some(self.mcp_tool_permissions.as_ref()), + &project, + requester, + ephemeral_policy.as_ref(), + ) + .await + .map_err(|e| { + ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}")) + })?; + Ok( + infrastructure::orchestrator::mcp::policy::effective_tool_catalogue( + ephemeral_policy.as_ref(), + &surface, + ) + .into_iter() + .map(tool_def_to_spec) + .collect(), + ) + } + async fn call(&self, name: &str, args_json: &str) -> Result { let value: Value = serde_json::from_str(args_json) .map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?; @@ -140,18 +180,7 @@ impl ToolInvoker for AppOpenAiToolInvoker { if let Some(policy) = &ephemeral_policy { enforce_policy(policy, &requester, name, &value)?; } - let project = self - .projects - .list_projects() - .await - .map_err(|e| ToolInvocationError::Execution(e.to_string()))? - .into_iter() - .find(|project| project.root.as_str() == project_root) - .ok_or_else(|| { - ToolInvocationError::Execution(format!( - "projet introuvable pour root `{project_root}`" - )) - })?; + let project = self.find_project(&project_root).await?; enforce_durable_tool_policy( self.mcp_tool_permissions.as_ref(), &project, @@ -209,6 +238,30 @@ impl ToolInvoker for AppOpenAiToolInvoker { } } +impl AppOpenAiToolInvoker { + async fn find_project(&self, project_root: &str) -> Result { + self.projects + .list_projects() + .await + .map_err(|e| ToolInvocationError::Execution(e.to_string()))? + .into_iter() + .find(|project| project.root.as_str() == project_root) + .ok_or_else(|| { + ToolInvocationError::Execution(format!( + "projet introuvable pour root `{project_root}`" + )) + }) + } +} + +fn tool_def_to_spec(tool: infrastructure::orchestrator::mcp::ToolDef) -> ToolSpec { + ToolSpec { + name: tool.name.to_owned(), + description: tool.description.to_owned(), + input_schema: tool.input_schema, + } +} + async fn enforce_durable_tool_policy( store: &dyn McpToolPermissionStore, project: &Project, @@ -239,38 +292,17 @@ async fn durable_tool_policy( ephemeral_policy: Option<&AgentToolPolicy>, requester: &str, ) -> Result, ToolInvocationError> { - let known_tools = infrastructure::orchestrator::mcp::tools::classified_tool_names(); - let policy = if let Some(agent_id) = requester_agent_id(requester) { - let doc = store - .load_mcp_tool_permissions(project) - .await - .map_err(|e| { - ToolInvocationError::Execution(format!("failed to load MCP tool permissions: {e}")) - })?; - doc.effective_policy( - agent_id, - infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS, - &known_tools, - ) - .map_err(|e| ToolInvocationError::Execution(format!("invalid MCP tool permissions: {e}")))? - } else if requester.is_empty() || requester == "mcp" || ephemeral_policy.is_none() { - McpToolPolicy::read_only( - infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS, - &known_tools, - ) - .map_err(|e| { - ToolInvocationError::Execution(format!("invalid read-only MCP tool fallback: {e}")) - })? - } else { - return Ok(None); - }; - Ok(Some(policy)) -} - -fn requester_agent_id(requester: &str) -> Option { - uuid::Uuid::parse_str(requester) - .ok() - .map(AgentId::from_uuid) + infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface( + Some(store), + project, + requester, + ephemeral_policy, + ) + .await + .map(|surface| surface.durable_policy) + .map_err(|e| { + ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}")) + }) } fn enforce_policy( @@ -643,6 +675,46 @@ mod tests { let _ = std::fs::remove_dir_all(temp); } + #[tokio::test] + async fn openai_tools_list_matches_durable_agent_policy() { + let temp = std::env::temp_dir().join(format!( + "idea-app-tauri-openai-mcp-tools-list-{}", + Uuid::new_v4() + )); + let core = backend::BackendCore::build(temp.clone()); + let agent = AgentId::from_uuid(Uuid::from_u128(84)); + let invoker = AppOpenAiToolInvoker::new( + Arc::clone(&core.orchestrator_service), + Arc::new(FakeProjects::with(project())) as Arc, + Arc::new(FakePolicies::default()), + mcp_permissions(allow_doc( + agent, + &["idea_memory_read", "idea_skill_read", "idea_create_skill"], + )), + Arc::new(FakeTicketTools::default()), + Arc::new(FakeTemplateTools::default()), + ); + + let names = invoker + .tools_for_context("/tmp/project", &agent.to_string()) + .await + .expect("tools list resolves") + .into_iter() + .map(|tool| tool.name) + .collect::>(); + + assert_eq!( + names, + vec![ + "idea_memory_read".to_owned(), + "idea_skill_read".to_owned(), + "idea_create_skill".to_owned(), + ] + ); + + let _ = std::fs::remove_dir_all(temp); + } + #[tokio::test] async fn openai_durable_agent_override_allows_explicit_write_tool() { let temp = std::env::temp_dir().join(format!( diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 58342bc..086e991 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -4967,59 +4967,14 @@ mod mcp_serve_peer_tests { assert_eq!(resp["id"], json!(1)); let tools = resp["result"]["tools"].as_array().expect("tools array"); let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); - for expected in [ - "idea_list_agents", - "idea_ask_agent", - "idea_ask_agents", - "idea_run_in_background", - "idea_launch_agent", - "idea_stop_agent", - "idea_update_context", - "idea_create_skill", - // FileGuard-mediated context/memory tools (cadrage C7). - "idea_context_read", - "idea_context_propose", - "idea_memory_read", - "idea_memory_write", - // Skill-awareness : lecture à la demande du corps d'un skill. - "idea_skill_read", - // Conversation inter-agent headless : réponse inline capturée depuis le Final. - "idea_ask_agent", - // Live-state (programme live-state, lot LS4). - "idea_workstate_read", - "idea_workstate_set", - // Public ticket tools (Issue domain). - "idea_ticket_create", - "idea_ticket_read", - "idea_ticket_list", - "idea_ticket_update", - "idea_ticket_update_status", - "idea_ticket_update_priority", - "idea_ticket_bulk_update_status", - "idea_ticket_bulk_update_priority", - "idea_ticket_bulk_delete", - "idea_ticket_read_carnet", - "idea_ticket_update_carnet", - "idea_ticket_link", - "idea_ticket_unlink", - "idea_sprint_list", - // Public template tools. - "idea_template_list", - "idea_template_read", - "idea_template_create", - "idea_template_update", - "idea_template_delete", - ] { - assert!( - names.contains(&expected), - "missing tool {expected}; got {names:?}" - ); - } + let expected_names: Vec<&str> = infrastructure::orchestrator::mcp::catalogue() + .into_iter() + .map(|tool| tool.name) + .collect(); assert!(!names.contains(&"idea_reply")); assert_eq!( - tools.len(), - 34, - "exactly the thirty-four exposed idea_* tools; got {names:?}" + names, expected_names, + "tools/list must expose exactly the canonical MCP catalogue" ); drop(client); // EOF ⇒ serve loop ends diff --git a/crates/backend/src/openai_tools.rs b/crates/backend/src/openai_tools.rs index e59e81b..939a612 100644 --- a/crates/backend/src/openai_tools.rs +++ b/crates/backend/src/openai_tools.rs @@ -13,7 +13,7 @@ use domain::ports::{ AgentToolPolicyStore, McpToolPermissionStore, ProjectStore, ToolInvocationError, ToolInvoker, ToolSpec, }; -use domain::{AgentId, AgentToolPolicy, IssueRef, McpToolPolicy, Project}; +use domain::{AgentToolPolicy, IssueRef, McpToolPolicy, Project}; use infrastructure::{TemplateToolProvider, TicketToolProvider}; use serde_json::Value; @@ -62,6 +62,22 @@ impl ToolInvoker for LateBoundOpenAiToolInvoker { .map_or_else(Vec::new, |inner| inner.tools()) } + async fn tools_for_context( + &self, + project_root: &str, + requester: &str, + ) -> Result, ToolInvocationError> { + let inner = self + .inner + .lock() + .expect("mutex sain") + .clone() + .ok_or_else(|| { + ToolInvocationError::Execution("ToolInvoker OpenAI non initialisé".to_owned()) + })?; + inner.tools_for_context(project_root, requester).await + } + async fn call(&self, name: &str, args_json: &str) -> Result { let inner = self .inner @@ -102,14 +118,38 @@ impl ToolInvoker for AppOpenAiToolInvoker { fn tools(&self) -> Vec { infrastructure::orchestrator::mcp::catalogue() .into_iter() - .map(|tool| ToolSpec { - name: tool.name.to_owned(), - description: tool.description.to_owned(), - input_schema: tool.input_schema, - }) + .map(tool_def_to_spec) .collect() } + async fn tools_for_context( + &self, + project_root: &str, + requester: &str, + ) -> Result, ToolInvocationError> { + let ephemeral_policy = self.policies.get_policy(requester); + let project = self.find_project(project_root).await?; + let surface = infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface( + Some(self.mcp_tool_permissions.as_ref()), + &project, + requester, + ephemeral_policy.as_ref(), + ) + .await + .map_err(|e| { + ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}")) + })?; + Ok( + infrastructure::orchestrator::mcp::policy::effective_tool_catalogue( + ephemeral_policy.as_ref(), + &surface, + ) + .into_iter() + .map(tool_def_to_spec) + .collect(), + ) + } + async fn call(&self, name: &str, args_json: &str) -> Result { let value: Value = serde_json::from_str(args_json) .map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?; @@ -140,18 +180,7 @@ impl ToolInvoker for AppOpenAiToolInvoker { if let Some(policy) = &ephemeral_policy { enforce_policy(policy, &requester, name, &value)?; } - let project = self - .projects - .list_projects() - .await - .map_err(|e| ToolInvocationError::Execution(e.to_string()))? - .into_iter() - .find(|project| project.root.as_str() == project_root) - .ok_or_else(|| { - ToolInvocationError::Execution(format!( - "projet introuvable pour root `{project_root}`" - )) - })?; + let project = self.find_project(&project_root).await?; enforce_durable_tool_policy( self.mcp_tool_permissions.as_ref(), &project, @@ -209,6 +238,30 @@ impl ToolInvoker for AppOpenAiToolInvoker { } } +impl AppOpenAiToolInvoker { + async fn find_project(&self, project_root: &str) -> Result { + self.projects + .list_projects() + .await + .map_err(|e| ToolInvocationError::Execution(e.to_string()))? + .into_iter() + .find(|project| project.root.as_str() == project_root) + .ok_or_else(|| { + ToolInvocationError::Execution(format!( + "projet introuvable pour root `{project_root}`" + )) + }) + } +} + +fn tool_def_to_spec(tool: infrastructure::orchestrator::mcp::ToolDef) -> ToolSpec { + ToolSpec { + name: tool.name.to_owned(), + description: tool.description.to_owned(), + input_schema: tool.input_schema, + } +} + async fn enforce_durable_tool_policy( store: &dyn McpToolPermissionStore, project: &Project, @@ -239,38 +292,17 @@ async fn durable_tool_policy( ephemeral_policy: Option<&AgentToolPolicy>, requester: &str, ) -> Result, ToolInvocationError> { - let known_tools = infrastructure::orchestrator::mcp::tools::classified_tool_names(); - let policy = if let Some(agent_id) = requester_agent_id(requester) { - let doc = store - .load_mcp_tool_permissions(project) - .await - .map_err(|e| { - ToolInvocationError::Execution(format!("failed to load MCP tool permissions: {e}")) - })?; - doc.effective_policy( - agent_id, - infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS, - &known_tools, - ) - .map_err(|e| ToolInvocationError::Execution(format!("invalid MCP tool permissions: {e}")))? - } else if requester.is_empty() || requester == "mcp" || ephemeral_policy.is_none() { - McpToolPolicy::read_only( - infrastructure::orchestrator::mcp::tools::READ_ONLY_TOOLS, - &known_tools, - ) - .map_err(|e| { - ToolInvocationError::Execution(format!("invalid read-only MCP tool fallback: {e}")) - })? - } else { - return Ok(None); - }; - Ok(Some(policy)) -} - -fn requester_agent_id(requester: &str) -> Option { - uuid::Uuid::parse_str(requester) - .ok() - .map(AgentId::from_uuid) + infrastructure::orchestrator::mcp::policy::resolve_effective_tool_surface( + Some(store), + project, + requester, + ephemeral_policy, + ) + .await + .map(|surface| surface.durable_policy) + .map_err(|e| { + ToolInvocationError::Execution(format!("failed to resolve MCP tool permissions: {e}")) + }) } #[cfg(test)] @@ -796,6 +828,42 @@ mod tests { let _ = std::fs::remove_dir_all(temp); } + #[tokio::test] + async fn openai_tools_list_matches_durable_agent_policy() { + let temp = + std::env::temp_dir().join(format!("idea-openai-mcp-tools-list-{}", Uuid::new_v4())); + let core = crate::BackendCore::build(temp.clone()); + let agent = AgentId::from_uuid(Uuid::from_u128(182)); + let invoker = AppOpenAiToolInvoker::new( + Arc::clone(&core.orchestrator_service), + Arc::new(FakeProjects::with(project())) as Arc, + Arc::new(FakePolicies::default()), + mcp_permissions(allow_doc( + agent, + &["idea_ticket_list", "idea_ticket_update_carnet"], + )), + Arc::new(FakeTicketTools::default()), + Arc::new(FakeTemplateTools::default()), + ); + + let tools = invoker + .tools_for_context("/tmp/project", &agent.to_string()) + .await + .expect("tools list should resolve durable policy"); + let names = tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(); + + assert!(names.contains(&"idea_ticket_list")); + assert!(names.contains(&"idea_ticket_update_carnet")); + assert!(!names.contains(&"idea_ask_agent")); + assert!(!names.contains(&"idea_ticket_update")); + assert!(!names.contains(&"idea_run_in_background")); + + let _ = std::fs::remove_dir_all(temp); + } + #[tokio::test] async fn openai_durable_agent_override_allows_explicit_write_tool() { let temp = diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 8ee845b..e10b22c 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -660,6 +660,29 @@ pub trait ToolInvoker: Send + Sync { /// Liste des outils exposés au modèle. fn tools(&self) -> Vec; + /// Liste des outils exposés dans le contexte déjà lié par l'adapter appelant. + /// + /// Les invokers simples peuvent conserver le comportement historique via + /// [`Self::tools`]. Les wrappers qui portent déjà l'identité requester et le + /// projet peuvent surcharger cette méthode pour refléter les permissions + /// effectives au moment où les outils sont exposés au modèle. + async fn tools_for_bound_context(&self) -> Result, ToolInvocationError> { + Ok(self.tools()) + } + + /// Liste des outils exposés pour un projet et un requester explicites. + /// + /// Sert aux adapters qui savent résoudre la policy durable à partir du root + /// projet et de l'identité agent avant d'injecter les tools dans une requête + /// OpenAI-compatible. + async fn tools_for_context( + &self, + _project_root: &str, + _requester: &str, + ) -> Result, ToolInvocationError> { + Ok(self.tools()) + } + /// Appelle un outil avec ses arguments JSON bruts. /// /// # Errors diff --git a/crates/infrastructure/src/orchestrator/mcp/policy.rs b/crates/infrastructure/src/orchestrator/mcp/policy.rs index 77baa83..f135470 100644 --- a/crates/infrastructure/src/orchestrator/mcp/policy.rs +++ b/crates/infrastructure/src/orchestrator/mcp/policy.rs @@ -1,10 +1,12 @@ -//! In-memory MCP tool policy registry. +//! MCP tool policy helpers and in-memory live policy registry. use std::collections::HashMap; use std::sync::RwLock; -use domain::AgentToolPolicy; -use domain::AgentToolPolicyStore; +use domain::ports::McpToolPermissionStore; +use domain::{AgentId, AgentToolPolicy, AgentToolPolicyStore, McpToolPolicy, Project, StoreError}; + +use super::tools::{self, ToolDef}; /// Stores per-requester MCP tool policies for live assistant sessions. #[derive(Default)] @@ -53,6 +55,106 @@ impl AgentToolPolicyStore for ToolPolicyRegistry { } } +/// Effective MCP tool surface resolved for one requester. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectiveToolSurface { + /// Durable MCP policy when one applies. + pub durable_policy: Option, + /// Whether the read-only fallback was used because the requester could not be + /// resolved to an agent policy. + pub used_read_only_fallback: bool, +} + +impl EffectiveToolSurface { + /// Returns whether `tool` is allowed by the durable policy, when present. + #[must_use] + pub fn permits_durable(&self, tool: &str) -> bool { + self.durable_policy + .as_ref() + .map_or(true, |policy| policy.permits(tool)) + } +} + +/// Resolves the durable MCP tool policy for the given requester. +/// +/// This is the shared source of truth for the surfaces that expose IdeA tools to +/// agents. Agent UUID requesters use `.ideai/mcp-tool-permissions.json`; +/// anonymous/legacy requesters and non-agent requesters without an ephemeral +/// policy fall back to the canonical read-only policy. Non-agent requesters with +/// an ephemeral policy (ticket assistants) are intentionally governed only by +/// that narrower session policy. +/// +/// # Errors +/// [`StoreError`] on store load failure or invalid persisted policy. +pub async fn resolve_effective_tool_surface( + store: Option<&dyn McpToolPermissionStore>, + project: &Project, + requester: &str, + ephemeral_policy: Option<&AgentToolPolicy>, +) -> Result { + let Some(store) = store else { + return Ok(EffectiveToolSurface { + durable_policy: None, + used_read_only_fallback: false, + }); + }; + + let known_tools = tools::classified_tool_names(); + if let Some(agent_id) = requester_agent_id(requester) { + let doc = store.load_mcp_tool_permissions(project).await?; + let policy = doc + .effective_policy(agent_id, tools::READ_ONLY_TOOLS, &known_tools) + .map_err(|err| StoreError::Invalid(err.to_string()))?; + return Ok(EffectiveToolSurface { + durable_policy: Some(policy), + used_read_only_fallback: false, + }); + } + + if requester.is_empty() || requester == "mcp" || ephemeral_policy.is_none() { + application::diag!( + "[mcp] unresolved requester `{}` uses read-only tool fallback", + if requester.is_empty() { + "mcp" + } else { + requester + }, + ); + let policy = McpToolPolicy::read_only(tools::READ_ONLY_TOOLS, &known_tools) + .map_err(|err| StoreError::Invalid(err.to_string()))?; + return Ok(EffectiveToolSurface { + durable_policy: Some(policy), + used_read_only_fallback: true, + }); + } + + Ok(EffectiveToolSurface { + durable_policy: None, + used_read_only_fallback: false, + }) +} + +/// Filters the canonical catalogue by the effective ephemeral + durable policies. +#[must_use] +pub fn effective_tool_catalogue( + ephemeral_policy: Option<&AgentToolPolicy>, + surface: &EffectiveToolSurface, +) -> Vec { + tools::catalogue() + .into_iter() + .filter(|tool| { + ephemeral_policy.map_or(true, |policy| policy.permits(tool.name)) + && surface.permits_durable(tool.name) + }) + .collect() +} + +fn requester_agent_id(requester: &str) -> Option { + uuid::Uuid::parse_str(requester) + .ok() + .map(AgentId::from_uuid) +} + #[cfg(test)] mod tests { use domain::IssueRef; diff --git a/crates/infrastructure/src/orchestrator/mcp/server.rs b/crates/infrastructure/src/orchestrator/mcp/server.rs index bd3605f..d1038af 100644 --- a/crates/infrastructure/src/orchestrator/mcp/server.rs +++ b/crates/infrastructure/src/orchestrator/mcp/server.rs @@ -347,25 +347,20 @@ impl McpServer { /// The `tools/list` result: the catalogue as MCP tool descriptors. async fn tools_list_result(&self) -> Result { let ephemeral_policy = self.ephemeral_tool_policy(); - let durable_policy = self.durable_tool_policy().await?; - let tools: Vec = tools::catalogue() - .into_iter() - .filter(|t| { - ephemeral_policy - .as_ref() - .map_or(true, |policy| policy.permits(t.name)) - && durable_policy - .as_ref() - .map_or(true, |policy| policy.permits(t.name)) - }) - .map(|t| { - json!({ - "name": t.name, - "description": t.description, - "inputSchema": t.input_schema, + let surface = self + .effective_tool_surface(ephemeral_policy.as_ref()) + .await?; + let tools: Vec = + super::policy::effective_tool_catalogue(ephemeral_policy.as_ref(), &surface) + .into_iter() + .map(|t| { + json!({ + "name": t.name, + "description": t.description, + "inputSchema": t.input_schema, + }) }) - }) - .collect(); + .collect(); Ok(json!({ "tools": tools })) } @@ -376,53 +371,30 @@ impl McpServer { } async fn durable_tool_policy(&self) -> Result, JsonRpcError> { - let Some(store) = &self.mcp_tool_permissions else { - return Ok(None); - }; - - let known_tools = tools::classified_tool_names(); - let policy = if let Some(agent_id) = self.requester_agent_id() { - let doc = store - .load_mcp_tool_permissions(&self.project) - .await - .map_err(|e| { - JsonRpcError::new( - error_codes::INTERNAL_ERROR, - format!("failed to load MCP tool permissions: {e}"), - ) - })?; - doc.effective_policy(agent_id, tools::READ_ONLY_TOOLS, &known_tools) - .map_err(|e| { - JsonRpcError::new( - error_codes::INTERNAL_ERROR, - format!("invalid MCP tool permissions: {e}"), - ) - })? - } else if self.requester.is_empty() - || self.requester == "mcp" - || self.ephemeral_tool_policy().is_none() - { - // Anonymous/legacy peers cannot be mapped to an agent override. Fail - // closed to the canonical read-only policy. Non-agent requesters with an - // ephemeral policy (ticket assistants) are governed by that narrower, - // session-scoped policy instead of the durable per-agent store. - McpToolPolicy::read_only(tools::READ_ONLY_TOOLS, &known_tools).map_err(|e| { - JsonRpcError::new( - error_codes::INTERNAL_ERROR, - format!("invalid read-only MCP tool fallback: {e}"), - ) - })? - } else { - return Ok(None); - }; - - Ok(Some(policy)) + let ephemeral_policy = self.ephemeral_tool_policy(); + Ok(self + .effective_tool_surface(ephemeral_policy.as_ref()) + .await? + .durable_policy) } - fn requester_agent_id(&self) -> Option { - uuid::Uuid::parse_str(&self.requester) - .ok() - .map(AgentId::from_uuid) + async fn effective_tool_surface( + &self, + ephemeral_policy: Option<&AgentToolPolicy>, + ) -> Result { + super::policy::resolve_effective_tool_surface( + self.mcp_tool_permissions.as_deref(), + &self.project, + &self.requester, + ephemeral_policy, + ) + .await + .map_err(|e| { + JsonRpcError::new( + error_codes::INTERNAL_ERROR, + format!("failed to resolve MCP tool permissions: {e}"), + ) + }) } fn enforce_durable_tool_policy( diff --git a/crates/infrastructure/src/session/factory.rs b/crates/infrastructure/src/session/factory.rs index 2252afe..3a0395a 100644 --- a/crates/infrastructure/src/session/factory.rs +++ b/crates/infrastructure/src/session/factory.rs @@ -41,6 +41,12 @@ impl ToolInvoker for ProjectScopedToolInvoker { self.inner.tools() } + async fn tools_for_bound_context(&self) -> Result, ToolInvocationError> { + self.inner + .tools_for_context(&self.project_root, &self.requester) + .await + } + async fn call(&self, name: &str, args_json: &str) -> Result { let mut value: Value = serde_json::from_str(args_json) .map_err(|e| ToolInvocationError::InvalidArguments(format!("JSON invalide: {e}")))?; @@ -257,6 +263,7 @@ mod tests { #[derive(Default)] struct RecordingToolInvoker { call: Mutex>, + tools_context: Mutex>, } #[async_trait] @@ -265,6 +272,20 @@ mod tests { Vec::new() } + async fn tools_for_context( + &self, + project_root: &str, + requester: &str, + ) -> Result, ToolInvocationError> { + *self.tools_context.lock().unwrap() = + Some((project_root.to_owned(), requester.to_owned())); + Ok(vec![ToolSpec { + name: "idea_memory_read".to_owned(), + description: "Read memory".to_owned(), + input_schema: json!({"type":"object"}), + }]) + } + async fn call(&self, name: &str, args_json: &str) -> Result { *self.call.lock().unwrap() = Some((name.to_owned(), args_json.to_owned())); Ok("ok".to_owned()) @@ -299,6 +320,28 @@ mod tests { ); } + #[tokio::test] + async fn project_scoped_tool_invoker_filters_tools_with_bound_context() { + let recorder = Arc::new(RecordingToolInvoker::default()); + let invoker = ProjectScopedToolInvoker { + inner: recorder.clone(), + project_root: "/project/root".to_owned(), + requester: "agent-1".to_owned(), + }; + + let tools = invoker + .tools_for_bound_context() + .await + .expect("tools resolve"); + + assert_eq!(tools.len(), 1); + assert_eq!(tools[0].name, "idea_memory_read"); + assert_eq!( + recorder.tools_context.lock().unwrap().clone(), + Some(("/project/root".to_owned(), "agent-1".to_owned())) + ); + } + #[test] fn fallback_requester_uses_cwd_file_name_only_when_explicit_identity_absent() { assert_eq!(fallback_requester_from_cwd("/tmp/run/7"), "7"); diff --git a/crates/infrastructure/src/session/openai_compat.rs b/crates/infrastructure/src/session/openai_compat.rs index 81ab2cf..dc30672 100644 --- a/crates/infrastructure/src/session/openai_compat.rs +++ b/crates/infrastructure/src/session/openai_compat.rs @@ -281,7 +281,7 @@ impl OpenAiCompatibleSession { send_tap(&tap, &ReplyEvent::Heartbeat); for iteration in 0..=self.config.effective_max_tool_iterations() { let transcript = self.transcript.lock().expect("mutex sain").clone(); - let tools = self.effective_tools(); + let tools = self.effective_tools().await?; let response = self.post_chat(&transcript, &tools, true).await; let response = match response { Ok(response) => response, @@ -330,13 +330,17 @@ impl OpenAiCompatibleSession { unreachable!("loop returns at max_tool_iterations"); } - fn effective_tools(&self) -> Vec { + async fn effective_tools(&self) -> Result, AgentSessionError> { if *self.tools_disabled.lock().expect("mutex sain") { - return Vec::new(); + return Ok(Vec::new()); + } + match &self.tool_invoker { + Some(invoker) => invoker + .tools_for_bound_context() + .await + .map_err(|e| AgentSessionError::Start(format!("résolution tools: {e}"))), + None => Ok(Vec::new()), } - self.tool_invoker - .as_ref() - .map_or_else(Vec::new, |invoker| invoker.tools()) } async fn post_chat(