fix(ops-agent): legacy-vs-legacy admission bewaakt, met zelfherstellende lease #142

Merged
janpeter merged 2 commits from feat/legacy-admission-lease into main 2026-08-12 08:45:34 +02:00
Owner

startLegacyRun vergeleek een muterende legacy-run alleen tegen v2-leases. De enige legacy-vs-legacy-check was LEGACY_RUN_ID_ACTIVE, gesleuteld op een per-run-uniek run_id, dus die vuurde nooit.

Gemeten matrix (zoals hij WAS)

v2-muterend actief legacy-muterend actief
nieuwe v2-muterende run via acquireLeaseSet bewaakt (LEGACY_MUTATION_ACTIVE)
nieuwe legacy-muterende run bewaakt (V2_MUTATION_ACTIVE) niet bewaakt

Drie van de vier hoeken waren al dicht; de v2-kant deed de check al. De fix is dezelfde check in de andere richting.

Waarom niet kaal acquireLeaseSet

Die is bewust niet zelfherstellend: een verlopen conflict gaat via retainExpiredConflicts naar retained + attention_required, en de acquire faalt alsnog. LEASE_TTL_MS betekent daar escaleer naar operator-reconciliatie, niet geef vrij. Voor v2 klopt dat — een gestrande gefaseerde deploy mag niet stilzwijgend worden overgenomen.

Een legacy-run heeft die staat niet en dus niets te reconcilieren. Kaal hergebruik had de gap ingeruild voor een wedge: een gecrashte deploy blokkeert dan alles tot iemand handmatig een forced release met bewijs doet.

Daarom precies EEN afwijking, alleen op host:<host>:legacy-mutation: een verlopen lease wordt overgenomen in plaats van retained. Store, CAS-lus, LEASE_TTL_MS en renewLeaseSet zijn hetzelfde mechanisme. Release zonder proof om dezelfde reden — ReconciledReleaseProof eist evidence uit een event-log dat een legacy-run niet heeft.

Heartbeat op LEASE_RENEW_INTERVAL_MS (15s): de TTL is 60s terwijl echte deploys 8 minuten duren, dus zonder vernieuwing valt de bescherming halverwege elke deploy weg.

De v2-kant leest nu ook de lease in plaats van de in-memory Map. Die Map had geen vervaltijd, dus een gecrashte legacy-run blokkeerde v2 tot de agent herstartte — die wedge bestond al vóór deze PR.

Classificatie hergebruikt (classifyLegacyFlow / read_only uit de whitelist, fail-closed muterend); geen tweede classificatie.

Integratiepunt: de deploy-trigger

Een 409 levert geen done-event op, viel in de fail-closed tak en kreeg code=-1. Drie gelijktijdigheidsweigeringen zouden de auto-deploy permanent hebben uitgeschakeld voor die sha: normaal gedrag dat de automatisering sloopt. De HTTP-status wordt nu apart gelezen en een 409 telt niet als mislukking. De flock blijft staan.

Bewijzen (op de live agent)

  1. Gap dicht. Twee gelijktijdige muterende execs: 37 ms aangetoonde overlap, B http=200 en draaide, A http=409 {"reason_code":"LEGACY_MUTATION_ACTIVE","conflicting_run_ids":[...]}. Herhaald tijdens een ECHTE redeploy_scrum4us: ook 409.
  2. Geen deadlock. kill -9 op de agent middenin een run; de wees-lease bleef staan. T+30s: 409 — een levende run wordt niet gestolen. T+65s: 200 — overgenomen.
  3. Echte flow werkt. redeploy_scrum4us: 22 stappen, alle exit_code: 0, done-event 0, geen gefaalde stap.
  4. Tests. 7 nieuw; tegen main falen er 4, waaronder beide die de gap asserteren. Trigger 8/8. Bestaande admission/locks/legacy-routes 41 groen. Typecheck schoon.

Bevinding buiten de fix

Alle 101 command-keys zijn muterend; nul read_only. classifyExecCommand is fail-closed en niemand heeft ooit read_only: true gezet — ook niet op git_status of systemctl_status. Deze PR serialiseert daarmee alle exec-calls, niet alleen deploys.

Daardoor is de eis read-only mag niet geblokkeerd worden op deze host niet aan te tonen: er zijn geen read-only commando's. Het gedrag staat wel in de tests, tegen een echte store. Statuscommando's read_only: true geven is een aparte wijziging aan commands.yml en zit bewust niet in deze PR.

🤖 Generated with Claude Code

`startLegacyRun` vergeleek een muterende legacy-run alleen tegen v2-leases. De enige legacy-vs-legacy-check was `LEGACY_RUN_ID_ACTIVE`, gesleuteld op een per-run-uniek `run_id`, dus die vuurde nooit. ## Gemeten matrix (zoals hij WAS) | | v2-muterend actief | legacy-muterend actief | |---|---|---| | nieuwe v2-muterende run | via `acquireLeaseSet` | **bewaakt** (`LEGACY_MUTATION_ACTIVE`) | | nieuwe legacy-muterende run | **bewaakt** (`V2_MUTATION_ACTIVE`) | **niet bewaakt** | Drie van de vier hoeken waren al dicht; de v2-kant deed de check al. De fix is dezelfde check in de andere richting. ## Waarom niet kaal `acquireLeaseSet` Die is bewust niet zelfherstellend: een verlopen conflict gaat via `retainExpiredConflicts` naar `retained` + `attention_required`, en de acquire faalt alsnog. `LEASE_TTL_MS` betekent daar *escaleer naar operator-reconciliatie*, niet *geef vrij*. Voor v2 klopt dat — een gestrande gefaseerde deploy mag niet stilzwijgend worden overgenomen. Een legacy-run heeft die staat niet en dus niets te reconcilieren. Kaal hergebruik had de gap ingeruild voor een **wedge**: een gecrashte deploy blokkeert dan alles tot iemand handmatig een forced release met bewijs doet. Daarom precies EEN afwijking, alleen op `host:<host>:legacy-mutation`: een verlopen lease wordt overgenomen in plaats van retained. Store, CAS-lus, `LEASE_TTL_MS` en `renewLeaseSet` zijn hetzelfde mechanisme. Release zonder proof om dezelfde reden — `ReconciledReleaseProof` eist evidence uit een event-log dat een legacy-run niet heeft. Heartbeat op `LEASE_RENEW_INTERVAL_MS` (15s): de TTL is 60s terwijl echte deploys 8 minuten duren, dus zonder vernieuwing valt de bescherming halverwege elke deploy weg. De v2-kant leest nu ook de lease in plaats van de in-memory Map. Die Map had geen vervaltijd, dus een gecrashte legacy-run blokkeerde v2 tot de agent herstartte — **die wedge bestond al vóór deze PR**. Classificatie hergebruikt (`classifyLegacyFlow` / `read_only` uit de whitelist, fail-closed muterend); geen tweede classificatie. ## Integratiepunt: de deploy-trigger Een 409 levert geen `done`-event op, viel in de fail-closed tak en kreeg `code=-1`. Drie gelijktijdigheidsweigeringen zouden de auto-deploy permanent hebben uitgeschakeld voor die sha: normaal gedrag dat de automatisering sloopt. De HTTP-status wordt nu apart gelezen en een 409 telt niet als mislukking. De `flock` blijft staan. ## Bewijzen (op de live agent) 1. **Gap dicht.** Twee gelijktijdige muterende execs: **37 ms aangetoonde overlap**, B `http=200` en draaide, A `http=409 {"reason_code":"LEGACY_MUTATION_ACTIVE","conflicting_run_ids":[...]}`. Herhaald tijdens een ECHTE `redeploy_scrum4us`: ook 409. 2. **Geen deadlock.** `kill -9` op de agent middenin een run; de wees-lease bleef staan. T+30s: 409 — een levende run wordt niet gestolen. T+65s: **200** — overgenomen. 3. **Echte flow werkt.** `redeploy_scrum4us`: 22 stappen, alle `exit_code: 0`, done-event 0, geen gefaalde stap. 4. **Tests.** 7 nieuw; tegen `main` falen er 4, waaronder beide die de gap asserteren. Trigger 8/8. Bestaande admission/locks/legacy-routes 41 groen. Typecheck schoon. ## Bevinding buiten de fix **Alle 101 command-keys zijn muterend; nul `read_only`.** `classifyExecCommand` is fail-closed en niemand heeft ooit `read_only: true` gezet — ook niet op `git_status` of `systemctl_status`. Deze PR serialiseert daarmee alle exec-calls, niet alleen deploys. Daardoor is de eis *read-only mag niet geblokkeerd worden* op deze host niet aan te tonen: er zijn geen read-only commando's. Het gedrag staat wel in de tests, tegen een echte store. Statuscommando's `read_only: true` geven is een aparte wijziging aan `commands.yml` en zit bewust niet in deze PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
startLegacyRun vergeleek een muterende legacy-run alleen tegen v2-leases. De
enige legacy-vs-legacy-check was LEGACY_RUN_ID_ACTIVE, gesleuteld op een
per-run-uniek run_id, dus die vuurde nooit: twee gelijktijdige deploys werden
allebei toegelaten.

GEMETEN MATRIX. Drie van de vier hoeken waren al bewaakt; de v2-kant leest
legacyRuns al en gooit LEGACY_MUTATION_ACTIVE. De fix is dezelfde check in de
andere richting.

WAAROM NIET KAAL acquireLeaseSet. Die is bewust niet zelfherstellend: een
verlopen conflicterende lease wordt via retainExpiredConflicts op 'retained'
gezet met attention_required, en de acquire faalt alsnog. LEASE_TTL_MS betekent
daar "escaleer na een minuut naar operator-reconciliatie", niet "geef vrij".
Voor v2 klopt dat — een gestrande gefaseerde deploy mag niet stilzwijgend
worden overgenomen. Een legacy-run heeft die staat niet en dus niets te
reconcilieren; zou hij die semantiek erven, dan wedged een gecrashte deploy de
pijplijn tot iemand handmatig een forced release met bewijs doet. Dat is de
gap ingeruild voor iets ergers.

Daarom precies EEN afwijking, alleen op de gereserveerde sleutel
host:<host>:legacy-mutation: een verlopen lease wordt overgenomen in plaats van
retained. Store, CAS-lus, LEASE_TTL_MS en renewLeaseSet zijn hetzelfde
mechanisme. Release zonder proof om dezelfde reden: ReconciledReleaseProof eist
evidence uit een event-log dat een legacy-run niet heeft.

Heartbeat op LEASE_RENEW_INTERVAL_MS (15s), want de TTL is 60s terwijl echte
redeploy_scrum4us-runs 8 minuten duren — zonder vernieuwing valt de
bescherming halverwege elke deploy weg.

De v2-kant leest nu ook de lease in plaats van de in-memory Map. Die Map had
geen vervaltijd, dus een gecrashte legacy-run blokkeerde v2 tot de agent
herstartte. Die wedge bestond al vóór deze wijziging.

Classificatie hergebruikt (classifyLegacyFlow / read_only uit de whitelist,
fail-closed muterend); geen tweede classificatie.

Tests: 7 nieuw, tegen main falen er 4 — waaronder beide die de gap asserteren.
Bestaande control-room-admission/locks/legacy-routes: 41 groen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(deploy-trigger): een 409-admissieweigering telt niet als mislukking
All checks were successful
CI / Root app checks (pull_request) Successful in 5m42s
CI / Ops-agent checks (pull_request) Successful in 15s
CI / Deploy artifact checks (pull_request) Successful in 11s
CI / Docker image build (pull_request) Successful in 1m20s
cbf1d11904
Gemeten integratiepunt. De trigger leest de exit-code uit het done-event van de
SSE-body; een 409 heeft geen done-event, viel dus in de fail-closed tak en
kreeg code=-1. Met legacy-vs-legacy admission erbij kan een tick samenvallen
met een deploy die elders is gestart (UI-knop) — de flock in dit script ziet
die niet. Drie van die weigeringen en de trigger geeft permanent op voor die
sha: normaal gedrag dat de automatisering uitschakelt.

Daarom de HTTP-status apart uitlezen (-o + -w) en op 409 exit 0 zonder de
teller te verhogen. Er is niets mislukt; de volgende tick probeert opnieuw.

Test doet vier ticks, een meer dan MAX_FAILS, en asserteert dat elke tick het
endpoint opnieuw aanroept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blokkerende of error-severity findings gevonden.

Beoordeling

De diff sluit aan op de productstandaard dat legacy v1-mutaties en v2-mutaties wederzijds uitgesloten zijn, terwijl expliciet read-only v1-werk beschikbaar blijft. De nieuwe legacy-mutatielease vult de legacy-vs-legacy admission-gap, gebruikt dezelfde duurzame lease-store/CAS-basis, laat verlopen legacy-leases zelf herstellen en voorkomt dat HTTP 409-admissionweigeringen de deploy-trigger als storing laten meetellen.

De toegevoegde tests dekken de belangrijkste regressierisico's: tweede muterende legacy-run weigeren, vrijgave, read-only parallelisme, verlopen lease-overname, niet-verlopen leaseweigering, v2-vs-legacy interactie en de deploy-trigger 409-route.

# Verdict: APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blokkerende of error-severity findings gevonden. ## Beoordeling De diff sluit aan op de productstandaard dat legacy v1-mutaties en v2-mutaties wederzijds uitgesloten zijn, terwijl expliciet read-only v1-werk beschikbaar blijft. De nieuwe legacy-mutatielease vult de legacy-vs-legacy admission-gap, gebruikt dezelfde duurzame lease-store/CAS-basis, laat verlopen legacy-leases zelf herstellen en voorkomt dat HTTP 409-admissionweigeringen de deploy-trigger als storing laten meetellen. De toegevoegde tests dekken de belangrijkste regressierisico's: tweede muterende legacy-run weigeren, vrijgave, read-only parallelisme, verlopen lease-overname, niet-verlopen leaseweigering, v2-vs-legacy interactie en de deploy-trigger 409-route.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/Ops-dashboard!142
No description provided.