carlos / platform Public

cmd/moveprobe: list every HTTP body in the README for copy review

Per the ruling, moveprobe's bodies stay out of the moves copy record; the
README's new Strings section lists each one with when it appears, and goes
through Paul's copy review with the rest of the README. The README also
says that /ready covers the worker's writes and that /report never answers
a partial 200. The one body that was a bare error now carries the
"control socket:" prefix like its neighbours. Review fix round 1, item 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 066e838870e1a4c6319062769a0e5f2724a684f8 parent 5fc8c50
2 files changed, +78 −3
  • cmd/moveprobe/README.md +77 −2
  • cmd/moveprobe/main.go +1 −1
diff --git a/cmd/moveprobe/README.md b/cmd/moveprobe/README.md
index 927beaa65..8eaed9b22 100644
--- a/cmd/moveprobe/README.md
+++ b/cmd/moveprobe/README.md
@@ -65,8 +65,8 @@ curl -H "Authorization: Bearer $MOVEPROBE_SECRET" https://mp1.example.com/report
| path | who can call it | what it does |
|---|---|---|
-| `/` and `/ready` | anyone | 200 when the key file, the platform key, the stored object and the secret all check out. Otherwise 500 with every failed check. A move probes `/`. |
-| `GET /report` | `MOVEPROBE_SECRET` | JSON: row counts by kind and region with their lowest and highest sequence numbers, the timer fires by name and due time, both key fingerprints, the stored object, whether the secret is set, the worker's pid, and the region the app saw. |
+| `/` and `/ready` | anyone | 200 when the key file, the platform key, the stored object and the secret all check out, and the worker's writes are landing. Otherwise 500 with every failed check. A move probes `/`. |
+| `GET /report` | `MOVEPROBE_SECRET` | JSON: row counts by kind and region with their lowest and highest sequence numbers, the timer fires by name and due time, both key fingerprints, the stored object, whether the secret is set, the worker's pid and failed writes, and the region the app saw. Any failed database read makes it a 500, never a partial report. |
| `POST /arm-once?in=<seconds>` | `MOVEPROBE_SECRET` | Arms a once-timer named `once-<n>` that calls `/once` after that many seconds. Answers 503 when the instance has no control socket. |
| `POST /tick`, `POST /once` | the platform | Timer fires. Each one writes a row with the timer's name and due time. |
@@ -108,3 +108,78 @@ the app's output. Stopping the app does not stop the worker. That is the
point: it shows whether a move's stop really stops every process the
instance started. Once the app has gone, the worker keeps writing for 60
seconds and then exits by itself.
+
+The worker tells the app whether each write landed. `/report` shows how many
+failed, how many of the latest failed in a row, and the last error. After
+five failures in a row, `/ready` fails until a write lands again.
+
+## Strings
+
+Every body the app can send, and when. Text in angle brackets is filled in
+when the app answers. `/ready` joins its failed checks with `; `.
+
+On `/` and `/ready`:
+
+- `ready`: every check passed (200).
+- `before-write mode: this box writes nothing`: the failure switch names
+ this region with no suffix. Every path gives this answer (503).
+- `after-write mode: not ready here`: the failure switch names this region
+ with `,after-write` (503).
+- `key mismatch: instance.key exists but the database has no key`: the
+ files directory has a key file, but the database never recorded one
+ (500).
+- `key mismatch: instance.key is missing`: the database has a key, but the
+ file is gone (500).
+- `key mismatch: instance.key differs from the database`: the file and the
+ database hold different keys (500).
+- `key mismatch: instance.key: <error>`: the file is there but cannot be
+ read (500).
+- `CARLOS_INSTANCE_KEY is not set`: the platform key is missing (500).
+- `CARLOS_INSTANCE_KEY differs from the one seen at first boot`: the
+ platform key changed (500).
+- `MOVEPROBE_SECRET is not set`: the sealed secret is missing (500).
+- `store: <names> not set`: one or more of the store variables is missing
+ (500).
+- `store: put <key>: <error>`: the boot's object could not be written
+ (500).
+- `store: get <key>: <error>`: the boot's object could not be read back
+ (500).
+- `store: <key> reads back different bytes`: the object came back changed
+ (500).
+- `worker: <n> writes failed in a row: <error>`: the worker is alive but
+ its last five or more writes failed (500).
+
+On `/report`:
+
+- The JSON report (200).
+- `report: <error>`: a database read failed (500).
+
+On `/arm-once`:
+
+- `{"name": "once-<n>", "at": "<time>", "due_at": <unix seconds>}`: the
+ timer is armed (200).
+- `in: want whole seconds, at most 400 days`: `in` is missing or out of
+ range (400).
+- `no control socket: CARLOS_CONTROL_SOCKET is not set`: the instance has
+ no control socket (503).
+- `CARLOS_ADMIN_TOKEN is not set`: the instance has no admin token, so it
+ cannot arm timers (503).
+- `database: <error>`: the timer number could not be taken (500).
+- `control socket: <error>`: the request to the control socket could not be
+ made or sent (500 or 502).
+- `control socket: <status> <reply>`: the control socket refused the timer
+ (502).
+
+On `/tick` and `/once`:
+
+- `ok`: the fire is recorded (200).
+- `want X-Carlos-Schedule and X-Carlos-Schedule-At`: a fire without the
+ timer's name or due time (400).
+- `database: <error>`: the row could not be written (500).
+
+On `/report`, `/arm-once`, `/tick` and `/once`:
+
+- `unauthorized`: the bearer is missing or wrong (401).
+
+Anywhere else, Go's own answers: `404 page not found` for an unknown path,
+and `Method Not Allowed` for a known path with the wrong method (405).
diff --git a/cmd/moveprobe/main.go b/cmd/moveprobe/main.go
index c65d84f21..262301f24 100644
--- a/cmd/moveprobe/main.go
+++ b/cmd/moveprobe/main.go
@@ -782,7 +782,7 @@ func (a *app) handleArmOnce(w http.ResponseWriter, r *http.Request) {
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://carlos/schedule/"+name, bytes.NewReader(body))
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, "control socket: "+err.Error(), http.StatusInternalServerError)
return
}
req.Header.Set("Authorization", "Bearer "+token)