Repository navigation
The token asserts caller identity, not humanity: enforce aud, and rename - #240
Merged
Merged
Conversation
The website session answered D1 and corrected the premise it rested on. There is no human gate on their search path and there will not be one -- nobody solves a captcha to run a search, and their only hCaptcha belongs to the contact form, spent on submit. A token here cannot honestly assert a human is present. So it asserts caller identity: minted server-side per request, EdDSA, with iss, aud, iat, exp at +120s, and sub -- an opaque per-visit id of 128 random bits, holding nothing about the reader. The backstop limit already keys on sub, so that part needed no change. **The audience check was worse than missing.** They asked us to enforce `aud`; we passed none. PyJWT rejects a token that carries `aud` when no audience is expected, so the endpoint would have refused *every* token they minted -- the first real one would have failed and looked like a signing problem. `aud` is now required and checked against `reactome-chatbot`, overridable by CALLER_TOKEN_AUDIENCE, which falls back rather than accepting an empty value: an empty expectation refuses every real token instead of loosening the check. MissingRequiredClaimError now names the claim. It reported "token has no expiry" for whatever was missing, which would have misreported a missing audience. Renamed throughout -- module, tests, request field, env var, compose secret, key files, the beta script and the specs -- because `human_token` now says something untrue about the security model, and only one consumer exists to update. The website has not started building. Also answers their cancellation question, measured rather than reasoned: a client hang-up raises CancelledError inside the answer generator and nothing further is produced. Three tokens read, three produced, then cancelled. They need not cancel upstream. Verified on the served path: a website-shaped token answers in 19.9s; a wrong audience and a missing audience are both refused in 0.0s. 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.
Builds on #239 (which must merge first — same branch lineage). The website session answered D1 and corrected the premise it rested on.
D1: the question had a false premise
There is no human gate on the search path, and there is not going to be one — nobody solves a captcha to run a search. Their only hCaptcha belongs to the contact form and is spent on submit. So a token here cannot honestly assert a human is present, and asking it to would have meant inventing a claim.
It asserts caller identity instead: minted server-side per request, EdDSA, with
iss,aud,iat,expat +120s, andsub— an opaque per-visit id of 128 random bits holding nothing about the reader. Our backstop limit already keys onsub, so that part needed no change.Abuse control moved with the premise: their panel is opt-in behind a click, so a crawled search never reaches a model, and their proxy rate limits by address.
The audience check was worse than missing
They asked us to enforce
aud. We passed none — and PyJWT rejects a token carryingaudwhen no audience is expected:aud=reactome-chatbotreactome-chatbotaud=elsewherereactome-chatbotInvalidAudienceErroraudreactome-chatbotMissingRequiredClaimErroraud=reactome-chatbotInvalidAudienceError← todaySo the endpoint would have refused every token they minted. The first real one would have failed and looked like a signing problem.
audis now required and checked againstreactome-chatbot, overridable viaCALLER_TOKEN_AUDIENCE— which falls back rather than honouring an empty value, since an empty expectation refuses every real token instead of loosening the check.MissingRequiredClaimErrornow names the claim; it reported "token has no expiry" for whatever was missing, which would have misreported a missing audience.Renamed throughout
human_tokennow says something untrue about the security model, and only one consumer exists to update — they have not started building. Module, tests, request field (caller_token), env var (CALLER_TOKEN_PUBLIC_KEY_PATH), compose secret, key files, the beta script, and the specs.A test also changed meaning rather than just names:
test_the_limit_is_per_callerused two tokens differing only in expiry and expected them to count separately. Withsub, that is wrong — they mint one token per request, so keying on the token would make the limit meaningless. It now uses two visits, and pins that a freshly minted token for the same visit does not buy a fresh allowance.Their cancellation question, measured
They asked whether a dropped proxy connection abandons the run. It does: a client hang-up raises
CancelledErrorinside the answer generator and nothing further is produced — 3 tokens read, 3 produced, then cancelled. They need not cancel upstream. Recorded in the contract.Verified on the served path
Real app, renamed env var, tokens shaped like theirs: a website-shaped token answers in 19.9s; a wrong audience and a missing audience are both refused in 0.0s.
CI-equivalent locally: ruff, format, mypy (129 files), full suite with no API keys set.
🤖 Generated with Claude Code