carlos / platform Public

spec: split instance update settings out of zero-downtime deploys (rev 7)

Astra round 6's six findings all concern per-instance settings
(manual/pin/major, directives). They move with those settings to
2026-10-07-instance-update-settings-design.md, which starts from them;
the zero-downtime spec keeps swap, pacing, canary, fallback and swap policy
with today's channel adoption unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 195854116ff0a4cb6edbadd5022eb34ed0babddc parent 34195fb
3 files changed, +369 −243
  • docs/superpowers/specs/2026-10-07-instance-update-settings-design.md +242 −0
  • docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md +70 −243
  • docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R6.md +57 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md b/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
new file mode 100644
index 000000000..d7101ee75
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
@@ -0,0 +1,242 @@
+# Instance update settings: auto-update, channel, major hold, pin
+
+Status: draft, split out of the zero-downtime deploys spec
+(`2026-10-07-zero-downtime-deploys-design.md`) at its rev 6, on
+2026-10-07. **Not ready for review.** Astra's round 6 on that spec
+(`CODEX-FINDINGS-ZERO-DOWNTIME-R6.md`) rejected the directive design below
+with 6 findings, all about these settings. §7 lists them with the answer
+proposed for each. They are the starting point for the next revision.
+
+## 1. Why
+
+Paul's ruling, 2026-10-05: running instances pick up a new release
+immediately "unless they're configured not to". The zero-downtime spec
+covers *how* a new release swaps in. This spec covers *whether and when* an
+instance takes one.
+
+## 2. Decisions taken in brainstorming
+
+These were settled with Paul on 2026-10-06/07.
+
+1. **Instances get Eleven's update model:** auto-update on or off, a
+ channel, a major-version hold and an exact pin. The app implementer sets
+ the defaults and decides how much of that each instance may change.
+2. **Instance settings are changed from the console and CLI, and by the app
+ itself**, within the app's policy. Only the implementer can change the
+ policy.
+3. **The app policy lives in app settings.**
+4. **By default instances may change nothing**, so existing apps behave as
+ they do today.
+
+## 3. Settings and policy
+
+### 3.1 Instance settings
+
+The settings live on the instance's provision record. The console owns the
+record, and boxes read it through reconcile as they do today.
+
+| Setting | Values | Meaning |
+|-----------|----------------------------|---------|
+| `updates` | `auto` / `manual` | With `manual`, nothing installs on its own. The installed build stays put, across sleep and wake, until someone installs or pins. |
+| `channel` | an existing channel | What the instance follows. It already exists per instance (`Record.Channel`). |
+| `major` | empty or an integer ≥ 0 | Follow the channel, but hold a release whose semver tag is absent or has another major. It is only accepted with `stable`, because only stable promotions cut tags (`stableTag`, `internal/release/release.go`). |
+| `pin` | empty or a build version | Be on exactly this build (§5). It must name a build the app has shipped. |
+
+There is no "download automatically". A box fetches a build once and every
+instance on it shares it, so an instance has nothing of its own to
+download.
+
+### 3.2 App policy additions
+
+| Setting | Values | Default | Meaning |
+|--------------------|--------|---------|---------|
+| `instance_control` | a subset of {`updates`, `channel`, `major`, `pin`} | empty | The settings the instance itself may change. |
+| instance defaults | one value per §3.1 setting | `auto`, the app's channel, empty, empty | Used where an instance has never set the value explicitly. |
+
+### 3.3 Who may change what
+
+- **App members** with write access, in the console or CLI, may set any
+ instance setting and the app policy. This uses today's `apiAccountApp`
+ gate.
+- **The instance itself** may change the settings named in
+ `instance_control`, and install by hand if `updates` is named. It does
+ this by calling the console API with an **instance-bound credential**,
+ which is #155's step 6: a token that names one instance, minted by the
+ console and checked by the console. The console can check a credential it
+ minted. It cannot check `CARLOS_ADMIN_TOKEN`, which the box mints
+ (`internal/activator/admintoken.go`), nor a bucket object written under a
+ box prefix, which is only a pseudonym (`internal/edge/storedoor.go`). So
+ neither of those is used for writes.
+- The control socket offers **read only**: `GET /updates` answers from the
+ box's copy of the record (§9.3). In-app writes ship once #155 step 6 has
+ landed. Until then an app can show its update state but not change it.
+- No console principal has "access to this instance but not its app", so
+ this spec doesn't invent one. Tenants without Carloku accounts act through
+ their app.
+- A refused change names the setting, and says the app keeps it for itself.
+
+## 4. Who decides which build an instance installs
+
+**An instance's build is decided by the console, which owns its settings
+and policy, and is verified by root**, by the same signature scheme that
+already protects channel pointers. The box never makes this decision, so a
+sandboxed agent can't steer root, and nothing depends on the agent and
+root agreeing.
+
+### 4.1 Two ways a build is chosen
+
+- **Channel-following instances** have every setting at its default:
+ `auto`, the app's channel, no `major`, no `pin`. They are adopted
+ exactly as today. Root reads the signed channel pointer, applies bake
+ holds (`internal/release/bake.go`) and `FastTrack`, stages the build and
+ repoints. Most instances are like this, and their install path doesn't
+ change.
+- **Directed instances** have any non-default setting, or a pending
+ install by hand. For each of these the console keeps a **signed install
+ directive** on the instance's provision record. Root installs exactly
+ the directive's build and ignores the channel for that host.
+
+### 4.2 The directive
+
+A directive is signed with the key that signs channel pointers. It binds:
+- account, app and host;
+- the host's **incarnation** (`hostclaim`), so a directive for a deleted
+ and recreated host is void;
+- the build;
+- a **revision**, which only ever increases for each host.
+
+When a pin is set, the directive's revision is the **pin revision**.
+
+**What root checks.** Before acting on a directive, root:
+1. verifies the signature, the account, app and host, and the
+ incarnation it sees for the host;
+2. checks that the revision is not lower than its **high-water mark** for
+ that host. The mark is kept root-owned at
+ `/opt/carlos/directives/<host>`, which the agent can't write.
+
+Then it stages the build (download and sha256 check, as `ensureStaged`
+does), repoints, stamps, and raises the mark. Unsigned, mismatched or
+replayed directives are refused and reported in box status. The agent
+copies the record's directive into the registry, as reconcile copies other
+record fields. Because of the signature, the agent can carry the
+directive but can't forge one.
+
+### 4.3 How the console computes a directive
+
+The console recomputes an instance's directive whenever its settings, its
+app's policy, or its channel head change. Promotions already go through
+the console. Take the first rule that applies:
+
+1. **`pin` set:** the pinned build. Setting a pin is itself an install
+ request, so it applies even in `manual`. No bake hold applies.
+2. **`updates = manual`:** the build the instance has installed now, as
+ last reported in box status. This never moves the instance on its own.
+ A new instance with nothing installed gets the channel head.
+3. **`major` set, and the channel head's tag is absent or has another
+ major:** the installed build. The instance is *held at major N*.
+4. **Otherwise:** the channel head, after bake holds computed from the
+ promotion time with `DefaultHolds`, and `FastTrack`.
+
+Box status is only a report. **For rules 2 and 3, the console accepts a
+reported build only if the app's signed history shows it was shipped to
+the instance's channel, or it was pinned for that host.** So a lying box
+can at most choose among builds already authorized for that instance.
+
+**Installing by hand** on a pinned instance is refused, naming the pin.
+Otherwise it issues a directive to the channel head, ignoring `manual`,
+`major` and bake holds. When an instance's settings return to default and
+its directive names the channel head, the console drops the directive, and
+the instance goes back to following the channel.
+
+### 4.4 Which routes this covers
+
+Settings, directives and the scheduler apply to exec routes only.
+Named-unit routes, the console's own route included, keep today's root
+adoption and its privileged restart unchanged (§13). The console refuses
+update settings on a named-unit route, saying they aren't supported there
+yet.
+
+### 4.5 Rollback and moves
+
+**A box rolled back to a binary from before directives** ignores them and
+adopts every host from its channel. So `manual`, pins and major holds stop
+being enforced there. The console compares each directed instance's
+reported installed build with its directive, flags any mismatch on the
+instance and in the roll docs, and enforcement resumes with the next
+forward roll.
+
+**Moves and restores need no state carried with them.** The directive
+lives on the console-owned record, so the destination installs exactly
+what the source was told to install.
+
+**Replay.** On a box that has never applied a directive for a host, an
+older directive for the same incarnation could be replayed by someone
+with write access to the record. It could only install a build that host
+was previously directed to. The high-water mark refuses anything older
+from the first application on, and the console's mismatch check flags
+it.
+
+## 5. Surfaces
+
+- `carlos apps set <app> --instance-control … --default-updates …
+ --default-channel … --default-major …`.
+- `carlos instances set <host> --updates … --channel … --major … --pin …`,
+ where an empty value clears the setting.
+- `carlos instances update <host>` installs now.
+- `carlos instances list` gives the reason an instance is behind: manual,
+ pinned or held at major N.
+- In the console, the instance page gains an Updates section, Eleven-style.
+ Settings the app keeps for itself are shown read-only.
+
+### 5.1 Instance API and control socket
+
+- **Console API:** `GET` and `PUT /api/v1/instances/<host>/updates` and
+ `POST …/updates/install`. They take app-member credentials, or the
+ instance-bound credential for that host (§4.4) limited to
+ `instance_control`. The instance-bound half ships with #155 step 6.
+- **Control socket:** `GET /updates`, mounted beside `schedule.Mount`'s
+ verbs with the same bearer and fixed-host rule. It returns the box's copy
+ of the settings, what the instance may change, and the installed,
+ serving and desired builds with the reason they differ.
+
+## 6. Interaction with zero-downtime deploys
+
+Settings decide which build is installed. The zero-downtime spec's
+scheduler, swap, failure records and fallback then act on that build, and
+need nothing else from this spec. A failure record from that spec holds a
+build whatever these settings say.
+
+## 7. Open: Astra round 6 findings, with proposed answers
+
+1. **The console signs a manual target taken from unauthenticated box
+ status.** *Proposed:* the console never derives a target from status.
+ Turning on `manual` freezes the directive at the console's own record of
+ what it last directed, or the channel's eligible head for a
+ channel-following instance. That target is kept across recomputation,
+ and only an install by hand or a pin changes it.
+2. **Removing a directive brings back channel adoption without a
+ signature.** *Proposed:* leaving directed mode is itself a signed
+ directive of kind `follow-channel`, at the next revision. Root keeps
+ each host's mode next to its high-water mark, and installs nothing for
+ a host that has a mark but no directive at that revision or above.
+3. **A directive replaced while root is staging.** *Proposed:* root
+ re-checks the directive's revision under the host guard just before
+ repointing, and reconcile publishes directives under that same guard.
+ The order is repoint, stamp, then raise the mark, and a pass recovers
+ any step it finds half done.
+4. **Directed instances skip the app's own bake windows.** *Proposed:*
+ `major` and auto-following directives become `follow-channel` with a
+ major ceiling. Root keeps applying the box's pipeline bake as it does
+ today and refuses heads past the ceiling, so the console never computes
+ bake.
+5. **Stale status can undo an install by hand.** *Proposed:* this goes
+ away with answer 1, because status is never an input.
+6. **Recomputation has no complete trigger.** *Proposed:* with answer 4,
+ the console only recomputes when settings change, not when a channel
+ moves or a bake expires. That needs no reconciliation loop.
+
+**The threat model has to be decided before the next round.** Is
+"which authorized build an instance runs" a boundary against its own
+box's agent? That agent already runs the instance and could exec any
+staged build directly. If the answer is no, findings 2 and 3 are about
+correctness, not security, and simpler fixes will do.
diff --git a/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md b/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md
index 43581f887..d88f8fa81 100644
--- a/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md
+++ b/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md
@@ -1,19 +1,18 @@
-# Zero-downtime deploys: overlap swaps, update settings, paced rollout
+# Zero-downtime deploys: overlap swaps and paced rollout
-Status: draft rev 6, 2026-10-07. Discussions #231 (deploy pickup) and #230
+Status: draft rev 7, 2026-10-07. Discussions #231 (deploy pickup) and #230
(per-app restart pace, first-route health step) feed into this. Astra
-rejected rev 1 with 17 findings (`CODEX-FINDINGS-ZERO-DOWNTIME.md`) and
-rev 2 with 12 new ones (`CODEX-FINDINGS-ZERO-DOWNTIME-R2.md`), and
-rev 3 with 7 new ones and 3 partials (`CODEX-FINDINGS-ZERO-DOWNTIME-R3.md`),
-rev 4 with 3 new ones and 2 partials (`CODEX-FINDINGS-ZERO-DOWNTIME-R4.md`),
-and rev 5 with 4 new ones and 1 partial (`CODEX-FINDINGS-ZERO-DOWNTIME-R5.md`).
-
-Rev 6 replaces the box-side machinery that rounds 2–5 kept breaking. That
-was the agent's exact repoint requests, root's install ledger, the
-agent–root handshake and carried build state. In their place the console
-computes **signed per-instance directives** (§5), and fallback never needs
-root (§8.2). Answers in earlier rounds that pointed at the removed
-machinery are superseded by §5 and §8. §16 maps every finding to its answer.
+rejected rev 1 with 17 findings, rev 2 with 12 new ones, rev 3 with 7, rev 4
+with 3 and rev 5 with 4. The findings files are
+`CODEX-FINDINGS-ZERO-DOWNTIME*.md` beside this spec.
+
+Rev 6's 6 findings all concerned per-instance update settings: manual,
+pin, major and in-app control. So rev 7 **splits those out** into
+`2026-10-07-instance-update-settings-design.md`, which starts from them.
+This spec keeps the swap, pacing, canary, fallback and swap policy. Here
+every instance follows its channel exactly as today. §16 maps every
+finding to its answer. Answers about directives and settings now live in
+the settings spec.
## 1. Why
@@ -45,12 +44,10 @@ These were settled with Paul on 2026-10-06/07 and are not reopened here.
it. It means stop-then-start with requests held. It exists for apps or
releases whose migrations can't share the database with the previous
build.
-3. **Instances get Eleven's update model:** auto-update on or off, a
- channel, a major-version hold and an exact pin. The app implementer sets
- the defaults and decides how much of that each instance may change.
-4. **Instance settings are changed from the console and CLI, and by the app
- itself**, within the app's policy. Only the implementer can change the
- policy.
+3. **Instances get Eleven's update model.** That is in the instance update
+ settings spec.
+4. **Instance settings are changed from the console, the CLI and the app.**
+ That is in the settings spec too.
5. **The app policy lives in app settings.** A release can still demand
`hold` (`carlos deploy --swap hold`), and that demand is stored with the
release.
@@ -81,7 +78,7 @@ These were settled with Paul on 2026-10-06/07 and are not reopened here.
connections. It can differ from the installed build while a rollout
queues, checks health, or falls back.
- **Desired build**: the build the instance should serve now. That is its
- installed build, which root put there from its channel or its directive
+ installed build, which root put there from its channel as it does today
(§5), unless a failure record holds that build, in which case it is the
good build (§8.2).
- **Generation**: one spawn of one instance, numbered upwards for the life
@@ -92,36 +89,20 @@ These were settled with Paul on 2026-10-06/07 and are not reopened here.
- **Cohort**: on one box, the instances of one app whose attempts share a
desired build. The canary rule (§6.2) applies per cohort.
-## 4. Settings and policy
+## 4. Swap policy
-### 4.1 Instance settings
-
-The settings live on the instance's provision record. The console owns the
-record, and boxes read it through reconcile as they do today.
-
-| Setting | Values | Meaning |
-|-----------|----------------------------|---------|
-| `updates` | `auto` / `manual` | With `manual`, nothing installs on its own. The installed build stays put, across sleep and wake, until someone installs or pins. |
-| `channel` | an existing channel | What the instance follows. It already exists per instance (`Record.Channel`). |
-| `major` | empty or an integer ≥ 0 | Follow the channel, but hold a release whose semver tag is absent or has another major. It is only accepted with `stable`, because only stable promotions cut tags (`stableTag`, `internal/release/release.go`). |
-| `pin` | empty or a build version | Be on exactly this build (§5). It must name a build the app has shipped. |
-
-There is no "download automatically". A box fetches a build once and every
-instance on it shares it, so an instance has nothing of its own to
-download.
-
-### 4.2 App policy
+### 4.1 App policy
| Setting | Values | Default | Meaning |
|--------------------|--------|---------|---------|
| `swap` | `overlap` / `hold` | `overlap` | How an awake instance moves to a new generation. |
| `swap_concurrency` | integer ≥ 1 | 1 | How many swaps of this app may run at once on one box, after a cohort's canary. |
-| `instance_control` | a subset of {`updates`, `channel`, `major`, `pin`} | empty | The settings the instance itself may change. |
-| instance defaults | one value per §4.1 setting | `auto`, the app's channel, empty, empty | Used where an instance has never set the value explicitly. |
-An empty `instance_control` keeps every existing app as it is today.
+These are app settings, changed by app members with write access in the
+console or CLI (today's `apiAccountApp` gate). They reach boxes with the
+app's config, as other app settings do.
-### 4.3 The release's demand
+### 4.2 The release's demand
`Manifest` gains `swap` (`"hold"` or empty). It is additive to schema 1,
like `Label`, and absent from older manifests. `carlos deploy --swap hold`
@@ -129,128 +110,16 @@ sets it when the release is shipped. An attempt onto build B runs in
`hold` if either the app policy or B's manifest says `hold`. Otherwise it
runs in `overlap`.
-### 4.4 Who may change what
-
-- **App members** with write access, in the console or CLI, may set any
- instance setting and the app policy. This uses today's `apiAccountApp`
- gate.
-- **The instance itself** may change the settings named in
- `instance_control`, and install by hand if `updates` is named. It does
- this by calling the console API with an **instance-bound credential**,
- which is #155's step 6: a token that names one instance, minted by the
- console and checked by the console. The console can check a credential it
- minted. It cannot check `CARLOS_ADMIN_TOKEN`, which the box mints
- (`internal/activator/admintoken.go`), nor a bucket object written under a
- box prefix, which is only a pseudonym (`internal/edge/storedoor.go`). So
- neither of those is used for writes.
-- The control socket offers **read only**: `GET /updates` answers from the
- box's copy of the record (§9.3). In-app writes ship once #155 step 6 has
- landed. Until then an app can show its update state but not change it.
-- No console principal has "access to this instance but not its app", so
- this spec doesn't invent one. Tenants without Carloku accounts act through
- their app.
-- A refused change names the setting, and says the app keeps it for itself.
-
-## 5. Who decides which build an instance installs
-
-**An instance's build is decided by the console, which owns its settings
-and policy, and is verified by root**, by the same signature scheme that
-already protects channel pointers. The box never makes this decision, so a
-sandboxed agent can't steer root, and nothing depends on the agent and
-root agreeing.
-
-### 5.1 Two ways a build is chosen
-
-- **Channel-following instances** have every setting at its default:
- `auto`, the app's channel, no `major`, no `pin`. They are adopted
- exactly as today. Root reads the signed channel pointer, applies bake
- holds (`internal/release/bake.go`) and `FastTrack`, stages the build and
- repoints. Most instances are like this, and their install path doesn't
- change.
-- **Directed instances** have any non-default setting, or a pending
- install by hand. For each of these the console keeps a **signed install
- directive** on the instance's provision record. Root installs exactly
- the directive's build and ignores the channel for that host.
-
-### 5.2 The directive
-
-A directive is signed with the key that signs channel pointers. It binds:
-- account, app and host;
-- the host's **incarnation** (`hostclaim`), so a directive for a deleted
- and recreated host is void;
-- the build;
-- a **revision**, which only ever increases for each host.
-
-When a pin is set, the directive's revision is the **pin revision**.
-
-**What root checks.** Before acting on a directive, root:
-1. verifies the signature, the account, app and host, and the
- incarnation it sees for the host;
-2. checks that the revision is not lower than its **high-water mark** for
- that host. The mark is kept root-owned at
- `/opt/carlos/directives/<host>`, which the agent can't write.
-
-Then it stages the build (download and sha256 check, as `ensureStaged`
-does), repoints, stamps, and raises the mark. Unsigned, mismatched or
-replayed directives are refused and reported in box status. The agent
-copies the record's directive into the registry, as reconcile copies other
-record fields. Because of the signature, the agent can carry the
-directive but can't forge one.
-
-### 5.3 How the console computes a directive
-
-The console recomputes an instance's directive whenever its settings, its
-app's policy, or its channel head change. Promotions already go through
-the console. Take the first rule that applies:
-
-1. **`pin` set:** the pinned build. Setting a pin is itself an install
- request, so it applies even in `manual`. No bake hold applies.
-2. **`updates = manual`:** the build the instance has installed now, as
- last reported in box status. This never moves the instance on its own.
- A new instance with nothing installed gets the channel head.
-3. **`major` set, and the channel head's tag is absent or has another
- major:** the installed build. The instance is *held at major N*.
-4. **Otherwise:** the channel head, after bake holds computed from the
- promotion time with `DefaultHolds`, and `FastTrack`.
-
-Box status is only a report. **For rules 2 and 3, the console accepts a
-reported build only if the app's signed history shows it was shipped to
-the instance's channel, or it was pinned for that host.** So a lying box
-can at most choose among builds already authorized for that instance.
-
-**Installing by hand** on a pinned instance is refused, naming the pin.
-Otherwise it issues a directive to the channel head, ignoring `manual`,
-`major` and bake holds. When an instance's settings return to default and
-its directive names the channel head, the console drops the directive, and
-the instance goes back to following the channel.
-
-### 5.4 Which routes this covers
-
-Settings, directives and the scheduler apply to exec routes only.
+## 5. Which build an instance installs
+
+**Unchanged.** Root adopts every exec route from its channel exactly as it
+does today, with the signed pointer, bake holds and `FastTrack`. It stages,
+repoints and stamps. This spec changes how the installed build is
+**swapped in**, not which build is installed. Per-instance settings that
+change *which* build (manual, pin, major) are in the settings spec.
+
Named-unit routes, the console's own route included, keep today's root
-adoption and its privileged restart unchanged (§13). The console refuses
-update settings on a named-unit route, saying they aren't supported there
-yet.
-
-### 5.5 Rollback and moves
-
-**A box rolled back to a binary from before directives** ignores them and
-adopts every host from its channel. So `manual`, pins and major holds stop
-being enforced there. The console compares each directed instance's
-reported installed build with its directive, flags any mismatch on the
-instance and in the roll docs, and enforcement resumes with the next
-forward roll.
-
-**Moves and restores need no state carried with them.** The directive
-lives on the console-owned record, so the destination installs exactly
-what the source was told to install.
-
-**Replay.** On a box that has never applied a directive for a host, an
-older directive for the same incarnation could be replayed by someone
-with write access to the record. It could only install a build that host
-was previously directed to. The high-water mark refuses anything older
-from the first application on, and the console's mismatch check flags
-it.
+adoption and its privileged restart unchanged (§13).
## 6. Rollout and pacing
@@ -260,7 +129,7 @@ Each box runs one rollout scheduler per (account, app). Each scheduler runs
in its own goroutine, off the agent's tick, so a slow rollout never delays
the tick or another app. The scheduler holds **a desired build per
instance**, never one build per app. Instances of one app on different
-channels or pins form different cohorts and never take each other's
+channels form different cohorts and never take each other's
targets.
It is fed by:
@@ -508,15 +377,14 @@ unit it expects.
### 7.7 Root's part: staging and installing
-Root's job is unchanged in kind. It stages and repoints channel-following
-hosts from the signed pointer, and directed hosts from their verified
-directive (§5). Swaps exec staged builds by path (§7.5 step 3), so they
+Root's job is unchanged. It stages and repoints from the signed pointer
+(§5). Swaps exec staged builds by path (§7.5 step 3), so they
don't depend on when root repoints. A swap waits only for its build to be
staged. Fallback execs the staged good build directly and needs nothing
from root (§8.2). **The agent never asks root for a specific build.** The
stage relay from #231 still triggers root passes, and still carries only a
timestamp. The agent touches it again every `stageRequestEvery` while any
-exec route's channel head or directive isn't installed, so a touch missed
+exec route's channel head isn't installed, so a touch missed
during a running pass is repeated.
### 7.8 A `hold` swap
@@ -657,14 +525,11 @@ status. While it stands, the scheduler makes no attempt onto that build for
any instance of the app on that box, and wakes boot the good build instead
(§8.2). Clearing:
-- **A newer installed build** for an instance, from its channel or its
- directive, no longer matches the record. The record stays for any
+- **A newer installed build** for an instance, from its channel, no longer
+ matches the record. The record stays for any
instance still on the failed build.
-- **`carlos instances update <host>`** clears it for that host only, and
+- **`carlos instances retry <host>`** clears it for that host only, and
the next attempt runs as a canary.
-- **A pinned build** is matched by build and directive revision. Setting the
- pin again bumps the revision and authorizes one more attempt, for that
- instance only.
- **`carlos deploy --retry`** writes an app-wide retry request with its own
id into the release store. It is signed like a channel pointer, so a box
can't forge one for another. Each box clears its record for that build when
@@ -694,13 +559,6 @@ a regression.
attempt: current, successor, retiring and spawning. The activator reports
those.
-**Bucket retention** (`internal/host/retention.go`) adds to its protected
-set every build named by a directive on any provision record in the
-deployment. Moves and restores of directed instances install those builds
-from the bucket. If the housekeeper can't list the records, it prunes
-nothing that pass, just as it declines on any incomplete protected set
-today.
-
### 8.6 Other cases
- **The agent restarts in the middle of a swap.** `Shutdown` cancels and
@@ -718,40 +576,22 @@ strings here describe meaning, not proposed copy.
### 9.1 CLI
-- `carlos apps set <app> --swap overlap|hold --swap-concurrency N
- --instance-control updates,channel,major,pin --default-updates auto|manual
- --default-channel C --default-major N`.
-- `carlos instances set <host> --updates … --channel … --major … --pin …`,
- where an empty value clears the setting.
-- `carlos instances update <host>` installs now (§5) and clears the
- instance's failure hold.
-- `carlos deploy --swap hold` stores the release's demand (§4.3).
+- `carlos apps set <app> --swap overlap|hold --swap-concurrency N`.
+- `carlos deploy --swap hold` stores the release's demand (§4.2).
`carlos deploy --retry` retries a build that failed (§8.4).
+- `carlos instances retry <host>` clears one instance's failure hold.
- `carlos deploy`'s watch follows the schedulers and reports per box: how
many instances have swapped, which failed and why, and which stayed on
their good build. It ends when every box is done or halted, or at its
existing deadline. A box counts as done when every instance's serving
build is its desired build.
-- `carlos instances list` shows installed, serving and desired builds and,
- when they differ, the reason: manual, pinned, held at major N, failed
- build, or rolling out.
+- `carlos instances list` shows the installed and serving builds and, when
+ they differ, the reason: failed build or rolling out.
### 9.2 Console
-App settings gain an Updates section with the §4.2 policy. Each instance
-page gains an Updates section with the §4.1 settings, editable by app
-members, and "Update now" when the instance is behind.
-
-### 9.3 Instance API and control socket
-
-- **Console API:** `GET` and `PUT /api/v1/instances/<host>/updates` and
- `POST …/updates/install`. They take app-member credentials, or the
- instance-bound credential for that host (§4.4) limited to
- `instance_control`. The instance-bound half ships with #155 step 6.
-- **Control socket:** `GET /updates`, mounted beside `schedule.Mount`'s
- verbs with the same bearer and fixed-host rule. It returns the box's copy
- of the settings, what the instance may change, and the installed,
- serving and desired builds with the reason they differ.
+App settings gain the §4.1 swap policy. The instance page shows the
+installed and serving builds, and any failure hold, with a retry action.
### 9.4 Events
@@ -786,18 +626,13 @@ keep timer names stable across the two builds. Otherwise deploy with
## 11. Security
-- The console checks only credentials it minted (§4.4). Bucket objects
- under box prefixes and box-minted admin tokens are never authority for a
- settings write.
-- Root installs only what a signed channel pointer or a signed directive
- names (§5). The agent carries directives but can't forge them, and root
- never runs anything the agent controls. The replay residual is stated in
- §5.5.
-- The console accepts a box-reported installed build into a directive
- only if the app's signed history or a pin authorized it for that
- instance (§5.3).
-- `pin` must name a shipped build of the same app. The console resolves it
- against the app's manifests, never as a path.
+- Root installs only what a signed channel pointer names, as today (§5).
+ The agent never asks root for a specific build, and root never runs
+ anything the agent controls. Fallback execs a staged build the agent
+ could already run (§8.2).
+- `carlos deploy --retry` is signed like a channel pointer, so one box
+ can't forge a retry that clears another box's failure records (§8.4).
+- Swap policy is an app setting, changed only by app members.
## 12. Testing
@@ -817,14 +652,6 @@ keep timer names stable across the two builds. Otherwise deploy with
good build, supersession and re-checks, attempt-identity dedupe, the
restart and config failure paths, and config acknowledgement only after
a switch.
-- **Directive computation (console):** a table test of §5.3 over pin,
- manual, major, bake holds and untagged heads. Reported builds outside
- the instance's signed history are refused. Directives are dropped when
- settings return to default.
-- **Directive verification (root):** bad signatures, the wrong host or
- incarnation, a replay below the high-water mark, and an old root binary
- ignoring directives are each refused or reported. Channel-following
- hosts adopt exactly as today.
- **Containment:** per-generation children, the parent-limit raise and
restore with `limitsOK`, retiring one generation without touching the
other, and parent stop evidence. Include a memory-hog successor that
@@ -839,10 +666,6 @@ keep timer names stable across the two builds. Otherwise deploy with
sleepers moved cold boot the good build directly while the symlink still
names the failed build. A newer build, a retry or an install by hand
lifts the hold.
-- **Moves of directed instances:** a `manual` instance moved to a fresh
- box installs its directive's build, not the channel head.
-- **Retention:** directive builds are protected deployment-wide. An
- unlistable record set prunes nothing.
- **Config activation:** a failed config swap keeps delivered ≠ active,
retries with backoff even though the file is unchanged, and acknowledges
only once the revision is active.
@@ -853,8 +676,9 @@ keep timer names stable across the two builds. Otherwise deploy with
no failure.
- **Cache aliases:** a custom domain, a wildcard and a path-routed request
each miss after the switch.
-- **Bootstrap:** an upgraded box with sleeping instances and an empty
- `GoodVersion` doesn't move them cold until a wake has served.
+- **Bootstrap:** on an upgraded box, awake healthy instances record their
+ good build at agent start. Sleeping ones record it on their first
+ healthy serve, and have no fallback until then.
- **Nested:** the generation template's ID split, the shared slice and data
mounts, and per-generation executable mounts.
- **Fallback:** the first-wake budget, waiting requests served by the good
@@ -884,17 +708,18 @@ take a third kind of attempt for them, and #230 stays open for that spec.
Until that follow-up, named-unit routes, the console's own route included,
keep **today's adoption unchanged**. Root reads their channel, stages,
repoints, restarts the unit with its rollback-on-failed-restart, and
-stamps the result. Nothing in this spec touches that path (§5.4).
+stamps the result. Nothing in this spec touches that path (§5).
## 14. Out of scope
- A fleet-wide canary (one box first).
- Keeping instances up across an agent restart or box roll.
- A configurable health path, or a settle on swaps after the canary.
-- Instance-only console principals (§4.4).
+- Per-instance update settings (manual, pin, major, in-app control). They
+ are in `2026-10-07-instance-update-settings-design.md`.
- The level-triggered stage marker noted on #231. The agent touches the
marker again every `stageRequestEvery` (15 s) while any exec route's
- channel head or directive isn't installed. That narrows the gap without
+ channel head isn't installed. That narrows the gap without
a unit change (§7.7).
## 15. Rollout of this feature
@@ -904,13 +729,7 @@ The box-side swap lands **dormant** behind a box flag,
uses today's stop-then-wake. It is proven on dev and then on
`bab/adopt-probe`.
-Directives aren't behind the flag. An instance with default settings has
-none and is adopted exactly as today. A box whose root binary predates
-directives shows *settings not enforced* for its directed instances (§5.5).
-
-**Order:** boxes first, then the console's settings, policy and
-directives. That way no directive is issued before the boxes can honour
-it.
+**Order:** boxes first, then the console's swap policy settings.
The changelog and the deploy docs tell implementers about the migrations
rule (§9.5) before the flag goes on across the fleet. From then on, every
@@ -991,3 +810,11 @@ exec app swaps in `overlap` unless it has chosen `hold`.
| 2 | A lease doesn't authenticate carried state | Superseded: no carried state; directives are console-signed and bound to host and incarnation (§5.2, §5.5) |
| 3 | No agent means legacy adoption | Superseded, as for R3-3 |
| 4 | Retention can delete a directed build | §8.5: directive builds are protected deployment-wide; an unlistable record set prunes nothing |
+
+### Round 6
+
+All 6 findings concern per-instance update settings: status-derived
+manual targets, leaving directed mode, superseded directives, bake
+windows, and when to recompute. They moved with those settings to
+`2026-10-07-instance-update-settings-design.md` §7, with a proposed
+answer for each.
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R6.md b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R6.md
new file mode 100644
index 000000000..ee76bb4d6
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R6.md
@@ -0,0 +1,57 @@
+REJECT
+
+R5 carried partial (R3-3): CLOSED — Root’s mode no longer depends on the agent’s session.
+R5-1: CLOSED — The agent-executable handshake is removed.
+R5-2: PARTIAL — Carried state is removed, but §5.3 still converts unauthenticated installation reports into signed authority; finding 1.
+R5-3: CLOSED — Agent absence no longer selects legacy adoption.
+R5-4: CLOSED — Directive builds receive deployment-wide retention protection.
+
+Earlier closures: root protection of manual/pinned hosts and request supersession reopen through findings 2–3. The `GoodVersion` separation, directed-instance move target, and stage-marker retransmission remain specified.
+
+1. **HIGH — §§5.3, 11: The console can sign an installation claim submitted by a box that does not host the instance.**
+
+ **Code:** [internal/status/snapshot.go:786](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/status/snapshot.go:786) writes unsigned status JSON under a caller-selected label. [infra/modules/region-host/main.tf:166](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/infra/modules/region-host/main.tf:166) grants hosts writes throughout the deployment bucket. [internal/edge/storedoor.go:495](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/edge/storedoor.go:495) explicitly documents that a box-prefix label is not authenticated.
+
+ **Failure:** H is manual on A, hosted by Y. Compromised box X replaces Y’s status with a report that H has installed B, where B appears in H’s channel history. On the next directive recomputation, the history check passes and the console signs B for H. Y then installs B without an owner request. The release signature proves B’s channel authorization, not that Y reported it or H installed it.
+
+ **Smallest fix:** Preserve an existing manual/major-held selection in console-owned state. Any report used to establish that selection must authenticate the reporting box and bind it to H’s current incarnation and placement; a status prefix and channel history are insufficient.
+
+2. **HIGH — §§5.1–5.2, 5.3: Removing a directive can restore channel adoption without signed authorization.**
+
+ **Code:** [internal/host/registryowner.go:18](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/registryowner.go:18) establishes that root and the agent share the agent-writable registry. [internal/release/adopt.go:943](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:943) selects the channel head for ordinary adoption.
+
+ **Failure:** H has an applied directive pinning A while its channel names B. A compromised agent removes the copied directive and represents the settings as defaults. Root takes §5.1’s unchanged channel-following path and installs B. The high-water mark is checked only when a directive is presented; it does not authenticate the transition out of directed mode. No old directive is replayed, so §5.5’s stated residual does not cover this.
+
+ **Smallest fix:** Make leaving directed mode an explicit signed revision. Root must retain that mode alongside its high-water mark and refuse a missing directive until it verifies a signed return-to-channel instruction.
+
+3. **HIGH — §§5.2, 6.3, 7.7: Root can commit a directive superseded during staging.**
+
+ **Code:** [internal/release/adopt.go:978](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:978) stages before repointing at line 1072 and stamping at line 1258. [internal/release/versions.go:267](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/versions.go:267) replaces the symlink without a directive-revision condition.
+
+ **Failure:** Root has already applied revision 10. It verifies revision 11 for B and starts downloading. Reconcile publishes revision 12 for C before that download finishes. Revision 11 still exceeds root’s high-water mark, and §5.2 instructs root to install it without rereading the directive. A sleeper can wake onto superseded B before the next pass installs C. The swap’s newest-attempt check does not protect this cold installation.
+
+ **Smallest fix:** Revalidate the directive revision under the host guard immediately before repointing. Reconcile must publish replacements under the same guard. Define recoverable ordering for repointing, stamping and advancing the high-water mark.
+
+4. **HIGH — §5.3 rule 4: Directed instances bypass the app’s configured bake window.**
+
+ **Code:** [internal/release/adopt.go:495](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:495) resolves app-specific pipeline windows through ratchet memory. [internal/release/bake.go:38](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/bake.go:38) instead defines fixed defaults, including 72 hours for stable.
+
+ **Failure:** An app’s stable channel requires seven days of baking. Its current head B has the permitted major and was promoted four days ago. Setting `major=1` makes H directed; rule 4 uses `DefaultHolds`, signs B immediately, and root ignores the channel’s bake policy. Merely adding a major restriction therefore waives an existing deployment protection.
+
+ **Smallest fix:** Resolve automatic directives against the app’s actual pipeline and applicable bake protections. Keep bake waivers explicit for pin, manual installation and `FastTrack`; changing to directed mode must not introduce another waiver.
+
+5. **MEDIUM — §5.3 rule 2: Stale status can undo a completed manual installation.**
+
+ **Code:** [internal/status/fleet.go:111](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/status/fleet.go:111) publishes box status every five minutes plus jitter. [internal/registry/registry.go:1720](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/registry/registry.go:1720) stamps installation locally, independently of that report.
+
+ **Failure:** Manual H reports A. The owner installs B, and root completes the installation before the next status publication. An app-policy edit or another channel promotion triggers recomputation during that interval. Rule 2 selects reported A and issues a newer directive restoring A. Both builds pass the signed-history check. The same sequence can replace the B directive before B is installed.
+
+ **Smallest fix:** Once issued, retain the console-selected manual target across recomputation. Track installation acknowledgement against its directive revision; an older status snapshot must not replace a newer installation request.
+
+6. **MEDIUM — §5.3: Directive recomputation lacks a complete reconciliation trigger.**
+
+ **Code:** [internal/release/bake.go:379](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/bake.go:379) changes eligibility as time passes without a pointer change. [cli.go:2493](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/cli.go:2493) also provides a direct-store promotion path, contradicting the premise that promotions already pass through the console.
+
+ **Failure:** A directed auto-update instance retains A when B is promoted inside its bake window. When the window expires, none of the specified recomputation events occurs. Root continues installing exactly A indefinitely. A direct-store promotion creates another missed-event path if recomputation is attached to console promotion handlers.
+
+ **Smallest fix:** Specify a persistent console reconciliation loop that detects channel changes and schedules recomputation at bake expiry, including recovery after console downtime.
\ No newline at end of file