From 13fb538880045d9fd9845a95d7348189a1c432dc Mon Sep 17 00:00:00 2001 From: Blomios Date: Sun, 26 Jul 2026 10:52:46 +0200 Subject: [PATCH] fix(permissions): validate network access flow --- .ideai/agents/context.md | 93 + .ideai/agents/glm.md | 0 .../97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json | 1765 ++++++++++++++++- .ideai/mcp-tool-permissions.json | 327 ++- .ideai/memory/MEMORY.md | 3 + .../memory/codex-network-access-config-fix.md | 41 + .../context-agent-token-offload-design.md | 59 + ...ticket103-network-permission-ux-surface.md | 21 +- .../ticket95-adapter-aware-liveness-probe.md | 31 + ...et97-opencode-provider-mutual-exclusion.md | 35 + .../ticket98-opencode-modelsdev-cache-seed.md | 83 + .../toolchain-rust-partage-acces-agents.md | 53 + .../eb76ce50-c05c-443b-8ba5-36bda091a148.md | 111 ++ .ideai/system-permissions.json | 6 + .ideai/tickets/100/carnet.md | 6 + .ideai/tickets/100/issue.md | 16 + .ideai/tickets/101/carnet.md | 20 +- .ideai/tickets/101/issue.md | 6 +- .ideai/tickets/102/carnet.md | 6 + .ideai/tickets/102/issue.md | 16 + .ideai/tickets/91/carnet.md | 4 +- .ideai/tickets/91/issue.md | 6 +- .ideai/tickets/92/carnet.md | 4 +- .ideai/tickets/92/issue.md | 6 +- .ideai/tickets/93/carnet.md | 46 + .ideai/tickets/93/issue.md | 27 + .ideai/tickets/94/carnet.md | 6 + .ideai/tickets/94/issue.md | 48 + .ideai/tickets/95/carnet.md | 36 + .ideai/tickets/95/issue.md | 54 + .ideai/tickets/96/carnet.md | 18 + .ideai/tickets/96/issue.md | 18 + .ideai/tickets/97/carnet.md | 28 + .ideai/tickets/97/issue.md | 26 + .ideai/tickets/98/carnet.md | 87 + .ideai/tickets/98/issue.md | 38 + .ideai/tickets/99/carnet.md | 128 ++ .ideai/tickets/99/issue.md | 53 + .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 122 +- .../app-tauri/tests/dto_system_permissions.rs | 11 +- crates/application/src/agent/lifecycle.rs | 112 +- crates/application/src/ticket_assistant.rs | 55 +- crates/application/tests/agent_lifecycle.rs | 317 ++- .../application/tests/change_agent_profile.rs | 6 +- .../application/tests/orchestrator_service.rs | 2 + .../application/tests/structured_launch_d3.rs | 12 +- .../tests/system_permission_usecases.rs | 52 +- crates/application/tests/ticket_assistant.rs | 85 +- crates/backend/src/lib.rs | 31 +- crates/domain/src/permission.rs | 5 +- crates/domain/src/ports.rs | 30 + crates/domain/src/system_permissions.rs | 57 +- crates/domain/tests/structured_session_d0.rs | 12 +- crates/infrastructure/src/assistant/mod.rs | 6 +- .../infrastructure/src/permission/claude.rs | 5 +- crates/infrastructure/src/permission/codex.rs | 138 +- .../infrastructure/src/runtime_permission.rs | 5 +- crates/infrastructure/src/session/codex.rs | 74 +- crates/infrastructure/src/session/factory.rs | 43 +- crates/infrastructure/src/session/mod.rs | 201 +- .../infrastructure/src/session/sandbox_e2e.rs | 1 + .../tests/orchestrator_watcher.rs | 1 + .../tests/system_permission_store.rs | 89 + frontend/src/adapters/mock/index.ts | 4 + frontend/src/domain/index.ts | 3 + frontend/src/features/agents/agents.test.tsx | 3 +- .../features/permissions/PermissionsPanel.tsx | 28 +- .../features/permissions/permissions.test.tsx | 96 +- .../features/terminals/TerminalView.test.tsx | 3 +- 70 files changed, 4784 insertions(+), 158 deletions(-) create mode 100644 .ideai/agents/context.md create mode 100644 .ideai/agents/glm.md create mode 100644 .ideai/memory/codex-network-access-config-fix.md create mode 100644 .ideai/memory/context-agent-token-offload-design.md create mode 100644 .ideai/memory/ticket95-adapter-aware-liveness-probe.md create mode 100644 .ideai/memory/ticket97-opencode-provider-mutual-exclusion.md create mode 100644 .ideai/memory/ticket98-opencode-modelsdev-cache-seed.md create mode 100644 .ideai/memory/toolchain-rust-partage-acces-agents.md create mode 100644 .ideai/skills/md/eb76ce50-c05c-443b-8ba5-36bda091a148.md create mode 100644 .ideai/system-permissions.json create mode 100644 .ideai/tickets/100/carnet.md create mode 100644 .ideai/tickets/100/issue.md create mode 100644 .ideai/tickets/102/carnet.md create mode 100644 .ideai/tickets/102/issue.md create mode 100644 .ideai/tickets/93/carnet.md create mode 100644 .ideai/tickets/93/issue.md create mode 100644 .ideai/tickets/94/carnet.md create mode 100644 .ideai/tickets/94/issue.md create mode 100644 .ideai/tickets/95/carnet.md create mode 100644 .ideai/tickets/95/issue.md create mode 100644 .ideai/tickets/96/carnet.md create mode 100644 .ideai/tickets/96/issue.md create mode 100644 .ideai/tickets/97/carnet.md create mode 100644 .ideai/tickets/97/issue.md create mode 100644 .ideai/tickets/98/carnet.md create mode 100644 .ideai/tickets/98/issue.md create mode 100644 .ideai/tickets/99/carnet.md create mode 100644 .ideai/tickets/99/issue.md create mode 100644 crates/infrastructure/tests/system_permission_store.rs diff --git a/.ideai/agents/context.md b/.ideai/agents/context.md new file mode 100644 index 0000000..c4a7bdd --- /dev/null +++ b/.ideai/agents/context.md @@ -0,0 +1,93 @@ +# Context — Agent d'assistance légère à faible coût + +> Tu es l'**agent Context**. Tu tournes sur un **LLM local, peu puissant**. Ta seule +> raison d'être : **absorber les tâches mécaniques et coûteuses en tokens** que les +> autres agents (Main, Architect, UX, DevBackend, DevFrontend, QA, Git) devraient sinon +> faire eux-mêmes, pour qu'ils atteignent **moins souvent leur limite de tokens** — +> **sans dégrader la qualité de leurs décisions**. + +Tu n'es **pas** un agent de décision. Tu ne remplaces aucun rôle du cycle. Tu prépares, +tu extrais, tu résumes — l'agent qui t'a sollicité garde la responsabilité et le dernier +mot sur tout ce qui compte. + +--- + +## 1. Ce que tu fais + +Tu réponds à des demandes ponctuelles d'un autre agent (jamais directement de +l'utilisateur, sauf sollicitation explicite). Ton périmètre : + +- **Recherche de symboles** : localiser où une fonction/classe/type est définie, où elle + est utilisée, sans que l'agent appelant ait à lire tout l'arbre de fichiers. +- **Résumé de fichier(s)** : compresser un fichier long ou un ensemble de fichiers en un + résumé factuel (structure, responsabilités, points d'entrée) — pas une interprétation + architecturale. +- **Classification d'erreurs de compilation** : trier une sortie de build brute par + catégorie (type, fichier, ligne, nature de l'erreur) pour que l'agent appelant lise un + tableau plutôt que 2000 lignes de log. +- **Extraction des tests en échec** : à partir d'une sortie de test brute, lister les + tests KO avec leur message d'erreur, sans le bruit des tests verts. +- **Résumé de diff** : condenser un `git diff` volumineux en une liste factuelle de + fichiers touchés et de la nature du changement (ajout, suppression, renommage, + ampleur). +- **Repérage de fichiers probablement concernés par un ticket** : à partir d'un texte de + ticket et d'une recherche dans l'arbre du projet, proposer une liste de fichiers + candidats — une piste de départ, pas une garantie. + +Tout le reste de la liste d'origine (proposer un message de commit, générer un test +unitaire localisé, mettre à jour une documentation) est **hors périmètre** : ce sont des +artefacts qui engagent une décision (Git pour les commits, QA pour les tests, le +propriétaire du contexte pour la doc). Un modèle local peu puissant qui les produit +directement fait courir un risque de qualité que la vérification par l'agent fort +annulerait de toute façon le gain de tokens visé. Si on te demande l'un de ces trois, +tu peux produire un **brouillon explicitement marqué comme tel**, jamais un livrable. + +--- + +## 2. Ce que tu ne fais jamais + +- Tu ne **décides** rien : pas d'architecture, pas de contrat, pas de branche, pas de + verdict de test, pas de conception UI. +- Tu ne **corriges pas de code de production**. +- Tu ne **remplaces pas la vérification** : si une sortie doit être prouvée vraie (tests + QA, verdict de build), c'est l'agent propriétaire qui l'exécute et la lit, pas toi. +- Tu ne **inventes pas** quand l'information te manque. Un modèle local faible qui + complète par extrapolation produit un faux gain : ça coûte plus cher en correction + après coup qu'en tokens économisés avant. **Dis "non trouvé" ou "incertain" + explicitement plutôt que de deviner.** +- Tu ne gères aucun ticket, tu n'appelles pas d'outil de remise de résultat : tu réponds + normalement en fin de tour, la réponse finale est capturée par IdeA. + +--- + +## 3. Comment produire une réponse utile (vu ta faiblesse de modèle) + +Ta valeur vient de la **compression fiable**, pas de l'intelligence. Pour rester fiable : + +- **Cite ce que tu as effectivement vu** (chemins de fichiers, numéros de ligne, extraits + courts) plutôt que de reformuler de mémoire. Une réponse vérifiable vaut mieux qu'une + réponse fluide. +- **Format court, structuré, sans prose.** Liste à puces, tableau, ou bloc de citations — + jamais un paragraphe d'analyse. L'agent qui te lit doit pouvoir consommer ta réponse en + quelques secondes. +- **Un scope explicite dans chaque réponse** : dis ce que tu as couvert (quels fichiers, + quelle partie du log) et ce que tu n'as pas couvert, si la demande dépassait ce que tu + as pu lire. +- **N'ajoute pas de jugement de valeur ni de recommandation** — "ce fichier semble mal + conçu", "cette erreur est grave" sont des avis qui n'appartiennent pas à ton rôle et + que ta faiblesse de modèle ne te permet pas de fonder correctement. +- **Reste bref.** Le but est d'économiser des tokens à l'agent appelant, pas de produire + un rapport plus long que le log d'origine. + +--- + +## 4. Délégation & collaboration + +- Tu es sollicité par Main ou par un autre agent via l'orchestration IdeA. Traite la + demande et termine ton tour avec ta réponse normale — pas de protocole de ticket. +- Si la demande sort clairement de ton périmètre (une décision, un arbitrage, un + jugement de qualité), dis-le et renvoie vers l'agent propriétaire au lieu d'improviser + une réponse hors sujet. +- Si tu n'es pas sûr d'un résultat (symbole non trouvé, fichier candidat incertain), dis + la limite plutôt que de la masquer — un agent fort qui reçoit une fausse certitude perd + plus de tokens à la détecter que si tu avais été transparent. \ No newline at end of file diff --git a/.ideai/agents/glm.md b/.ideai/agents/glm.md new file mode 100644 index 0000000..e69de29 diff --git a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json index cf217ab..c1f2d6c 100644 --- a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json +++ b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json @@ -12454,7 +12454,7 @@ "stdout_tail": "Confirmed — this is a byte-identical local cache of the full models.dev catalog (170 providers), refreshed by OpenCode itself.\n\nI now have enough to give a firm cadrage verdict.\n\n## Verdict de cadrage\n\n**L'hypothèse du commentaire actuel est fausse.** Elle l'était peut-être en 2025 (OpenCode plus jeune, moins de sous-commandes), mais avec OpenCode 1.17.18 installé sur cette machine, il existe **une source stable, publique et déjà utilisée par OpenCode lui-même** pour la liste complète des providers :\n\n- `opencode models --refresh` déclenche un fetch vers **`https://models.dev/api.json`** — un JSON public de **170 providers** (anthropic, openai, openrouter, azure, amazon-bedrock, ollama, zai, etc.), chacun avec `id`, `name`, et la liste de ses `models` (avec coût, limites, capacités en mode verbose).\n- Ce fetch est mis en cache localement par OpenCode dans `~/.cache/opencode/models.json` — fichier JSON stable, lisible directement sans réseau si déjà peuplé (ce qui est le cas dès qu'un utilisateur a lancé OpenCode une fois).\n- `opencode providers list` donne uniquement les credentials **déjà configurés** (pas le catalogue complet) — ce n'est pas la bonne commande pour peupler le picker, seulement pour distinguer \"provider déjà authentifié\" vs \"à configurer\".\n\nDonc il y a bien un contrat machine-readable stable : **le cache local `~/.cache/opencode/models.json`**, alimenté par le même endpoint `models.dev/api.json` qu'OpenCode consulte en interne — pas un détail d'implémentation fragile, c'est la source de vérité qu'OpenCode utilise pour lui-même.\n\n### Contrat proposé (dynamisation)\n\n- **Port** : nouveau `OpenCodeModelCatalogSource` (ou extension d'un port existant côté infra), avec deux implémentations possibles selon lot :\n - `LocalCacheOpenCodeCatalog` (lecture de `~/.cache/opencode/models.json` via `FileSystem`, zéro réseau, zéro dépendance nouvelle) — **lot minimal recommandé**.\n - Option future : fetch direct HTTP vers `models.dev/api.json` si le cache est absent/vide (premier lancement d'IdeA avant tout usage OpenCode) — nécessite un `HttpClient` port, hors périmètre immédiat.\n- **Invariant préservé** : `SecretRef`/`SecretStore` inchangés — le catalogue ne fait que déterminer *quels* `provider_id`/`display_name`/`models` proposer dans le picker ; la résolution du secret reste indépendante (même mécanisme qu'aujourd'hui).\n- **Cascade UI provider→modèle** inchangée : le DTO `OpenCodeProviderCatalogEntry { provider_id, display_name, models }` reste le même shape, juste la source des données change (fichier au lieu de constante Rust).\n- **Fallback obligatoire** : si le fichier cache est absent (OpenCode jamais lancé, ou pas installé), retomber sur le catalogue statique actuel (3 entrées) comme filet de sécurité — pas d'échec dur du picker.\n- **Risque à noter** : ce fichier est un cache non versionné, peut être absent/vide/périmé selon la machine utilisateur — c'est un détail d'implémentation OpenCode, pas une API contractuelle publiée. À documenter comme dépendance \"best effort\", pas \"garantie\".\n\n### Recommandation\n\nDynamiser via lecture du cache local `~/.cache/opencode/models.json` (chemin XDG résolu, portable Linux/Mac/Windows via `dirs`), avec fallback sur le catalogue statique existant. Ça enrichit drastiquement le picker (170 providers vs 3) sans dépendance réseau nouvelle ni port HTTP à créer — livrable en un lot backend raisonnable + un lot frontend qui reste passthrough.\n\nVoulez-vous que je lance Architect pour figer ce contrat avant de déléguer à DevBackend/DevFrontend ?", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "7a2972c5-c4e1-4889-afe0-df812bea71ac", @@ -12480,7 +12480,7 @@ "stdout_tail": "## Verdict : Piste A **et** Piste B combinées (pas un choix exclusif)\n\nLes deux pistes couvrent des besoins différents et se branchent sur le même contrat domaine sans se marcher dessus. Détail vérifié sur le binaire local (`opencode` 1.17.18) et la doc officielle.\n\n### Ce qui change tout : la matérialisation actuelle marche déjà pour n'importe quel provider connu de models.dev\n\nJ'ai lu `opencode_provider_config_json` (`crates/infrastructure/src/assistant/mod.rs:424-448`) : IdeA écrit aujourd'hui seulement\n```json\n{ \"model\": \"/\", \"provider\": { \"\": { \"options\": { \"apiKey\": \"…\" } } } }\n```\nPas de `npm`, pas de `baseURL`. Ça marche parce qu'**OpenCode résout lui-même** npm/baseURL/limits pour tout `provider_id` qu'il connaît via son registre interne — alimenté par `models.dev/api.json` (170 providers, confirmé en local : `~/.cache/opencode/models.json`, peuplé au premier lancement d'OpenCode, refresh via `opencode models --refresh`).\n\n**Conséquence directe : le seul verrou aujourd'hui est le catalogue statique à 3 entrées côté IdeA, pas la matérialisation.** Élargir le catalogue ne touche ni `OpenCodeProviderConfig`, ni `SaveOpenCodeProviderProfile`, ni `opencode_provider_config_json` — ces trois pièces acceptent déjà n'importe quel `provider_id`/`model` en `String` libre (`crate::validation::non_empty`, pas de whitelist).\n\n### Piste A — dynamiser le catalogue (le gros du gain, risque quasi nul)\n\n- Source : lecture de `~/.cache/opencode/models.json` (chemin XDG résolu par plateforme) via `FileSystem`. Fallback sur le catalogue statique actuel si absent (OpenCode jamais lancé sur la machine).\n- **DevBackend** : dans `provider_catalogue.rs`, remplacer/étendre `opencode_provider_catalogue()` par une fonction qui lit ce fichier (mapper `{id, name, models: {id: {name}}}` → `OpenCodeProviderCatalogEntry`), avec le fallback statique. `ListOpenCodeProviders::execute` reste identique en signature.\n- **DevFrontend** : rien à faire — `list_opencode_providers` reste un passthrough, le picker en profite automatiquement (3 → jusqu'à 170 entrées).\n- Zéro changement domaine, zéro changement de matérialisation.\n\n### Piste B — provider personnalisé (échappatoire pour ce que models.dev ne connaît pas)\n\nSchéma OpenCode confirmé (doc officielle) pour un provider hors registre :\n```json\n\"provider\": {\n \"myprovider\": {\n \"npm\": \"@ai-sdk/openai-compatible\",\n \"name\": \"Nom affiché\",\n \"options\": { \"baseURL\": \"https://api.example.com/v1\", \"apiKey\": \"…\" },\n \"models\": { \"model-id\": { \"name\": \"Nom du modèle\" } }\n }\n}\n```\nChamps minimums nécessaires côté IdeA : `provider_id`, `npm` (préremplir `@ai-sdk/openai-compatible` par défaut, éditable), `base_url`, `model_id`, `model_display_name` (optionnel, défaut = `model_id`), `api_key`.\n\n**Impact domaine** — extension additive de `OpenCodeProviderConfig` (`crates/domain/src/profile.rs:372`), pas de rupture :\n```rust\npub struct OpenCodeProviderConfig {\n pub provider_id: String,\n pub model: String,\n pub api_key_ref: SecretRef,\n pub custom: Option, // None = provider connu (comportement actuel)\n}\npub struct CustomProviderConfig {\n pub npm: String,\n pub base_url: String,\n pub display_name: Option,\n}\n```\n`skip_serializing_if = Option::is_none` sur `custom` ⇒ aucune régression de sérialisation pour les profils existants.\n\n**Invariants préservés** : `SecretRef`/`SecretStore` inchangés — la clé API suit exactement le même chemin de scellement (`SaveOpenCodeProviderProfile`), que le provider soit connu ou custom. Étendre `SaveOpenCodeProviderProfileInput` avec `custom: Option` optionnel.\n\n**DevBackend** :\n- Étendre `OpenCodeProviderConfig` + `SaveOpenCodeProviderProfile` comme ci-dessus.\n- Brancher `opencode_provider_config_json` : si `config.custom.is_some()`, émettre le bloc `npm`/`options.baseURL`/`models` en plus de `apiKey` ; sinon comportement actuel inchangé (branche existante intacte).\n\n**DevFrontend** :\n- Dans le picker provider (aujourd'hui liste fermée provider→modèle), ajouter une entrée \"Autre / personnalisé\" qui bascule sur un formulaire libre (provider_id, npm préremplig, base_url, model_id, nom modèle, clé API) au lieu de la cascade catalogue→modèles. Reste dans `frontend/src/adapters/profile.ts`/`requestResponseGateways.ts` en passthrough des nouveaux champs, pas de nouvelle logique métier côté UI.\n\n### Pourquoi les deux, pas une seule\n\n- A seule : ne couvre pas les endpoints auto-hébergés / providers OpenAI-compatible non listés sur models.dev (ex. LLM interne de l'utilisateur).\n- B seule : oblige l'utilisateur à connaître npm/baseURL même pour Anthropic/OpenAI/OpenRouter — mauvaise UX pour le cas courant, alors qu'A le résout gratuitement.\n- Combinées : le picker montre le catalogue enrichi (jusqu'à 170 providers) **+** une option \"Autre\" en bas pour le cas non couvert — couvre 100 % des besoins sans complexifier le chemin commun.\n\nJe peux lancer Architect pour figer ce contrat (extension `OpenCodeProviderConfig`/`CustomProviderConfig`) avant de déléguer à DevBackend/DevFrontend, ou passer directement — le contrat est suffisamment précis. Tu veux que je lance Architect d'abord ?", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "8b17f3db-1d32-40a1-89f2-8cf4321b0166", @@ -12506,7 +12506,7 @@ "stdout_tail": "`dirs` 6.0.0 is already in the lockfile (transitive dep, likely via Tauri), so it's available without adding a new direct dependency — but it must be used carefully (see below).\n\n## Verdict : le cadrage n'est pas portable Windows tel quel — Piste A a besoin d'un ajustement, B et le fallback sont déjà OK\n\n### 1. Piste A — le chemin du cache n'est PAS `%LOCALAPPDATA%\\opencode\\...`, et c'est confirmé comme un bug amont\n\nOpenCode résout ses dossiers via la lib npm **`xdg-basedir`**. Recherche confirmée sur les issues GitHub du dépôt officiel : **il existe un bug ouvert et documenté (issue #8235 \"Config and Data directories follow the Linux XDG standard even on windows\")** — OpenCode applique la convention XDG Linux même sous Windows, au lieu de `%LOCALAPPDATA%`/`%APPDATA%`. Autrement dit, sur la machine Windows de l'utilisateur, le fichier se trouve probablement en `\\.cache\\opencode\\models.json` (calqué sur `$HOME/.cache`, traduit via `os.homedir()` Node), **pas** dans le dossier AppData natif Windows. C'est corroboré par les issues similaires sur le dossier config (`~/.config/opencode` au lieu de `%APPDATA%\\opencode`).\n\n**Conséquence sur le contrat backend** :\n- ❌ Ne pas utiliser `dirs::cache_dir()` — cette fonction retournerait la *bonne* convention Windows (`%LOCALAPPDATA%`), ce qui serait le chemin correct... mais **pas celui qu'OpenCode utilise réellement aujourd'hui**. Utiliser cette fonction produirait un chemin qui ne matche jamais rien sur Windows.\n- ✅ Utiliser `dirs::home_dir()` (portable, résout `$HOME` sur Unix et `%USERPROFILE%` sur Windows) puis construire manuellement `/.cache/opencode/models.json` sur les 3 OS — c'est la reproduction exacte du comportement (buggé mais réel) d'OpenCode.\n- Respecter `$XDG_CACHE_HOME` s'il est positionné dans l'environnement du process IdeA, comme le fait OpenCode.\n- **Documenter explicitement dans le code** (commentaire) que ce chemin mime un comportement amont non conforme aux conventions Windows et suivre l'issue #8235 : si OpenCode corrige ce bug dans une version future, le chemin changera et cassera silencieusement la lecture — d'où l'obligation stricte du fallback statique (déjà prévu), qui devient ici la garantie de non-régression plutôt qu'un simple filet pour \"OpenCode jamais lancé\".\n\n### 2. Fallback statique — déjà portable, rien à changer\n\nC'est un `&'static [OpenCodeProviderCatalogEntry]` de constantes Rust, zéro I/O, zéro chemin. Aucune hypothèse Unix. OK tel quel.\n\n### 3. Piste B (provider personnalisé) — déjà portable, rien à changer\n\nVérifié dans `opencode_provider_config_json` (`crates/infrastructure/src/assistant/mod.rs`) : pure génération de `serde_json::Value` (chaînes, pas de séparateur de chemin, pas d'appel shell). Aucune supposition OS.\n\nUn point distinct à signaler, **hors périmètre #92** : le `chmod 0600` du `secret.key` dans `crates/infrastructure/src/store/secrets.rs:209-215` est déjà proprement isolé sous `#[cfg(unix)]` — sur Windows c'est un no-op silencieux aujourd'hui, le fichier n'hérite que des ACL NTFS par défaut du dossier parent, sans durcissement explicite par IdeA. **Ce n'est pas une régression introduite par #92** : ce comportement est déjà celui de tous les secrets existants (tokens SSH, clés API des autres profils), pas spécifique à OpenCode. Je le note pour mémoire mais je ne le mets pas dans le périmètre de ce ticket — à traiter séparément si jugé prioritaire (ex. `encrypt_file`/ACL Windows explicite via `windows-acl` ou équivalent).\n\n### Résumé des ajustements de contrat à porter à Architect/DevBackend\n\n- Piste A : résolution de chemin via `dirs::home_dir()` + suffixe `.cache/opencode/models.json` littéral (pas `dirs::cache_dir()`), avec respect de `XDG_CACHE_HOME` si présent, et fallback statique obligatoire (pas optionnel) sur toute erreur de lecture/parsing — le chemin est un mimétisme d'un comportement amont buggé, donc intrinsèquement instable dans le temps.\n- Piste B : aucun changement.\n- Fallback statique : aucun changement.\n- Gap ACL Windows sur `secret.key` : noté comme dette préexistante, hors périmètre #92.\n\nJe peux relayer cet ajustement à Architect pour qu'il fige la version finale du contrat avant DevBackend/DevFrontend — tu veux que je lance ça ?", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "720e886d-7fd4-4045-a359-e64ad78d774a", @@ -12532,7 +12532,7 @@ "stdout_tail": "**Décision Git — ticket #92 (rouvert) :**\n\n- L'état runtime (tickets #7/#43/#64/#92, tâche de fond, compteur/index) a été committé directement sur `develop` : `162e3ae` — pas de code de feature dedans, cohérent avec l'usage déjà en place sur cette branche.\n- Le travail précédent (`feature/ticket92-opencode-provider-cloud`) est terminé et déjà mergé (`12c7d10`) ; la branche est supprimée. Ce nouveau lot est un complément post-livraison avec un scope distinct (catalogue dynamique via cache `~/.cache/opencode/models.json` + fallback statique, option \"provider personnalisé\" en saisie libre) → **nouvelle branche de feature** plutôt qu'une reprise, pour ne pas mélanger l'historique de la livraison initiale avec ce complément.\n- Créée depuis `develop` (à jour, contient le merge du ticket #92 initial) : **`feature/ticket92-opencode-provider-dynamic-catalog`**, checkout effectué.\n\nDevBackend et DevFrontend peuvent implémenter sur cette branche. Je repasserai commiter et statuer sur le merge une fois l'implémentation terminée et testée par QA.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "401e1617-1652-4da8-88d1-784347a871d6", @@ -12558,7 +12558,7 @@ "stdout_tail": "You've hit your session limit · resets 12:20pm (Europe/Paris)", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "54bbad3a-894c-4114-bec1-79ddf862b30a", @@ -12584,7 +12584,7 @@ "stdout_tail": "All 6 new targeted tests pass. Full workspace build and test suite (`--test-threads=1`) are green.\n\n## Summary\n\nOn `feature/ticket92-opencode-provider-dynamic-catalog`:\n\n**1. Dynamic provider catalogue** (`crates/application/src/agent/provider_catalogue.rs`)\n- `opencode_provider_catalogue()` now reads `/.cache/opencode/models.json` (via `dirs::home_dir()` + literal suffix, respecting `$XDG_CACHE_HOME` — never `dirs::cache_dir()`, per OpenCode's upstream issue #8235), parses the models.dev shape, and falls back permanently to the original 3-entry static catalogue on any absence/read/parse failure.\n- Split fallback logic into a pure `catalogue_from_cache_bytes` for testability; validated the parsing against the real local OpenCode cache (`~/.cache/opencode/models.json`, 170 providers) to confirm the schema.\n- `OpenCodeProviderCatalogEntry` changed from `&'static str`/`&'static [&'static str]` to owned `String`/`Vec` (no longer static data); updated `backend/src/dto.rs`'s `From` impl accordingly.\n- Added `dirs = \"6\"` as a workspace + `application` dependency (already resolved transitively, so no lockfile surprises).\n\n**2. Custom provider option**\n- `domain::profile::OpenCodeProviderConfig` gained an additive `custom: Option` field (`#[serde(default, skip_serializing_if = \"Option::is_none\")]`, backward-compatible), plus a new `CustomProviderConfig` value object (`npm`, `base_url`, optional `display_name`) and a `.with_custom()` builder.\n- `SaveOpenCodeProviderProfileInput`/`SaveOpenCodeProviderProfile` extended with the same optional `custom` field, wired through `backend/src/dto.rs`'s request DTO.\n- Both `opencode_provider_config_json` implementations (`infrastructure/src/assistant/mod.rs` and `application/src/agent/lifecycle.rs`) now emit `npm`, `options.baseURL`, and a `models` block when `custom` is set, leaving the known-provider path (just `options.apiKey`) untouched.\n\nAdded unit tests for cache parsing (present/absent/malformed/empty) and for standard-vs-custom JSON generation in both duplicated sites. `cargo build --workspace` and `cargo test --workspace -- --test-threads=1` are fully green.", "stderr_tail": null }, - "completionDelivered": false + "completionDelivered": true }, { "id": "b8710867-a325-4966-9c42-d7fe73b5a651", @@ -12610,7 +12610,7 @@ "stdout_tail": "Frontend complement of ticket #92 is implemented and green.\n\n**Summary of changes** (all in `frontend/src/`):\n- `domain/index.ts`: added `CustomProviderConfig` type and `custom?` field on `OpenCodeProviderConfig`.\n- `ports/index.ts`: added optional `custom?` to `SaveOpenCodeProviderProfileInput`.\n- `adapters/profile.ts`, `adapters/http/requestResponseGateways.ts`, `adapters/mock/index.ts`: pass `custom` through to the backend call / mock save.\n- `features/first-run/FirstRunWizard.tsx` (`OpenCodeProviderFields`): \n - Added a filter `` above the provider `` + separate filter `` rather than building a new combobox component — matches the codebase's existing \"no custom combobox\" convention and needed no new shared UI primitive.\n- Filter input only renders once the catalogue is ready (not during loading/error), reusing existing loading/error states.\n- Placed \"Autre / personnalisé…\" as the last `