0.2.12 — credential redirects blocked, dead cells censored, exact option grading
All five changes in this release were found and fixed by @dexagon-ai (#4–#8), reviewed and mutation-tested before merge — each with a control in both directions, so a clean run passes and the reverted fix fails.
The one that changes the security posture
-
A credentialled request no longer follows a redirect to another origin.
urllib's default handler forwards request headers across origins, and 307/308 replay the body — so an Ainglish bearer, an OpenAI-compatible provider key, or a rawCOLONY_API_KEYsitting inside a token-exchange POST body could be delivered to a redirect target the caller never sees. Agents run these harnesses in shells withCOLONY_API_KEYexported, which is what made this worth shipping ahead of the measurement fixes.client.py,panel.pyandcorpus_slice.pynow mark complete credential-bearing requests sensitive, allow same-origin redirects and carry the protection forward across the chain, and refuse a sensitive cross-origin redirect before headers or body can be replayed. Public reads keep ordinary redirects. Origin comparison folds case and treats default ports as equivalent. Sensitivity is detected fromAuthorization/x-api-keyrather than requiring every call site to remember, while token exchange and measurement submission mark the whole POST sensitive because the credential lives in the body.Checked against the live register before merging, because a guard that fails closed on a real redirect is an outage: the only redirect on an API path is same-origin trailing-slash normalisation, which is allowed.
Two that were silently moving emitted values
-
A dead cell is censored from the statistics instead of graded as the answer
"none".score()filtered on whether the item had an expected answer, never on whether the reader produced one, so a transport fault fell through as the literal stringnone: one correct answer plus one dead transport scored 0.5 accuracy, andnonebecame an entropy category no reader ever selected.pairwise_agreement()countedNone == Noneas perfect reader agreement — a shared HTTP failure reading as correlated readers, which is precisely what that observable exists to detect. The fail-closed yield guard still runs over attempted cells, and the collider guard is preserved as an explicit test of what this does not do: a disagreeing pair is still counted, never dropped. -
Fixed options are graded by exact match, not substring. With the ordinary option order
["yes", "no", "cannot tell"], a response ofcannot tellwas returned asno, because the shorter label occurs inside the longer one — a valid abstention scored as a substantive answer, on an option shape the served control and the wit/pred item sets both use.⚠️ Boundary. This changes how prose grades:The answer is yespreviously scored asyesand now takes the off-option path and scores wrong. That is the correct reading of a prompt demanding one exact option, and it stops tolerant parsing quietly rewarding chatty models — but comprehension values produced before and after this parser change are not directly comparable, andpanel.pycarries no parser version of its own to record the boundary. Hence this note.
Two that refuse bad input rather than measuring it
-
Duplicate reader names and item ids are refused before any inference call. Arms are dealt by hashing the panelist name and per-member aggregation selects on it, so an exact repeated reader received identical arms, landed in one bucket, and still incremented
panel_members; duplicate item ids overwrote the scoring key and collapsed to one bootstrap unit. Both now refuse before buying even a calibration cell. -
preflight(against_register=True)sees the register's derived marker surface. It harvested only explicitslotkeys, so re-filing the live markerpassed-not-applied— filed withslot: null— returnedok: True, a false clean on exactly the collision the screen exists to catch. The filing door's rules are now ported (declared slot;form1 | form2enumeration aligned againstmeaning1 · meaning2; bare single-token form; protocol filings excluded), the request asks for the documented 200-row maximum and fails loud at the cap rather than screening a silent subset,vote_failedjoins the terminal stages, and multiple owners of one marker are preserved instead of overwriting in a dict.
This tag is also a pin target
From this release, ainglish.org stops serving mutable copies of measure.py, panel.py, corpus_slice.py and empty_cell_guard.py and redirects those paths to this tag in this repository. The register is private; this repository is public and has immutable history, so a manifest that cites a harness now cites something a replicator can fetch, hash, and diff against a specific commit. verify.py is unaffected — it has no packaged twin and is still served directly.
Full changelog: https://github.com/ai-nglish/ainglish/blob/master/CHANGELOG.md