carlos / platform Public

plan: tasks 14-17 are in, and what is still waiting on a person

Ticks what landed and, more usefully, records the three pieces that
cannot land without approved copy — including the tenant-facing page the
final review argues should ship WITH the merge rather than after it,
because the enqueue door accepts callback_path the moment this merges and
the console will POST signed bodies to apps with no documented way to
verify them.

Also records what is known-and-unfixed here rather than leaving it in a
review transcript nobody will find: no `at` on a tag verdict and no
reason on `skipped` (the Status seam carries neither), a size-ceiling
`skipped` metered as bytes, and the long-poll checking consent once for
the length of its wait.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev f64485d5f8f745475205cb7764df10af83d28752 parent bf52043
1 file changed, +59 −27
  • docs/superpowers/plans/2026-09-02-scan-on-demand.md +59 −27

CI passed — run details

diff --git a/docs/superpowers/plans/2026-09-02-scan-on-demand.md b/docs/superpowers/plans/2026-09-02-scan-on-demand.md
index a883f2976..8e8f61eb5 100644
--- a/docs/superpowers/plans/2026-09-02-scan-on-demand.md
+++ b/docs/superpowers/plans/2026-09-02-scan-on-demand.md
@@ -512,19 +512,19 @@ of it was written. The additions below are all its findings.
**Files:** `internal/console/scansign.go` (new — signing is its own file, not
more of the path validator) + test.
-- [ ] `TestSignatureRejectsTamperedBody`, `TestSignatureIsPerApp` (app A's does
+- [x] `TestSignatureRejectsTamperedBody`, `TestSignatureIsPerApp` (app A's does
not verify for app B), `TestSignatureRejectsStaleTimestamp`,
`TestTwoMasterRotationVerifies`.
-- [ ] **`VerifyCallback(key, header, body, now)` — the reference verifier**, and
+- [x] **`VerifyCallback(key, header, body, now)` — the reference verifier**, and
it is the deliverable §11.3 was missing. "A stale timestamp is refused" and
"both masters verify" are properties of the APP's verifier, which is tenant
code; without a reference implementation the tests test nothing anybody runs.
The docs reproduce it line for line.
-- [ ] `TestSignatureIsOverRawBodyBytes` — an app that re-serialises the JSON
+- [x] `TestSignatureIsOverRawBodyBytes` — an app that re-serialises the JSON
before verifying gets a different byte string and fails. Said in the docs.
-- [ ] `TestVerifierToleratesTwoSignaturesAndUnknownKeyIds` — the rotation
+- [x] `TestVerifierToleratesTwoSignaturesAndUnknownKeyIds` — the rotation
window depends on it.
-- [ ] `TestPreviousEqualToCurrentIsRefused` — a rotation that rotated to
+- [x] `TestPreviousEqualToCurrentIsRefused` — a rotation that rotated to
nothing, refused exactly as `NewTagSigner` refuses it.
Per-app key `HMAC(master, "carlos-scan-webhook-v1" + 0x00 + sqid + "/" + app +
@@ -547,12 +547,12 @@ label and the per-app `gen` are review 7's; so is the key id's distinct label.
`internal/console/apiscan.go` (`regrantForScanning`, `apiscan.go:492`, is the
hook), `apisystem.go`, root `system.go`, `authcli.go`.
-- [ ] `TestGrantDeliversWebhookKey` — sealed via `appconfig.Apply`
+- [x] `TestGrantDeliversWebhookKey` — sealed via `appconfig.Apply`
(`generation.go:119`), gated by `storeSealKey` (`apistore.go:425`). **Grant
stamps `webhook_key_id` as well as rotation does**, or the first
`rotate-webhook-key` after any grant re-seals and restarts every app under an
unchanged master.
-- [ ] `TestRotateWebhookKeySkipsUnchangedApps` — **not "re-seals identical
+- [x] `TestRotateWebhookKeySkipsUnchangedApps` — **not "re-seals identical
plaintext".** `appconfig.Seal` uses a fresh ephemeral key and nonce on every
call (`internal/appconfig/crypto.go:85-87`), so the ciphertext always differs
and Sync would restart the unit every time; the console holds only the public
@@ -560,25 +560,25 @@ hook), `apisystem.go`, root `system.go`, `authcli.go`.
id** in the record — `ScanBlock` gains `webhook_key_id`, additive and safe
under the PARSER LIST rule — and skips any app already carrying the current
one. The test asserts the skip.
-- [ ] `TestRotateWebhookKeyDoesNotDropPrevious` — dropping `_PREVIOUS` is a
+- [x] `TestRotateWebhookKeyDoesNotDropPrevious` — dropping `_PREVIOUS` is a
separate operator step, because "done" means sealed, not converged
(`server.go:566-569` makes this point for store rotation).
-- [ ] **`TestGrantFoldsBothSecretsIntoOneApply`** — a first grant writes the
+- [x] **`TestGrantFoldsBothSecretsIntoOneApply`** — a first grant writes the
store credential and the callback secret. Two `Apply` calls are two
generation bumps and therefore two unit restarts for one operator action.
-- [ ] **`TestRevokeUnsetsTheCallbackSecret`** — a key outliving the consent it
+- [x] **`TestRevokeUnsetsTheCallbackSecret`** — a key outliving the consent it
was issued under is the same mistake as a result object outliving it.
-- [ ] **`TestRotateIsBudgetedAndResumable`** — "paced" needs a mechanism, and
+- [x] **`TestRotateIsBudgetedAndResumable`** — "paced" needs a mechanism, and
it is `scan sweep`'s: one budget per call, an answer saying how many apps
remain, the CLI loops. No background job, no pacing state; idempotence by key
id is what makes a re-run safe.
-- [ ] **`TestNoMasterRefusesACallbackPath`** — with no `CARLOS_SCAN_WEBHOOK_KEY`
+- [x] **`TestNoMasterRefusesACallbackPath`** — with no `CARLOS_SCAN_WEBHOOK_KEY`
the enqueue door refuses `callback_path` with 503 `callbacks_not_configured`.
A 202 would promise a delivery that cannot happen. (The door already accepts
the field, so this is a fix, not only an addition.)
-- [ ] **`TestPerAppKeyGenerationRotatesOneApp`** — `webhook_gen` in `ScanBlock`;
+- [x] **`TestPerAppKeyGenerationRotatesOneApp`** — `webhook_gen` in `ScanBlock`;
a tenant that leaks its key must not need a deployment-wide rotation.
-- [ ] **`TestReclaimedAppNameGetsADifferentKey`** — and this one needs
+- [x] **`TestReclaimedAppNameGetsADifferentKey`** — and this one needs
`webhook_gen` to be RANDOM rather than a counter. `PurgeApp` deletes
`store.json` with the rest of the app prefix (`trash.go:229-241`), so a
counter from zero restarts at zero for whoever claims the name next and
@@ -606,7 +606,7 @@ re-sealed hold only new.
**Files:** `internal/console/scandeliver.go` (new — the worker pool),
`scandrain.go`, `server.go`.
-- [ ] **`TestDeliveryDoesNotBlockTheTick`** — review 7's blocker. Delivery in
+- [x] **`TestDeliveryDoesNotBlockTheTick`** — review 7's blocker. Delivery in
the drain's goroutine puts a tenant's hanging endpoint in the path of every
other tenant's verdict: 50 items times a bounded retry schedule, inside a
tick that sweeps apps sequentially. That is the starvation §7.2 and §7.3
@@ -614,26 +614,26 @@ re-sealed hold only new.
fed from `done` after `scanNotify`, a bounded queue (overflow drops and logs
— best effort by construction), a per-app in-flight cap, retries scheduled by
the pool.
-- [ ] `TestCallbackGoesToPlatformHostOnly` — never a custom domain.
-- [ ] `TestCallbackRefusesRedirects` — Go follows them by default and a tenant
+- [x] `TestCallbackGoesToPlatformHostOnly` — never a custom domain.
+- [x] `TestCallbackRefusesRedirects` — Go follows them by default and a tenant
302 turns a signed POST into a GET at an internal address
(`sesevents.go:143-158` is the precedent).
-- [ ] **`TestTransportDoesNotSkipTLSVerification`** — a tested ABSENCE, because
+- [x] **`TestTransportDoesNotSkipTLSVerification`** — a tested ABSENCE, because
TLS name verification is the second lock that makes DNS rebinding irrelevant
(§6.2). Needs `Config.CallbackTransport http.RoundTripper` (nil = production)
as the test seam; name it, or the test cannot intercept an https POST.
-- [ ] **`TestNeverPostsUnsigned`** — with no signer there is no request.
-- [ ] `TestCallbackRetriesAreBounded` — 2xx success; 3xx and 4xx not retried;
+- [x] **`TestNeverPostsUnsigned`** — with no signer there is no request.
+- [x] `TestCallbackRetriesAreBounded` — 2xx success; 3xx and 4xx not retried;
5xx/429/timeout retried, three attempts at 1s/4s/16s; 10s client timeout;
response body drained through a 4KiB `LimitReader` and discarded.
-- [ ] `TestDroppedCallbackStillReadableFromStatusDoor` — the §13 promise that
+- [x] `TestDroppedCallbackStillReadableFromStatusDoor` — the §13 promise that
makes lazy retries acceptable.
-- [ ] `TestCallbackBodyCarriesRecorded` — or a callback saying `clean` and a
+- [x] `TestCallbackBodyCarriesRecorded` — or a callback saying `clean` and a
door saying `recorded:false` disagree.
-- [ ] **`TestCallbackBodyOmitsTheVersionToken`** — §11.7. The body carries
+- [x] **`TestCallbackBodyOmitsTheVersionToken`** — §11.7. The body carries
account, app, key, the bare ETag, verdict, recorded, and provenance; never
the opaque version.
-- [ ] **`TestNoPlatformHostIsNotAnError`** — an app with no ready route has
+- [x] **`TestNoPlatformHostIsNotAnError`** — an app with no ready route has
nowhere to be called back yet; it polls. Refuse at the door rather than
building `https:///hooks`.
@@ -649,12 +649,12 @@ re-sealed hold only new.
`docs/site/cli.md`,
`docs/superpowers/plans/2026-09-01-carloku-scanning-rollout.md`.
-- [ ] `TestScanUsageCountsOnDemandOnly` — sweep scans are not attributed to a
+- [x] `TestScanUsageCountsOnDemandOnly` — sweep scans are not attributed to a
tenant who did not ask for them.
-- [ ] `TestScanUsageFoldsPerTick` — not per object. `Drained.Scanned` and
+- [x] `TestScanUsageFoldsPerTick` — not per object. `Drained.Scanned` and
`Size` already exist for this; the fold accumulates per account across
`drainScanApp` calls and writes once at the end of `scanDrainTick`.
-- [ ] `TestStoreScanStatusReportsDeploymentState` — a per-app record saying
+- [x] `TestStoreScanStatusReportsDeploymentState` — a per-app record saying
`enabled: clamav` while the deployment switch is off is a new way for the
status output to lie.
- [ ] `TestPrintScanStatusShowsQueueDepth` (per app, `objstore.QueueDepth`) and
@@ -683,6 +683,38 @@ rollback becomes `disable`, no restart.
---
+---
+
+## Where this stands (2026-09-08)
+
+Tasks 1-17 are implemented on `worktree-store-scan-on-demand`. 14-17 were
+written by codex (gpt-5.6-sol) against this plan, then mutation-tested and in
+several places re-tested here; every commit says which parts came from where.
+
+**What is NOT done, and why.** The two printer tests above, and the docs, need
+copy a person has approved — a printer line is a sentence somebody reads, and
+this project reviews user-facing language before it ships. Three pieces are
+waiting:
+
+1. The CLI reference for both `rotate-webhook-key` verbs (drafted, held out of
+ the commits, in `cli-copy-for-review.diff`).
+2. Two status lines: `queue: N item(s) waiting` and `queue: N apps with work`.
+3. **The tenant-facing half**, which the final review argues should land WITH
+ the merge rather than after it: once merged, the enqueue door accepts
+ `callback_path` from any Service credential and the console will POST signed
+ bodies to apps that have no documented way to verify them. That page must
+ carry `VerifyCallback`, and it must say that the delivered secret is HEX
+ while the MAC is keyed on the raw bytes — an author who feeds the
+ environment string straight into HMAC fails every verification, and the
+ next move after that is to turn verification off.
+
+**Known and recorded, not fixed.** A tag verdict answers without `at`, and
+`skipped` without a reason, because the `Scanner.Status` seam carries neither;
+widening it is its own task and is the same seam §5.2's example assumes.
+Metering counts a size-ceiling `skipped` as bytes, which is wrong and unpriced
+— fix before pricing. The long-poll checks consent once for up to 30s of
+waiting, answering from tags rather than bytes.
+
## Remaining §13 coverage, assigned
First-tick warning when wired with no record → task 4. `request`/`revoke`/