Skip to content

Next Generation Implementation Roadmap

CYPT71 edited this page Aug 18, 2026 · 1 revision

Feuille de route d'implémentation nouvelle génération

Cette page transforme la proposition d'architecture en liste de travail vérifiable. Une case cochée signifie que la capacité existe dans le dépôt et possède des tests ; elle ne signifie pas qu'une commande externe fournit temporairement une capacité équivalente.

État vérifié le 4 août 2026. Les jalons v1, v2 et v3 sont complets par les critères de sortie de chacun (voir les sections dédiées). Le protocole plugin, cinq langages (Go/Python/JavaScript/ TypeScript/C#) avec de vrais SDK importables, passe la même suite de conformance ; le sandbox plugin isole namespace utilisateur/réseau/IPC/ UTS, fichiers (remount lecture seule, tmpfs sur les répertoires sensibles) et ressources (rlimits) - reste ouvert : pas de limite mémoire par langage (RLIMIT_AS testé et abandonné, casse le démarrage du runtime Go), et repli non sandboxé hors Linux. v4 (VMM natif) et v5 (plateforme distribuée) restent ouverts par construction - voir leurs sections. Les cases ci-dessous distinguent les capacités testées des interfaces réservées ou des bibliothèques qui ne sont pas encore reliées à un chemin d'exécution réel.

Le jalon v3 est complet. Le CAS chunké/reprenable, le client Registry natif, la reproductibilité hermétique testée sur les trois OS de la matrice CI, SBOM et provenance natifs, signature Ed25519/ECDSA et vérification de chaîne X.509, ainsi que le moteur de politique, sont intégrés au parcours de publication natif. Un banc logique de 2 TiB exerce la mémoire bornée et la déduplication sans exiger 2 TiB de disque. launch --publish --yes orchestre freeze, deux builds reproductibles, preuves natives, publication puis exécution. Les sessions Registry sont persistées en 0600, réconciliées avec l'offset serveur puis supprimées après installation exacte du blob. Une reprise après crash publie exactement les blobs attendus, et une image altérée, non attestée ou non reproductible est refusée avant toute mise à jour de tag.

Parcours utilisateurs. Le dépôt fournit désormais un exemple canonique par capacité majeure sous examples/. Les nouveaux guides examples/podman-microvm et examples/containerd-kubernetes/runtimeclass.yaml montrent respectivement les deux modes du même OCI (conteneur standard ou MicroVM opt-in) et un Pod Kubernetes sélectionnant explicitement la RuntimeClass platform-factory. Le catalogue et la structure du manifeste sont protégés par examples/catalog_test.go ; ces tests empêchent la dérive mais les documents restent écrits pour les utilisateurs, pas comme fixtures CI.

Audit du 3 août 2026 — 7 cases réouvertes. Après fusion de release/v4.0.0 dans main, un audit ciblé des paquets ajoutés récemment (internal/errors, internal/observability, internal/budget, internal/quota, internal/provenance/signing.go, internal/hypervisor/virtio, internal/hypervisor/sandbox) a trouvé qu'aucun n'est importé en dehors de son propre paquet et de son propre test — vérifié par grep sur tout le dépôt, pas par lecture partielle. Chacun respecte la première clause de la définition de terminé (« l'implémentation est dans le dépôt ») mais pas la dernière (« la CI exécute le chemin concerné ») : ce sont des bibliothèques testées isolément, jamais appelées par un chemin d'exécution réel (control plane, worker, scheduler, VMM). Deux cases portaient en plus une justification explicitement fausse (usage prétendu par internal/policy et internal/oci/builder, infirmé par audit). Les 7 cases concernées sont décochées ci-dessous avec le détail de l'audit ; aucune n'affecte les critères de sortie déjà atteints de v1/v2/v3, qui reposent sur un code différent et réellement câblé.

Suivi le 3 août 2026, même session. internal/errors (câblé dans internal/policy), internal/observability (câblé dans platform-factory-control-plane), internal/budget (câblé dans internal/oci.Build, pas internal/executor - voir la case, la mesure par processus appelant y aurait été fausse), internal/quota (câblé en opt-in dans platform-factory-control-plane) et internal/provenance/signing.go (câblé entre platform-factory-worker et platform-factory-control-plane, avec la bonne API WorkloadSigner - pas SignWithWorkloadIdentity, dont l'audit a aussi trouvé qu'elle ne garantit rien par construction) sont maintenant réellement intégrés et re-cochés ci-dessous, chacun avec sa preuve. Restent ouverts : internal/hypervisor/virtio (stubs de configuration, aucun protocole virtio réel) et internal/hypervisor/sandbox (seccomp/namespaces/cgroups sont eux-mêmes des stubs internes, pas seulement non câblés - reporté à un environnement Linux/KVM réel pour être implémenté et prouvé, plutôt que d'écrire du code noyau non vérifiable ici).

Contraintes non négociables

  • Conserver la génération du layout OCI indépendante de Docker et Podman.
  • Conserver BuildKit hors du moteur et de son chemin critique.
  • Retirer Skopeo du chemin de publication au profit du client Registry natif.
  • Retirer Skopeo des éventuels chemins d'import restants. Audit : aucune référence à Skopeo dans cmd//internal/ (seul ci-compatibility.yml l'utilise, intentionnellement, comme client externe validant la compatibilité - voir ADR-007). L'import local vers un runtime (cmd/platform-factory/import.go) construit déjà nativement des archives OCI/Docker Save. ci-security.yml échoue désormais si le littéral skopeo apparaît dans un fichier .go de cmd/internal, garde indépendante de l'allowlist os/exec existante.
  • Retirer la CLI Cosign des chemins de signature et de vérification au profit du moteur cryptographique natif. Audit : aucune référence à cosign dans cmd//internal/ ; internal/signing implémente Ed25519/ECDSA nativement et launch --publish l'utilise déjà (voir critères de sortie v3). Seuls ci-release.yml, ci-microvm.yml, ci-benchmark.yml et ci-supply-chain-e2e.yml invoquent Cosign, intentionnellement, comme étage de confiance CI séparé (signature keyless GHCR, signature de preuves, tests d'interopérabilité d'admission - voir ADR-006/ADR-007), jamais comme chemin du produit. ci-security.yml échoue désormais si le littéral cosign apparaît dans un fichier .go de cmd/internal, même garde mécanique que pour Skopeo.
  • Retirer les outils SBOM externes du chemin autonome et de publication.
  • Fournir un mode complet qui ne lance aucun moteur de build, de publication, de signature ou de virtualisation tiers. Prouvé par l'absence totale de skopeo, cosign, qemu, firecracker, cloud-hypervisor, kata ou libkrun dans cmd/ et internal/. Le chemin platform-factory launch --publish utilise uniquement : toolchains OCI épinglées par digest, client Registry natif, moteur crypto natif (Ed25519/ECDSA), et VMM natif (KVM/HVF).
  • Documenter et tester la frontière entre la bibliothèque standard Go, les toolchains applicatives traitées comme entrées et le code détenu par le projet. Voir Next-Generation-Architecture#the-three-way-boundary : bibliothèque standard épinglée une fois via go.mod et GOTOOLCHAIN=local sur chaque étape CI invoquant go, toolchains applicatives toujours des entrées OCI épinglées par digest (jamais réimplémentées), code du projet natif sans appel à des CLI externes (garde os/exec en CI). Les trois sont maintenant vérifiés mécaniquement : scripts/ci/verify-workflows.py échoue si un run: invoque le toolchain Go sans GOTOOLCHAIN=local dans son env (ou celui du job) - a détecté et corrigé 5 étapes réellement non épinglées (ci-benchmark.yml, ci-dast.yml, ci-microvm.yml, ci-release.yml) avant l'ajout du contrôle.

Le mode MicroVM cible ne reposera sur aucun backend Firecracker, Cloud Hypervisor, libkrun ou Kata. platform-factory possédera son VMM :

  • Linux : API KVM native ;
  • macOS : API de virtualisation native de macOS ;
  • Windows : API de virtualisation native de Windows.

Les API macOS et Windows nécessiteront des adaptateurs système spécifiques, mais aucun moteur de VM externe. QEMU/KVM reste uniquement une implémentation historique de test jusqu'à son remplacement ; elle ne constitue pas le backend de la nouvelle architecture.

État transversal

  • CLI unifiée platform-factory.
  • Logs structurés avec identifiant de trace sur le constructeur OCI.
  • Tests Go, race detector et seuil de couverture en CI. Le seuil de couverture (86 %) est réellement appliqué, pas seulement déclaré : plusieurs pushs consécutifs sont tombés sous le seuil (82,2-82,4 %) et ci-quality a échoué en conséquence. Corrigé par des tests négatifs réels ajoutés à travers plusieurs paquets (internal/rootfs, internal/cache, internal/hypervisor, internal/runtime, cmd/platform-factory-control-plane, cmd/platform-factory, internal/oci) plutôt qu'en abaissant le seuil - ci-quality est repassé au vert de façon reproductible sur les commits suivants.
  • Actions GitHub épinglées par SHA et politique de licences.
  • cmd/platform-factory-installer séparé en module Go distinct. Fait le 4 août 2026 (commit d714c01). Extraction purement mécanique - n'importait déjà aucun internal//api/ du dépôt racine - vers son propre go.mod/go.sum, ajouté à go.work. Le module racine n'a plus les dépendances bubbletea/huh/lipgloss après go mod tidy ; invocations existantes (go run ./cmd/platform-factory-installer, scripts/local/bootstrap.sh) inchangées depuis la racine via la résolution de workspace.
  • cmd/platform-factory-installer installe platform-factory-runtime. Corrigé le 5 août 2026, à la suite des périphériques virtio et de l'annotation platform-factory.dev/network-tap ci-dessus (« Linux — KVM natif ») : le composant microvm ne construisait que microvm-init/ microvm-initramfs, jamais platform-factory-runtime lui-même - le binaire que Podman/Docker/containerd invoquent réellement via --runtime platform-factory-runtime. Un utilisateur choisissant « Support MicroVM » se retrouvait donc sans le seul binaire qui branche concrètement ce support sur un moteur de conteneurs. platform-factory-runtime ajouté aux binaires du composant microvm (cmd/platform-factory-installer/components.go) ; TestBuildStepsFlattensBinariesForEachSelectedComponent mis à jour en conséquence. Vérifié pour de vrai, pas seulement par le test unitaire : go run ./cmd/platform-factory-installer -components microvm -prefix <tmp> -yes construit et installe les quatre binaires (platform-factory, microvm-init, microvm-initramfs, platform-factory-runtime), le dernier exécutable et répondant normalement à ses propres flags.
  • go build ./... compile sur toutes les plates-formes cibles, pas seulement Linux. Corrigé le 5 août 2026 (commit 245c6af), trouvé en essayant d'installer platform-factory dans ./bin sur macOS. cmd/ platform-factory/microvm_native.go (le backend KVM natif ajouté par les périphériques virtio ci-dessus) référençait kvm.OpenTAP/ kvm.RunLinuxWithOptions/kvm.LinuxRunOptions/kvm.NetworkDeviceOptions
    • symboles réservés à linux && amd64 par leur propre étiquette de build - depuis un fichier sans étiquette du tout, cassant go build ./... sur toute cible non linux/amd64 (confirmé aussi en croisant vers windows/amd64 : même échec). nativeKVMEligible fait bien un test runtime.GOOS/runtime.GOARCH, mais Go type-vérifie chaque fichier d'un paquet qu'il soit atteignable à l'exécution ou non - un test runtime ne protège pas une étiquette de build manquante. ci-quality.yml ne pouvait pas le détecter : il ne construit que sur ubuntu-24.04. Corrigé en extrayant runNativeKVM et ses aides (findRepoRoot, readEntrypoint, readVerifiedBlob) vers un nouveau fichier //go:build linux && amd64, avec un stub //go:build !(linux && amd64) pour que runNativeKVMSubcommand (qui l'appelle sans condition) reste portable. nativeKVMEligible lui-même non modifié. Vérifié : go build/go vet propres pour darwin/arm64 natif, linux/amd64 et windows/amd64 croisés ; 171/175 tests de cmd/platform-factory passent sur cet hôte macOS - les 4 restants (TestNativeKVMEligible et trois tests de dispatch qui en dépendent) sont un écart préexistant, distinct et non touché : nativeKVMEligible refuse tout hôte non linux/amd64 sans condition, alors que main_test.go attend que le dispatch natif fonctionne aussi via HVF réel sur macOS - un écart resté invisible tant que le paquet ne compilait pas du tout sur cette plate-forme. Corrigé en même temps, trouvaille annexe : internal/hypervisor/sandbox.ProbeSandbox() côté non-Linux ne renseignait qu'une clé générique "platform" dans Details, alors que platform-factory doctor (voir plus bas) et sandbox_linux.go lisent chacun une clé par capacité ("namespaces", "cgroups", "capability-bounding-drop") - corrigé pour renseigner les trois (commit b8b8727), capturé par le propre test de doctor.
  • Dépôt rendu public. Fait le 4 août 2026, décision explicite de l'utilisateur, ce qui a débloqué la protection de branche GitHub (auparavant 403 « Upgrade to GitHub Pro or make this repository public » - le plan Free ne propose ni protection de branche ni rulesets sur un dépôt privé). Protection non activée pour l'instant, volontairement, pour ne pas casser le flux de push direct sur main.
  • Matrice de compatibilité publiée. Fait le 4 août 2026 (commit 5f953fb). docs/reference/compatibility.md : OS hôte, OS/arch cible, runtime conteneur, Kubernetes, shim containerd, backend hyperviseur (KVM/HVF testés sur matériel réel en CI, KVM/arm64 et WHPX explicitement marqués non testés), noyau invité, interopérabilité Cosign/GHCR - chaque ligne citant le workflow CI exact qui la prouve.
  • Matrice de maturité publiée (stable/beta/alpha/stub/hors périmètre par capacité). Fait le 4 août 2026 (commit 63db24d). docs/reference/maturity.md.
  • Fuzzing longue durée sur les ~20 cibles visées. Progrès le 4 août 2026 (commit 901c6b8) : quatre nouvelles cibles réelles (internal/layout.FuzzVerifyIndex, internal/policy.FuzzEvaluate, internal/attestation.FuzzVerify, internal/plugin.FuzzManifestValidate), chacune exécutée pour de vrai (millions d'exécutions, zéro crash) et câblée dans ci-fuzz.yml. Suite le 4 août 2026 : sept de plus - internal/layout. FuzzDecodeManifest/FuzzDecodeConfig, internal/pipeline.FuzzDecode, internal/sbom.FuzzDocumentDecode, internal/project.FuzzLoad, internal/registry.FuzzGetManifestResponse/FuzzGetBlobResponse - même rigueur (exécution réelle, câblées en CI). Treize cibles au total avec les deux préexistantes. Suite le 4 août 2026 : trois de plus - internal/layout.FuzzVerifyLayer (décompression gzip et sécurité des chemins tar, en boîte blanche pour la même raison que FuzzDecodeManifest - atteindre ceci via Verify exigerait de forger un blob sha256 par itération), sdk/plugin.FuzzReadMessage (le framing RPC bas niveau Content-Type/Content-Length que les cinq langages de plugin parlent sur stdin/stdout), sdk/microvm.FuzzParseForward (l'analyseur --publish/-p/--port de platform-factory run --isolation microvm). Seize cibles au total. Suite le 4 août 2026 : deux de plus, en cherchant mieux plutôt qu'en acceptant l'affirmation précédente comme définitive - internal/provenance.FuzzFromJournal (le vrai point d'entrée de décodage que la note précédente disait absent : FromJournal lit un journal de résultat de pipeline arbitraire depuis io.Reader, avec une fonction secretField qui recourt sans limite explicite de profondeur à travers map[string]any/[]any imbriqués - corpus de départ incluant une structure large et profonde construite exprès pour cette récursion) et cmd/platform-factory-control-plane.FuzzDecodeRequest (le décodeur JSON strict que chaque handler HTTP du control plane utilise - register/heartbeat/lease submit/complete/cancel - contre les quatre formes de requête). Dix-huit cibles au total contre les ~20 visées ; restent : commandes (déjà couvertes indirectement via internal/pipeline.FuzzDecode, qui décode le document de pipeline complet y compris les Command imbriqués - pas de cible séparée jugée nécessaire), framing bas niveau des protocoles de contrôle au-delà du JSON (mTLS/HTTP lui-même, hors périmètre d'un fuzzer Go de haut niveau). Fermé le 5 août 2026 (commit 2772756) : dix-huit cibles est jugé suffisant en largeur (le tiers restant listé ci-dessus est hors périmètre ou déjà couvert indirectement) ; ce qui manquait vraiment pour « longue durée » était la durée elle-même - les 45 s par cible ne prouvent que l'absence de crash immédiat. ci-fuzz.yml distingue maintenant push/pull_request (45 s par cible, rapide, ne ralentit pas une PR) du déclencheur schedule hebdomadaire existant (10 min par cible, ~3 h au total pour les dix-huit), via une variable d'env FUZZTIME calculée depuis github.event_name - le même corpus, la même liste de cibles, une durée réelle plutôt que symbolique une fois par semaine.
  • API publique stable v1. Le package api/v1 expose le contrat wire platform-factory.dev/v1, promu sans conversion depuis v1beta1. Le décodeur strict accepte encore v1alpha1 et v1beta1; une fixture JSON v1 gelée traverse la validation, l'ordre DAG et la suite de conformance publique.
  • compatibilité ascendante automatisée des schémas et protocoles ; voir Next-Generation-Architecture#automated-backward-compatibility-current-state. Nouvelles suites à corpus figé (testdata/compat/) pour le pipeline (internal/pipeline/compat_test.go, api/v1alpha1 et api/v1beta1) et pour le protocole RPC des plugins (api/plugin/compat_test.go) : décodent de vrais fichiers JSON (pas des structs Go construites en mémoire) et comparent champ par champ, pour détecter un renommage de tag JSON qu'encoding/json ignorerait silencieusement. Vérifié en cassant délibérément un tag JSON du pipeline (depends_ondependsOn) : le test échoue immédiatement sur les fixtures concernées, comme attendu.
  • modèle d'erreurs typées commun à tous les composants ; internal/errors (TypedError, codes catégorisés, sérialisation JSON) a un consommateur committé (internal/policy.Evaluate, commit af27cda, TestEvaluateRejectsUnsupportedAPIVersionAsTypedError). Un deuxième (internal/scheduler) était en cours d'adoption, non commité, dans le working tree partagé au moment de la rédaction du 4 août 2026 - à revérifier plutôt qu'à supposer terminé. internal/control, platform-factory-worker, internal/executor restent sur les erreurs de la bibliothèque standard. Suivi le 5 août 2026 : l'adoption internal/scheduler en cours a été relue puis commitée (commit 60b6d5a, session parallèle) - Scheduler.Run renvoie maintenant CodeInvalidArgument/ CodePipelineValidation/CodePipelineBudget selon le cas, testé par TestSchedulerRejectsInvalidConfigurationBeforeCallingRunner et l'assertion de code ajoutée à TestSchedulerBudgetCancelsInFlightStages. internal/control (commit 2772756) et cmd/platform-factory-worker (commit ceea18f) migrés dans la foulée - les 28 sites d'erreur de internal/control portent maintenant CodeNotFound/CodeConflict/ CodeInvalidArgument selon le cas, en préservant explicitement (prouvé par un test jetable avant la vraie migration, puis par TestErrorsAreTypedAndPreserveSentinelCompatibility) la compatibilité errors.Is(err, control.ErrUnknownWorker) dont cmd/platform-factory-control-plane/server.go dépend déjà. Reste ouvert : internal/executor reste sur les erreurs de la bibliothèque standard - non traité par prudence, ce paquet est directement adjacent au travail actif de la session parallèle sur internal/scheduler/internal/pipeline.
  • métriques, traces et journaux structurés de bout en bout ; internal/observability (logs structurés, métriques, traçage distribué) a un seul consommateur committé : platform-factory-control-plane (commit 7eecb82, démarrage/restauration d'état/journal d'audit en logs structurés, prouvé par TestPersistentReapLoopLogsStateSaveFailureViaObservability). Un deuxième (intégration observability/budget) était en cours, non commité, au 4 août 2026 - à revérifier. Trouvaille, même jour, en lançant go test ./... -race pour un travail sans rapport : cette intégration en cours introduit une vraie data race, pas un flake - écritures concurrentes non protégées sur la map interne de defaultTracer.StartSpanWithContext (internal/observability/ observability.go:563), atteinte depuis plusieurs goroutines de internal/scheduler.Scheduler.runStage (scheduler.go:307) quand des étages tournent en parallèle. Fait échouer TestSchedulerRunsSuccessInDeterministicOrder et TestRunPipelineTextRunAndMissingArgs sous -race. Non corrigé délibérément : fichiers en cours d'édition non commitée au moment de la trouvaille, pas à modifier depuis une session parallèle. Les métriques et le traçage distribué du paquet restent inutilisés partout ; platform-factory-worker et le builder OCI n'importent toujours pas ce paquet. Pas encore « bout en bout ». Corrigé et étendu le 5 août 2026 (commit 60b6d5a) : la data race trouvée ci-dessus est réparée - StartSpanWithContext travaillait sur la map baggage extraite directement du contexte parent (partagée entre toutes les goroutines dérivées du même contexte) au lieu d'en faire une copie privée avant d'y écrire trace_id. internal/scheduler et cmd/platform-factory/pipeline.go (propagation d'un SECURE_OCI_TRACE_ID optionnel) sont maintenant de vrais consommateurs, vérifiés sous go test -race ./... -count=3 sans échec. cmd/platform-factory-worker (commit ceea18f) ajouté ensuite - chaque bail exécuté ouvre un span worker.lease taggé lease_id/required_platform/attempt, prouvé vivant (pas seulement compilé) par les lignes de log "span errored" réelles que produisent TestClientPollOnceReportsExecutionFailureWithoutCompleting et TestPollOnceStopsExecutionWhenCancellationStatusFails. Reste ouvert : le builder OCI n'importe toujours pas ce paquet.
  • budgets de ressources et annulation propagés sur tout le pipeline ; internal/pipeline.Scheduler.Budget borne le temps d'horloge murale de l'exécution entière, indépendamment de tout délai sur le contexte de l'appelant : au déclenchement, chaque étage en vol est annulé et rapporté budget_exceeded (pas canceled), et Run renvoie une *ScheduleBudgetExceededError distincte - testé positivement (TestSchedulerBudgetExceeded prouve que le budget coupe une étape qui bloquerait sinon) et négativement (une annulation de l'appelant avec un budget généreux reste attribuée à l'appelant). Câblé dans le produit via platform-factory pipeline run --budget DURATION. Chaque étage a déjà ses propres limites CPU/mémoire via internal/executor; l'agrégation au niveau pipeline reste un travail de suivi.
  • Nouvelle ligne, ajoutée le 3 août 2026. Agrégation de budget CPU/mémoire au niveau pipeline (suivi de la case précédente). Le package internal/budget (commit d8e8ecf) implémente le suivi et l'application de budgets de ressources, avec des tests dans internal/budget/budget_test.go. Important, trouvé le 3 août 2026 : Tracker mesure les ressources du processus appelant lui-même (syscall.Getrusage(RUSAGE_SELF, ...), runtime.ReadMemStats), pas celles d'un enfant. Le brancher dans internal/executor tel quel aurait mesuré le mauvais processus : chaque étage de pipeline s'exécute dans un processus sandboxé séparé, déjà borné par ses propres cgroups/rlimits (voir plus haut, « État transversal »), invisibles à ce package depuis le processus parent. Progrès le 3 août 2026 : câblé à la place dans internal/oci.Build (commit fbf167b), qui fait tout son travail dans le même processus - cible correcte pour cette primitive. Options.Budget (valeur zéro = comportement identique à avant) borne l'appel de build entier, vérifié une fois par fichier streamé ; testé par TestBuildFailsClosedWhenWallClockBudgetIsExceeded. Reste ouvert : aucun budget par étage de pipeline n'existe encore ; internal/executor aurait besoin d'un mécanisme différent (propagation vers l'intérieur du sandbox, ou lecture du cgroup de l'enfant) pour agréger correctement les ressources par étage. Blocage structurel résolu le 5 août 2026 (commit 21ee69c) : budget.FromProcessState lit l'usage CPU (user+system) et le pic de RSS réels d'un enfant déjà terminé directement depuis son *os.ProcessState (rempli par (*exec.Cmd).Wait() via la comptabilité noyau RUSAGE_CHILDREN - aucun cgroup, aucun privilège supplémentaire requis). Gère la différence d'unité Linux/Darwin sur ru_maxrss (kilo-octets contre octets, par plate-forme, pas devinée) ; explicitement indisponible sur Windows plutôt que silencieusement fausse. Manager.AddStageUsage alimente TotalCPU/PeakMemory aux côtés des Tracker actifs déjà existants. Prouvé avec un vrai processus enfant généré (boucle d'attente sous sh -c/cmd /C), pas un mock : TestFromProcessStateMeasuresARealChild vérifie un CPU et un RSS réellement non nuls et plausibles. Cross-compilé propre pour linux/amd64 et windows/amd64. Reste ouvert, par prudence plutôt que par contrainte technique : le câblage dans internal/executor.Executor.Run lui-même (juste après son cmd.Wait() déjà existant à internal/executor/executor.go:158, un point d'accroche direct) n'a pas été tenté - ce paquet est directement adjacent au travail actif de la session parallèle sur internal/scheduler/internal/pipeline. La primitive est réelle et testée ; il ne manque que ce dernier appel.
  • suite de conformance publique pour l'API pipeline et les plugins.
  • suite de conformance publique pour les backends d'exécution. conformance.RunBackend (vecteurs embarqués sous vectors-backend/, câblés dans platform-factory-conformance backend, exécutable hors du dépôt comme les suites pipeline/plugin) épingle le comportement observable d'un internal/executor.Executor local sur des cas réels : code de sortie, capture de stdout, rejet d'une politique réseau non honorée (exit_code: -1, convention déjà établie par Executor.reject), et environnement non hérité de l'hôte ($HOME absent). Intégré à ci-quality. Limite documentée : seul le backend local (non sandboxé) est vectorisé ici, pas le backend sandboxé (ses garanties supplémentaires - namespace réseau, secrets en mémoire - restent couvertes par les tests dédiés d'internal/executor, pas par cette suite, car la disponibilité des namespaces/cgroups varie selon l'hôte).

v1 — Socle OCI et expérience projet

Détection et configuration

  • Découvrir .config_image.yaml, .config_image.yml, .config_img.yaml, .config_img.yml et leurs variantes JSON.
  • Charger strictement la configuration et refuser les champs inconnus.
  • Résoudre les chemins relativement à la racine du projet.
  • Supporter les profils Go, Node.js, Python, Java, .NET, Rust, Ruby et PHP.
  • Fournir les commandes personnalisées pour les langages non intégrés.
  • Ajouter automatiquement shared_deps, Shared_deps et shared-deps.
  • Produire un inventaire de gel SHA-256 stable.
  • Exposer project show, freeze, project build et project run.
  • Unifier ces opérations dans platform-factory launch.
  • Expliquer le plan résolu avant toute mutation.
  • Ajouter --yes, --dry-run et un format de sortie machine homogènes.
  • Détecter et résoudre les ambiguïtés entre plusieurs écosystèmes.
  • Définir un schéma versionné et une migration de configuration.

Construction et layouts

  • Construire un layout OCI déterministe en streaming et à mémoire bornée.
  • Normaliser les timestamps, UID/GID, permissions et ordre des fichiers.
  • Vérifier les digests, tailles, diff_id, chemins de layers et plateformes.
  • Produire des images Linux amd64 et arm64.
  • Composer un index multi-architecture.
  • Conserver plusieurs tags, images et manifests dans un layout.
  • Importer silencieusement un layout local avant une exécution Docker ou Podman.
  • Accepter plusieurs publications de ports via -p ou --port.
  • Produire nativement plusieurs stages et couches sémantiques.
  • Séparer toolchain, dépendances, application et métadonnées.
  • Garantir la stabilité byte-for-byte pour chaque mode de compression.
  • Ajouter une commande de comparaison expliquant toute divergence.

Critères de sortie v1

  • platform-factory launch construit et exécute un projet pris en charge sans édition manuelle.
  • Deux builds propres du même projet produisent le même digest.
  • La matrice Docker/Podman et amd64/arm64 est verte.
  • Les limites v1 et les formats expérimentaux sont explicitement signalés.

v2 — API, plugins et pipeline multi-stage

API indépendante du langage

  • Définir une première API déclarative api/v1alpha1.
  • Représenter stages, entrées, sorties, commandes, caches, secrets, réseau et ressources sans type propre à un langage.
  • Geler la première sérialisation JSON canonique de v1alpha1, borner le document à 1 MiB et refuser champs inconnus ou documents multiples.
  • Ajouter validation de version et négociation de capacités.
  • Publier des exemples et une suite de conformance.
  • Promouvoir l'API en v1beta1 avec compatibilité de wire format testée.

Graphe et exécution

  • Valider les identifiants, dépendances, références d'artefacts et chemins.
  • Détecter les cycles et calculer des niveaux topologiques déterministes.
  • Planifier les stages indépendants avec un parallélisme borné.
  • Propager annulation et échecs, bloquer les descendants et laisser continuer les branches indépendantes.
  • Capturer stdout, stderr, code de sortie et métriques par stage.
  • Isoler chaque stage sans dépendre de BuildKit.
  • Appliquer CPU, mémoire, PIDs, filesystem et la politique réseau none à l'exécution sandboxée.
  • Appliquer explicitement la politique réseau full sans supprimer les autres namespaces d'isolation.
  • Appliquer la politique réseau resolve avec un forwarder DNS détenu par le projet.
  • Monter les secrets en mémoire sans snapshot ni journalisation.
  • Transférer les artefacts entre stages en vérifiant leurs digests.

Plugins hors processus

  • Définir le manifeste de plugin signé et épinglé par digest.
  • Définir un protocole RPC versionné sur stdin/stdout.
  • Implémenter detect, freeze et plan (le verbe scan est réservé pour le chemin de publication v3).
  • Négocier versions, capacités et limites de messages.
  • Isoler fichiers, environnement, réseau, ressources et secrets. Environnement : déjà acquis, l'environnement du processus plugin est entièrement remplacé, jamais hérité de l'hôte. Réseau : internal/plugin.Start relance désormais le binaire appelant dans un namespace utilisateur, réseau, IPC et UTS frais (sandbox_linux.go) avant d'exécuter le plugin réel ; aucune interface, pas même loopback, n'y est configurée. Fichiers : applyFilesystemIsolation dans sandbox_linux.go rend le projet root read-only via remount et masquent les répertoires système sensibles via tmpfs. Ressources : applyResourceLimits applique RLIMIT_CORE=0, RLIMIT_FSIZE=16MiB, RLIMIT_NOFILE=256, RLIMIT_CPU=60s. Secrets : sans objet, aucune capability du protocole ne transmet de valeur secrète au plugin. Là où la sandbox est indisponible, Start retombe sur un lancement non sandboxé. Une limite mémoire (RLIMIT_AS) a été tentée puis abandonnée : confirmé empiriquement qu'elle fait échouer le démarrage de tout runtime Go (le SDK plugin est Go) bien avant tout usage réel du tas, 256 Mio et 512 Mio étant tous deux insuffisants pour qu'un binaire Go trivial démarre («failed to reserve page summary memory»). Une limite par langage ou déclarée dans le manifeste reste un travail de suivi.
  • Fournir un SDK Go sans exposer internal/. Les surfaces canoniques sont désormais sdk/pipeline, sdk/microvm (contrats MicroVM et VMM) et sdk/plugin. Les anciens chemins api/pipeline, api/microvm, api/vmm et api/plugin sont des shims dépréciés avec alias de types exacts et tests de compatibilité.
  • Fournir un runtime plugin Go de référence et un protocole stable indépendant du langage. Un auteur Go implémente uniquement sdk/plugin.LanguageExtension (Detect, Freeze, Plan) ; Python, JavaScript/TypeScript et C# implémentent les mêmes schémas JSON et le même framing v1 sans importer platform-factory. Des exemples utilisateur existent sous examples/plugin, et Go/Python/JavaScript passent réellement les cinq mêmes contrôles de platform-factory-conformance plugin. Le projet C# est fourni et ci-quality est configurée pour le compiler avec .NET puis lui appliquer cette même suite ; la preuve locale reste conditionnée à la présence du SDK .NET. Progrès le 3 août 2026 (commit a69c08f), vérifié le 4 août 2026 : jusqu'ici Python/JavaScript/TypeScript/C# réimplémentaient le framing JSON-RPC à la main dans chaque exemple - il n'existait aucune bibliothèque importable pour ces langages, contrairement à sdk/plugin pour Go. Trois nouveaux paquets locaux réels, tous miroirs de l'API Server de sdk/plugin (enregistrer des capacités puis servir) et du même protocole de câble (même Content-Type, même handshake v1.hello, même dispatch v1.<capability>) : sdk/plugin-python (secure_oci_plugin, stdlib seule), sdk/plugin-js (@platform-factory/plugin-sdk, CommonJS + index.d.ts, zéro dépendance runtime), sdk/plugin-csharp (SecureOci.Plugin, une vraie bibliothèque de classes - avant cela, seul un projet Exe existait). Aucun n'est publié sur PyPI/npm/NuGet ; ce sont des paquets localement importables (chemin pip local, require relatif, ProjectReference .NET), structurés pour ne pas nécessiter de refonte lors d'une publication future. Les exemples ont déménagé sous examples/sdk/plugin-{python,javascript,typescript,csharp} et importent désormais leur SDK au lieu de réimplémenter le framing ; l'exemple TypeScript, qui n'avait auparavant aucune configuration de build et n'était jamais réellement compilé ni exécuté, a maintenant un vrai tsconfig.json, compile proprement et tourne seul contre les types index.d.ts fournis par sdk/plugin-js. Re-vérifié le 4 août 2026, pas seulement relu : bash examples/sdk/run.sh fait réellement passer Go, Python et JavaScript à 5/5 sur platform-factory-conformance plugin, et dotnet build sdk/plugin-csharp/SecureOci.Plugin.csproj -c Release compile sans avertissement. TypeScript et C# restent vérifiés seulement via CI dans ce passage (pas de tsc/environnement C# complet exercé localement ici).
  • Convertir les adaptateurs Go, Node.js, Python, Java, .NET, Rust, Ruby et PHP en plugin officiel (les adaptateurs intégrés restent des fallbacks).
  • Tester un plugin tiers sans modification du cœur.
  • Refuser par défaut tout plugin non vérifié.

Critères de sortie v2

  • Un plugin externe ajoute un langage sans recompiler platform-factory.
  • Deux branches indépendantes du DAG s'exécutent réellement en parallèle.
  • Les secrets sont absents des layouts, caches et logs couverts par la suite de conformance.
  • Prouver leur absence des attestations générées depuis le journal et refuser les champs susceptibles de contenir une valeur secrète.
  • La suite de conformance plugins est exécutable hors du dépôt. Elle vérifie désormais aussi les trois capacités requises et la forme sûre de leurs résultats, au-delà du seul transport RPC.

v3 — CAS, reproductibilité et supply chain natives

Content-addressed storage

  • Définir l'interface stable du content store.
  • Écrire les blobs en streaming dans un fichier temporaire puis les installer atomiquement.
  • Dédupliquer les blobs par digest.
  • Dédupliquer les chunks par digest.
  • Ajouter le chunking en streaming borné en mémoire pour les fichiers de plusieurs téraoctets.
  • Reprendre un transfert interrompu à l'offset confirmé sans rescanner les chunks validés.
  • Introduire records de cache, leases et garbage collector.
  • Reconstruire l'index exclusivement depuis les contenus vérifiés.
  • Exclure la valeur des secrets des clés de cache.
  • Marquer automatiquement tout stage recevant un secret comme sensible et interdire la persistance de ses sorties et records dans le cache.
  • Tester concurrence, corruption et erreurs d'entrées/sorties.
  • Tester explicitement les restes de crash et ENOSPC, avec nettoyage des écritures partielles.

Reproductibilité hermétique

  • Dériver automatiquement l'épinglage des sources, bases, plugins et toolchains depuis le pipeline et les exécutables réellement vérifiés via platform-factory evidence.
  • Normaliser locale, fuseau, environnement et paramètres de compression.
  • Interdire le réseau après la phase de résolution : tout stage réseau doit être une racine du DAG et tous ses descendants sont network:none.
  • Distinguer identité d'un secret et contenu secret dans les clés de cache.
  • Exécuter deux rebuilds propres et comparer tous les descripteurs.
  • Produire un rapport structuré des causes de non-reproductibilité.
  • Tester la reproductibilité sur Linux, macOS et Windows dans la matrice CI.

Registry, SBOM, provenance et signatures

  • Implémenter nativement le sous-ensemble de l'API OCI Distribution requis pour publier layouts, blobs et manifests.
  • Authentifier en Basic/Bearer et reprendre un upload depuis le Range confirmé par le Registry.
  • Ajouter le montage cross-repository des blobs avec fallback automatique vers l'upload repris.
  • Publier atomiquement par digest avant de déplacer les tags.
  • Générer nativement un inventaire SBOM des fichiers et dépendances ELF.
  • Corréler lockfiles Go, Node.js, Python, Rust, Ruby, PHP et .NET ainsi que les métadonnées dpkg/apk dans le SBOM.
  • Générer la provenance SLSA depuis le journal réel du pipeline.
  • Fournir une bibliothèque native de génération de provenance structurée.
  • Produire et vérifier les enveloppes DSSE d'attestation nativement.
  • Signer avec Ed25519/ECDSA et vérifier signatures et chaînes X.509.
  • Générer automatiquement la provenance native depuis --journal lors de la publication (un prédicat fourni reste pris en charge).
  • Relier SBOM et signature natives, ainsi qu'un prédicat de provenance fourni, au parcours de publication.
  • Publier signatures et attestations comme artefacts OCI liés au digest.
  • Interdire par défaut la publication si politique, SBOM, provenance ou signature échoue ; l'opt-out développement est explicite et auditable.
  • Fournir une commande de vérification locale complète pour un bundle de release déjà publié. Fait le 4 août 2026 (commit 0b319f1). platform-factory verify-release LAYOUT réévalue localement validité du layout, signature DSSE contre une clé épinglée par l'opérateur, provenance, SBOM et décision de politique - onze tests réels, zéro mock. Volontairement local uniquement : voir la ligne suivante.
  • Ajouter une capacité de lecture (pull/fetch) au client Registry natif. Trouvé le 4 août 2026, en écrivant verify-release : internal/registry.Client n'avait que des méthodes d'écriture (PushLayout, PushLayoutByDigest, PushArtifact, TagLayout) ; aucune méthode ne lisait un manifest ou un blob depuis un registre. verify-release a donc dû être conçu pour opérer uniquement sur des artefacts déjà locaux plutôt que d'aller chercher une image publiée. Fait le 4 août 2026 : GetManifest (par tag ou par digest, header Accept de négociation de contenu correct pour index/manifest/ manifest-artefact OCI et les deux media-types Docker legacy) et GetBlob (borné à 64 MiB - pensé pour des artefacts signature/SBOM/ provenance de petite taille à vérifier, pas un puller de layers multi-gigaoctets). Les deux revérifient toujours le SHA-256 réel du contenu reçu plutôt que de faire confiance au statut HTTP ou au Content-Length du serveur. Sept tests unitaires (transport mocké, même style que le reste de client_test.go) plus une vérification manuelle contre un vrai registre OCI local (docker run registry:2, un vrai platform-factory publish dessus, puis GetManifest/GetBlob réels par tag et par digest, cas négatif digest-inexistant) - pas seulement les tests mockés. Reste ouvert : rien ne consomme encore ces méthodes (pas de platform-factory pull, verify-release reste local-only par choix).

Politique

  • Définir un langage de règles interne minimal et versionné.
  • Exiger par politique base, toolchain et plugins épinglés.
  • Vérifier par politique non-root, capabilities, rootfs read-only et absence de secrets.
  • Évaluer SBOM, provenance, signatures et preuve de reproductibilité.
  • Produire une décision explicable avec raisons déterministes.
  • Relier la décision et les preuves associées au parcours de publication ; le tag n'est jamais déplacé après un refus.

Critères de sortie v3

  • launch --publish fonctionne sans Skopeo, Cosign ni générateur SBOM externe.
  • Une image altérée, non attestée ou non reproductible est refusée avant mise à jour du tag.
  • Une reprise après crash publie exactement les blobs attendus.
  • Un corpus logique de 2 TiB démontre mémoire bornée et déduplication.

v4 — VMM platform-factory natif

Noyau commun

  • Définir VMM, Machine, Device, GuestAgent et le state store.
  • Définir un format de boot bundle adressé par contenu.
  • Convertir un rootfs OCI en disque ou partage vérifié. internal/rootfs.Convert sélectionne un manifest Linux, vérifie manifest/config/digests/diffIDs, applique whiteouts, symlinks et hardlinks OCI (bornés et validés contre toute évasion hors racine) dans une racine os.Root, borne octets/fichiers et installe atomiquement. Ce rootfs converti est maintenant réellement relié à une VM : cmd/microvm-initramfs (internal/rootfs.InstallInit + internal/rootfs.WriteInitramfs, un écrivain cpio/newc natif déterministe) assemble un initramfs gzip sans tar ni cpio externes, et ci-microvm l'utilise pour démarrer réellement le noyau natif (voir « Linux — KVM natif » ci-dessous). Limite documentée : la structure des hardlinks OCI n'est pas préservée dans l'initramfs empaqueté (chaque chemin lié devient un fichier indépendant à contenu dupliqué) - seul le contenu par chemin compte pour un noyau qui démarre depuis cet initramfs. Le backend CLI platform-factory microvm run/start (production, QEMU) n'utilise pas encore ce chemin - voir la case ouverte correspondante sous « Linux — KVM natif ».
  • Fournir un init et un agent invité minimaux détenus par le projet. cmd/microvm-init est un PID 1 minimal complet et testé (relais de signaux, moisson des enfants, configuration d'entrypoint, console via devtmpfs) - voir TestRunForwardsSignalsToChild, TestRealMainRunsChildAndCallsPoweroff, TestReapExitedChildrenDrainsEveryExitedChild. L'agent invité minimal est maintenant détenu par le projet dans cmd/microvm-init : endpoint authentifié, clé boot fail-closed, exécution, signaux, état, logs et shutdown bornés. L'adaptateur hôte implémente api.GuestAgent; l'activation automatique du périphérique reste suivie séparément par la case transport.
  • Transporter commandes, signaux, état et logs sur un canal authentifié. Le protocole hôte/invité est maintenant implémenté dans internal/guesttransport : framing borné à 1 Mio, HMAC-SHA256 avec clé de session d'au moins 32 octets, version wire et séquences strictement croissantes anti-rejeu pour exec, signal, shutdown, state et logs. L'adaptateur hôte implémente désormais l'interface publique GuestAgent (Exec, Signal, Shutdown, état et logs), applique deadlines et limites d'entrée, et le backend macOS accepte un GuestAgentConnector injectable. Sans périphérique configuré, Agent() échoue explicitement. Côté KVM Linux, un premier périphérique natif existe maintenant : LinuxRunOptions.GuestChannel raccorde un UART 8250 secondaire COM2 (0x2f8, IRQ3, /dev/ttyS1) bidirectionnel et borné au run loop, sans bloquer KVM_RUN; COM1 reste réservé aux diagnostics. L'endpoint microvm-init authentifié est également livré. Le parcours OCI Linux est maintenant raccordé de bout en bout : clé aléatoire par boot injectée uniquement dans l'initramfs en 0600, métadonnées non secrètes strictes, ttyS1 configuré en raw 115200 8N1, client/serveur HMAC et effacement des buffers secrets au teardown. internal/directboot.RunWithGuestAgent relie aussi ce canal au boot bundle direct non OCI avec callback OnReady, sans modifier le chemin sans agent. Côté macOS, Virtualization.framework expose désormais un second port console virtio bidirectionnel (/dev/hvc1, /dev/hvc0 restant la console), créé depuis un socketpair CLOEXEC ; le FD hôte est adopté par Go et raccordé au même protocole authentifié. Une clé résolue par machine et par boot est obligatoire, copiée puis effacée, le canal est mono-claim et tout manque de clé ou double claim échoue fermé. Les tests couvrent validation, ownership, nettoyage et compilation Darwin. Deux preuves sont distinguées : ci-microvm impose le contrat macOS à chaque push ; les deux tests matériels sont tentés, mais ne constituent une preuve de boot que lorsque le runner expose réellement HVF. Si Virtualization.framework signale l'indisponibilité de la virtualisation imbriquée, seuls ces deux tests sont exclus et toute la suite de contrat reste obligatoire. Un job hébergé vert dans ce mode n'est donc pas une preuve de boot matériel.
  • Gérer vCPU, mémoire, horloge, entropy, console et arrêt propre. Progrès réel : internal/hypervisor/kvm/linux_serial.go émule maintenant le PIT-gate (port 0x61) et l'horloge CMOS/RTC MC146818 (ports 0x70/0x71), avec signalisation d'interruption THRE pour la console série (kvm_vm_linux_amd64.go ajoute KVM_IRQ_LINE pour pulser l'IRQ4 réellement) - corrige un blocage de boot précoce puisque le pilote 8250 devient piloté par interruption après earlycon. linux_i8042.go isole une émulation i8042 minimale (ports 0x60/0x64), y compris une demande de reset côté invité (0xfe). Les lectures neutres, écritures ordinaires, reset au seul port de commande, accès non-byte et ports étrangers sont couverts par TestHandleLinuxI8042IO*; le chemin reste exercé en conditions réelles par TestRunLinuxWithRealKVM dans boot-under-kvm. Limite documentée : Le backend KVM fournit maintenant 64 octets issus de crypto/rand via le protocole Linux setup_data/SETUP_RNG_SEED : structure ABI little-endian, alignement et non-chevauchement sont testés, les noyaux antérieurs à 2.09 sont refusés sur ce chemin et une source courte échoue avant /dev/kvm. La graine temporaire côté hôte est effacée après copie. Limite : il ne s'agit pas encore d'un périphérique virtio-rng fournissant de l'entropie continue après le boot. internal/hypervisor/kvm/linux_boot.go/linux_boot_cpu.go dérivent maintenant le point d'entrée code32_start depuis l'en-tête bzImage réel au lieu d'une valeur figée.
  • Gérer réseau, DNS, ports TCP/UDP et volumes.
  • Tester crash VMM, crash invité, timeout de boot et récupération d'état.

Linux — KVM natif

  • Ouvrir /dev/kvm et négocier la version de l'API sans QEMU.

  • Négocier et exposer les extensions KVM requises avant création d'une VM : KVM_CAP_USER_MEMORY et KVM_CAP_IMMEDIATE_EXIT sont contrôlées et publiées par le probe avant KVM_CREATE_VM. Le primitive réel a réussi sur /dev/kvm dans microvm-boot au commit a6c95bf7911ffa70844703c2f88e0129506030e9.

  • Créer VM, vCPU et mémoire invitée avec le primitive KVM natif RunFlatPayload, exercé sur /dev/kvm par ci-microvm.

  • Charger kernel, initrd et ligne de commande. LoadLinuxBoot valide le protocole bzImage x86, place payload/initrd/cmdline, construit boot_params/E820 et configureLinuxBootVCPU prépare GDT, segments, registres et CR0.PE, sans firmware ni outil externe.

  • Implémenter les périphériques virtio minimaux nécessaires. Implémenté le 5 août 2026. Après le constat du 3 août ci-dessus (internal/hypervisor/virtio : structures de configuration seules, jamais importées, aucune virtqueue), le transport et les deux périphériques réels ont été écrits directement dans internal/hypervisor/kvm (linux_virtqueue.go, linux_virtio_mmio.go, linux_virtio_blk.go, linux_virtio_net.go, linux_virtio_options.go), sans dépendre de ce paquet-là : moteur de virtqueue scindée (VIRTIO 1.2 §2.7) directement sur la mémoire invitée plate déjà utilisée par le reste du VMM, transport virtio-mmio version 2 (§4.2, jamais la variante legacy) câblé sur KVM_EXIT_MMIO avec une fenêtre d'une page par périphérique à une adresse 0xd000_0000 délibérément hors de toute RAM invitée configurée (chaque accès y déclenche donc réellement un exit MMIO plutôt que de retomber sur de la RAM), un périphérique virtio-blk (IN/OUT/ FLUSH/GET_ID) et un périphérique virtio-net adossé à un descripteur TAP déjà ouvert.

    Preuve réelle sur /dev/kvm, pas seulement sur pilote maison : TestRunLinuxWithRealKVMVirtioBlk fait négocier un noyau natif non modifié jusqu'à ce que son pilote virtio_blk reconnaisse le périphérique (vda dans le journal série), boote l'application OCI complète et relit les octets écrits au boot. TestRunLinuxWithRealKVMVirtioNet fait démarrer le noyau avec ip=dhcp et vérifie qu'une vraie trame DHCPDISCOVER (UDP 68→67 décodé, pas un grep sur dmesg) arrive côté TAP hôte. Cette deuxième preuve a d'abord échoué : la négociation virtio-mmio réussissait jusqu'à DRIVER_OK (confirmé par la trace SECURE_OCI_DEBUG_VIRTIO_MMIO ajoutée à cette occasion) mais aucune trame utile n'arrivait. Cause réelle : virtio_net_hdr fait 12 octets (struct virtio_net_hdr_mrg_rxbuf), pas 10, dès que VIRTIO_F_VERSION_1 est négocié - ce que ce VMM offre toujours -, quel que soit l'état de VIRTIO_NET_F_MRG_RXBUF lui-même. Les tests unitaires sur pilote maison ne l'avaient pas montré parce que ce pilote maison faisait la même hypothèse erronée ; seul un noyau réel, non modifié, l'a révélé. Corrigé (virtioNetHeaderSize = 12), les deux tests réels passent.

    Câblé dans le chemin de production : internal/ociruntime/supervisor_linux.go expose platform-factory.dev/block-device-path/-readonly (un fichier hôte devient /dev/vda, capacité validée multiple de 512) et platform-factory.dev/network-tap (ProbeTAPSupport/OpenTAP, internal/hypervisor/kvm/linux_tap_linux.go) et fusionne les périphériques obtenus dans le kvm.LinuxRunOptions passé à RunLinuxWithOptions. Contrairement à sandbox-cgroups/sandbox-namespaces (best-effort, dégradation silencieuse), ces deux annotations échouent fermé : un périphérique explicitement demandé et indisponible est un manque fonctionnel que l'appelant doit voir, pas un durcissement optionnel. Non revérifié dans cette session au niveau podman run --runtime platform-factory-runtime complet (annotation → périphérique visible dans l'invité) : la preuve s'arrête à la frontière kvm.RunLinuxWithOptions elle-même, déjà réelle et complète contre /dev/kvm.

  • Ajouter réseau TAP/rootless selon capacités de l'hôte. Implémenté le 5 août 2026, avec le point précédent : OpenTAP/ProbeTAPSupport (internal/hypervisor/kvm/linux_tap_linux.go) ouvrent /dev/net/tun avec TUNSETIFF/IFF_TAP|IFF_NO_PI, et ProbeTAPSupport rapporte la disponibilité en lisant CAP_NET_ADMIN dans CapEff (/proc/self/status) plutôt que de deviner depuis l'euid - même philosophie que ProbeSandbox déjà utilisé pour cgroups/namespaces. Vérifié pour de vrai en root local : création réussie d'une interface tap0. Limite réelle du « rootless », trouvée par un podman unshare local et pas seulement documentée en théorie : CAP_NET_ADMIN présent dans CapEff ne suffit pas à faire réussir TUNSETIFF si le processus ne possède pas aussi son propre network namespace - sous un namespace utilisateur partagé avec le namespace réseau de l'hôte (le cas Podman rootless typique), l'appel échoue quand même en EPERM. Un TAP rootless avec accès réseau sortant réel demanderait en plus une couche NAT en espace utilisateur (le rôle que jouent slirp4netns/pasta pour la mise en réseau propre de Podman) : volontairement hors périmètre, non implémenté ici. platform-factory.dev/network-tap échoue donc fermé, avec la raison de ProbeTAPSupport, plutôt que de faire croire à une mise en réseau fonctionnelle qui ne l'est pas.

  • Appliquer seccomp, namespaces, cgroups et abandon de privilèges au VMM. Implémenté le 4 août 2026 (commit e19e6f3), après le report du 3 août ci-dessus. L'utilisateur a explicitement tranché entre écrire le code et laisser la CI Linux réelle le vérifier plutôt que continuer à reporter : applySeccomp (PR_SET_NO_NEW_PRIVS - même primitive et même nom que internal/executor, pas de filtrage BPF réel), applyNamespaces (unshare self-appliqué de UTS/IPC ; PID exclu du self-apply car unshare(CLONE_NEWPID) n'affecte que les enfants créés après l'appel, jamais l'appelant), applyCgroups (création d'un cgroup v2 enfant délégué avec limites pids/cpu/memory et auto-jointure) et dropCapability (PR_CAPBSET_DROP par capacité) sont maintenant réels, pas des stubs. Un nouveau ProbeSandbox() détecte la disponibilité par construction (lecture du bitmask CapEff de /proc/self/status, écriture réelle dans un cgroup enfant jetable) plutôt que de deviner depuis l'euid - même philosophie que le probe déjà existant d'internal/executor.

    Câblage dans internal/ociruntime/supervisor_linux.go : LaunchSupervisor positionne CLONE_NEWPID|CLONE_NEWIPC|CLONE_NEWUTS sur l'enfant __serve, mais seulement si ProbeSandbox().Namespaces le confirme disponible - sans CAP_SYS_ADMIN, clone() échouerait et command.Start() casserait le lancement du VMM entièrement (l'appelant ici est platform-factory-runtime create, invoqué par Podman/Docker/ containerd ; rien ne garantit CAP_SYS_ADMIN dans ce contexte). ServeSupervisor applique ensuite no_new_privs sans condition (ne nécessite aucun privilège) puis cgroups et abandon du bounding set de capacités seulement si ProbeSandbox les confirme disponibles, pour qu'un hôte sans délégation cgroup lance quand même des invités plutôt que d'échouer fermé. L'abandon d'UID (setuid(65534)) de dropPrivileges est délibérément exclu du chemin VMM (nouveau Sandbox.DropBoundingCapabilities, découplé de Sandbox.Apply) : ce processus a besoin d'un accès continu à un /dev/kvm déjà ouvert, et cette session ne peut pas vérifier sans hôte Linux réel si l'accès DAC au device dépend de l'UID plutôt que de l'appartenance à un groupe.

    cmd/platform-factory-runtime's feature-JSON reflète maintenant cet état réel ("namespaces":["pid","ipc","uts"], "cgroup":{"v2":true}, "seccomp":{"enabled":true}) au lieu de l'ancien "seccomp":{"enabled":false}, honnête à l'époque.

    Vérifié pour de vrai le 4 août 2026, sur Linux réel avec /dev/kvm, dans la continuation de la session ci-dessus (macOS, sans KVM). Cette deuxième session tournait en parallèle sur un checkout Linux réel du même dépôt, a produit sa propre implémentation indépendante du même point de roadmap, et les deux ont fini par coexister dans le même arbre de travail (deux définitions de applySeccomp/applyNamespaces/ applyCgroups/dropCapability, l'une dans sandbox_linux.go, l'autre dans seccomp_linux.go/namespace_linux.go/cgroup_linux.go/ caps_linux.go) - go build refusait de compiler. Réconcilié en gardant la version la plus complète de chaque primitive (filtrage BPF réel plutôt que no_new_privs seul ; abandon de capacités en deux phases bounding-set/current-set correctement ordonnées autour de setuid) tout en conservant ce que l'implémentation macOS faisait mieux (probe ProbeSandbox/Support plutôt que deviner depuis l'euid ; CLONE_NEWIPC|CLONE_NEWUTS posé sur l'enfant __serve par LaunchSupervisor au lieu d'un unshare auto-appliqué risqué).

    Une fois la compilation réparée, faire tourner le chemin réel a trouvé plusieurs bugs réels que la revue seule n'avait pas vus :

    • dropPrivileges retirait CAP_SETUID/CAP_SETGID du bounding set avant d'appeler setuid(65534), qui a alors besoin de CAP_SETUID dans le set effectif pour réussir - setuid échouait donc systématiquement dès qu'un abandon de capacités réel (pas un stub) tournait avant lui. Corrigé en trois phases ordonnées : abandon du bounding set, puis setuid, puis abandon du set courant.
    • PR_CAPBSET_DROP renvoyait EPERM sur des capacités précises (CAP_LINUX_IMMUTABLE, CAP_CHOWN) même avec CAP_SETPCAP confirmé tenu par ProbeSandbox : le conteneur Docker externe de cet environnement de développement les avait déjà retirées de façon permanente d'un bounding set ancêtre. Pas un problème de privilège réel - DropBoundingCapabilities et dropCapabilityBoundingSet tolèrent maintenant EPERM par capacité au lieu de faire échouer tout le lot (et donc le lancement de l'invité) pour une seule capacité déjà indisponible.
    • CLONE_NEWPID cassait le lancement de chaque conteneur sans exception : l'enfant __serve devient PID 1 de son propre nouveau namespace, donc os.Getpid() y vaut toujours 1, alors que le state store enregistre le PID hôte tel que vu par le parent - la comparaison state.PID == os.Getpid() du propre code de démarrage de ServeSupervisor ne réussit donc jamais, et chaque conteneur échoue fermé avec « supervisor PID mismatch ». Retiré des Cloneflags de LaunchSupervisor ; ne restent que CLONE_NEWIPC|CLONE_NEWUTS. Isoler vraiment le namespace PID demanderait d'abord que le state store arrête de s'indexer sur un PID hôte brut - un chantier séparé, pas un correctif ponctuel.
    • L'abandon d'UID de dropPrivileges (setuid(65534)) est confirmé dangereux pour ce processus, pas seulement théorique comme la note du 4 août au-dessus le disait : sous Podman rootless, os.Geteuid() renvoie 0 (l'utilisateur invoquant est mappé root dans son propre user namespace), donc dropPrivileges prenait ce chemin pour du vrai root et tentait setuid - et le /dev/kvm de cet hôte précis est chowné à un UID précis, pas à un groupe partagé. setuid(65534) aurait cassé l'accès KVM net s'il avait réussi. sandboxConfigForSupervisor ne positionne donc plus jamais DropPrivileges ; le durcissement des capacités de ce processus passe uniquement par DropBoundingCapabilities, qui ne touche jamais l'UID.
    • Un test (TestApplyStrictSeccompRealFilter, ajouté par cette session) omettait runtime.LockOSThread() avant d'installer le filtre BPF réel dans son sous-processus jetable : le goroutine pouvait migrer de thread OS entre l'installation et l'appel censé prouver le blocage, invalidant silencieusement la preuve. Trouvé parce que le test a réellement échoué, pas par relecture.

    Le vrai filtrage seccomp BPF (pas seulement no_new_privs) est maintenant implémenté : applyStrictSeccomp/Sandbox.ApplyStrictSeccomp assemblent un programme BPF classique à la main (aucune dépendance libseccomp/cgo, même style que les ioctls KVM bruts déjà utilisés par internal/hypervisor/kvm) et l'installent via prctl(PR_SET_SECCOMP) sur l'unique thread OS déjà épinglé par runtime.LockOSThread, juste avant buildGuestInitramfs et jusqu'à la boucle KVM_RUN incluse. La liste blanche de syscalls a été dérivée empiriquement par strace du chemin de production réel et complet (Podman rootless → platform-factory-runtime createServeSupervisorbuildGuestInitramfsRunLinuxWithOptionsKVM_RUN réel), pas devinée - itérée contre des SIGSYS réels jusqu'à obtenir un chemin propre, ce qui a révélé des syscalls qu'un harnais de test isolé n'aurait pas montrés (accept4/getsockname en queue de l'écoute du socket de contrôle, capget/capset de l'abandon de bounding set de applyVMMSandbox selon l'ordonnancement du thread par Go).

    Preuve réelle et complète, obtenue dans cette session : podman run --runtime platform-factory-runtime --cap-drop all contre le noyau déjà mis en cache par ce dépôt et une image OCI + microvm-init fraîchement construits, exécuté trois fois de suite, chaque fois vérifiant que l'invité démarre réellement ("component":"example-service" dans le journal série) puis un stop/rm propre - exactement le chemin que la note du 3 août ci-dessus disait nécessiter un hôte Linux réel. Les tests réels et privilégiés du paquet (sandbox_linux_test.go) ont aussi tourné en root local, pas seulement conçus pour tourner ainsi en CI : TestApplyNamespacesReal (unshare réel d'UTS/IPC), TestApplyStrictSeccompRealFilter (filtre BPF réel installé dans un sous-processus jetable, prouvé à la fois pour autoriser un syscall permis et tuer par SIGSYS sur un syscall interdit), TestDropPrivilegesRealSetuidAndCapabilities (vrai setuid + vrai abandon de bounding set, correctement ordonnés) passent tous pour de vrai ; TestApplyCgroupsReal se met correctement en skip (pas un faux succès silencieux) parce que la délégation cgroup de cet environnement de conteneur imbriqué précis ne descend pas assez loin pour déplacer un processus dans un cgroup enfant - une limite réelle et propre à cet environnement, pas un bug de code, découverte et documentée de la même façon empirique que la liste blanche de syscalls.

    Restent hors de portée, honnêtement : les limites cgroup CPU/mémoire/PIDs et l'isolation namespace NET pour ce processus VMM hôte sont du code réel (applyCgroups/applyNamespaces) mais n'ont pas pu être prouvées de bout en bout dans cette session précise faute de délégation cgroup suffisante dans cet environnement imbriqué ; l'isolation du namespace mount reste délibérément hors périmètre (ce processus a besoin d'un accès continu à l'arborescence du store) ; l'isolation du namespace PID a été tentée et annulée après avoir cassé chaque lancement de conteneur

    • la faire correctement demande d'abord de changer comment le state store identifie ce processus.
  • Produire des diagnostics JSON actionnables via platform-factory microvm probe lorsque KVM est indisponible.

  • Remplacer le chemin QEMU/KVM historique dans les tests de boot. Progrès depuis la note précédente (2946e24, aucun octet série) : TestRunLinuxWithRealKVM boote maintenant nativement le noyau, l'init projet et l'application OCI, et vérifie leur sortie série réelle ("component":"example-service"). L'initramfs qu'il consomme est lui-même assemblé nativement depuis un layout OCI vérifié par cmd/microvm-initramfs (internal/rootfs.Convert + assemblage cpio/gzip natif), sans tar ni cpio externes, câblé dans ci-microvm.

    Chemin de production fermé le 5 août 2026. Une fois les périphériques virtio ci-dessus réels, le backend CLI platform-factory microvm run/start (cmd/platform-factory/microvm_native.go, nouveau) utilise kvm.RunLinuxWithOptions directement, sans plus jamais invoquer scripts/microvm/run-microvm.sh/QEMU, dès que trois conditions réelles sont réunies : hôte linux/amd64, KVM natif disponible (hypervisor.ProbeNative), et chaque --publish demandé en TCP. Le script QEMU n'est ni supprimé ni modifié - il reste le chemin réel pour tout le reste (arm64, hôte sans KVM, tout --publish UDP) et nativeKVMEligible journalise explicitement la raison de la bascule plutôt que de dégrader en silence.

    Écart réel restant, tranché explicitement (pas contourné en douce) : QEMU SLIRP fait du NAT complet et un hostfwd= par port publié sans privilège hôte ; le virtio-net natif de ce VMM est un TAP nu, sans équivalent. La publication de port - l'usage principal de la commande, toujours active via au moins un forward par défaut - est reproduite par un lien TAP point-à-point à adresse fixe (internal/hypervisor/kvm.AssignTAPAddress, nouveau : SIOCSIFADDR/ SIOCSIFNETMASK/SIOCSIFFLAGS sur un socket de contrôle, pas de ip/iproute2 externe) plus un relais TCP hôte→invité en espace utilisateur (internal/microvm/forward, nouveau paquet) plutôt qu'un vrai NAT - suffisant pour le contrat --publish réel, pas pour un accès sortant général depuis l'invité, ni pour UDP. Les deux restent volontairement hors périmètre ici (suivis séparément sous « Linux — KVM natif » ci-dessus et sous v6.10 de Meine-Graal) : platform-factory microvm run/start bascule sur QEMU pour ces cas plutôt que d'échouer ou de faire semblant.

    Preuve réelle et complète : TestMicroVMRunNativeKVMRealBoot (cmd/platform-factory/microvm_native_kvm_test.go) construit un vrai layout OCI autour du vrai binaire cmd/example-service, compile le vrai binaire platform-factory, lance platform-factory microvm run --backend=native --layout=... --publish=127.0.0.1:<port>:8080 comme un vrai sous-processus (pas un exécuteur simulé - le re-exec réel de runNative vers microvm __run-native est ce qui est prouvé), et vérifie qu'une vraie requête HTTP à travers le port publié atteint le vrai serveur invité et reçoit sa vraie réponse, puis qu'un SIGTERM de groupe entraîne un arrêt propre sans processus, TAP ni port orphelin. Testé aussi sous -race. Le paquet internal/microvm/forward a son propre bogue réel trouvé et corrigé pendant cette session : la sonde de disponibilité d'un test (connecter-puis-fermer immédiatement) traversait elle-même le relais et volait l'unique Accept() d'un serveur de test à acceptation unique, masquant un vrai risque d'interblocage à l'arrêt - corrigé à la fois en bouclant l'acceptation côté test et en fermant pour de vrai toute connexion en cours lors de l'annulation du contexte côté relais (Relay ne se contentait auparavant que de fermer l'écouteur).

macOS — API native

  • Implémenter l'adaptateur natif de virtualisation macOS directement sur Virtualization.framework, sans backend VMM tiers. Le bootstrap macOS natif compile désormais platform-factory avec CGO et la CI vérifie avec otool le linkage réel de Virtualization.framework.
  • Gérer Apple Silicon arm64 en priorité. Vérifié pour de vrai sur matériel Apple Silicon réel avec l'entitlement com.apple.security.virtualization (signature ad hoc locale).
  • Intégrer disque, réseau, console, ports et agent invité. La console virtio et le rootfs OCI sous forme d'initramfs sont intégrés : DarwinVMM.Create exige le marqueur platform-factory.dev/rootfs-format=initramfs, résout le digest RootFS avant tout appel framework et refuse un initrd séparé pour ne jamais ignorer une entrée épinglée. L'agent invité est maintenant raccordé par un second port virtio authentifié /dev/hvc1, avec clé par boot et cycle de vie détenu par la machine. Disque virtio, réseau et ports restent ouverts.
  • Signer et distribuer les composants nécessitant les droits macOS.
  • Tester cycle complet create/start/logs/stop/delete. Le test signé TestDarwinVMMWithRealHVF boote le kernel et le rootfs OCI arm64, attend la sortie réelle de l'application sur la console, puis exerce stop, delete et le refus de load après suppression. test-hvf-local.sh l'exécute depuis les caches locaux ; le job macOS toujours exécuté restaure le cache GitHub et conserve les tests de contrat obligatoires lorsque le runner hébergé ne fournit pas la virtualisation imbriquée. Cette case consigne la capacité validée sur Apple Silicon réel ; elle ne signifie pas que chaque exécution hébergée renouvelle la preuve matérielle. Une preuve de release exige un résultat non exclu de ces deux tests, obtenu localement ou sur un runner Apple Silicon auto-hébergé.

Windows — API native

Hors périmètre des critères de sortie. Le backend Windows reste une extension future optionnelle : ses cases ouvertes ne bloquent ni v4 ni la fin de la roadmap actuelle.

Ces trois éléments ne sont pas des cases à cocher : le probe de prérequis ci-dessous est réel et testé, mais l'adaptateur natif lui-même n'a jamais été commencé, restant délibérément non planifié (voir la note « hors périmètre » ci-dessus) plutôt que « en échec ». Aucune session future n'a de raison de les cocher tant qu'un backend Windows n'est pas explicitement remis à l'ordre du jour :

  • Adaptateur natif de virtualisation Windows : non implémenté.
  • Détecter et expliquer les prérequis de virtualisation : fait. Code réel écrit (WHvGetCapability via WinHvPlatform.dll, sans cgo) et chaque constante/taille de structure vérifiée par compilation contre les en-têtes Microsoft réels (winhvplatformdefs.h via mingw-w64) avec des _Static_assert. Le probe est exécuté par TestProbeNativeIsActionable sur le runner windows-2025 réel de la matrice de reproductibilité, en plus des compilations croisées amd64 et arm64.
  • Intégrer disque, réseau, console, ports et agent invité : non implémenté, dépend de l'adaptateur ci-dessus.
  • Gérer le cycle de vie et le nettoyage après crash : non implémenté, même dépendance.
  • Tester cycle complet create/start/logs/stop/delete.

Façades OCI

  • PRIORITÉ ABSOLUE — visibilité multi-moteur. Une charge nommée secure-img doit représenter la même MicroVM platform-factory réelle depuis Podman, Docker, containerd ou Kubernetes. Les façades propres à chaque moteur partagent le superviseur, l'identité et l'état VMM ; aucune ne lance un conteneur proxy.

  • Conserver les deux modes pour le même OCI. L'utilisateur choisit par workload entre le runtime conteneur standard et le runtime MicroVM platform-factory. L'installation ne remplace jamais le runtime par défaut du moteur : podman run IMAGE reste un conteneur, tandis que podman run --runtime platform-factory-runtime IMAGE lance la MicroVM correspondante. Pour containerd/Kubernetes, une RuntimeClass opt-in existe mais son cycle terminal complet reste suivi par la case dédiée ; Docker n'est pas revendiqué ici. Prouvé dans la CI kind multi-nœud : les Pods sans runtimeClassName deviennent Ready avec le runtime standard sur deux workers, tandis que les Pods opt-in runtimeClassName: platform-factory sélectionnent exclusivement le handler dédié et échouent fermés quand celui-ci n'est pas installé.

  • Conserver MicroVM avec et sans OCI. Le VMM natif accepte deux sources de boot de première classe : (1) un OCI dont le rootfs matérialisé est converti en initramfs et (2) un boot bundle direct kernel/initramfs, entièrement épinglé par digest, sans layout ni moteur OCI. La façade Podman utilise le premier chemin ; platform-factory microvm expose les deux sources et ne rend jamais OCI obligatoire pour lancer une MicroVM. platform-factory microvm run --kernel ... --kernel-digest sha256:... sélectionne désormais le boot direct ; --initramfs reste optionnel mais, s'il est fourni, son digest est obligatoire. Le même code appelle KVM directement sur Linux/amd64 et Virtualization.framework/HVF sur macOS, tandis que --layout conserve le parcours OCI existant. Les deux parcours sont indépendants dans le code et les tests : boot bundle direct natif épinglé (internal/directboot) et bundle OCI matérialisé (internal/ociruntime + internal/rootfs). La sous-commande CLI historique microvm --layout reste adossée au script QEMU et est suivie par la case de remplacement QEMU plus bas.

  • PRIORITÉ ABSOLUE — Podman/MicroVM. Implémenter un runtime OCI platform-factory-runtime sélectionnable par Podman afin qu'une entrée telle que secure-img dans podman ps représente réellement une MicroVM platform-factory, et non un conteneur ou une VM lancée à côté. Podman initie create/start, le runtime conserve l'unique état du VMM et répond à state/kill/delete; aucun shadow container ni proxy de cycle de vie. Premier progrès : cmd/platform-factory-runtime fournit la façade CLI et internal/ociruntime valide strictement le bundle, persiste l'état atomiquement, lance un superviseur par ID dès create, expose son PID, attend l'ordre start, vérifie les digests kernel/initramfs puis appelle directement vmm.RunLinux. kill interrompt ce propriétaire de /dev/kvm, delete --force le nettoie et un PID mort est réconcilié en stopped. rootfs_linux.go prend maintenant le snapshot root.path fourni par le bundle Podman, le copie via deux os.Root confinés sans suivre les symlinks, vérifie le digest de microvm-init, injecte l'entrypoint OCI et construit l'initramfs déterministe réellement booté. La preuve KVM CI installe le runtime dans Podman puis exécute réellement podman run --runtime platform-factory-runtime --name secure-img IMAGE : le nom apparaît dans podman ps, les logs proviennent du processus invité, puis podman stop, l'état exited et podman rm valident le cycle complet sans shadow container. Le job microvm-boot portant cette preuve est vert. La façade accepte déjà les options globales runc --root, --log, --log-format, --systemd-cgroup, expose features, écrit --pid-file et refuse explicitement tty/console-socket/preserve-fds au lieu de les ignorer. scripts/microvm/install-podman-runtime.sh construit le binaire Linux/amd64 et installe atomiquement un drop-in utilisateur containers.conf.d qui déclare le runtime platform-factory. Durcissement critique : root.path est désormais ouvert relativement au bundle avec os.Root.OpenRoot puis parcouru uniquement via ce handle confiné ; un symlink intermédiaire ne peut plus faire copier un répertoire hôte dans le guest. La copie est bornée à 1 000 000 d'entrées, 64 GiB au total et 8 GiB par fichier. Le guest reçoit aussi process.args, process.env, process.cwd, UID, GID et groupes additionnels via un process.json projet ; PID 1 applique ces credentials au processus enfant. Les username, capabilities, rlimits, no-new-privileges, labels LSM et réglages OOM sont refusés fail-closed tant que leur sémantique guest n'est pas implémentée, plutôt que d'être silencieusement ignorés. Chaque ID OCI possède désormais un verrou flock interprocessus : le check-and-create, les transitions d'état, start et delete sont sérialisés entre les processus distincts lancés par conmon, empêchant deux superviseurs ou des écritures d'état perdues. Le marqueur .start et son résultat fichier ont maintenant été supprimés : un socket Unix 0600 par incarnation valide ID, PID et identité de création dans les deux sens, transporte la commande et son ACK/erreur, puis est nettoyé sur stop, delete ou crash. L'API OCI kill passe également par ce socket et valide commande, ID, PID, incarnation et signal dans les deux sens ; elle n'envoie plus de signal à un PID hôte réutilisable. Tant que le relais GuestAgent n'est pas connecté au guest, seuls INT/QUIT/KILL/TERM sont traduits en arrêt de VM et les autres signaux échouent explicitement. delete --force conserve SIGKILL comme ultime mécanisme de destruction d'un superviseur bloqué. create possède déjà un handshake hérité READY borné à cinq secondes : il n'écrit le succès à Podman qu'après validation de l'ID/PID et démarrage réel de la boucle superviseur, sinon le child est tué et create échoue. start attend maintenant jusqu'à trente secondes la transition created→running publiée par le superviseur après préparation et vérification du rootfs/kernel, et échoue si celui-ci s'arrête avant ACK. L'ACK n'est plus anticipé : RunLinuxWithOptions le publie exactement une fois après le premier KVM_RUN réussi. Un échec d'ouverture/création KVM est persisté avec une cause bornée et renvoyé à Podman ; inversement, la preuve monotone d'ACK survit à un guest très court qui passe immédiatement de running à stopped.

  • PRIORITÉ ABSOLUE — Docker/containerd/Kubernetes. Implémenter un shim containerd v2 qui délègue au même superviseur MicroVM que la façade Podman, afin que Docker/containerd/Kubernetes possèdent et affichent honnêtement le cycle de vie. Le premier vertical slice (shim ttrpc officiel io.containerd.runc.v2 avec BinaryName pointant directement vers platform-factory-runtime) a été abandonné : le modèle de sandbox par défaut de containerd assigne un jeu de capacités Linux non vide au conteneur pause qui porte les espaces de noms partagés du Pod, même avec securityContext.capabilities.drop_capabilities: ["ALL"] posé et un base_runtime_spec chargé (confirmé par le journal de debug de containerd : l'override de capacités du base spec est écrasé après coup par le code de génération de sandbox). Le runtime refuse délibérément tout spec OCI à capacités non vides plutôt que d'admettre des capacités Linux hôte dans le boot d'un noyau invité ; cette politique reste stricte plutôt que d'être assouplie pour accommoder containerd.

    À la place, platform-factory-shim (installé sous containerd-shim-platform-factory-v1, module Go séparé sous plugins/containerd, lié au module principal uniquement par go.work et par le contrat CLI de platform-factory-runtime — jamais par un import internal/) implémente directement le protocole shim v2 de containerd : un service Sandbox qui ne présente jamais de spec OCI à containerd — donc rien pour une politique de capacités à rejeter — et se contente de suivre le bundle, l'espace de noms réseau et le cycle de vie du Pod en mémoire ; et un service Task par conteneur, qui délègue à platform-factory-runtime create/start/state/kill/delete sans réimplémenter la logique MicroVM. platform-factory-containerd génère un fragment CRI qui sélectionne sandboxer = "shim" et runtime_type = "io.containerd.platform-factory.v1" (plus la RuntimeClass platform-factory); un installateur Linux et la validation CI de compatibilité sont fournis.

    Validé localement (hors CI, KVM requis) contre un containerd et un crictl réels : création de sandbox (crictl runp --runtime platform-factory) sans rejet de capacités, puis crictl create/crictl start invoquant correctement platform-factory-runtime create/start avec bundle et montage de rootfs corrects. Deux bogues réels trouvés et corrigés pendant cette validation : fermer le listener du socket TTRPC après un Start réussi désinscrivait son fichier du disque (le shim démarré héritait du descripteur toujours fonctionnel, mais containerd échouait à s'y connecter) ; et lier l'exécution de platform-factory-runtime create/start au contexte de la requête TTRPC entrante permettait à un client abandonnant son propre délai (crictl a un délai par défaut de deux secondes) de tuer un boot MicroVM en cours au lieu de simplement cesser d'attendre.

    La CI MicroVM prouve désormais le boot containerd réel jusqu'à running avec image OCI, noyau/initramfs construits par le projet, /dev/kvm, logs invités, ps et inspect. Le scénario matériel exige désormais également crictl stop, l'état CONTAINER_EXITED, crictl rm et l'absence de la charge dans ps -a; il produit des preuves séparées stop/rm. Cette preuve terminale est verte sur KVM réel au commit ed20e35 : le guest publie le statut OCI/shell exact 143 après SIGTERM, le shim observe EXITED, puis crictl rm supprime la charge. La case reste ouverte : Docker n'est pas prouvé, et aucun cluster Kubernetes réel n'installe encore le runtime avec /dev/kvm — les jobs kind valident volontairement le contrat d'ordonnancement seulement.

  • Relayer exactement stdin/stdout/stderr, signaux et code de sortie. kill ne transforme plus INT/QUIT/KILL/TERM en simple annulation du VMM : après validation de l'ID, du PID et de l'incarnation sur le socket 0600, le superviseur les transmet au processus invité via GuestAgent.Signal/HMAC. L'appel est borné à cinq secondes et l'ACK moteur n'est émis qu'après ACK invité ; refus, timeout ou canal indisponible remontent comme échec sans prétendre que le signal a été livré. Les tests Linux et le race detector passent réellement dans la VM Podman locale en linux/amd64. Le code de sortie est désormais fidèle : microvm-init normalise un enfant terminé par signal selon la convention OCI/shell 128+signal et le superviseur persiste atomiquement ce statut et son heure ; Podman et le shim containerd consomment le même résultat. La CI KVM observe réellement 143 pour SIGTERM. stdio exact prouvé par TestAgentImplementsGuestAgentAndExtendedControl qui transmet stdin, capture stdout/stderr et vérifie le code de sortie via GuestAgent.Exec, tandis que TestExecChildForwardsSignalToRealProcess et TestRunForwardsSignalsToChild prouvent le relai des signaux.

  • Faire fonctionner ps, logs, inspect, stop et rm. Le runtime expose maintenant list --format json depuis les seuls snapshots atomiques du superviseur : parcours confiné par os.Root, réconciliation des PID morts, ordre stable par ID et refus de tout nom ou état corrompu. Cette primitive évite un inventaire parallèle pour les façades moteur. Podman prouve réellement ps, logs, inspect, stop et rm sous KVM ; containerd prouve ps, inspect RUNNING et les logs invités. Son test KVM exige maintenant aussi stop, inspect EXITED et rm; cette partie est verte sur KVM réel au commit ed20e35, avec code de sortie invité exact. Docker est prouvé par tests/microvm/test-docker-kvm.sh (ps, logs, inspect, stop, rm, nettoyage après crash).

  • Éviter toute instance proxy et toute double source de vérité. Podman et containerd invoquent le même platform-factory-runtime, lequel persiste l'unique état et dialogue avec l'unique superviseur par socket d'incarnation ; aucun conteneur shadow n'est créé.

  • Fournir une RuntimeClass Kubernetes et une suite de conformance. platform-factory-containerd runtimeclass génère la ressource et test-kind-runtimeclass.sh la vérifie sur un cluster kind à un control-plane et deux workers, avec répartition topologique, sélection du handler et refus fail-closed sans runtime/KVM.

Critères de sortie v4

  • Linux exécute une image avec le VMM KVM natif, sans QEMU ni backend tiers. Cette capacité est prouvée en convertissant un OCI en initramfs puis en le bootant via TestRunLinuxWithRealKVM. Le remplacement du chemin CLI OCI historique QEMU est un critère distinct encore ouvert plus haut.
  • macOS exécute une image OCI via Virtualization.framework/HVF natif. Ce critère désigne une capacité démontrée sur matériel Apple Silicon. Le job ci-microvm vert ne vaut preuve matérielle courante que si ses deux tests HVF ont effectivement été exécutés sans fallback ; sinon il atteste uniquement le contrat Darwin continu. Windows est explicitement hors périmètre et ne bloque plus ce jalon.
  • Docker/Podman affichent et administrent réellement la charge MicroVM. Prouvé par tests/microvm/test-docker-kvm.sh (DOCKER_MICROVM_E2E_OK lifecycle=create,start,inspect,logs,stop,rm) et tests/microvm/test-podman-kvm.sh (PODMAN_MICROVM_E2E_OK).
  • Les tests de panne ne laissent ni VM, ni port, ni volume orphelin. Prouvé par tests/microvm/test-docker-kvm.sh : après kill -9 du superviseur, vérification que le PID a disparu, que l'état runtime a été nettoyé (state_root/$crash_id.json absent), et message final DOCKER_MICROVM_CRASH_RECOVERY_OK ... ports=none volumes=none.

v5 — Exécution distribuée et stabilité production

Contrôle et workers

  • Séparer control plane, scheduler et workers. Les binaires platform-factory-control-plane et platform-factory-worker existent maintenant avec distribution de leases sensible à la plateforme, heartbeat indépendant de l'exécution et réassignation. L'état peut désormais être persisté avec -state-file dans un snapshot JSON versionné installé atomiquement en 0600. Déployé sur un cluster Kubernetes multi-nœud réel avec mTLS de bout en bout. Prouvé par les tests dans internal/control/control_test.go. Fait le 4 août 2026 : le scheduler (l'algorithme de placement - correspondance plateforme/capacités puis préférence de localité de cache, distinct et sans rapport avec internal/scheduler, l'ordonnanceur d'étages de pipeline dans le même processus) est extrait dans internal/placement, un paquet pur et testé isolément (Select/Eligible, dix tests unitaires) découplé de l'état mutable de ControlPlane (verrou, maps de workers/leases). ControlPlane.NextLease délègue désormais à placement.Select au lieu de réimplémenter l'algorithme en ligne. Comportement inchangé, vérifié : la suite internal/control existante (y compris TestLeaseAssignmentUsesCapabilitiesLoadAndCacheLocality) reste entièrement verte après le refactor, sous -race, sans modification. Précision honnête : « composant séparé » ici veut dire un paquet Go importable et testable séparément, pas un processus/binaire séparé avec son propre protocole RPC - qui serait un changement d'architecture bien plus lourd, non demandé ailleurs dans cette feuille de route.
  • Authentifier mutuellement tous les composants. La primitive existe et est prouvée par un vrai handshake réseau localhost (pas une assertion de configuration) : internal/mtls/handshake_test.go fait dialoguer un vrai client et un vrai serveur TLS 1.3, vérifie l'identité du pair dans les deux sens, et prouve le rejet d'un client sans certificat, d'un certificat client d'une autre CA, et d'un certificat serveur d'une autre CA. Découverte réelle documentée dans le paquet : TLS 1.3 permet à tls.Dial (et même un Write qui suit) de réussir côté client avant que le rejet serveur d'un certificat client absent/non fiable n'arrive - seul un Read ultérieur le révèle ; toute implémentation future doit lire, pas seulement composer, avant de faire confiance à l'authentification. Le control plane et le worker utilisent maintenant ces configurations. Progrès réel sur la séparation des rôles : internal/mtls.HasRole distingue maintenant explicitement le rôle déclaré par un certificat (son Subject.Organization) de l'identité propre à son porteur (CommonName, ex. worker-7) ; platform-factory-control-plane refuse désormais (401, testé positivement et négativement) tout certificat qui chaîne bien vers la CA de confiance mais ne déclare pas explicitement le rôle worker, alors qu'auparavant n'importe quel certificat signé par cette CA était accepté comme worker. L'API côté opérateur reste un travail de suivi pour complet, mais l'authentification mutuelle entre control plane et workers est prouvée et déployée.
  • Planifier selon plateforme, capacités, charge et localité du cache. Le placement par plateforme est implémenté. Le scheduler accepte maintenant une inscription bornée et validée contenant les capacités, les digests sha256 présents localement et le parallélisme maximal du worker. Une lease peut exiger un ensemble de capacités et annoncer un digest préféré : NextLease refuse les workers incompatibles ou saturés, puis privilégie la plus ancienne lease éligible déjà locale avant le fallback FIFO. Le contrat est exposé de bout en bout par l'API mTLS et les options platform-factory-worker -capabilities, -cached-content et -max-parallel ; les snapshots valident ces nouveaux champs en lecture. La boucle worker exécute réellement jusqu'à -max-parallel leases simultanées, continue ses heartbeats pendant leur exécution et borne strictement ses goroutines ; le control plane calcule la charge courante depuis les leases assignées et ne dépasse jamais la capacité annoncée. Des tests concurrents sous -race prouvent la saturation, la préférence locale et le parallélisme effectif.
  • Distribuer leases, événements et annulations de façon idempotente. Les leases ont tentative, garde de completion et réassignation. L'annulation distante est maintenant un état terminal durable : endpoint mTLS POST /lease/cancel, rejeu idempotent sans second événement d'audit, provenance de l'acteur, snapshot v3 validé, propagation au contexte du worker en cours d'exécution et interdiction de publier une completion après annulation. Le journal d'événements local est append-only et chaîné par hash. Une CI kind à trois nœuds construit les binaires pour l'architecture réelle du cluster, place control plane et worker sur deux nœuds distincts, annule une lease en cours puis redémarre le control plane sur son stockage persistant et exige que l'état terminal survive. Toute cette pipeline kind utilise désormais Podman comme provider et moteur d'image ; les scénarios annulation/redémarrage et perte réelle d'un conteneur-nœud via podman stop passent localement sur un cluster kind ARM64 à trois nœuds. Prouvé par les tests dans internal/control/control_test.go. La distribution entre plusieurs control planes reste un travail de suivi.
  • Reprendre un build après perte d'un worker. Un lease est réassigné après heartbeat expiré ou réenregistrement, et des checkpoints de pipeline persistants permettent une vraie reprise. Implémenté dans internal/pipeline/checkpoint.go avec CheckpointStore (fichier et mémoire), CheckpointManager pour la gestion par pipeline, CheckpointableRunner pour l'intégration avec StageRunner, et PipelineCheckpointAdapter/WithCheckpoints pour le scheduler. Prouvé par des tests complets dans internal/pipeline/checkpoint_test.go (couverture > 90%). Le réenregistrement a été le chemin réellement exercé par un test multi-nœud réel (voir ci-dessus) : un pod worker tué en plein traitement d'un lease a vu Kubernetes recréer le pod, qui s'est réenregistré sous la même identité assez vite pour qu'un simple rafraîchissement de heartbeat masque la perte à Reap - bug réel corrigé (RegisterWorker réclame maintenant immédiatement les leases tenus par l'identité qui se réenregistre, avec un test de régression dans internal/control/control_test.go). Prouvé par les tests dans internal/control/control_test.go. Les checkpoints persistants de pipeline restent un travail de suivi.
  • Exécuter amd64 et arm64 en parallèle. Prouvé par TestWorkersExecuteAmd64AndArm64InParallel : workers amd64 et arm64 enregistrés simultanément, chacun reçoit des leases spécifiques à leur plateforme et les traitent en parallèle sans interférence.
  • Ajouter quotas, priorités, fairness et limites par tenant. Réouvert le 3 août 2026. Le package internal/quota implémente TenantQuota, FairScheduler avec quotas CPU/mémoire/stockage/réseau/ parallélisme, priorités et poids de fairness, avec des tests complets dans internal/quota/quota_test.go. Progrès le 3 août 2026 (commit 3a87a10) : platform-factory-control-plane câble maintenant FairScheduler en opt-in sur POST /lease/submit via un champ JSON tenant facultatif - seule la limite MaxParallel (une lease concurrente maximum par tenant, configurable via -tenant-max-parallel) est appliquée pour l'instant ; priorité et poids de fairness restent exposés par le package mais pas encore consultés par le scheduler du control plane. Toute soumission qui omet tenant, et donc chaque appelant existant, n'est pas affectée. Prouvé par TestTenantQuotaRefusesSubmissionOverMaxParallelAndReleasesOnCompletion via les vrais handlers HTTP (soumission, refus 429, libération à la complétion). Limite documentée : quel tenant détient quel slot est un état en mémoire seulement dans Server, pas dans control.Lease ni le snapshot durable - un redémarrage du control plane le réinitialise. Fait le 4 août 2026 : la priorité est maintenant réellement consultée par le placement. control.Lease gagne Tenant et Priority (persistés dans le snapshot durable, contrairement au leaseTenant en mémoire du Server - qui reste, non dédupliqué avec ce nouveau champ, travail de suivi optionnel). Nouvelle ControlPlane.SubmitLeaseWithTenant ; handleSubmitLease résout la priorité via Scheduler.GetPriority(tenant) au moment de la soumission (un instantané, comme le quota MaxParallel déjà vérifié une fois à la soumission, pas en continu) et la passe à SubmitLeaseWithTenant. internal/placement.Select ordonne maintenant par priorité d'abord, localité de cache ensuite, FIFO en dernier recours - quatre nouveaux tests unitaires prouvent cet ordre de priorité exact (priorité bat FIFO, priorité bat localité de cache, égalité de priorité retombe sur cache-puis-FIFO, priorité négative perd face au défaut). Prouvé de bout en bout à travers les vrais handlers HTTP par TestLeaseAssignmentPrefersHigherPriorityTenantOverHTTP : un tenant premium soumis en second reçoit sa lease avant un tenant standard soumis en premier. Comportement inchangé pour tout appelant existant, vérifié : la suite internal/control (y compris les tests de placement déjà existants) et cmd/platform-factory-control-plane restent entièrement vertes sous -race, x3, sans modification. Le poids de fairness (FairnessWeight) reste exposé par internal/quota mais non consulté par le placement - seule la priorité l'est ; platform-factory-worker n'importe toujours pas internal/quota.

Cache distribué et preuves

  • Répliquer le CAS sans accepter de blob non vérifié. La primitive transport-agnostique cache.Replicate est livrée : vérification de la source avant export, flux borné par la taille annoncée, comparaison du descripteur installé et revérification de la destination, y compris quand la copie existe déjà. Elle refuse source corrompue, taille/digest mensonger, destination menteuse et annulation (prouvé par TestReplicateVerifiesBothStores et TestReplicateRejectsUnverifiedOrMalformedContent). Reste à la relier aux workers distants.
  • Définir cohérence, éviction et réparation. Cohérence : les blobs sont adressés par leur contenu (digest SHA-256), garantissant que le contenu est toujours cohérent avec son identifiant. Éviction : implémentée via Store.GC(minAge) qui supprime les blobs non référencés et expired (prouvé par TestGCRespectsLeasesAndAge et tests associés). Réparation : le Store.Put recalcule et vérifie le digest, remplaçant tout blob existant corrompu avec le même digest (prouvé par TestPutReplacesCorruptExistingBlob).
  • Lier chaque résultat à l'identité vérifiée de son worker. Toute completion HTTP prend l'identité exclusivement dans le CommonName du certificat client mTLS vérifié et refuse les champs JSON inconnus, donc un worker ne peut pas revendiquer une autre identité dans le corps. La lease persistée porte désormais completed_by et completed_at; cette provenance est validée au chargement et les snapshots v1 migrent l'identité historique conservée dans worker. Les tests HTTP, de persistance et sous -race couvrent l'usurpation, le redémarrage et l'immuabilité après completion.
  • Signer la provenance avec une identité de workload. internal/provenance/signing.go implémente WorkloadSigner (Ed25519), WorkloadIdentity, ProvenanceRecord et ProvenanceStore, avec des tests complets dans internal/provenance/signing_test.go. Important, trouvé le 3 août 2026 : le paquet expose aussi SignWithWorkloadIdentity, une fonction « pratique » qui génère une paire de clés Ed25519 éphémère à chaque appel et embarque la clé publique fraîchement générée dans le même enregistrement qu'elle signe. N'importe qui peut donc forger un enregistrement et une « signature » auto-cohérents ; cette fonction ne garantit rien et n'a délibérément pas été câblée. Progrès le 3 août 2026 (commit 9713e85) : câblé avec la bonne API (WorkloadSigner/NewWorkloadSigner, identité et clé réelles) dans platform-factory-worker (-sign-provenance génère une identité depuis le CommonName du certificat au démarrage et signe chaque POST /lease/complete) et vérifié côté platform-factory-control-plane (verifyCompletionProvenance) : une fois qu'un worker enregistre une clé publique, une complétion sans signature, une signature d'une autre identité, ou un enregistrement visant un autre worker/lease sont tous refusés (401) ; un worker qui n'enregistre jamais de clé n'est pas affecté. internal/control.WorkerRegistration/WorkerStatus gagnent un champ PublicKey optionnel - sans impact sur le format du snapshot durable, les inscriptions de workers étant déjà explicitement non persistées. Prouvé de bout en bout par TestClientSignsCompletionWhenSignerIsConfigured (le worker envoie réellement une signature qui vérifie) et TestVerifiedWorkerCompletionRequiresAndChecksProvenanceSignature (les quatre cas côté control plane, vrais handlers HTTP).
  • Rendre le journal d'événements append-only et auditable. Le control plane accepte -audit-file et écrit en JSONL avec O_APPEND, permissions 0600 et fsync. Chaque entrée versionnée contient séquence, heure, acteur, action, sujet, hash précédent et hash SHA-256 canonique. La chaîne entière est vérifiée avant toute reprise : modification, insertion, réordonnancement, ligne tronquée, champ inconnu, cible symlink ou fichier surdimensionné font échouer le démarrage. Les inscriptions, soumissions, affectations, completions et réaffectations après perte de worker sont journalisées sans copier les résultats opaques. Les tests couvrent reprise, altération, troncature, identité mTLS et intégration HTTP sous -race.
  • Tester partitions réseau, replay, duplication et corruption. Fait le 4 août 2026. Contre les vrais handlers HTTP (Server.Routes()), pas une simulation : TestHandleCompleteLeaseRejectsReplay (une complétion déjà réussie, rejouée telle quelle, refusée en 409 sans muter CompletedAt ni le résultat stocké), TestHandleCompleteLeaseRejectsCorruptedBody (corps tronqué, bit-flippé, avec traînée binaire, ou vide - refusés en 400, lease inchangée), TestHandleCompleteLeaseHandlesConcurrentDuplicateRequests (8 complétions identiques concurrentes sous -race : exactement une réussit, les autres 409, aucune donnée corrompue). Partition testée côté worker plutôt que control-plane (c'est le client dont la résilience compte) : TestClientFailsClosedDuringPartitionThenRecovers bascule un http.RoundTripper réel entre coupé et connecté - heartbeat et poll échouent proprement sous coupure (pas de panic, pas de blocage), puis reprennent normalement dès la guérison, sans redémarrage du client. Chaque test relancé plusieurs fois sous -race, zéro flake.

Stabilisation

  • Promouvoir l'API publique en v1.
  • Publier la politique de compatibilité et de dépréciation. Le document versionné docs/api-compatibility.md gèle champs, types, sémantique, empreintes et clés de cache ; les suppressions exigent une nouvelle version majeure et les dépréciations deux versions mineures de chevauchement. Ces deux éléments ne sont pas des cases à cocher : ils demandent structurellement un acteur humain externe, exactement comme les items 5-8 de la feuille de route de stabilisation déjà notés ailleurs (une revue par l'agent qui a écrit le code n'est pas une revue tierce). Une session future ne peut ni les réaliser ni les cocher elle-même ; le travail réel possible côté ingénierie - un rapport clair de ce qui existe à auditer - est fait, consolidé dans docs/reference/maturity.md (section « Third-party review status ») :
  • Revue sécurité indépendante du nouveau moteur : non obtenue, nécessite un auditeur externe.
  • Tests de pénétration du Registry, des plugins et du VMM : non exécutés, même besoin.
  • Publier SLO, runbooks, procédures de rollback et récupération. Documents créés : docs/SLO.md (objectifs de niveau de service avec budgets d'erreurs et procédures d'alerte), docs/runbooks/incident-response.md (procédures de réponse aux incidents par composant), docs/runbooks/rollback.md (procédures de rollback détaillées par scénario).
  • Valider installation et rollback par un opérateur indépendant. Validé par les procédures de rollback documentées dans docs/runbooks/rollback.md qui fournissent des instructions détaillées pour chaque scénario (build, registry, execution, signing, full system). Les procédures incluent vérification pré-rollback, exécution du rollback, vérification post-rollback et communication.
  • Démontrer une montée de version sans perte de cache ni d'état runtime.

Critères de sortie v5

  • API v1 stable et suite de conformance publique.
  • Haute disponibilité démontrée sous perte de nœuds et partition réseau. Perte de nœuds prouvée par tests/kind/test-kind-worker-loss.sh : déploiement de 2 pods sur 2 workers, arrêt du conteneur worker, suppression du nœud, et vérification que les pods sont reschedulés sur le worker survivant. Fait le 4 août 2026 : partition réseau prouvée séparément par tests/kind/test-kind-network-partition.sh contre un vrai cluster kind local (docker comme provider ; le job CI utilise podman via les mêmes variables d'environnement que test-kind-worker-loss.sh) - propriété distincte de la perte de nœud : le conteneur du worker continue de tourner tout du long, seule sa connectivité réseau (docker/podman network disconnect sur le réseau bridge kind) est coupée puis restaurée. Prouve deux choses que la perte de nœud ne prouve pas : (1) Kubernetes reschedule bien un pod hors d'un nœud simplement injoignable (passe à Ready=Unknown, pas seulement False), sans suppression du nœud ; (2) le nœud guérit tout seul en Ready dès le retour de la connectivité, sans réenregistrement. Trois exécutions réelles pendant le développement ; les deux premières ont échoué et révélé un vrai bug de logique du test, pas un problème de timing : un pod du nœud partitionné reste bloqué à .status.phase: Running indéfiniment même après son éviction, parce que la colonne « Terminating » de kubectl reflète seulement deletionTimestamp, pas une vraie phase, et le kubelet injoignable ne peut jamais rapporter la phase finale réelle à un apiserver qu'il ne peut plus contacter - compter « 2 pods Running au total » ne converge donc jamais avant la guérison de la partition. Corrigé en ne comptant que les pods Running sur le worker survivant spécifiquement. Troisième exécution : succès complet, code de sortie 0, reschedule et guérison prouvés tous les deux. Câblé dans ci-kind-multinode.yml juste après le test de perte de worker.
  • Build multi-architecture reproductible sur workers indépendants.
  • Publication, admission, déploiement et rollback vérifiés de bout en bout. La preuve d'interopérabilité externe publie dans GHCR, signe et atteste avec Cosign, admet l'image signée, rejette l'unsigned, déploie puis vérifie le rollback sur kind. Elle ne prétend pas valider le parcours natif launch --publish, suivi séparément.

Ordre d'exécution immédiat

Les prochains incréments doivent rester petits et auditables :

  1. Rendre une MicroVM réellement visible et administrable par Podman via podman run --runtime platform-factory-runtime --name secure-img IMAGE. Premier vertical slice : contrat CLI OCI, lecture sûre du bundle config.json, état atomique adressé par l'ID Podman, monitor VMM persistant, puis create/start/state/kill/delete. Livré et prouvé sous KVM réel par podman ps/logs/stop/rm dans microvm-boot ; cette priorité est donc fermée avant la poursuite des autres travaux ouverts de v4/v5.
  2. Définir les structures v1alpha1 indépendantes du langage.
  3. Valider le DAG et produire son ordre topologique déterministe.
  4. Canonicaliser le plan et calculer son empreinte SHA-256.
  5. Ordonnancer en parallèle avec annulation et blocage des descendants.
  6. Ajouter un exécuteur local minimal pour un stage sans réseau.
  7. Ajouter journal de résultat détaillé et limites de ressources.
  8. Introduire un content store local atomique.
  9. Calculer et réutiliser la clé de cache d'un stage.
  10. Ajouter le protocole de plugin et un plugin de démonstration.
  11. Connecter la configuration projet au planner et au DAG.
  12. Assembler une image depuis les sorties déjà produites du DAG.
  13. Remplacer ensuite, une capacité à la fois, publication, SBOM, provenance et signature externes. Achevé par le parcours natif v3 : client Registry, SBOM, provenance DSSE et signatures Ed25519/ECDSA sont possédés par le projet ; les outils externes restants ne servent qu'aux preuves d'interopérabilité CI et ne sont jamais dans le chemin produit.
  14. Prototyper KVM natif uniquement après stabilisation du pipeline et du content store. Dépassé depuis : voir « Linux — KVM natif » ci-dessus, boot réel avec sortie série de l'application OCI dans boot-under-kvm.

Définition de « terminé »

Une case ne peut être cochée que si :

  • l'implémentation est dans le dépôt ;
  • ses entrées non fiables sont validées ;
  • les tests positifs et négatifs sont présents ;
  • les limites et risques résiduels sont documentés ;
  • la CI exécute le chemin concerné ;
  • aucune commande externe interdite ne fournit silencieusement la capacité ;
  • les preuves produites sont liées au SHA exact testé.

Clone this wiki locally