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>
77 lines
6.0 KiB
JSON
77 lines
6.0 KiB
JSON
[
|
|
{
|
|
"description": "Traffic reports who is calling this organization's API right now: the request\ncount for the last minute split by AGENCY LANE — agent, human, bot, unknown —\nand the busiest callers behind it, each with its request count, its\nauthentication-failure count, how many distinct paths it touched, and any\nverdict currently held against it.\n\nThe lane split is the answer to the question a generic bot filter cannot\nanswer: which of this traffic is the customer's own automation and which is\nsomebody working through a list. It is computed from credentials we issued, not\nfrom the client's self-description, so a scraper cannot move itself into the\nagent lane by editing a header.\n\nA validated caller appears as a FINGERPRINT — a one-way, per-process digest. It\nis stable enough to recognise the same caller across a minute and cannot be\nturned back into a key, so this report is safe to read, screenshot and paste.\n\nIt also reports what the sensor's own ceilings are doing (strain, tracked,\nceiling, refused) and how many screens the scorer did not answer (unscored), so\na control that has stopped measuring or a judge that has stopped answering is a\nnumber here rather than a quiet day.\n\nScoped to the caller's own validated organization. A SuperAdmin may inspect a\nspecific tenant with ?org=\u003cslug\u003e, or the lane that has no tenant — every caller\nthe identity boundary could not validate — with an empty ?org=.",
|
|
"inputSchema": {
|
|
"properties": {},
|
|
"type": "object"
|
|
},
|
|
"name": "gatewayTraffic"
|
|
},
|
|
{
|
|
"description": "Read returns the EFFECTIVE edge policy the caller is subject to: the platform CORS\nallowlist and pre-auth per-IP flood cap in force, plus the caller's own authenticated\nrate ceiling, edge-cache TTLs and accepted-method allowlist. A SuperAdmin may inspect\na specific tenant's effective policy with ?org=\u003cslug\u003e.",
|
|
"inputSchema": {
|
|
"properties": {},
|
|
"type": "object"
|
|
},
|
|
"name": "get_v1_gateway_config"
|
|
},
|
|
{
|
|
"description": "Write updates one policy scope and returns the policy in force after the write.\nA body carrying any PLATFORM field (cors_origins, per_ip_rpm, window_sec) is a\nplatform write and requires SuperAdmin; otherwise it is a per-org write (org_rpm,\ncache_ttl_sec, cache_paths, methods) scoped to the caller's own org — or, for a\nSuperAdmin, the tenant named by ?org=\u003cslug\u003e. A body that sets nothing is a 400.\nThe abuse gate's mode is an OPERATOR field: setting it requires SuperAdmin,\nwhichever organization it lands on. updated_at and updated_by are\nserver-stamped; a client-supplied value is ignored.",
|
|
"inputSchema": {
|
|
"properties": {
|
|
"cache_paths": {
|
|
"additionalProperties": {
|
|
"type": "integer"
|
|
},
|
|
"description": "CachePaths overrides CacheTTLSec per path PREFIX (key \"/v1/models\" → seconds).\nThe longest matching prefix wins.",
|
|
"type": "object"
|
|
},
|
|
"cache_ttl_sec": {
|
|
"description": "CacheTTLSec is the org's default edge-cache TTL for its responses, in seconds;\n0 means no caching. Unset inherits the platform default.",
|
|
"type": "integer"
|
|
},
|
|
"cors_origins": {
|
|
"description": "CORSOrigins is the PLATFORM-scope CORS allowlist EdgeCORS admits: an exact\norigin, a bare host, or a \"*.host\" wildcard. Writable only by a SuperAdmin —\nCORS is evaluated before identity, so it has no tenant to scope to.",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"type": "array"
|
|
},
|
|
"methods": {
|
|
"description": "Methods is the allowlist of HTTP methods the edge accepts for this org. Empty\nmeans all are accepted.",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"type": "array"
|
|
},
|
|
"mode": {
|
|
"description": "Mode is the abuse gate's posture for THIS scope: \"shadow\" scores traffic and\nrecords the verdict without acting on it, \"live\" enforces it. Unset means\nshadow.\n\nIt is the one per-org field that does NOT inherit. Every other field here\nlayers a platform default under the org's own value, which is right for a\ndefault: a tenant that sets no rate ceiling should get the platform's. Mode\nis not a default, it is an ARMING DECISION — it is what makes a statistical\njudgement start refusing real traffic — and inheriting it means arming one\nscope arms every tenant that never asked for it, without a write to their\nrow and without anything in their config changing. So a tenant is live only\nif that tenant's OWN row says live, and the platform row's mode governs\nexactly one scope: the anonymous lane, which has no tenant of its own.\n\nIt is also not self-service. Writing it requires SuperAdmin (see the\n/v1/gateway config op): the subject of an abuse control does not get to\nswitch the control off.",
|
|
"type": "string"
|
|
},
|
|
"org_rpm": {
|
|
"description": "OrgRPM is the org's OWN authenticated rate ceiling, requests per minute, as\nScopeRateLimit enforces it. Unset inherits the platform default, then the\nstatic boot default.",
|
|
"type": "integer"
|
|
},
|
|
"per_ip_rpm": {
|
|
"description": "PerIPRPM is the PLATFORM-scope pre-auth flood cap: requests EdgeRateLimit\nadmits per WindowSec from one client IP. SuperAdmin-only, same reason.",
|
|
"type": "integer"
|
|
},
|
|
"updated_at": {
|
|
"description": "UpdatedAt is the unix second this policy row was last written. Server-stamped;\na client-supplied value is ignored.",
|
|
"type": "integer"
|
|
},
|
|
"updated_by": {
|
|
"description": "UpdatedBy is the validated user id that wrote this policy row. Server-stamped;\na client-supplied value is ignored.",
|
|
"type": "string"
|
|
},
|
|
"window_sec": {
|
|
"description": "WindowSec is the window PerIPRPM is counted over, in seconds. SuperAdmin-only.",
|
|
"type": "integer"
|
|
}
|
|
},
|
|
"type": "object"
|
|
},
|
|
"name": "put_v1_gateway_config"
|
|
}
|
|
]
|