carlos / platform Public

settings-1 task 13: docs for resign and digests; the writers-before-readers roll note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev bb343ec3a7ddf0d95344fda6e0f3f15054606dc8 parent 461b82b
3 files changed, +26 −0
  • README.md +3 −0
  • docs/site/cli.md +18 −0
  • internal/console/apidigests.go +5 −0
diff --git a/README.md b/README.md
index eee29768b..28ead67b6 100644
--- a/README.md
+++ b/README.md
@@ -278,6 +278,9 @@ A release-specific note goes here when a release changes something an
operator needs to check or act on before rolling it. Read the note for
the release you are about to roll.
+<!-- TODO-COPY digests.roll-note -->
+**Pointer digests (#253): writers before readers.** Once a box has verified a channel pointer that carries manifest digests, it refuses pointers for that channel without them. So digests are turned on in order. First upgrade every writer: every console, and every CLI or deploy script that promotes, rolls back or fast-tracks. Then run `carlos channels digests on`. Then watch `carlos channels digests`: a channel marked as written by an old writer means one is still in use. Rolling a writer back below this release stops it writing pointers boxes accept; `carlos channels digests off` suspends digests while it is fixed, but does not clear the boxes that have already seen them. When `carlos channels digests` lists nothing, set `CARLOS_REQUIRE_POINTER_DIGEST=on` in every box's host.env, so a new box refuses old pointers too.
+
**v0.25.0: instances move to the nested layout on their own.** This is
the first release on which an isolated (nested) instance wakes, and nested
is the default. On each box, migration starts unattended on the first
diff --git a/docs/site/cli.md b/docs/site/cli.md
index 8e1963123..4aaa6a086 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -511,6 +511,24 @@ release earned one at stable.
carlos channels --app hello
```
+<!-- TODO-COPY digests.cli-heading-resign -->
+### carlos channels resign
+
+Re-signs a channel's current pointer with the sha256 of each build's manifest, so boxes that insist on digests accept it. It shows every digest first and signs only when you type yes. Use it on a channel nobody has promoted to since digests were turned on. It refuses until `carlos channels digests on` has been run for the account.
+
+```sh
+carlos channels resign --app hello stable
+```
+
+<!-- TODO-COPY digests.cli-heading-digests -->
+### carlos channels digests
+
+Shows whether channel pointers carry manifest digests, and lists the channels that still do not. on and off are for platform operators: on tells upgraded writers to sign digests, and off stops them while a writer is being fixed. Turn it on only after every console, CLI and deploy script that promotes has been upgraded.
+
+```sh
+carlos channels digests
+```
+
### carlos pipeline
Shows an app's release channels and the rules for moving a version through
diff --git a/internal/console/apidigests.go b/internal/console/apidigests.go
index dd356808f..c1c5ec307 100644
--- a/internal/console/apidigests.go
+++ b/internal/console/apidigests.go
@@ -44,6 +44,7 @@ func (s *Server) handleAPIChannelResign(w http.ResponseWriter, r *http.Request)
return
}
if len(s.cfg.Key) == 0 {
+ // TODO-COPY digests.console-no-key
jsonError(w, http.StatusServiceUnavailable, "this console has no release key")
return
}
@@ -72,9 +73,11 @@ func (s *Server) handleAPIChannelResign(w http.ResponseWriter, r *http.Request)
return
}
log.Printf("console: api resign %s/%s %s: %v", acct.Sqid, app, req.Channel, err)
+ // TODO-COPY digests.console-pointer-write
jsonError(w, http.StatusBadGateway, "could not write the pointer")
return
}
+ // TODO-COPY digests.activity-resigned
s.auditActivity(acct, app, addr, Activity{
Action: "resign", TargetType: "channel", Target: req.Channel,
Detail: "re-signed " + req.Channel + " at " + plan.Version + " with manifest digests (cli)",
@@ -116,6 +119,7 @@ func (s *Server) handleAPISystemPointerDigests(w http.ResponseWriter, r *http.Re
return
}
if len(s.cfg.Key) == 0 {
+ // TODO-COPY digests.console-no-key
jsonError(w, http.StatusServiceUnavailable, "this console has no release key")
return
}
@@ -137,6 +141,7 @@ func (s *Server) handleAPISystemPointerDigests(w http.ResponseWriter, r *http.Re
}
if err != nil {
log.Printf("console: pointer digests %s: %v", a.Sqid, err)
+ // TODO-COPY digests.console-marker-write
jsonError(w, http.StatusBadGateway, "could not write the marker for "+a.Sqid)
return
}