Three findings, one root cause: the sensor and the enforcement were keyed on the raw Authorization value, validated or not — a string the caller picks. A held verdict was walked out of five times in five by editing that header; the lane with no tenant could be filled on demand; and a caller could open one table entry per request. A CALLER IS A FACT WE STATED. Signal now carries two fingerprints of the same credential under different trust: Cred, set only when the identity boundary VALIDATED it, which is the only thing that can key a caller; and Presented, whatever the request carried, counted only as spread because a wall of invalid credentials from one address IS the stuffing signature. callerKey cannot see the second one. Both are built in ONE place (observation), pinned by a test. The scorer is asked about the same thing the sensor holds, so a refused caller cannot be re-judged as somebody else by editing a header. NOTHING IS REMOVED TO MAKE ROOM FOR SOMETHING ELSE. The reclaim pass that dropped the oldest half is deleted, not guarded: a table reclaims keys that are DEAD (unseen for a window, under no live verdict) and REFUSES what does not fit. So a flood cannot erase a neighbour's counts, cannot release a held verdict, and cannot overrun the ceiling — the previous rule skipped pinned keys and then admitted anyway, which is how 25,000 held callers lived in a table that published 20,000. THE BOUND IS IN BYTES. A cap on the NUMBER of keys is not a bound when the values behind them are not bounded, so every string that can enter an entry is clamped at the door, every entry has a published worst-case size, and every admission charges it against ONE process budget. Count x size IS the byte bound; a test fills a table with worst-case values, measures it, and fails if the published numbers understate it. The per-scope ceilings stay as the FAIRNESS bound, so the anonymous lane cannot take the room the tenants need. A BOUND THAT BINDS SAYS SO. Strain is graded — clear, full, refuse, blind — carried on the observation so an unmeasured caller is not screened as a brand new one, reported on the scope's own view, and announced once per rise rather than once per request. The lane that has no tenant is readable at last: it is named by the empty scope (?org=), which cannot collide with any tenant. WHAT THE SENSOR CANNOT SEE IT DOES NOT INVENT. A request with no validated credential and no client address has no identity; keying it under the empty address would file the whole internet in one row, read as the worst stuffing run ever recorded, and let one verdict refuse everybody. It is counted as traffic, named blind, and nothing is held against it. This is the live shape today: the balancer in front of the ingress is TCP with no PROXY protocol, so no client address reaches this process at all. THE LANE IS DERIVED FROM THE COUNTS, so it is computed inside the observation that produced them. Passing it in meant stating it before those counts existed, and every request ever counted landed in "unknown". ABSENCE IS NOT SILENCE. The fail policy turns on two facts, the same rule hanzoai/iam applies at its own gate: a privileged grant waits for a scorer that is THERE and did not answer, and proceeds when there is no scorer at all — or when every slot is held and nothing has come back for a stall, which is a deadlocked scorer, not a queue. Without that second fact one hung goroutine 403s every armed org's key store until the pod restarts. AN UNANSWERED SCREEN IS NOT A SALE. Only a scored verdict is billed; the count is split into answered and unanswered so a scorer that has gone dark is a number on the org's own report. A fact we do not have is ABSENT from the signals, never empty — an empty string is a value a scorer can group by. A SCOPE KEY NAMES THE ROUTE. canonicalService and the rate limiter's exemptions read the router's path, not the spelling: "/V1/AI/chat" reached the "/v1/ai/chat" handler while producing a different rate bucket and a different spend-cap axis. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
14 KiB
14 KiB