Convention du CHANGELOG.md¶
Cette page documente les règles d'écriture du fichier CHANGELOG.md à la racine du dépôt. Pour la consultation du changelog lui-même, voir la page Changelog.
Source de vérité et pipeline d'inclusion¶
Le changelog vit dans CHANGELOG.md à la racine du dépôt — c'est la seule version maintenue. La page docs/changelog.md se contente de l'inclure via la directive pymdownx.snippets :
<!--
Convention d'écriture de ce fichier : docs/reference/changelog.md
(page publiée : https://docs.kirexo.fr/reference/changelog/).
Règle critique : PAS DE LIENS MARKDOWN RELATIFS. Ce fichier est inclus
dans docs/changelog.md via `--8<-- "CHANGELOG.md"` (pymdownx.snippets) ;
mkdocs résout alors les liens relatifs depuis docs/changelog.md, donc
un lien `docs/reference/foo.md` devient `docs/docs/reference/foo.md`
→ warning → strict mode → build cassé → conteneur docs en exit 1.
À la place, utiliser une URL absolue :
- GitLab (préféré, fonctionne aussi en lecture brute sur GitLab) :
https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/reference/foo.md
- Doc publiée :
https://docs.kirexo.fr/reference/foo/
-->
# Changelog
Toutes les évolutions notables du projet Kirexo sont consignées ici.
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le projet adhère, autant que possible, au [Versionnement Sémantique](https://semver.org/lang/fr/).
Tant qu'aucune version `1.0.0` n'a été publiée, les évolutions sont versionnées par étape de cadrage (cf. `Étapes/étape<n>.md`). Une version `0.X.0` correspond à la clôture de l'étape *X*.
## [0.3.0] — 2026-05-19
### Correctifs rétrospective étape 3 — 2026-05-18
- **Fusion `FirewallWhitelistStore` → `FirewallWhitelist`** : une seule classe `src/DevContainer/FirewallWhitelist.php` combine désormais la logique pure statique (`isValidDomain`, `parseDomainLines`, `isAlreadyCovered` + constantes `DOMAIN_REGEX`, `DEFAULT_BASELINE_PATH`, `DEFAULT_LOCAL_PATH`) et les méthodes d'instance d'I/O fichier (`readBaseline`, `readLocal`, `readLocalRejected`, `isInBaseline`, `isInLocal`, `appendLocal`, `removeFromLocal`). La méthode `parseLocal` est **renommée `parseDomainLines`** pour aligner la sémantique avec le helper bash `firewall_parse_domain_lines` (`.devcontainer/init-firewall.lib.sh`).
- **Extraction de logique pure dans `ToolsUpgrader`** : `selectFrankenphpCandidate()` (sélection de tag candidat pour l'image base FrankenPHP, comparaison de tuples `(franken-major, php-major, php-minor)` + exclusion régression PHP) et `buildDockerfileUrls()` (reconstruction des URLs des `RUN curl …` à partir des `ARG *_VERSION`). Permet la couverture unitaire de `fetch_frankenphp_drift()` et de la sous-étape `host:check` HEAD HTTP sans réseau.
- **Section « Ce que l'isolation ne protège pas »** ajoutée dans [`docs/explanation/devcontainer.md`](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/explanation/devcontainer.md) entre « Comment ça marche — flow réseau » et « Pourquoi NE PAS monter `/var/run/docker.sock` ». Deux limites assumées documentées : exfiltration via l'API Anthropic (les credentials lus dans `${HOME}/.claude/.credentials.json` peuvent partir dans le contenu d'une requête vers `api.anthropic.com` légitimement whitelisté — le RO empêche la persistance entre sessions, pas l'exfiltration en session) et contournement potentiel via DNS-over-HTTPS (préférer des FQDNs précis, pas des suffixes génériques de CDN).
- **Contrôle négatif documentaire** : `203.0.113.1` (IP RFC 5737 TEST-NET-3, garantie non routable) remplace `example.com` dans le tutoriel [Ouvrir Kirexo dans un Dev Container PHPStorm](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/tutorials/devcontainer-phpstorm.md). Reste proche du modèle de menace (filtrage par socket, pas seulement par résolution DNS) et évite qu'un domaine de test finisse par accident dans la whitelist.
- **Références obsolètes à `init-firewall.sh` pour l'ajout de domaine** corrigées : `.devcontainer/AGENTS.md` et `.devcontainer/whitelist.local.txt.dist` pointent désormais vers `baseline-domains.txt` (source de vérité versionnée de la baseline) et `castor devcontainer:whitelist-add`. La whitelist n'est plus inlinée dans `init-firewall.sh`.
- **`castor devcontainer:check-tools` / `upgrade-tools` étendus au drift de l'image base FrankenPHP** : interrogation de Docker Hub (`hub.docker.com/v2/repositories/dunglas/frankenphp/tags/`), warning non bloquant dans `upgrade-tools` (le bump cible une instruction `FROM`, pas un `ARG` — donc manuel), compté dans le `exit 2` de `--check`. Couvre indirectement les paquets apt et `install-php-extensions` livrés via la base.
- **`castor host:check` étendu de 3 à 5 sous-étapes** : ajout de **hadolint** sur le `Dockerfile` (`docker run --rm -i hadolint/hadolint:latest hadolint --failure-threshold error - < Dockerfile`, seules les erreurs sont bloquantes ; warnings et info remontés mais tolérés) et de **HEAD HTTP en parallèle** sur les URLs construites par `ToolsUpgrader::buildDockerfileUrls()` à partir des `ARG *_VERSION` (Castor, glab, tarball Node, `SHASUMS256.txt`). Détecte une release upstream supprimée/renommée avant un rebuild qui rejetterait silencieusement un fichier corrompu via `sha256sum -c`. Coût observé : ~0.8 s en cache, ~3.5 s au premier appel (pull de l'image hadolint).
- **Documentation Diátaxis mise à jour** :
- [`docs/reference/devcontainer.md`](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/reference/devcontainer.md) : `parseLocal()` → `parseDomainLines()` dans la ligne `baseline-domains.txt`, nouvelle section « Surveillance de l'image base FrankenPHP » sous l'outillage.
- [`docs/reference/commandes-castor.md`](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/reference/commandes-castor.md) : descriptions de `host:check` (5 sous-étapes), `check-tools` et `upgrade-tools` enrichies du volet image base FrankenPHP.
- [`docs/explanation/devcontainer.md`](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/explanation/devcontainer.md) : section « Ce que l'isolation ne protège pas ».
### Ajouté
- `CHANGELOG.md` racine au format Keep a Changelog, exposé dans la doc Diátaxis en complément de la nav existante.
- **Dev Container** dérivé de [`dunglas/symfony-docker`](https://github.com/dunglas/symfony-docker) :
- `.devcontainer/devcontainer.json`, `compose.devcontainer.yaml`, `post-create.sh`, `post-start.sh`.
- Stack complète accessible depuis l'IDE (PHPStorm 2024.1+ ou VS Code) sans installer PHP/Node/Composer côté hôte.
- Stratégie d'isolation **sans socket Docker monté** — `castor docker:*`, `cert:trust`, `doctor`, `docs:build` refusent de tourner depuis l'intérieur (cf. `assert_outside_container()` dans `castor.php`).
- **Pare-feu interne** (`init-firewall.sh`) restreignant le trafic sortant à une whitelist de domaines :
- Policies `OUTPUT/INPUT/FORWARD DROP` posées immédiatement après le flush.
- `dnsmasq` + `ipset allowed-domains` alimentés à la volée à chaque résolution DNS.
- Plage CIDR intra-stack unique `172.30.0.0/24` (réseau Docker custom `kirexo`) au lieu des trois RFC1918.
- Override local `whitelist.local.txt` (gitignored) concaténé à la baseline si présent.
- Smoke-tests post-reload : contrôle négatif **bloquant** sur `203.0.113.1` (RFC 5737), contrôles positifs **non bloquants** sur `gitlab.com` / `symfony.com` / `packagist.org` / `registry.npmjs.org`, résolution intra-stack **bloquante**.
- **Mode `bypassPermissions` Claude Code** activé par `post-create.sh` via création de `.claude/settings.local.json`, avec opt-out :
- One-shot par variable d'environnement `KIREXO_CLAUDE_BYPASS=0`.
- Persistant par marqueur fichier `.devcontainer/no-claude-bypass` (gitignored).
- **Embarquement direct de Node + Claude Code** dans le stage `frankenphp_dev` du `Dockerfile` (et non via les `features` du devcontainer) pour contourner un bug du pipeline JetBrains qui re-cible le stage `frankenphp_dev` sur un Dockerfile temporaire.
- **Vérification d'intégrité par SHA-256** sur les binaires Castor, glab et Node (ARG `*_VERSION` + `*_SHA256`). Claude Code repose sur le `dist.integrity` SRI sha512 du registre npm.
- **`autocomplete Castor` system-wide** dans le devcontainer via `/etc/bash.bashrc` (sourcing de `bash-completion` + `eval "$(castor completion bash)"`).
- **Logique pure testable** extraite dans `src/DevContainer/` :
- `FirewallWhitelist` — validation FQDN, parsing baseline, dédoublonnage.
- `ToolsUpgrader` — extraction et substitution d'`ARG` dans le `Dockerfile` et la doc.
- **Nouvelles cibles Castor** (cf. `docs/reference/commandes-castor.md`) :
- `castor devcontainer:upgrade-tools [--check|--apply]` — résout les `latest` (Castor via GitHub API, glab via GitLab API, Node majeure LTS via `nodejs/Release/schedule.json`, Node patch via `nodejs.org/dist/index.json`, Claude Code via npm registry), recalcule les SHA-256, met à jour le `Dockerfile` et la doc.
- `castor devcontainer:firewall-reload` — wrapper sur `sudo init-firewall.sh`, refuse l'hôte.
- `castor devcontainer:whitelist-add <domain>` / `whitelist-remove <domain>` — édite `whitelist.local.txt` puis recharge le pare-feu, avec validation FQDN et dédoublonnage.
- `castor devcontainer:doctor` — pendant interne de `castor doctor` (vérifie l'amorçage, dnsmasq, l'ipset, FrankenPHP sur `127.0.0.1`, la résolution intra-stack, les migrations à jour).
- `castor devcontainer:frankenphp-restart` — relance le supervisor FrankenPHP après bail-out ou kill manuel.
- `castor lint:shell` — `shellcheck` sur les scripts `.devcontainer/*.sh` et `.claude/hooks/*.sh`, intégrée à `castor lint`.
- **Supervisor FrankenPHP** (`frankenphp-supervisor.sh`) avec backoff exponentiel borné (`INITIAL_DELAY=2`, `MAX_DELAY=30`) et bail-out après 5 crashs consécutifs sans stabilisation (>60s).
- **Marqueurs d'amorçage** `.devcontainer/.bootstrap.{ok,failed}` écrits par `post-create.sh` et consultés en première ligne par `castor devcontainer:doctor`.
- **Tests unitaires** :
- `FirewallWhitelistTest` — validation FQDN, parsing baseline et locale, dédoublonnage.
- `ToolsUpgraderTest` — extraction et substitution d'ARG, calcul du diff de versions.
- `FirewallRegexConsistencyTest` — garde-fou contre le drift entre la regex bash (`DOMAIN_VALIDATION_REGEX`) et la regex PHP (`FirewallWhitelist::DOMAIN_REGEX`) : égalité textuelle + dataset croisé `preg_match` ↔ `[[ =~ ]]`.
- **Documentation Diátaxis** :
- Tutoriel : [Ouvrir Kirexo dans un Dev Container PHPStorm](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/tutorials/devcontainer-phpstorm.md).
- Guides : [Whitelister un domaine](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/how-to/whitelister-un-domaine.md), [Debugger le pare-feu](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/how-to/debugger-le-pare-feu-devcontainer.md), [Activer l'autocomplétion Castor sur l'hôte](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/how-to/activer-lautocompletion-castor.md).
- Référence : [Dev Container](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/reference/devcontainer.md).
- Explication : [Choix d'isolation du Dev Container](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/explanation/devcontainer.md).
- `.devcontainer/whitelist.local.txt.dist` versionné comme modèle pour le fichier d'override local.
### Modifié
- `assert_outside_container()` (`castor.php`) accepte désormais une suggestion contextuelle d'alternative ; `castor doctor` et `castor docs:build` lancés depuis l'intérieur du devcontainer affichent une commande utilisable directement (`castor devcontainer:doctor` et `curl -sIf http://localhost:8000/` respectivement) en plus du refus.
- `Dockerfile` (stage `frankenphp_dev`) : ajout de `sudo`, `iptables`, `ipset`, `dnsmasq`, `jq`, `iproute2`, `ca-certificates`, `shellcheck`, `bash-completion`. Création d'un user `app` aligné sur l'UID/GID hôte.
- `frankenphp/Caddyfile` : ajout de `skip_install_trust` au bloc global pour empêcher Caddy de tenter d'installer sa CA racine dans le trust store du conteneur (qui exigerait `sudo`).
- `compose.override.yaml` : définition d'un réseau Docker custom `kirexo` (`172.30.0.0/24`) + build args `USER_ID`/`GROUP_ID`.
### Sécurité
- `bypassPermissions` Claude Code rendu acceptable par l'addition de deux barrières structurelles : isolation filesystem (le conteneur ne voit que `/app`) + isolation réseau (pare-feu iptables sur whitelist). Sans ces deux barrières, ne pas reproduire ce paramétrage.
- GitHub volontairement **exclu** de la whitelist : le projet est hébergé sur GitLab, et les binaires GitHub indispensables (Castor, glab) sont récupérés au **build de l'image** via le réseau hôte, pas au runtime depuis le conteneur.
- **Durcissements post-audit** (4 mécanismes ajoutés en seconde passe, cf. [Référence](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/reference/devcontainer.md)) :
- **Surcharge RO ciblée** sur `${HOME}/.claude/.credentials.json`, `${HOME}/.claude/settings.json` et `${HOME}/.claude/CLAUDE.md` — un Claude éventuellement compromis dans le conteneur ne peut plus *réécrire* ces fichiers (exfiltration via la prochaine session côté hôte impossible). L'historique de conversations (`projects/`, `sessions/`, `history.jsonl`) reste en RW pour rester persisté côté hôte.
- **Bind-mount RO** sur `init-firewall.sh`, `init-firewall.lib.sh`, `baseline-domains.txt` et `firewall-healthcheck.sh` — un sous-processus malveillant ne peut plus réécrire les scripts puis attendre le prochain reload pour s'exécuter en root via le sudoers NOPASSWD. Pour modifier ces fichiers, redémarrer le devcontainer.
- `security_opt: ["no-new-privileges:true"]` — empêche un binaire setuid/setgid d'élever ses droits dans le conteneur.
- **Healthcheck enrichi** d'une 4e condition `sudo -n /app/.devcontainer/firewall-healthcheck.sh` (wrapper read-only qui vérifie `iptables -S OUTPUT | grep -q '^-P OUTPUT DROP'`). Détecte un `iptables -F` manuel oublié qui laissait jusqu'ici le pare-feu silencieusement ouvert avec dnsmasq encore vert.
### Ajouté (post-audit)
- **Tests bats** dans `tests/Bash/firewall/` (43 tests, 3 fichiers) — couvrent les fonctions pures `firewall_is_valid_domain`, `firewall_parse_domain_lines`, `firewall_is_already_covered`. Premier lot de tests shell du projet ; convention bats documentée dans `tests/Bash/firewall/README.md`.
- `.devcontainer/init-firewall.lib.sh` — fonctions pures extraites de `init-firewall.sh` pour permettre les tests bats sans NET_ADMIN. La regex `FIREWALL_DOMAIN_REGEX` y est définie en `readonly`, partagée avec `App\DevContainer\FirewallWhitelist::DOMAIN_REGEX` côté PHP.
- `.devcontainer/firewall-healthcheck.sh` — wrapper sudo read-only utilisé par le healthcheck Docker pour valider que la policy `OUTPUT DROP` est toujours posée.
- **Cible Castor `test:bash`** + intégration dans `castor test` et `castor all` (entre `test:unit` et `test:e2e`). Lance `bats --recursive tests/Bash/` dans le conteneur.
- **Pare-feu activé dans `post-create.sh`** (avant `castor install`) — la phase d'amorçage Composer/importmap/Tailwind est maintenant filtrée par la whitelist au lieu de tourner avec un réseau ouvert.
- **Mermaid** activé dans mkdocs (`pymdownx.superfences` + `custom_fences`) — diagrammes du flow réseau (DNS → dnsmasq → ipset → iptables, cas rejet, ordre d'exécution) ajoutés dans `docs/explanation/devcontainer.md`.
- **`bats`** ajouté au stage `frankenphp_dev` du Dockerfile (paquet Debian, v1.x).
- **Auto-relance de FrankenPHP** via le healthcheck Docker du service `php` (devcontainer) — rattrape le cas où PhpStorm zappe le `postStartCommand` en s'attachant à un conteneur déjà démarré. Logique déportée dans `.devcontainer/php-healthcheck.sh` (bind-mount RO, deny-list Claude), avec deux garde-fous (`pgrep frankenphp-supervisor.sh` + lockfile mtime `RELAUNCH_MIN_INTERVAL=30s`), détachement via `setsid -f` et audit dans `var/log/devcontainer/frankenphp.log`. Cf. [Référence — Auto-relance de FrankenPHP](https://gitlab.com/shaurifr/kirexo/-/blob/main/docs/reference/devcontainer.md#auto-relance-de-frankenphp).
### Modifié (post-audit)
- `castor.php` allégé (1602 → 868 lignes) : extraction des cibles `devcontainer:*` + helpers associés dans `castor/devcontainer.php`, importé via `Castor\import()`. Inventaire des cibles identique (46), comportement inchangé.
- `init-firewall.sh` refactoré pour sourcer `init-firewall.lib.sh` — suppression des deux boucles inlinées de parsing baseline/locale, validation FQDN déléguée à `firewall_is_valid_domain`. Sémantique inchangée.
- `Dockerfile` (stage `frankenphp_dev`) : sudoers étendu à un **second** script (`firewall-healthcheck.sh`) en NOPASSWD — auparavant seul `init-firewall.sh` était autorisé.
- `compose.devcontainer.yaml` : ajout `security_opt`, 3 bind-mounts RO `${HOME}/.claude/*`, 5 bind-mounts RO `.devcontainer/*` (dont `php-healthcheck.sh`), healthcheck déporté dans `php-healthcheck.sh` (4 conditions enchaînées + auto-relance FrankenPHP).
- `.devcontainer/AGENTS.md` : pointer explicite vers `/CLAUDE.md` comme source de vérité primaire, rappel du quota 5 fichiers PHP et de la délégation obligatoire aux sous-agents.
### Défaut connu (à corriger en suivi)
- **`FIREWALL_DOMAIN_REGEX` accepte les IPs littérales** : la regex actuelle laisse passer `1.2.3.4` comme FQDN valide (labels purement numériques autorisés). La doc dit le contraire. Découvert pendant l'écriture des tests bats — le test verrouille le comportement actuel avec un commentaire pointant l'inversion à faire le jour où on durcit la regex (côté lib **et** côté `FirewallWhitelist::DOMAIN_REGEX` PHP, sinon `FirewallRegexConsistencyTest` cassera).
## [0.2.0] — Étape 2 — Cadrage, outillage et documentation
### Ajouté
- `castor.php` à la racine — task runner Castor, source unique des cibles de qualité et de développement (`cs:fix`, `phpstan`, `lint`, `test:unit`, `test:e2e`, `qa`, `all`, …).
- Documentation [Diátaxis](https://diataxis.fr/) initiale publiée via `mkdocs-material` (service `docs` du compose).
- `mkdocs.yml` à la racine + image custom `mkdocs/Dockerfile` (plugins `git-revision-date-localized` et `social`).
- Sous-agents Claude Code dans `.claude/agents/` (`reviewer`, `test-writer`, `doc-writer`) avec hooks de délégation et de comptage de fichiers PHP de production.
## [0.1.0] — Étape 1 — Bootstrap
### Ajouté
- Bootstrap du projet à partir de [`dunglas/symfony-docker`](https://github.com/dunglas/symfony-docker) (FrankenPHP + Symfony + Docker Compose).
- Services Postgres, Redis, RabbitMQ, Typesense, Gotenberg, Mailpit ajoutés au compose.
- `CLAUDE.md` initial — cadrage technique, patterns d'architecture (CQRS, plugins isolés, Voters, Live Components, DTOs immuables, Repository pattern), règles de développement.
[0.3.0]: https://gitlab.com/shaurifr/kirexo/-/compare/v0.2.0...v0.3.0
[0.2.0]: https://gitlab.com/shaurifr/kirexo/-/compare/v0.1.0...v0.2.0
[0.1.0]: https://gitlab.com/shaurifr/kirexo/-/tags/v0.1.0
La configuration mkdocs (cf. mkdocs.yml) étend base_path à la racine du projet pour permettre cette inclusion, et check_paths: true fait échouer le build si le snippet est introuvable.
Règle : pas de liens Markdown relatifs¶
Interdit dans CHANGELOG.md : tout lien Markdown relatif vers une page de la doc, par exemple [Référence](docs/reference/devcontainer.md).
Pourquoi : après inclusion via --8<--, le rendu du lien est résolu par mkdocs relativement à la page hôte (docs/changelog.md), pas relativement à la racine du dépôt. Un lien docs/reference/devcontainer.md pointe alors vers docs/docs/reference/devcontainer.md qui n'existe pas. mkdocs émet un warning, et comme mkdocs.yml est en strict: true, le build échoue avec un exit code 1 — le conteneur kirexo-docs-1 ne démarre plus.
Alternatives valides¶
Par ordre de préférence dans le contexte du CHANGELOG.md :
-
URL GitLab absolue vers le fichier sur la branche
main— style en place dans leCHANGELOG.md(cf. liens vers les pages de doc dans la section Documentation Diátaxis de la version[0.3.0]) :Avantage : le lien fonctionne aussi bien depuis la page mkdocs que depuis la vue GitLab du
CHANGELOG.md(où la doc n'est de toute façon pas rendue). C'est le style à adopter par défaut. -
URL absolue vers la doc publiée (
https://docs.kirexo.fr/...) — utilisable une fois la doc déployée publiquement. Même propriété : fonctionne dans les deux contextes de lecture (GitLab et mkdocs). -
Lien externe absolu (RFC, doc d'un projet tiers, etc.) — sans contrainte particulière, c'est le seul cas où on peut écrire un lien de manière naturelle.
Rappel in situ¶
Un commentaire HTML en tête du CHANGELOG.md racine renvoie vers cette page. Il est invisible dans le rendu mkdocs comme dans le rendu GitLab, mais visible pour tout contributeur (humain ou agent) qui ouvre le fichier pour l'éditer. Ne pas le supprimer.