merge(batch): intègre #133 — §22.1 contrat de service des assets multi-fichiers ESM
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2375,4 +2375,26 @@ pub enum ScheduledTask {
|
||||
|
||||
(1) activation réelle du seam LLM (non activé, défaut heuristique, contrat ADR LS5) ; (2) balayage périodique de rotation (idempotent, non câblé) ; (3) discordance D19-4 vs `.gitignore` sur `.ideai/conversations/` (à trancher Git/Main) ; (4) intégration MCP e2e UX ; (5) évolutions multi-fenêtres du registre de sessions ; (6) auto-update mémoire/contexte *en cours* de session. Détail : `docs/LS8` §7.
|
||||
|
||||
## 22. Plugins — service des assets multi-fichiers & persistance plugin-owned (cadrage 2026-08-02, tickets #133 / #138)
|
||||
|
||||
> Débloque l'implémentation de #134/#135 (asset serving + confinement) et #139 (storage plugin-owned). Contexte : bug diagnostiqué — un plugin ESM composé de plusieurs fichiers (`dist/index.js` important `./constants.js`) casse au chargement avec `Importing a module script failed.` car le protocole `idea-plugin://` ne sert que 3 chemins nommés. En creusant le même chantier, un second trou est apparu : le SDK et son exemple de référence font persister l'état **interne** du plugin (`hello-plugin.txt`, `hello-plugin.json`) sous `.ideai/` du projet ouvert, ce qui pollue le repo utilisateur et survit à la désinstallation. Les deux décisions ci-dessous sont indépendantes mais partagent la même frontière de fond : *ce qui appartient au plugin ne doit jamais fuiter dans le projet, et doit disparaître intégralement à la désinstallation*.
|
||||
|
||||
### 22.1 #133 — Contrat de service des assets `idea-plugin://` (multi-fichiers ESM)
|
||||
|
||||
**Constat.** `asset_allowed` (`crates/app-tauri/src/plugins.rs:504-536`) vérifie déjà, avant toute décision : (a) l'entrée registre existe et `lifecycle_state.is_runtime_active()`, (b) `entry.content_hash == hash` de l'URL (intégrité du **package entier**), et le confinement de chemin (`target.starts_with(&root)` après `canonicalize`, lignes 467-483) est appliqué **après** `asset_allowed` sur toute requête autorisée. Une fois ces trois garanties posées, la fonction restreint encore le service au triplet `declared_main || declared_icon || rel.starts_with("assets/")` — un import ESM relatif sur un quatrième fichier (`./constants.js`, `./core/x.js`) est donc rejeté 403 alors que le fichier appartient au même package déjà intégralement vérifié.
|
||||
|
||||
**Décision.** Cette restriction par fichier n'ajoute aucune garantie de sécurité réelle : le modèle de menace est fixé par `content_hash` à l'installation (#135 audite ce chemin) — si le package est compromis, l'attaquant contrôle déjà `main` (donc l'exécution), peu importe quels fichiers *siblings* on l'autorise à récupérer ensuite. Restreindre le service à 3 chemins nommés casse des graphes de modules ESM légitimes sans arrêter quoi que ce soit que hash+confinement n'arrêtent pas déjà. **`asset_allowed` doit donc autoriser tout chemin relatif dès lors que les trois gardes déjà en place (registre actif, hash de contenu, confinement canonicalize) sont satisfaites — le manifeste (`validator.validate`) reste appelé comme garde d'intégrité globale mais cesse de gater le service fichier par fichier.**
|
||||
|
||||
Hors périmètre, figé : aucune résolution `node_modules`/bare specifiers. Le protocole ne fait que du service de fichier confiné, jamais de résolution de module Node. Un plugin avec des dépendances tierces les bundle ou les vendore en chemins relatifs — à son choix, jamais une obligation d'IdeA.
|
||||
|
||||
Contrat de confinement/désinstallation (formalisé, déjà vrai en pratique, à garder invariant) : la racine servie est exclusivement `app_data/plugins/installed/<pluginId>/` ; aucune install/serve ne peut jamais écrire ou exposer un chemin en dehors du project root ou de `.ideai/` de l'utilisateur — le plugin n'est jamais un citoyen du repo. Désinstallation = suppression complète de `installed/<id>` + entrée registre, zéro résidu (audité en #135). Ce point est distinct de l'état *propre* au plugin (§22.2), qui vit dans un répertoire frère, pas dans `installed/`.
|
||||
|
||||
**Débloque #134** : remplacer la dernière ligne de `asset_allowed` —
|
||||
```rust
|
||||
Ok(declared_main || declared_icon || rel.as_str().starts_with("assets/"))
|
||||
```
|
||||
— par une autorisation qui ne dépend plus de `declared_main`/`declared_icon`/du préfixe `assets/`, uniquement des gardes déjà calculées plus haut dans la fonction (entrée trouvée + `is_runtime_active()` + `content_hash == hash`). Le confinement canonicalize en aval (467-483) reste inchangé et continue de protéger contre toute évasion de racine. Tests de non-régression attendus sur path traversal et hash/lifecycle invalides (déjà spécifiés dans #134).
|
||||
|
||||
**Débloque #135** : le périmètre d'audit (écriture confinée à l'install, désinstallation 100%) est celui décrit ci-dessus ; #135 vérifie que `RelativePath::new` (rejette déjà `..` et absolu, `crates/domain/src/plugin.rs`) est bien appliqué côté install, pas seulement côté serve.
|
||||
|
||||
*Document maintenu par l'Agent Architecture — base du jalon « cadrage architecture » avant tout code applicatif.*
|
||||
|
||||
Reference in New Issue
Block a user