@@ -315,6 +315,11 @@ AgentGate is deterministic. Agents never see tokens, calls go only to configured
315315| ` AGENTGATE_ADMIN_SECRET ` | Secret for admin API access | Yes |
316316| ` AGENTGATE_PUBLIC_URL ` | Base URL used to build the OAuth callback (default ` http://localhost:8080 ` ) | No |
317317| ` AGENTGATE_API_KEY_CACHE_TTL ` | Verified-key cache duration. Default ` 30s ` ; ` 0 ` disables caching. | No |
318+ | ` AGENTGATE_RATE_KEY_RPS ` | Requests per second per API key ID. Default ` 20 ` . Zero requires tenant admission disabled. | No |
319+ | ` AGENTGATE_RATE_KEY_BURST ` | Per-key token bucket burst. Default ` 40 ` . Zero requires tenant admission disabled. | No |
320+ | ` AGENTGATE_RATE_TENANT_RPS ` | Requests per second per tenant. Default ` 100 ` ; ` 0 ` disables this bucket. | No |
321+ | ` AGENTGATE_RATE_TENANT_BURST ` | Per-tenant token bucket burst. Default ` 200 ` ; ` 0 ` disables this bucket. | No |
322+ | ` AGENTGATE_TENANT_MAX_INFLIGHT ` | In-flight upstream requests per tenant. Default ` 32 ` ; ` 0 ` disables this cap. | No |
318323| ` AGENTGATE_BISCUIT_MAX_DURATION ` | Go duration per Biscuit Datalog world run. Default ` 250ms ` ; range ` 1ms ` through ` 5s ` , inclusive. Invalid values stop startup. | No |
319324| ` GITHUB_CLIENT_ID ` / ` GITHUB_CLIENT_SECRET ` | GitHub OAuth credentials | For GitHub OAuth |
320325| ` SLACK_CLIENT_ID ` / ` SLACK_CLIENT_SECRET ` | Slack OAuth credentials | For Slack OAuth |
@@ -332,6 +337,50 @@ SQLite stores a public ID and a peppered HMAC-SHA256, never the secret. Use `POS
332337Legacy ` ag_live_ ` rows keep their bcrypt hashes. Successful validation adds an indexed HMAC without changing the presented key.
333338Revoked rows never enter legacy bcrypt scans.
334339
340+ #### Request admission
341+
342+ API keys have no organization ID. The organization store binds admins, not agent keys.
343+ For explicitly scoped keys, tenant identity is the authorized ` on_behalf_of ` principal.
344+ Such keys share that principal's rate bucket and concurrency cap.
345+ Each key's bucket spans services and human principals.
346+ Wildcard keys allowing ` "*" ` use 1 key-scoped tenant for every principal.
347+ Different wildcard keys don't share human quotas.
348+ Each key selects at most 64 tenant identities from its fixed scope list.
349+ When the list exceeds 64 entries, only the first 63 retain shared human identity.
350+ The remaining principals share 1 key-scoped fallback tenant.
351+ Human and fallback identities use separate namespaces. No per-key tenant-tracking map is needed.
352+
353+ Rate settings accept finite, non-negative numbers. Bursts and concurrency caps require integers.
354+ Invalid environment values stop startup. Either zero rate or zero burst disables that bucket.
355+ Both key settings must stay positive while tenant rate limiting or tenant concurrency is enabled.
356+ Per-service limits are optional. Set ` rate_limit ` under a service in the loaded registry YAML.
357+ They apply per API key and service, alongside the global limits.
358+ See [ registry rate limits] ( docs/registry.md#rate-limits ) for YAML examples.
359+
360+ The key bucket runs after API key authentication and body validation.
361+ In-memory service and principal scope checks run next.
362+ Scope denials consume only key tokens. Their receipts retain ` scope_denied ` .
363+ Tenant and service buckets run after scope checks, before delegation verification or vault access.
364+ Rate denials return HTTP 429 with ` rate_limited ` .
365+ ` Retry-After ` rounds the reservation delay up to whole seconds, with a minimum of 1.
366+ Concurrency admission runs immediately before upstream dispatch.
367+ Full tenants return HTTP 429 with ` concurrency_limited ` and ` Retry-After: 1 ` .
368+ Concurrency rejection refunds the key, tenant, and service charges without losing elapsed refill.
369+ Slots remain held through response completion, including SSE streams and receipt commits.
370+ Failures, disconnects, stream termination, and dispatch panics release them.
371+ Every authenticated, schema-valid attempt still commits a receipt before its response, including admission denials.
372+ Both admission errors record policy ` rate_limited ` . Receipt failures return ` receipt_failed ` instead.
373+
374+ Admission state is process-local. Replicas don't share quotas.
375+ The combined bucket and semaphore cache holds at most 100000 entries.
376+ Requests trigger idle sweeps at most once per minute. Entries idle for 10 minutes become eviction candidates.
377+ Only full buckets without pending permits or in-flight requests can be evicted.
378+ Slow buckets retain their quota until fully refilled.
379+ At capacity, admission scans up to 64 entries from the LRU tail.
380+ Pinned or depleted entries move to the front. The first evictable entry is replaced.
381+ If none qualifies, admission fails closed with HTTP 429 and ` Retry-After: 1 ` .
382+ Restart the gateway after changing admission settings or service YAML.
383+
335384#### Biscuit token security caps
336385
337386Tokens are limited to 4096 bytes and 8 attenuation blocks.
@@ -344,7 +393,7 @@ Authority checks follow the same caps.
344393
345394These checks run before authorizer construction. Rejections match both ` delegation.ErrDenied ` and ` delegation.ErrTokenShape ` .
346395Errors contain fixed reasons, not token contents.
347- The structural caps bound fact joins and block processing before the request reaches the rate limiter .
396+ The early rate limiter runs before these checks. Structural caps then bound fact joins and block processing .
348397Biscuit-go v2.2.0 deadlines do not cover check evaluation and cannot reliably stop timed-out goroutines.
349398Runtime limits alone do not bound per-request CPU.
350399
0 commit comments