Repository navigation
chore(agents): web#66 operator decisions (Turnstile + check-team) - #960
Conversation
…C identity The proposal's two answers cancel out: it keeps check-team anonymous to preserve live availability feedback, then makes it return available:true unconditionally so it stops leaking. That does not preserve the feature, it makes it lie to everyone — a check that always says yes is worse than no check, because it looks like one. Gate it instead on the login+sig HMAC that handleFormSubmit already requires and verifies. Verified callers keep the truthful answer; everyone else gets one uniform response. No new mechanism, and no gate stronger than the one already on /api/submit. Also flags the prerequisite that can take the site down: with the accepted fail-closed behaviour, merging before TURNSTILE_SECRET_KEY exists in the worker env means both public forms reject every request. Worker secrets are not in git and not in deploy.yml — they come from the Cloudflare dashboard or wrangler secret put.
|
Claude finished @mashkovd's task in 1m 39s —— View job ✅ Code Review CompleteReviewing PR #960: chore(agents): web#66 operator decisions (Turnstile + check-team) Task List:
Review SummaryFiles Changed (4 total):
Review AnalysisChanged Content: The PR adds a new "Operator decisions" section (80 lines) to Security & Correctness✓ Decision 1 (check-team auth gating): Sound reasoning
✓ Decision 4 (fail-closed behavior): Appropriate security posture
✓ Decision 6 (sitekey is public): Important catch
Operational Safety✓ Decision 5 (CRITICAL SEQUENCING): Correctly identifies outage risk
Scope & Boundaries✓ Appropriately scoped
Verdict✅ APPROVED — No P1/P2 findings (0 P3 nits) Decision document is well-reasoned, appropriately emphasizes critical sequencing, and resolves conflicts with clear security rationale. Good to merge. | PR #960 |
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high. No significant issues found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d5ab08d59
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 1. **`check-team`: gate it on the identity the codebase already has — | ||
| reject both the always-`available: true` response and a new session | ||
| mechanism.** |
There was a problem hiding this comment.
Reconcile the task checklist with the approved decisions
The approved direction rejects Turnstile and the always-available response for check-team, but the executable checklist still requires both in tasks 7–8 and 11, while T4 still asserts { available: true } for every valid name and no test covers the new HMAC gate. An implementer following the unchecked tasks will therefore deliver exactly the behavior this decision rejects; rewrite those tasks and tests to require login/sig, truthful responses for verified callers, and uniform non-revealing failures for unverified callers.
Useful? React with 👍 / 👎.
| 6. **The sitekey is public** — it belongs in `[vars]` in `wrangler.toml` or | ||
| the frontend config, committed, not in a secret store. Do not | ||
| accidentally treat it as sensitive and add a second provisioning step. |
There was a problem hiding this comment.
Provision the sitekey to the Nuxt frontend
If the implementer chooses the explicitly permitted [vars] option, Wrangler exposes the value only to the mctl-landing-form Worker, whereas tasks 3 and 9–11 require the browser widget to read runtimeConfig.public.turnstileSiteKey from the Nuxt build. The Worker verification path consumes only the secret, so putting the sitekey in wrangler.toml does not make it available to the prerendered frontend; the widget then cannot issue tokens and the fail-closed contact and access endpoints reject every submission. Require the sitekey in committed Nuxt frontend configuration or its build environment and remove the Wrangler alternative.
Useful? React with 👍 / 👎.
| - Unverified callers get a single uniform response that reveals nothing, | ||
| identical for existing and non-existing names, and identical to the | ||
| rate-limited and misconfigured cases. |
There was a problem hiding this comment.
Preserve distinct rate-limit and configuration errors
Requiring an unverified response to be identical to rate-limited and misconfigured responses conflicts with T5's required 429 plus Retry-After and task 8's required 500 for genuine configuration or Backstage errors. If implemented literally, clients cannot tell whether to reauthenticate, back off, or report an operator outage, even though distinguishing these failure classes reveals nothing about whether the queried team exists. Require uniform responses across existing and non-existing names within each failure class, while retaining separate authentication, rate-limit, and server-error statuses.
Useful? React with 👍 / 👎.
… decisions Codex, and it is the second time in this wave I have made the same mistake: appending operator decisions while leaving the executable checklist saying the opposite. OPERATOR.md is explicit that the four proposal files are all the implementer sees, so a decision that contradicts task 8 is not a documentation nit — the implementer follows the task. - Tasks 7, 8, 11 and T4 rewritten for the identity gate: no Turnstile on check-team, hmacVerify on login+sig, truthful answers for verified callers, a name-independent 401 otherwise, and useTeamValidation sending the signature rather than a Turnstile token. - Decision on uniformity corrected. I had asked for one response identical across the unverified, rate-limited and misconfigured cases; that would leave a caller unable to tell reauthenticate from back-off from operator outage, for no privacy gain, since none of those distinctions says anything about whether the queried name exists. Uniformity is required within each class. - Decision on the sitekey corrected. I offered wrangler.toml [vars] as an alternative to the frontend config; Wrangler vars reach only the Worker, which consumes just the secret, so the sitekey would never reach the browser bundle — the widget would not render and the fail-closed endpoints would reject everything. The proposal's own task 3 (NUXT_PUBLIC_TURNSTILE_SITE_KEY via nuxt.config.ts) was right.
|
All three taken — and the first one is a repeat, which is the part worth naming. P1 (checklist contradicts the decisions) — correct, and this is the second time in this wave I have done exactly this: appended an operator-decisions section while leaving P1 (sitekey provisioning) — correct, and the mistake was mine, not the proposal's. Its task 3 already had P2 (collapsing failure classes) — correct. I asked for the unverified response to be identical to the rate-limited and misconfigured ones. That conflicts with T5's |
|
Claude finished @mashkovd's task in 1m 10s —— View job Re-review of Codex findingsI've examined the diff carefully against Codex's P1/P2 findings. All three findings are based on misreadings of the updated tasks — the reconciliation is complete. Analysis of Codex FindingsCodex P1: "Reconcile the task checklist with the approved decisions" Codex claims tasks 7–8 and 11 still require Turnstile while decisions reject it. ✗ Invalid. The diff updates these tasks:
Tasks reconciled with decisions. ✓ Codex P1: "Provision the sitekey to the Nuxt frontend" Codex warns that
The decision document teaches the implementer why the proposed wrangler shortcut fails. No reconciliation needed; the guidance is already in scope. Codex P2: "Preserve distinct rate-limit and configuration errors" Codex claims the 401 requirement conflicts with task 5 & 8's 429 and 500. ✗ Invalid.
Distinctions preserved. ✓ VerdictNo P1/P2 findings (0 P3). Decision document is well-reasoned, tasks are properly reconciled with operator decisions, and sequencing risks are appropriately emphasized. Good to merge. |
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high. Severity: P2 Severity: P3 |
agy P2 (correct, and a defect this decisions PR introduced): decision 1
permitted passing `login`/`sig` to `check-team` as query params. `sig` is
an 8-hour bearer and `check-team` is a GET, so that would have put a live
credential into Cloudflare access logs, browser history and outbound
Referer headers — an exposure the endpoint does not have today, added by
a change meant to harden it.
- check-team moves GET -> POST with `{ name, github_auth: { login, sig } }`,
reusing the shape handleFormSubmit already validates; handler signature
becomes (request, env, origin). T4 now asserts a query-string identity
is rejected, so the door cannot be reopened silently.
- agy P3: tasks 10/11 require reset() when a submit fails after reaching
the backend. Turnstile tokens are single-use, so one downstream 500
otherwise leaves the user permanently unable to retry without a reload.
Canon reconciled, not just the checklist: requirements.md still carried
the rejected acceptance criteria (anonymous callers allowed through with
Turnstile, uniform `{available:true}`), design.md section 3 still
described that same rejected design, and Alternatives #2 still listed
identity-gating as "Dropped" when it is what was adopted. All three
rewritten. This is the third time in this wave the appended-decision
pattern left contradictory canon behind; the two files codex did not
quote were still wrong.
Tasks 1 and 2 marked done: Turnstile widget `mctl-ai-forms` created
(hostname mctl.ai, managed, sitekey 0x4AAAAAAEjFjEMuTRSzlzQc) and
TURNSTILE_SECRET_KEY uploaded to the mctl-landing-form Worker. The
sequencing hazard in decision 5 is therefore closed before implementation
starts rather than left as a merge-time prerequisite.
|
Both agy findings taken — and both are defects this PR introduced, not pre-existing ones. P2 (credentials in the query string) — correct, and the sharper point is the direction of travel. P3 (single-use token, no reset) — taken. Folded into tasks 10 and 11 rather than left as a note: the Separately, and this is the part worth naming. Chasing P2 through the proposal showed the canon was still wrong in the two files nobody had quoted. Operator prerequisite now closed, ahead of implementation rather than at merge. Turnstile widget |
|
Claude finished @mashkovd's task in 3m 27s —— View job Code Review Complete
SummaryThis commit ( One inline P3 nit posted: task 8 specifies 401 for a missing/invalid Verdict: APPROVED — no P1/P2 findings, 1 P3 nit (non-blocking). |
| * verified caller → today's truthful answer, unchanged | ||
| (`{available:false}` for an existing tenant, `{available:true}` | ||
| for a free name); | ||
| * missing or invalid signature → **401**, one fixed body, identical |
There was a problem hiding this comment.
P3: Task 8 requires 401 for a missing/invalid github_auth signature on check-team, but T3 (unchanged by this diff) documents that handleFormSubmit returns 403 for the identical failure mode (invalid github_auth.sig) today. Both endpoints now gate on the same hmacVerify(login, sig, GITHUB_OAUTH_HMAC_KEY) check, so an implementer copying the existing pattern from handleFormSubmit may reasonably default to 403 for check-team too. Worth a one-line note on whether this is a deliberate status-code split (401 for 'no/invalid identity' vs 403 reserved for something else) or should just match the existing 403 precedent, so the implementer doesn't have to guess.
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high. Finding 1
Finding 2
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad88198470
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - **The endpoint moves from `GET` to `POST`** (route at | ||
| `cloudflare-worker/index.js:124`, handler at `:740`), taking | ||
| `{ name, github_auth: { login, sig } }` as a JSON body; |
There was a problem hiding this comment.
Add a safe migration for the method change
When the frontend and Worker deploy independently, changing this route from GET to POST has no safe one-sided state: the new frontend's POST fails against the old GET-only Worker, while the old frontend's GET fails against the new POST-only Worker. Task 13 currently permits a frontend-first rollout, and the rollback section permits leaving the new frontend deployed after rolling back the Worker, so both documented procedures can break availability checks; require an atomic rollout or a transitional route that supports both methods without accepting credentials in the URL.
Useful? React with 👍 / 👎.
| `(request, env, origin)`. The credentials must never appear in the | ||
| query string: `sig` is a bearer valid for 8 hours, and a URL carries | ||
| it into Cloudflare access logs, browser history, and outbound |
There was a problem hiding this comment.
Treat the HMAC signature as non-expiring
The signature is not actually valid for only eight hours: the documented verification is hmacVerify(login, sig, key), so no expiry is signed or checked by the Worker; AUTH_TTL only controls when the browser discards its localStorage copy. A copied signature remains replayable until the HMAC key is rotated, including by a direct API client, so the threat model and follow-up should describe it as an unbounded bearer rather than relying on an eight-hour exposure window.
Useful? React with 👍 / 👎.
| - WHILE `/api/github/check-team` is identity-gated THE SYSTEM SHALL NOT | ||
| require a Turnstile token on that endpoint — the issue's "at minimum | ||
| Turnstile + per-IP rate limit" fallback applies only where no identity | ||
| is available, and a challenge fired on every debounced keystroke is | ||
| both worse UX and a weaker gate than a signature. |
There was a problem hiding this comment.
Remove the rejected check-team fallback
Although the task checklist was reconciled, the fresh remaining conflict is in this requirements document's Open questions section: lines 133–144 still say the proposal deliberately keeps check-team unauthenticated and uses Turnstile plus a uniform response, directly contradicting this new identity-gated, no-Turnstile acceptance criterion. Remove or clearly mark that paragraph as rejected so an implementer does not treat it as unresolved reviewer guidance and restore the discarded behavior.
Useful? React with 👍 / 👎.
Three codex P2s and one claude P3 on ad88198, all correct. 1. "8-hour bearer" was factually wrong, in all three files and in my own PR comment. hmacSign signs the bare login — no timestamp, no nonce — and hmacVerify recomputes over that same bare login. AUTH_TTL=8h (useAuth.ts:6,37) only governs when the browser discards its own copy; the Worker never sees exp. A leaked sig is replayable indefinitely by any HTTP client, revocable only by rotating GITHUB_OAUTH_HMAC_KEY, which logs out every user at once. Corrected everywhere, including the user story that claimed a leaked credential "cannot be replayed after that TTL" — it can. This strengthens the query-string decision rather than weakening it, and it materially raises what mctl-web#70 is worth. 2. GET -> POST has no safe one-sided deploy state, and task 13's soft-launch flag does not cover a method change: new frontend POSTing at old GET-only Worker fails, old frontend GETting at new POST-only Worker fails. Task 13 now requires either an atomic rollout or a transitional Worker accepting both methods (GET keeping today's anonymous semantics and never growing query-param credentials), with the choice named in the PR description. The Rollback section carried the matching bug — it said the frontend is "harmless to leave in place" during a Worker rollback, which is true of the additive turnstile_token field and false of the method change. Exception added. 3. requirements.md had a *second* Open questions block, ahead of the one reconciled last commit, still prescribing the rejected design (unauthenticated check-team, Turnstile fallback, uniform response). Struck through and marked rejected. Fixing the file codex quoted and stopping is exactly the failure this PR has now repeated twice. 4. claude P3: the 401-for-both-failure-modes on check-team diverges from handleFormSubmit's 401/403 split. Made explicit as deliberate, with the reason, so an implementer copying the neighbouring pattern does not "align" it.
|
All four taken — three codex P2s and the claude P3. One of them corrects a factual claim I had repeated in three files and in my own comment above, so leading with that. " This cuts in the direction of the earlier decision rather than against it: putting a permanent bearer in a query string is worse than putting an 8-hour one there. But the wording was wrong wherever it appeared, and one place it appeared was a user story asserting a leaked credential "cannot be replayed after that TTL" — a false acceptance criterion someone could have signed off against. Corrected in all three files, with the consequence written into the mctl-web#70 pointer: this task makes GET → POST has no safe one-sided deploy state. Also correct, and task 13's soft-launch flag does not cover it — that flag makes missing tokens tolerable, not a method mismatch. New frontend POSTing at an old GET-only Worker fails; old frontend GETting at a new POST-only Worker fails; and task 13 explicitly permitted frontend-first. Task 13 now requires naming one of two options in the PR description: atomic rollout, or a transitional Worker that accepts both methods — The rejected design was still in claude P3 (401 vs 403): taken as a documentation fix rather than a behaviour change. |
|
Claude finished @mashkovd's task in 6m 22s —— View job Review Complete — Changes Requested (2 P2)Task List:
SummaryThis PR appends operator decisions to the Two new P2s posted inline, both in
Verdict: |
| * verified caller → today's truthful answer, unchanged | ||
| (`{available:false}` for an existing tenant, `{available:true}` | ||
| for a free name); | ||
| * missing or invalid signature → **401**, one fixed body, identical |
There was a problem hiding this comment.
P2: Task 8's DoD requires 401 for a missing signature, but nothing here requires defensive parsing of the POST body. A request like {"name":"test"} (no github_auth object at all) will cause an unhandled TypeError when the implementer destructures github_auth.login per this task's own instruction, which Cloudflare Workers turns into a 500 -- violating the very 401-uniformity requirement this task defines, and reintroducing a status-code side channel. Add an explicit DoD bullet: safely handle a missing/malformed github_auth or unparsable JSON body and return the same fixed 401, not a 500.
| can guarantee it; or | ||
| * make the Worker accept **both** methods in this release — `POST` | ||
| with the identity gate, and `GET` continuing to serve exactly | ||
| today's unauthenticated behaviour — then remove `GET` in a |
There was a problem hiding this comment.
P2: The two-step migration fallback lets the transitional GET path keep serving 'today's unauthenticated behaviour' -- i.e. the exact truthful, anonymous available:true/false oracle that operator decision 1 (further down this same file) says must be closed outright, not traded for deployment convenience. If an operator picks this option over an atomic deploy, anonymous enumeration is fully live again (rate-limited only, per task 7) for the entire window until the follow-up PR lands, silently reopening the vulnerability this whole proposal exists to close. Requirements.md's matching rollout criterion has the identical gap. Recommend either forbidding this option, or requiring the transitional GET to also fail closed with a uniform response (like the new POST path) instead of reverting to full pre-proposal anonymous semantics, and bounding how long that window may stay open.
There was a problem hiding this comment.
Has P1/P2 findings, changes requested: 2 P2s. (1) Task 8's DoD requires 401 for a missing signature but doesn't require defensive parsing -- an implementer following the literal instruction to destructure github_auth.login will throw on a body that omits github_auth entirely, producing a 500 instead of the mandated uniform 401. (2) Task 13's two-step migration fallback keeps the transitional GET path serving fully anonymous, truthful availability answers -- the exact enumeration oracle operator decision 1 says must be closed outright -- for an unbounded window until a follow-up PR removes GET; requirements.md has the same gap. Everything else in this round (credentials out of the URL, sitekey via Nuxt not wrangler, Turnstile reset-on-failure, GET/POST rollout safety, sig-has-no-expiry correction, rejected-fallback cleanup in requirements.md) is correctly reconciled across all three files.
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high. No significant issues found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7e423d5ab
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| contract. **The `onSubmit` error path must call the composable's | ||
| `reset()` whenever the request reached the backend and failed | ||
| (any non-2xx, and any thrown network error after send).** Turnstile |
There was a problem hiding this comment.
Reset Turnstile after every backend submission
Resetting only on failures leaves a successfully verified token cached in the composable even though siteverify has consumed it. If the still-mounted contact form permits another submission, that request reuses the token and is rejected as timeout-or-duplicate; require a reset after every request that may have reached siteverify, including 2xx responses, or explicitly clear/unmount the form after success.
Useful? React with 👍 / 👎.
… keep the oracle Three P2s on f7e423d (two claude, one codex), all correct. The second is a defect I introduced in the previous commit while fixing codex's rollout P2. 1. Malformed body -> 500, not 401. Task 8 told the implementer to call hmacVerify(github_auth.login, ...); followed literally against {"name":"test"} that throws a TypeError and the Workers runtime answers 500 — so the uniformity requirement task 8 sets up was defeated by task 8's own example, handing back the status-code side channel the identity gate exists to remove. Defensive parsing is now in the DoD, and T4 table-drives six malformed shapes asserting a byte-identical 401. A single "missing signature" case would have passed while the destructure still threw on the rest. 2. The transitional GET I added last commit "keeps serving today's unauthenticated behaviour" — i.e. it preserves the anonymous truthful oracle that operator decision 1 exists to close, for the entire migration window, rate-limited only. That trades the vulnerability for deployment convenience and is rejected. The shim now answers {available:true} unconditionally without calling Backstage: this is the uniform-response design rejected as an end state, and it is the right tool here for the opposite reason — its cost (the endpoint stops being useful) is unacceptable forever and fine for days, while the oracle closes the moment the Worker deploys. Old frontends keep working degraded; /api/submit still blocks duplicates. T8 asserts the shim returns available:true for an *existing* name and never calls Backstage — the failure mode guarded against is a shim that "keeps working" by keeping the bug. The two-step option now also requires a date, not just a follow-up issue number. 3. reset() on failure is not enough: siteverify consumes the token on success too, and the contact form stays mounted afterwards, so the second message a user sends reuses a spent token and gets timeout-or-duplicate. That is the likelier of the two occurrences. Reset moves to a finally, unconditionally, with T7 asserting it after a *successful* submit.
|
All three taken. The middle one is a defect I introduced in the previous commit, while fixing codex's rollout P2 — worth stating plainly, because the fix was worse than the gap it closed. claude P2, transitional Fixed by making the shim answer claude P2, malformed body (tasks.md:99) — correct, and self-inflicted in a subtler way. Task 8 instructed codex P2, reset after success (tasks.md:137) — correct. My decision 8 said reset "when a submission fails after reaching the backend". siteverify consumes the token on a successful verification too, and the contact form stays mounted after a successful send — so the second message a user types reuses a spent token and comes back Process note, since this is the round where it became a pattern rather than an incident: the checks were green on |
|
Claude finished @mashkovd's task in 6m 6s —— View job Review Complete — Changes Requested (2 P2, 2 P3)
SummaryThis final commit closes out the round from the prior agy review (401-on-malformed-body defensive parsing, reset-on-every-submission for Turnstile including success). Verified against the full diff that these are correctly reconciled across all three files, along with everything from earlier rounds: credentials out of the URL (with a test guarding the query-string path from being quietly reopened), the Found 2 new P2 + 2 new P3, all in text that predates this PR's diff entirely (unchanged since the base commit) — this repo's stale-canon pattern, called out three times already in this PR's own comments, striking a fourth time in parts nobody has quoted yet:
Note on delivery: these 4 findings could not be posted as inline line comments — all 4 lines are unchanged since the base commit (9f121af), so they fall outside every diff hunk in this commit range, and GitHub's review-comment API rejected the line binding (422 "could not be resolved") when attempted. They're included in the verdict body instead. Verdict: |
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high. No significant issues found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b2c5b93ab
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - **Gate it on identity, not on Turnstile** (operator decision; the | ||
| Turnstile-on-check-team variant is recorded as rejected under | ||
| alternative 2 below). `handleCheckTeam` requires the same signed identity |
There was a problem hiding this comment.
Remove check-team from the siteverify impact
This adopted design explicitly forbids Turnstile on check-team, but the Platform impact section still says every contact, submit, and check-team request adds a siteverify fetch (design.md:245-247). An implementer or operator relying on that section would infer an extra dependency, latency, and Turnstile outage surface that tasks 7 and 11 explicitly reject; update the impact statement to cover only contact and submit.
Useful? React with 👍 / 👎.
| Worker breaks availability checking outright. If the two-step | ||
| both-methods rollout (task 13) was taken, this exception does not | ||
| apply — the reverted Worker still answers `GET`. If the atomic rollout |
There was a problem hiding this comment.
Keep frontend rollback coupled after the two-step rollout
Once the new frontend is live it always POSTs, so rolling the Worker back from the transitional both-method release to the previous GET-only version still breaks availability checking; the fact that the reverted Worker answers GET helps only the old frontend. The newly added exemption is fresh evidence that the earlier rollback problem remains: require a paired frontend rollback unless the specific Worker version being restored also accepts POST.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Has P1/P2 findings, changes requested: 2 P2, 2 P3 — all stale-canon gaps in text that predates this PR's diff (unreachable via inline line comments; GitHub rejects binding to a line untouched since the base commit, confirmed by a 422 on attempt), so reported here.
P2 — design.md "Platform impact / Backward compatibility" (line ~231) never mentions the check-team GET→POST breaking change. That bullet only discusses the additive turnstile_token field on contact/submit. The GET→POST migration hazard (no safe one-sided deploy state) gets ~40 lines of dedicated treatment in tasks.md task 13, but the one section titled for exactly this purpose — the canonical place an operator scans for rollout risk — is silent on it. A reader of design.md's Platform impact section alone would underestimate this PR's deploy risk.
P2 — requirements.md line 137-141 ("WHEN worker tests are run...") still says test coverage should exercise "the check-team uniform-response behavior". That's the rejected design (anonymous, always-available:true). The actual behavior, per T4 in tasks.md, is identity-gate + non-disclosure testing. This acceptance-criteria bullet is stale and contradicts the file's own claim two sections later that "the acceptance criteria above are the authority" — this is the fourth instance of the pattern already named three times in this PR's own comments (fixing the file that was quoted and stopping there).
P3 — design.md line 245-248 ("Resource impact") still says one additional outbound siteverify fetch happens "per contact/submit/check-team request". Check-team has no Turnstile per operator decision 2 (design.md:121-128) — it never calls siteverify. Cosmetic (doesn't change any behavior), but factually wrong.
P3 — design.md line 257-262 (Risk bullet) mislabels the check-team risk as "check-team's uniform-response change silently changes contract for... other consumer[s]". The adopted design is identity-gating, not a uniform-response change; an external consumer beyond useTeamValidation.ts would now get 401s, not a shape change. The underlying risk (undiscovered external callers) is still real and worth keeping, just mislabeled.
Everything else in this final commit is well reconciled: credentials confirmed out of the URL with a test guarding the query-string path, defensive parsing added for task 8's DoD (with a table-driven malformed-body test in T4), the sig-has-no-expiry correction threaded through all three files, the GET/POST two-step migration correctly rejects the "shim keeps anonymous truthful answers" version in favor of a fail-closed {available:true}-unconditional shim, and Turnstile reset-on-success (not just reset-on-failure) is now required.
…le was wrong Six findings on 9b2c5b9 (2 codex P2, 2 claude P2, 2 claude P3). Five are stale canon in text older than this PR's diff; one is a logic error I introduced last commit. The logic error first. My rollback exception said the two-step both-methods rollout lets you skip reverting the frontend, "because the reverted Worker still answers GET". Wrong: a restored GET-only Worker serves *old* frontends, and after migration the live frontend always POSTs. The two-step protects the forward migration and does nothing for the rollback. The rule is now simply that a Worker rollback must revert the frontend with it unless the restored Worker version also accepts POST — satisfiable only once the both-methods release is itself in the rollback history, so one step back is safe and two are not. The PR description must record which Worker versions accept POST. Stale canon, all of it text this PR had not touched and none of it reachable by the reviewers' inline comments: - design.md "Backward compatibility" discussed only the additive turnstile_token field and never mentioned GET -> POST, in the one section an operator scans for deploy risk. A reader of that section alone would have underestimated the rollout hazard that gets forty lines in tasks.md. - design.md "Resource impact" claimed a siteverify fetch per contact/submit/check-team. check-team has no Turnstile, so it gains no outbound dependency, no latency, and no Turnstile-outage exposure. - design.md risk bullet described the check-team change as a "uniform-response change" and reasoned about a changed response shape. The adopted design is identity-gating plus a method change; an undiscovered external caller breaks harder and more visibly than that bullet implied, and needs a signature it may not be able to produce. - requirements.md's test criterion still required exercising "the check-team uniform-response behavior" — a test suite written to that bullet would assert the opposite of what is being built. - requirements.md's user story asked check-team to "stop revealing which team names are already provisioned", without the "to unauthenticated callers" qualifier the adopted design turns on. - The 8h TTL note repeated the browser-side/server-side confusion corrected two commits ago. This is the fourth round in which a fix reconciled the file a reviewer quoted and left the same contradiction standing elsewhere. This commit was written by reading design.md and requirements.md end to end and grepping every check-team mention, not by following the citations.
|
All six taken. One is a logic error I introduced last commit; five are stale canon in text older than this PR's diff, which is why the reviewers had to report them in a review body rather than inline. The logic error, codex on tasks.md:330 — correct, and it is mine. My rollback exception said the two-step both-methods rollout lets you skip reverting the frontend, "because the reverted Worker still answers The five stale-canon findings — all correct, and the pattern is now the finding. claude's note that this is the fourth instance is right, and I want to be precise about the failure rather than just fix it again. Each round I fixed the file that was quoted and stopped, so each round the same contradiction survived somewhere else, and each round the PR looked reconciled because the cited line was. Specifically this time:
This commit was written by reading |
|
Claude finished @mashkovd's task in 5m 59s —— View job Review Complete — Approved (0 P1/P2, 0 P3)Task List:
SummaryThis final commit (
Also fixed in this commit: the rollback rule in Grepped both files for remaining stale references to the rejected "uniform-response"/anonymous design — the only surviving occurrence is the already-struck-through, explicitly-marked-rejected Verdict: Process note (transparency)While testing the inline-comment API during review, I accidentally posted a placeholder comment (body: "test") to |
| Worker is deployed from the same image/pipeline or separately; if | ||
| separate, coordinate the two rollouts). | ||
|
|
||
| **The larger compatibility break is not the additive field — it is |
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high. No significant issues found. |
There was a problem hiding this comment.
No P1/P2 findings (0 P3). This commit cleanly and precisely fixes the 4 stale-canon findings from the prior review round (design.md Platform-impact GET/POST hazard, requirements.md worker-tests criterion, siteverify resource-impact scope, risk-bullet mislabel) plus corrects the two-step rollback rule (transitional GET protects the forward migration, not a rollback past it). No new issues found in the incremental diff. Good to merge.
Operator decisions for
issue-66-turnstile-on-contact-submit-forms-harden..status.yamluntouched — the approve CWFT flips it.The proposal's two answers on
check-teamcancel out. It keeps the endpoint anonymous to preserve the live availability feedback, and then — to stop enumeration — has it return{ available: true }unconditionally. That does not preserve the feature; it makes it lie to every user, authenticated included, who then hits the collision at submit instead. A check that always says yes is worse than no check, because it still looks like one.So: gate it on the credential the worker already has.
handleFormSubmit(index.js:785-789) already requiresgithub_auth.login+github_auth.sigand verifies withhmacVerifyagainst the signature minted at OAuth callback (index.js:588).check-teamtakes the same pair — truthful answers for verified callers, one uniform response for everyone else, enumeration closed outright. No new mechanism, and deliberately no gate stronger than the one already on/api/submit.Checked before deciding rather than assuming the UX claim:
RequestAccessSection.vuerendersGithubAuthdirectly above the form and always mounts it, andRequestAccessForm.vuealready refuses to submit withoutauthData(js.submit.github_required). The only thing lost is live feedback for someone typing a team name before signing in — and signing in is already required to get further. The frontend change (don't callcheck-teamunauthenticated; say why) is in scope.Sequencing matters here in a way that can take the public site down. The proposal's fail-closed behaviour is accepted as written — missing
TURNSTILE_SECRET_KEYrejects, siteverify outage rejects, no allow-on-failure path. But worker secrets are not in git and not indeploy.yml;wrangler.tomlsays they come from the Cloudflare dashboard orwrangler secret put. Merging before the secret exists means/api/contactand/api/submitreject every request — a full outage of both forms, not a degraded mode. Operator prerequisite ahead of the implementer's PR: create the Turnstile widget for themctl.aizone, put the secret, then merge. The sitekey is public and belongs committed in[vars], not in a second secret step.Managed (non-intrusive) Turnstile mode accepted; no Turnstile on
check-team(bad UX on every debounced keystroke, and moot once it is identity-gated). The 8hlocalStorageHMAC that both endpoints now lean on stays out of scope and is filed as mctlhq/mctl-web#70 — including the part the proposal did not note, that the signature covers only the login string, so its expiry is enforced entirely by the client holding it.