carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Zero-downtime deploys: rev 18 amendment (signed host retries)
2
3Status: approved by Paul, 2026-10-13. Nothing here is merged,
4released or rolled.
5
6This file amends `2026-10-07-zero-downtime-deploys-design.md` (rev 17), which
7lives on the design branch (`platform-design-zero-downtime-deploys`) and is
8not edited from this branch. It quotes each line it replaces and gives the
9replacement. After sign-off, one copy step puts the text into the spec and
10raises the status to rev 18.
11
12Why a revision at all. Rev 14 dropped a separate signed retry object and let
13`carlos promote --retry` be the retry signal. That holds for the app-wide
14retry and is unchanged. It never covered `carlos instances retry <host>`, the
15one-host retry, which rev 17 describes only as "clears it for that host only"
16and which §11 wrongly lists under the re-signed pointer. The built feature
17is a console-signed grant, bound to one failure book and to one restriction
18state of that book, and the spec has to say so before the flag goes on.
19
20Built on `zdd-flag-readiness-2`, on top of the grant's first form on
21`zdd-flag-readiness` (`d68a1dde`, then `c12c35c3` for the book identity;
22the branch is pending merge):
23
24- the restriction generation and the book rules: `7c7c6141`, `191c12b9`
25- the host-retry log, the box's read of it, and the console's report check:
26 `6e9e84bd`, `9f27cdfa`, `4d8957c1`, `18bde560`
27- the console page for a declined box: `6e6ad000`, `65d4e135`
28
29## 1. Status line (spec line 3)
30
31Replace the opening words of the line, "Status: rev 17, 2026-10-07. Rev 17
32adds", with:
33
34> Status: rev 18, 2026-10-12. Rev 18 makes `carlos instances retry <host>` a
35> root-signed grant bound to one failure book and one restriction state, and
36> says where it rides and how a box checks it (§8.4, §11). Rev 17 adds
37
38Replace this sentence (line 3):
39
40> Rev 14 adopts `carlos promote --retry` from `main` (9e628024) as the retry
41> signal, in place of a separate signed retry object. That is a simplification
42> found while planning.
43
44with:
45
46> Rev 14 adopted `carlos promote --retry` from `main` (9e628024) as the
47> app-wide retry signal, in place of a separate signed retry object. That
48> stands. Rev 18 adds a signed object back for the one-host retry only,
49> because a one-host retry cannot ride a channel pointer (§11).
50
51The rest of line 3 ("**Accepted by Astra in round 13**" and "Awaiting Paul's
52review") stays, with one sentence added after "Awaiting Paul's review":
53"Rev 18 has not been through a numbered spec round. The code that builds it
54was reviewed task by task on `zdd-flag-readiness-2`, and Astra's whole-branch
55review of that code is separate."
56
57## 2. §8.4, the one-host retry (spec lines 704 and 705)
58
59Replace:
60
61> - **`carlos instances retry <host>`** clears it for that host only, and
62> the next attempt runs as a canary.
63
64with:
65
66> - **`carlos instances retry <host>`** clears it for that host only, on a
67> grant the console signs (§11). On a box that arms the scheduler, the next
68> attempt runs as a canary. A box that declined the overlap flag (for
69> example with `host-claims-on`) still honours the grant: the failure hold
70> is cleared and its next wake starts the build as it does today, with no
71> canary, because that box runs none. The console offers the retry only for
72> a host whose box reports that it can take the grant, and refuses it with
73> a message otherwise (§11).
74
75Lines 706 onward (`carlos promote --retry <version> <channel>`) are
76unchanged.
77
78## 3. §11, the retry sentence (spec lines 836 and 837)
79
80Replace:
81
82> - A retry is a re-signed channel pointer (`carlos promote --retry`), so one
83> box can't forge a retry that clears another box's failure records (§8.4).
84
85with:
86
87> - An app-wide retry is a re-signed channel pointer (`carlos promote
88> --retry`), so one box can't forge a retry that clears another box's
89> failure records (§8.4). A one-host retry (`carlos instances retry
90> <host>`) is a grant the console signs with the deployment root key. A box
91> clears a failure hold only on such a grant, checked as described below.
92
93## 4. §11, new paragraph: the host retry grant
94
95Add after the sentence above:
96
97> **The host retry grant.** `carlos instances retry <host>` makes the console
98> sign a `HostRetryGrant` and write it to the app's host-retry log. The grant
99> is `{v: 1, account, app, host, build, epoch, book, issued_at}`. `build` is
100> the build whose restriction the grant lifts, which is not the build the
101> host should move to. `epoch` and `book` are what the host's box last
102> reported for that restriction (`held_epoch` and `held_book` in its status),
103> so the grant names the exact state the member saw.
104>
105> *Signature.* The envelope is signed with the deployment root key. A scoped
106> release key is refused at signing: a channel delegate has no say over
107> another instance's failure hold. A box verifies against the root public key
108> it already holds for channel pointers, and a box without one drops every
109> grant. The log is writable by every box, so an unsigned entry is a request
110> anyone with a box could have written, and it clears nothing.
111>
112> *Time.* A box accepts a grant for 24 hours after `issued_at` and tolerates
113> one minute of clock skew into the future. That window is a backstop. What
114> binds a grant to one failure is the next two items, not the clock.
115>
116> *Book and generation.* A box's failure book has a random identity, and it
117> keeps one restriction generation per app, a counter that moves on every
118> change to any restriction of that app (a record made, changed or removed, a
119> host retried, a hold-ran marker set, renewed, replaced or removed). The box
120> publishes the generation only once it is on disk, and never hands the same
121> value out twice. A grant clears a host's restriction only if the box's book
122> is the one the grant names, the book is saved, the app's generation still
123> equals the grant's epoch, and the grant's build still restricts the host.
124> So a grant clears the state the member saw and nothing that changed after
125> it, not even after an agent restart: a reopened book starts past every
126> value it ever saved, and a book written by an earlier agent that may have
127> published unsaved values is given a new identity when it is adopted, so
128> every grant issued before names the old one. Epochs restart per book, which
129> is why the grant needs both. A retry spends the grant: it moves the
130> generation, so the same grant clears nothing a second time.
131>
132> The guarantee rests on two assumptions about the book's file that the box
133> cannot check from inside. Only an agent that follows this rule writes it
134> (the file is written as schema 3, which an earlier agent refuses to open),
135> and it is never restored from an older copy. Restoring or cloning an agent
136> data dir must give the copy a new identity, by dropping the file's `book`
137> key before the agent starts with the flag on. The proof sketch and the
138> assumptions are in the doc comment on `FailureBook`
139> (`internal/rollout/failures.go`).
140>
141> *Where it rides.* Grants live in `apps/<account>/<app>/host-retry.json`, a
142> CAS-guarded request log of the same shape as the restart-scope log
143> (`apps/<account>/<app>/restart-scope.json`, which carries `carlos restart`
144> narrowed to a host or an environment), kept to the last 32 entries. They do not ride
145> the restart-scope log. A console built before the grant existed
146> re-marshals that whole log whenever it appends a restart, and in doing so
147> strips the signature from every pending retry, so the member's retry
148> silently clears nothing. No released binary reads or writes the host-retry
149> log, so no older console can rewrite it and no older box ever sees a grant.
150> The restart-scope log no longer accepts a retry kind or a signature when it
151> is written. A box still honours a signed grant that an unreleased build left
152> in the restart-scope log, and checks it the same way. A box reads the
153> host-retry log on the serial move, at the cost of one extra read per app,
154> and does not skip the entries it finds at agent start: a grant written
155> before an agent restart is checked and, if still current, applied, and a
156> spent or stale one is a no-op under the generation rule.
157>
158> *What a box drops.* An unsigned entry, a forged signature, a grant for
159> another account, app or host than its entry names, a grant outside its
160> window, one with no book or a zero epoch, one for another book, and one for
161> a generation that has moved are each dropped with a log line and clear
162> nothing. A box that declined the overlap flag and still keeps a failure
163> book honours grants too, with no canary.
164>
165> *The console.* A console with no signing key answers 503 and writes
166> nothing. Before it signs, it reads the box's report for the host and
167> refuses, with a message and a 409 on the API, a box whose report carries no
168> `retry_v` (a build older than the host-retry log), no `held_book`, or an
169> epoch of zero (a book that owes a save). It offers no Retry button for
170> those hosts. Nothing is written for a refused retry, so the sequence of box
171> and console rolls cannot make a grant apply unsafely: a Retry pressed
172> before the box has updated is refused with a message, never lost silently.
173> `retry_v` is additive on the wire, and older readers ignore it.
174
175## 5. Unchanged
176
177`carlos promote --retry` stays the app-wide re-signed pointer. The §9.1 list
178(spec lines 761 and 762: "`carlos promote --retry` (existing) retries a build
179that failed (§8.4)." and "`carlos instances retry <host>` clears one
180instance's failure hold.") and the line-998 table row ("§8.4: `carlos deploy
181--retry`, and host-scoped clearing") still hold as written.
182
183## 6. Rulings this amendment records
184
185- **Paul, 2026-10-11 (plan 6, Q4).** Host retries would stay on the unsigned
186 restart-scope log for now, with the trust of `carlos restart`, and signing
187 them in the console was owed before the flag goes on anywhere hold mode is
188 used. This amendment is that signing, as built: the console signs, and a
189 box ignores an unsigned retry.
190- **Paul, 2026-10-12 (the crash-epoch hard limit).** A stale grant must never
191 clear a later failure, including across an agent crash, so a generation is
192 made durable before it is used and never repeats. The earlier bound (the
193 sequence advanced by 2^20 on every open, c12c35c3) is replaced by that rule
194 (`7c7c6141`). The cost accepted: a box whose disk is failing shows a hold
195 with no Retry until a save lands.
196
197## 7. What Paul is asked to sign off
198
1991. The §8.4 and §11 text above, including the declined-box sentence (a
200 declined box clears the hold and runs no canary).
2012. That the one-host retry is a signed object while the app-wide retry stays
202 a re-signed pointer.
2033. That the hard rule, and the data-dir copy rule that goes with it (drop the
204 `book` key), are the supported way to keep the guarantee.
205
206New member-facing strings from the console's retry pages are separate: they
207are in `copy-review/strings.json` on the branch and wait for the copy
208review.
209