merge feature/mcp-tool-permissions-sync dans develop (#82: sync permissions MCP <-> tools exposés)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -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
|
||||
|
||||
@ -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 <crate>` 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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
issueRef: "#107"
|
||||
version: 3
|
||||
version: 4
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785136945441
|
||||
updatedAt: 1785271098557
|
||||
---
|
||||
|
||||
@ -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.
|
||||
6
.ideai/tickets/108/carnet.md
Normal file
6
.ideai/tickets/108/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#108"
|
||||
version: 3
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785328001206
|
||||
---
|
||||
17
.ideai/tickets/108/issue.md
Normal file
17
.ideai/tickets/108/issue.md
Normal file
@ -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
|
||||
6
.ideai/tickets/109/carnet.md
Normal file
6
.ideai/tickets/109/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#109"
|
||||
version: 4
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785328001167
|
||||
---
|
||||
17
.ideai/tickets/109/issue.md
Normal file
17
.ideai/tickets/109/issue.md
Normal file
@ -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
|
||||
6
.ideai/tickets/111/carnet.md
Normal file
6
.ideai/tickets/111/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#111"
|
||||
version: 2
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785331658707
|
||||
---
|
||||
17
.ideai/tickets/111/issue.md
Normal file
17
.ideai/tickets/111/issue.md
Normal file
@ -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.
|
||||
6
.ideai/tickets/112/carnet.md
Normal file
6
.ideai/tickets/112/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#112"
|
||||
version: 3
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785332227989
|
||||
---
|
||||
17
.ideai/tickets/112/issue.md
Normal file
17
.ideai/tickets/112/issue.md
Normal file
@ -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.
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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).
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
issueRef: "#67"
|
||||
version: 2
|
||||
version: 3
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1784193467784
|
||||
updatedAt: 1785271075247
|
||||
---
|
||||
|
||||
@ -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 »).
|
||||
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
---
|
||||
|
||||
@ -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) ».
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
issueRef: "#91"
|
||||
version: 4
|
||||
version: 5
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1784994105390
|
||||
updatedAt: 1785271075172
|
||||
---
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"nextNumber": 107
|
||||
"nextNumber": 113
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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<Vec<ToolSpec>, 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<String, ToolInvocationError> {
|
||||
let inner = self
|
||||
.inner
|
||||
@ -102,14 +118,38 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
||||
fn tools(&self) -> Vec<ToolSpec> {
|
||||
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<Vec<ToolSpec>, 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<String, ToolInvocationError> {
|
||||
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<Project, ToolInvocationError> {
|
||||
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<Option<McpToolPolicy>, 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<AgentId> {
|
||||
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<dyn ProjectStore>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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!(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Vec<ToolSpec>, 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<String, ToolInvocationError> {
|
||||
let inner = self
|
||||
.inner
|
||||
@ -102,14 +118,38 @@ impl ToolInvoker for AppOpenAiToolInvoker {
|
||||
fn tools(&self) -> Vec<ToolSpec> {
|
||||
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<Vec<ToolSpec>, 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<String, ToolInvocationError> {
|
||||
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<Project, ToolInvocationError> {
|
||||
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<Option<McpToolPolicy>, 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<AgentId> {
|
||||
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<dyn ProjectStore>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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 =
|
||||
|
||||
@ -660,6 +660,29 @@ pub trait ToolInvoker: Send + Sync {
|
||||
/// Liste des outils exposés au modèle.
|
||||
fn tools(&self) -> Vec<ToolSpec>;
|
||||
|
||||
/// 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<Vec<ToolSpec>, 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<Vec<ToolSpec>, ToolInvocationError> {
|
||||
Ok(self.tools())
|
||||
}
|
||||
|
||||
/// Appelle un outil avec ses arguments JSON bruts.
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
@ -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<McpToolPolicy>,
|
||||
/// 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<EffectiveToolSurface, StoreError> {
|
||||
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<ToolDef> {
|
||||
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<AgentId> {
|
||||
uuid::Uuid::parse_str(requester)
|
||||
.ok()
|
||||
.map(AgentId::from_uuid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use domain::IssueRef;
|
||||
|
||||
@ -347,25 +347,20 @@ impl McpServer {
|
||||
/// The `tools/list` result: the catalogue as MCP tool descriptors.
|
||||
async fn tools_list_result(&self) -> Result<Value, JsonRpcError> {
|
||||
let ephemeral_policy = self.ephemeral_tool_policy();
|
||||
let durable_policy = self.durable_tool_policy().await?;
|
||||
let tools: Vec<Value> = 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<Value> =
|
||||
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<Option<McpToolPolicy>, 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<AgentId> {
|
||||
uuid::Uuid::parse_str(&self.requester)
|
||||
.ok()
|
||||
.map(AgentId::from_uuid)
|
||||
async fn effective_tool_surface(
|
||||
&self,
|
||||
ephemeral_policy: Option<&AgentToolPolicy>,
|
||||
) -> Result<super::policy::EffectiveToolSurface, JsonRpcError> {
|
||||
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(
|
||||
|
||||
@ -41,6 +41,12 @@ impl ToolInvoker for ProjectScopedToolInvoker {
|
||||
self.inner.tools()
|
||||
}
|
||||
|
||||
async fn tools_for_bound_context(&self) -> Result<Vec<ToolSpec>, ToolInvocationError> {
|
||||
self.inner
|
||||
.tools_for_context(&self.project_root, &self.requester)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn call(&self, name: &str, args_json: &str) -> Result<String, ToolInvocationError> {
|
||||
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<Option<(String, String)>>,
|
||||
tools_context: Mutex<Option<(String, String)>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -265,6 +272,20 @@ mod tests {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
async fn tools_for_context(
|
||||
&self,
|
||||
project_root: &str,
|
||||
requester: &str,
|
||||
) -> Result<Vec<ToolSpec>, 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<String, ToolInvocationError> {
|
||||
*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");
|
||||
|
||||
@ -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<ToolSpec> {
|
||||
async fn effective_tools(&self) -> Result<Vec<ToolSpec>, 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(
|
||||
|
||||
Reference in New Issue
Block a user