Repository navigation
Settle the analysis-summary contract, and fix what sub actually means - #264
Merged
Merged
Conversation
Agreed with the website session. Nothing in the contract is open now. Human presence rides as `human`, `human_iat` and `human_sub` on the caller token, minted only when their Turnstile-backed identity cookie validated. Freshness is 30 minutes, inclusive at exactly 1800.000s, enforced at both ends so neither side is a single point of failure. The analysis token is passed straight through and never validated by the caller: this service already distinguishes 404, 410 and the undocumented 500 a malformed token returns, and a second validator disagrees with the first eventually. A refusal now carries a `reason` -- `no_caller`, `no_human` or `stale_human` -- because "failed" is not something an interface can say to a person, and `stale_human` is the only refusal a reader can act on. Advisory: an unrecognised value must read as a plain refusal. The claim is `human_sub` rather than `sub` because `sub` was already taken. And then the more useful correction, which came back from them: this repository's description of `sub` was wrong. Their `callerSubject()` prefers the identity cookie's subject whenever the reader has passed a challenge, falling back to a per-visit value only when they have not -- so `sub` has been browser-scoped for every gated reader since the gate shipped, and the answer endpoint's backstop limiter has been counting people across visits rather than within one. That is the stronger throttle and it is kept deliberately. What was wrong was the docstring in caller_token.py and the comment in rate_limit.py, both of which named the fallback case as though it were the only case. Someone reasoning about a burst on that endpoint would have had the wrong model, and the analysis-summary limiter would have looked inexplicably similar to one they thought was scoped differently. Also recorded: while that preference holds, `sub` and `human_sub` carry the same value whenever both are present. They stay separate because they mean different things and would diverge the moment it changed, and because agreement between them is not a signal anything should test. Documentation and comments only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bound was written as "1800.000 accepted, 1800.001 refused", and both sides were about to pin a test at exactly that edge. It is not a distinction the claim can carry. `human_iat` is epoch seconds, derived as the cookie's expiry minus a constant TTL, so it arrives already rounded to a second -- a sub-second edge is a boundary neither side can be on, tested against a clock finer than the value being tested. Restated as `now - human_iat <= 1800` in whole seconds, with the consequence written down: effective precision is one second, and a challenge solved 1800.4s ago presents as 1800 and is accepted. The point they made about boundaries -- that one nobody tests is one nobody agreed on -- holds just as well for a boundary tested at a precision the data does not have. Also added while looking: the summary must not end with a prose list of its citations. They arrive as `citation` events, and the duplicate is exactly what cost the website a pattern it could not write correctly on the answer route. This endpoint's prompt is its own, so the fix is not to ask for the list rather than to strip it afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Settles every open item in the analysis-summary contract with the website session, and fixes a wrong description of deployed behaviour that the exchange turned up.
The contract is closed
human,human_iat,human_subon the caller token, minted only when their Turnstile-backed identity cookie validated.humanis absent rather than false, so a missing claim and a failed check are indistinguishable here.reason:no_caller,no_human,stale_human. "Failed" is not something an interface can say to a person, andstale_humanis the only refusal a reader can act on. Advisory — an unrecognised value must read as a plain refusal.The more useful half:
subwas documented wrongI asked for
human_subrather thansubbecausesubwas already taken by an opaque per-visit id. They corrected me: theircallerSubject()prefers the identity cookie's subject whenever the reader has passed a challenge, falling back to the per-visit value only when they have not.So
subhas been browser-scoped for every gated reader since the gate shipped, andidentity_ofhas been keying the answer endpoint's backstop limiter on an identifier that lasts as long as that cookie — counting a person across visits, not within one.That is the stronger throttle and it is kept. No behaviour changes here. What was wrong was
caller_token.py's docstring andrate_limit.py's comment, both of which named the fallback case as though it were the only case. Anyone reasoning about a burst on that endpoint had the wrong model, and the analysis-summary limiter would later have looked inexplicably similar to one they believed was scoped differently.Also written down: while that preference holds,
subandhuman_subcarry the same value whenever both are present. They stay separate because they mean different things, would diverge the moment the preference changed, and because agreement between them is not a signal anything should test.Documentation and comments only, no behaviour change. 501 passed, 1 skipped; mypy over all 137 files, ruff and ruff format clean.
🤖 Generated with Claude Code