carlos / platform Public
Zero-downtime deploys: rev 18 amendment (signed host retries)
Status: approved by Paul, 2026-10-13. Nothing here is merged, released or rolled.
This file amends 2026-10-07-zero-downtime-deploys-design.md (rev 17), which lives on the design branch (platform-design-zero-downtime-deploys) and is not edited from this branch. It quotes each line it replaces and gives the replacement. After sign-off, one copy step puts the text into the spec and raises the status to rev 18.
Why a revision at all. Rev 14 dropped a separate signed retry object and let carlos promote --retry be the retry signal. That holds for the app-wide retry and is unchanged. It never covered carlos instances retry <host>, the one-host retry, which rev 17 describes only as "clears it for that host only" and which §11 wrongly lists under the re-signed pointer. The built feature is a console-signed grant, bound to one failure book and to one restriction state of that book, and the spec has to say so before the flag goes on.
Built on zdd-flag-readiness-2, on top of the grant's first form on zdd-flag-readiness (d68a1dde, then c12c35c3 for the book identity; the branch is pending merge):
- the restriction generation and the book rules:
7c7c6141,191c12b9 - the host-retry log, the box's read of it, and the console's report check:
6e9e84bd,9f27cdfa,4d8957c1,18bde560 - the console page for a declined box:
6e6ad000,65d4e135
1. Status line (spec line 3)
Replace the opening words of the line, "Status: rev 17, 2026-10-07. Rev 17 adds", with:
> Status: rev 18, 2026-10-12. Rev 18 makes carlos instances retry <host> a > root-signed grant bound to one failure book and one restriction state, and > says where it rides and how a box checks it (§8.4, §11). Rev 17 adds
Replace this sentence (line 3):
> Rev 14 adopts carlos promote --retry from main (9e628024) as the retry > signal, in place of a separate signed retry object. That is a simplification > found while planning.
with:
> Rev 14 adopted carlos promote --retry from main (9e628024) as the > app-wide retry signal, in place of a separate signed retry object. That > stands. Rev 18 adds a signed object back for the one-host retry only, > because a one-host retry cannot ride a channel pointer (§11).
The rest of line 3 ("Accepted by Astra in round 13" and "Awaiting Paul's review") stays, with one sentence added after "Awaiting Paul's review": "Rev 18 has not been through a numbered spec round. The code that builds it was reviewed task by task on zdd-flag-readiness-2, and Astra's whole-branch review of that code is separate."
2. §8.4, the one-host retry (spec lines 704 and 705)
Replace:
> - carlos instances retry <host> clears it for that host only, and > the next attempt runs as a canary.
with:
> - carlos instances retry <host> clears it for that host only, on a > grant the console signs (§11). On a box that arms the scheduler, the next > attempt runs as a canary. A box that declined the overlap flag (for > example with host-claims-on) still honours the grant: the failure hold > is cleared and its next wake starts the build as it does today, with no > canary, because that box runs none. The console offers the retry only for > a host whose box reports that it can take the grant, and refuses it with > a message otherwise (§11).
Lines 706 onward (carlos promote --retry <version> <channel>) are unchanged.
3. §11, the retry sentence (spec lines 836 and 837)
Replace:
> - A retry is a re-signed channel pointer (carlos promote --retry), so one > box can't forge a retry that clears another box's failure records (§8.4).
with:
> - An app-wide retry is a re-signed channel pointer (carlos promote > --retry), so one box can't forge a retry that clears another box's > failure records (§8.4). A one-host retry (carlos instances retry > <host>) is a grant the console signs with the deployment root key. A box > clears a failure hold only on such a grant, checked as described below.
4. §11, new paragraph: the host retry grant
Add after the sentence above:
> The host retry grant. carlos instances retry <host> makes the console > sign a HostRetryGrant and write it to the app's host-retry log. The grant > is {v: 1, account, app, host, build, epoch, book, issued_at}. build is > the build whose restriction the grant lifts, which is not the build the > host should move to. epoch and book are what the host's box last > reported for that restriction (held_epoch and held_book in its status), > so the grant names the exact state the member saw. > > Signature. The envelope is signed with the deployment root key. A scoped > release key is refused at signing: a channel delegate has no say over > another instance's failure hold. A box verifies against the root public key > it already holds for channel pointers, and a box without one drops every > grant. The log is writable by every box, so an unsigned entry is a request > anyone with a box could have written, and it clears nothing. > > Time. A box accepts a grant for 24 hours after issued_at and tolerates > one minute of clock skew into the future. That window is a backstop. What > binds a grant to one failure is the next two items, not the clock. > > Book and generation. A box's failure book has a random identity, and it > keeps one restriction generation per app, a counter that moves on every > change to any restriction of that app (a record made, changed or removed, a > host retried, a hold-ran marker set, renewed, replaced or removed). The box > publishes the generation only once it is on disk, and never hands the same > value out twice. A grant clears a host's restriction only if the box's book > is the one the grant names, the book is saved, the app's generation still > equals the grant's epoch, and the grant's build still restricts the host. > So a grant clears the state the member saw and nothing that changed after > it, not even after an agent restart: a reopened book starts past every > value it ever saved, and a book written by an earlier agent that may have > published unsaved values is given a new identity when it is adopted, so > every grant issued before names the old one. Epochs restart per book, which > is why the grant needs both. A retry spends the grant: it moves the > generation, so the same grant clears nothing a second time. > > The guarantee rests on two assumptions about the book's file that the box > cannot check from inside. Only an agent that follows this rule writes it > (the file is written as schema 3, which an earlier agent refuses to open), > and it is never restored from an older copy. Restoring or cloning an agent > data dir must give the copy a new identity, by dropping the file's book > key before the agent starts with the flag on. The proof sketch and the > assumptions are in the doc comment on FailureBook > (internal/rollout/failures.go). > > Where it rides. Grants live in apps/<account>/<app>/host-retry.json, a > CAS-guarded request log of the same shape as the restart-scope log > (apps/<account>/<app>/restart-scope.json, which carries carlos restart > narrowed to a host or an environment), kept to the last 32 entries. They do not ride > the restart-scope log. A console built before the grant existed > re-marshals that whole log whenever it appends a restart, and in doing so > strips the signature from every pending retry, so the member's retry > silently clears nothing. No released binary reads or writes the host-retry > log, so no older console can rewrite it and no older box ever sees a grant. > The restart-scope log no longer accepts a retry kind or a signature when it > is written. A box still honours a signed grant that an unreleased build left > in the restart-scope log, and checks it the same way. A box reads the > host-retry log on the serial move, at the cost of one extra read per app, > and does not skip the entries it finds at agent start: a grant written > before an agent restart is checked and, if still current, applied, and a > spent or stale one is a no-op under the generation rule. > > What a box drops. An unsigned entry, a forged signature, a grant for > another account, app or host than its entry names, a grant outside its > window, one with no book or a zero epoch, one for another book, and one for > a generation that has moved are each dropped with a log line and clear > nothing. A box that declined the overlap flag and still keeps a failure > book honours grants too, with no canary. > > The console. A console with no signing key answers 503 and writes > nothing. Before it signs, it reads the box's report for the host and > refuses, with a message and a 409 on the API, a box whose report carries no > retry_v (a build older than the host-retry log), no held_book, or an > epoch of zero (a book that owes a save). It offers no Retry button for > those hosts. Nothing is written for a refused retry, so the sequence of box > and console rolls cannot make a grant apply unsafely: a Retry pressed > before the box has updated is refused with a message, never lost silently. > retry_v is additive on the wire, and older readers ignore it.
5. Unchanged
carlos promote --retry stays the app-wide re-signed pointer. The §9.1 list (spec lines 761 and 762: "carlos promote --retry (existing) retries a build that failed (§8.4)." and "carlos instances retry <host> clears one instance's failure hold.") and the line-998 table row ("§8.4: carlos deploy --retry, and host-scoped clearing") still hold as written.
6. Rulings this amendment records
- Paul, 2026-10-11 (plan 6, Q4). Host retries would stay on the unsigned restart-scope log for now, with the trust of
carlos restart, and signing them in the console was owed before the flag goes on anywhere hold mode is used. This amendment is that signing, as built: the console signs, and a box ignores an unsigned retry. - Paul, 2026-10-12 (the crash-epoch hard limit). A stale grant must never clear a later failure, including across an agent crash, so a generation is made durable before it is used and never repeats. The earlier bound (the sequence advanced by 2^20 on every open, c12c35c3) is replaced by that rule (
7c7c6141). The cost accepted: a box whose disk is failing shows a hold with no Retry until a save lands.
7. What Paul is asked to sign off
- The §8.4 and §11 text above, including the declined-box sentence (a declined box clears the hold and runs no canary).
- That the one-host retry is a signed object while the app-wide retry stays a re-signed pointer.
- That the hard rule, and the data-dir copy rule that goes with it (drop the
bookkey), are the supported way to keep the guarantee.
New member-facing strings from the console's retry pages are separate: they are in copy-review/strings.json on the branch and wait for the copy review.