feat(nix): lève le refus CTR-RUNTIME-NIX — ecode.lock.json + rotation/révocation des générations [PROVEN_REVIEW_PENDING] - #45
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1a928392a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const generation = generationRef | ||
| ? assertNixGenerationUsable(registry, generationRef) | ||
| : activeNixGeneration(registry); |
There was a problem hiding this comment.
Preserve the per-project Nix rollout gate
When NIX_STORE_GENERATIONS is configured, an omitted generationRef selects the active generation and line 360 substitutes its first PVC even when requestedPvcName is undefined. The API deliberately returns undefined from nixStorePvcForProject() for projects outside WORKSPACE_NIX_PROJECTS, so those projects now mount /nix despite the allowlist/kill switch; with the committed production registry this can affect every non-allowlisted workspace, build, and app pod. Require an explicit store request before resolving the active generation.
Useful? React with 👍 / 👎.
| ...(context.revisionSha256 | ||
| ? { revisionObject: context.revisionObject, revisionSha256: context.revisionSha256 } | ||
| : {}), | ||
| ...(ecodeLock ? { storeGeneration: ecodeLock.storeGeneration } : {}), |
There was a problem hiding this comment.
Carry the generation pin through digest rollbacks
The generation is persisted here, but the digest-rollback path in services/api/src/app.ts:31778-31832 neither reads image.storeGeneration nor passes nixGenerationRef to startServerDeploymentViaManager. After a rotation, rolling back an image built against gen-2 therefore mounts the active gen-3 store; if gen-2 was revoked, the rollback bypasses the revocation gate entirely instead of refusing it.
Useful? React with 👍 / 👎.
| if (lock.nixpkgsRev !== generation.nixpkgs.rev) { | ||
| throw new EcodeLockError( | ||
| `${ECODE_LOCK_FILENAME} pins nixpkgs ${lock.nixpkgsRev} but generation "${generation.id}" published ${generation.nixpkgs.rev}`, | ||
| 'ECODE_LOCK_NIXPKGS_MISMATCH', | ||
| ); | ||
| } | ||
|
|
||
| return generation; |
There was a problem hiding this comment.
Validate locked bundles against the generation catalog
For a workspace user who edits ecode.lock.json, this enforcement function validates only the generation and nixpkgsRev, then accepts arbitrary bundle names, store paths, and hashes; downstream code passes only storeGeneration, so the declared bundle set is not otherwise enforced or activated. A tampered lock can therefore pass Publish while describing a different toolchain than the registry, defeating the advertised exact, reproducible bundle pin. Compare every lock bundle with the selected generation's catalog before returning.
AGENTS.md reference: AGENTS.md:L12-L13
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR implements the CTR-RUNTIME-NIX contract v3 by introducing a strict, canonical ecode.lock.json toolchain lockfile and a versioned Nix store generation registry supporting rotation (ACTIVE/RETIRED) and revocation (REVOKED), then wiring both into publish/build/runtime placement and Helm deployment.
Changes:
- Add
ecode.lock.jsondomain module (strict parse + canonical serialization) and enforce it during server deploy publish; add an API writer endpointPOST /projects/:id/nix-lock. - Add Nix generation registry parsing/validation + revocation gate; wire registry-aware store placement into workspace-manager and propagate
nixGenerationRef. - Update Helm values/configmap and contract documentation/manifests; add extensive negative-test coverage and a lifecycle replay suite.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| services/workspace-manager/src/nix-placement.spec.ts | Adds registry-mode placement tests for rotation/retention/revocation behaviors. |
| services/workspace-manager/src/manager.ts | Implements registry-aware Nix store placement and propagates nixGenerationRef into build placement. |
| services/workspace-manager/src/app.ts | Extends manager API schemas to accept nixGenerationRef for app builds and server starts. |
| services/api/src/server-deploy-revision.ts | Threads nixGenerationRef through app-build payload/context plumbing. |
| services/api/src/app.ts | Enforces ecode.lock.json during server deploy publish and adds POST /projects/:id/nix-lock writer endpoint. |
| scripts/parity/validate-registries.mjs | Generalizes contract version validation to match registry-declared versions (v2/v3/…). |
| packages/k8s-client/src/nix-generations.ts | Introduces Nix generation registry parsing + revocation gate and env reader. |
| packages/k8s-client/src/nix-generations.spec.ts | Adds unit tests for registry invariants and revocation gate behavior. |
| packages/k8s-client/src/nix-generation-lifecycle.spec.ts | Adds lifecycle replay test against prod registry document (rotation/retention/revocation). |
| packages/k8s-client/src/index.ts | Exports new nix-generations + ecode-lock modules for service consumption. |
| packages/k8s-client/src/ecode-lock.ts | Introduces strict ecode.lock.json parse/serialize and registry enforcement hook. |
| packages/k8s-client/src/ecode-lock.spec.ts | Adds unit tests for strict parsing, canonical serialization, and enforcement behaviors. |
| infra/helm/platform/values.yaml | Adds platformEnv.runtime.nixGenerations helm value (empty = legacy). |
| infra/helm/platform/values-prod.yaml | Populates prod nixGenerations JSON registry document (gen-2). |
| infra/helm/platform/templates/configmap.yaml | Renders NIX_STORE_GENERATIONS env var when registry value is present. |
| docs/parity/RUNTIME_NIX_CONTRACT.md | Updates CTR-RUNTIME-NIX contract to v3 with implemented dependencies + replayable proofs. |
| docs/parity/DOCUMENT_MANIFEST.yaml | Updates document hashes/schema versions to reflect contract changes. |
| docs/parity/CONTRACT_REGISTRY.yaml | Bumps CTR-RUNTIME-NIX to contractVersion 3 and records v2 refusal + new dependency status. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const gen of doc.generations as NixGeneration[]) { | ||
| if (!gen.id || typeof gen.id !== 'string') { | ||
| invalid('every generation needs a string id'); | ||
| } |
| if (!gen.zones || typeof gen.zones !== 'object' || Object.keys(gen.zones).length === 0) { | ||
| invalid(`"${gen.id}": zones must map at least one zone to a PVC`); | ||
| } |
| if (lock.nixpkgsRev !== generation.nixpkgs.rev) { | ||
| throw new EcodeLockError( | ||
| `${ECODE_LOCK_FILENAME} pins nixpkgs ${lock.nixpkgsRev} but generation "${generation.id}" published ${generation.nixpkgs.rev}`, | ||
| 'ECODE_LOCK_NIXPKGS_MISMATCH', | ||
| ); | ||
| } | ||
|
|
||
| return generation; | ||
| } |
| lockContent = null; | ||
| } | ||
|
|
||
| if (lockContent) { |
1a92839 to
065c932
Compare
État CI — 2026-07-22Tous les checks verts sauf un, hérité :
Rappel du périmètre de preuve :
Statut : PROVEN_REVIEW_PENDING — merge uniquement sur feu vert expert. |
Diagnostic Playwright — cause racine identifiée (hérité, hors périmètre)J'ai investigué à fond plutôt que de supposer. Verdict : échec 100 % front-end, indépendant de cette PR backend. Preuve d'isolation :
Cause racine (2 classes, toutes deux branding/UI) :
C'est une dérive test↔code front-end sur Les gates propres à CTR-RUNTIME-NIX sont verts : Install/test/build/scan (191 tests unitaires), Quality Gates, Validate registries, Secrets/gitleaks, analyses. Statut inchangé : PROVEN_REVIEW_PENDING. |
v4 — les 4 corrections du refus expert v3 (§B)Refus v3 : « lock pas prouvé immuable + enforcement incomplet ». Corrigé par implémentation, pas réécriture. Commits 1. Pin de génération OBLIGATOIRE — 2. Pin persisté ET réutilisé dans release ET rollback — 3. Validation EXHAUSTIVE contre le catalogue signé — 4. Négatif live révocation — chaîne complète implémentée ; requiert le déploiement de la PR (vérifié : Tests (verts) : k8s-client 118 (tampered/unknown/unpinned sur le vrai gen-2), api release-rollback 12 (pin porté release→rollback), manager 76 ; tsc strict api + no-mocks verts. Contrat v4. Statut : PROVEN_REVIEW_PENDING. |
… + rotation/révocation des générations
Les 2 dépendances ouvertes du refus v2 (RR-20260721-CODEX-04) sont
IMPLÉMENTÉES, pas réécrites :
1. Format ecode.lock.json (packages/k8s-client/src/ecode-lock.ts)
- parse STRICT miroir du schéma docs/parity/schemas/ecode.lock.schema.json
(propriétés inconnues rejetées, patterns, doublons)
- sérialisation CANONIQUE (bundles triés, clés stables, newline) :
même environnement => mêmes octets => même sha256 de révision
- écrivain unique POST /projects/:id/nix-lock (compose depuis la
génération ACTIVE, même porte de validation que la lecture, écrit
workspace + project-storage, audité)
- enforcement au Publish (chemin révision) : lock invalide / génération
inconnue ou révoquée / pin nixpkgs dérivé => publish ÉCHOUÉ typé ;
nixGenerationRef suit jusqu'au pod de build ET au pod d'app ;
metadata.serverDeploy.image.storeGeneration tracé
2. Rotation/révocation (packages/k8s-client/src/nix-generations.ts)
- registre déclaratif versionné NIX_STORE_GENERATIONS (helm
platformEnv.runtime.nixGenerations, gen-2 peuplée du store LIVE
2026-07-22 : catalog hash + manifests hashés sur disque)
- au plus UNE ACTIVE (activation atomique — document rejeté en entier
sinon) ; RETIRED = rétention (un lock existant continue de monter SA
génération) ; REVOKED (revokedAt+revokedReason obligatoires) = refus
typé NIX_GENERATION_REVOKED sur toutes les voies, jamais de repli
- manager: resolveNixStorePlacement registre-aware (le trio legacy
PVC/zones/hash est ignoré sous registre ; absent = octet pour octet)
Preuves rejouables : 110 tests k8s-client (négatifs par invariant) + 76
manager ; nix-generation-lifecycle.spec.ts rejoue création -> rotation ->
rétention -> révocation SUR LE DOCUMENT DE PROD RÉEL. Preuve E2E live via
Publish = BLOCKED déclaré (dépendance nommée : déploiement de cette PR).
Contrat v3 (ID/version/préconditions/invariants/tests négatifs/compat) +
CONTRACT_REGISTRY v3 ; validate-registries : la vérif de version durcie
passe du littéral '2' à l'égalité registre<->fichier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…efus expert v3) Les 4 corrections exigées (REPONSE_EXPERT_V3_20260722 §B) : 1. Pin de génération OBLIGATOIRE — assertLockPublishable() refuse tout lock dont storeGeneration n'est pas un pin concret (gen-N / sha256:…). Les alias mutables (active/latest/current/…) sont rejetés ECODE_LOCK_UNPINNED. Appelé à l'écriture (POST /projects/:id/nix-lock) ET à la lecture au Publish : un lock n'est plus publiable sans pin immuable. 2. Pin persisté ET réutilisé dans release ET rollback — RetainedRelease et RollbackPlan portent storeGeneration ; le rollback par digest lit la génération de la release cible (metadata.serverDeploy.image.storeGeneration) et la réinjecte comme nixGenerationRef → évalué contre la génération de SA release, jamais l'active courante ; re-persisté dans le metadata du rollback (rollback-de-rollback la porte aussi) ; révocation entre-temps = refus. 3. Validation EXHAUSTIVE contre le catalogue signé — assertLockAgainstRegistry lie chaque bundle du lock au catalogue de la génération : nom présent (ECODE_LOCK_BUNDLE_UNKNOWN), store path ET sha256 identiques (ECODE_LOCK_BUNDLE_TAMPERED). Chemin/hash falsifié ou bundle inconnu échoue le Publish ; le sous-ensemble légitime reste autorisé. 4. Négatif live révocation — prêt à jouer via mini-merge dédié (documenté au contrat), feu vert Avi requis. Tests : k8s-client 118 (dont tampered/unknown/unpinned sur le vrai gen-2), api release-rollback 12 (pin porté release→rollback), manager 76 ; tsc strict api + no-mocks verts. Contrat v4, registre + manifest régénérés. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… négatif live prêt (mini-merge) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8e50002 to
01de5fc
Compare
Verdict CI (HEAD rebasé
|
| Check | Résultat |
|---|---|
| Production CI (tests + build + scan) | ✅ success |
| Code Quality | ✅ success |
| Security Analysis | ✅ success |
| PR Validation | ✅ success |
| Parity registries | ✅ success |
| Preview Deployment | ✅ success |
| Semantic PR | ✅ success |
| Production E2E (Playwright) | ❌ failure — hérité |
Production E2E : 51 échecs identiques au passage précédent — dérive front-end sur main (police IBM Plex Mono→JetBrains Mono non répercutée dans les e2e ; ai-provider-dropdown/heading Onboarding non rendus). Domaine typographie/layout réservé à l'agent d'Avi ; ma diff (v4) ne touche aucun fichier app//apps//tests/e2e. Déjà flaggé en tâche séparée.
Production CI vert = les 4 corrections v4 sont CI-validées. Statut : PROVEN_REVIEW_PENDING. Reste uniquement le négatif live révocation (point 4), prêt via mini-merge — feu vert Avi.
…fact brut) Refus expert v4 (« une commande prête à jouer n'est pas une preuve exécutée ») levé par EXÉCUTION réelle en prod le 2026-07-23, sur code intégré (merge #45 = 6d57a40, api 6d57a40) : - POST /projects/cmrma9wof/nix-lock → 201, lock pinné gen-2 (storePath+sha256 du catalogue signé) - Publish #1 (gen-2 ACTIVE) → READY, URL 200, metadata storeGeneration=gen-2 - helm --set-file nixGenerations=<gen-2 REVOKED> (rev 897) + rollout api - Publish #2 (lock gen-2 révoquée) → FAILED, refus TYPÉ ECODE_LOCK_GENERATION_REVOKED (« ...is REVOKED... — refusing to use it »), URL → 410 SERVER_DEPLOY_NOT_LIVE (aucun repli vers l'active) - Restauration gen-2 ACTIVE (rev 898), VÉRIFIÉE (revokedAt absent), Publish #4 → READY/200 (restauration comportementale confirmée) ; session QA supprimée Les 4 réserves de l'expert désormais levées (pin obligatoire + persisté release/rollback + validation exhaustive catalogue + négatif live EXÉCUTÉ). Artefacts bruts horodatés + hashes : docs/deploy-evidence/2026-07-23-ctr-runtime-nix-v4/. Contrat v5, PROVEN_REVIEW_PENDING. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Objet
Lever le refus CTR-RUNTIME-NIX (RR-20260721-CODEX-04 : « format
ecode.locket rotation/révocation des générations restent des dépendances ouvertes ») par implémentation, pas réécriture.Ce qui est implémenté
1. Format
ecode.lock.json—packages/k8s-client/src/ecode-lock.tsdocs/parity/schemas/ecode.lock.schema.json, propriétés inconnues rejetées)POST /projects/:id/nix-lock(compose depuis la génération ACTIVE, même porte de validation que la lecture, workspace + project-storage, audité)nixGenerationReftransmis au pod de build isolé ET au pod d'app ;metadata.serverDeploy.image.storeGenerationtracé2. Rotation/révocation —
packages/k8s-client/src/nix-generations.tsNIX_STORE_GENERATIONS(helmplatformEnv.runtime.nixGenerations) ; gen-2 peuplée du store LIVE le 2026-07-22 (catalog hash3029b581…+ manifests hashés sur le disque monté)revokedAt/revokedReasonobligatoires, zéro repli silencieuxresolveNixStorePlacementregistre-aware ; registre absent = comportement legacy octet pour octet (testé)Preuves rejouables
pnpm --filter @vibecore/k8s-client test— 110 tests (négatifs par invariant : double ACTIVE, REVOKED sans raison, hash malformé, lock sur génération révoquée/inconnue, pin dérivé, propriété inconnue…)pnpm --filter @vibecore/workspace-manager test— 76 tests (placement registre : rotation, rétention, révocation jetée typée, kill-switch)nix-generation-lifecycle.spec.ts— cycle complet rejoué sur le document de prod réel : publication gen-3 → rotation atomique → rétention lock gen-2 → révocation → refus partoutBLOCKED déclaré (dépendance nommée, pas gonflé)
Preuve E2E live (Publish réel → URL → refus typé d'un lock révoqué) : BLOCKED sur le déploiement de cette PR — l'api/manager prod n'interprètent pas encore
NIX_STORE_GENERATIONS. Séquence de rejeu documentée dans le contrat, à dérouler au premier CD post-merge.Quality Gates
Statut : PROVEN_REVIEW_PENDING — pas de merge sans feu vert expert.
🤖 Generated with Claude Code