|
13 | 13 | All three algorithms support both memory and Redis backends with identical |
14 | 14 | semantics. The Redis backend uses atomic Lua scripts for each algorithm — |
15 | 15 | one round-trip per check with no race conditions. |
| 16 | +
|
| 17 | +Security contract — fail-open on error: |
| 18 | + Both hook methods (prompt_pre_fetch, tool_pre_invoke) catch all unexpected |
| 19 | + exceptions and allow the request through. This is a deliberate design |
| 20 | + choice: an internal engine failure (Rust panic, Redis timeout, config bug) |
| 21 | + must never block legitimate traffic. The trade-off is that a sustained |
| 22 | + engine failure silently disables rate limiting until the error is resolved. |
| 23 | + Operators should monitor for rate-limiter error logs and treat them as |
| 24 | + high-priority alerts. |
16 | 25 | """ |
17 | 26 |
|
18 | 27 | # Future |
@@ -165,6 +174,19 @@ def _select_most_restrictive( |
165 | 174 | ) -> tuple[bool, int, int, int, dict[str, Any]]: |
166 | 175 | """Select the most restrictive rate limit from multiple dimensions. |
167 | 176 |
|
| 177 | + Multi-dimension aggregation contract: |
| 178 | + - Any blocked dimension → overall result is blocked. |
| 179 | + - Among blocked dimensions: the one with the **lowest** retry_after |
| 180 | + (soonest unblock) determines the Retry-After header. This signals |
| 181 | + the next state change — the caller learns when at least one dimension |
| 182 | + will re-open, even if other dimensions remain blocked longer. An |
| 183 | + alternative (max) would guarantee success on retry but delays the |
| 184 | + first attempt and hides which dimension unblocked. This is a |
| 185 | + deliberate product-level choice shared by both the Python and Rust |
| 186 | + implementations. |
| 187 | + - Among allowed dimensions: the one with the fewest remaining requests |
| 188 | + determines the header values (closest to exhaustion). |
| 189 | +
|
168 | 190 | Args: |
169 | 191 | results: List of (allowed, limit, reset_timestamp, metadata) tuples. |
170 | 192 |
|
@@ -1173,6 +1195,8 @@ async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginC |
1173 | 1195 | return PromptPrehookResult(metadata=meta) |
1174 | 1196 |
|
1175 | 1197 | except Exception: |
| 1198 | + # Deliberate fail-open: engine errors must not block legitimate traffic. |
| 1199 | + # See module docstring "Security contract — fail-open on error". |
1176 | 1200 | logger.exception("RateLimiterPlugin.prompt_pre_fetch encountered an unexpected error; allowing request") |
1177 | 1201 | return PromptPrehookResult() |
1178 | 1202 |
|
@@ -1262,5 +1286,7 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo |
1262 | 1286 | return ToolPreInvokeResult(metadata=meta) |
1263 | 1287 |
|
1264 | 1288 | except Exception: |
| 1289 | + # Deliberate fail-open: engine errors must not block legitimate traffic. |
| 1290 | + # See module docstring "Security contract — fail-open on error". |
1265 | 1291 | logger.exception("RateLimiterPlugin.tool_pre_invoke encountered an unexpected error; allowing request") |
1266 | 1292 | return ToolPreInvokeResult() |
0 commit comments