69efb6ab95
Añade rate limiting de tool calls por room usando sliding window:
- tools/ratelimit.go: RateLimiter con sliding window per key (room),
Allow() para verificar/registrar llamadas, Cleanup() para limpiar
entries expiradas
- tools/registry.go: SetRateLimiter() y ExecuteForRoom() que verifica
el rate limit antes de ejecutar, logueando tool_rate_limited si excede
- internal/config/schema.go: ToolRateLimitCfg en SecurityCfg con
enabled, max_calls_per_min y cleanup_interval_s
- agents/runtime.go: inicializa rate limiter desde config y arranca
goroutine de cleanup periodico
- agents/commands.go: usa ExecuteForRoom en !tool command
Config YAML:
security:
tool_rate_limit:
enabled: true
max_calls_per_min: 10
Parte de issue 0019c (prompt injection hardening — rate limiting).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
71 lines
1.6 KiB
Go
71 lines
1.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// RateLimiter tracks tool call counts per key (typically roomID) using a
|
|
// sliding window. It is safe for concurrent use.
|
|
type RateLimiter struct {
|
|
maxCalls int
|
|
window time.Duration
|
|
mu sync.Mutex
|
|
buckets map[string][]time.Time
|
|
}
|
|
|
|
// NewRateLimiter creates a rate limiter that allows maxCalls per window per key.
|
|
func NewRateLimiter(maxCalls int, window time.Duration) *RateLimiter {
|
|
return &RateLimiter{
|
|
maxCalls: maxCalls,
|
|
window: window,
|
|
buckets: make(map[string][]time.Time),
|
|
}
|
|
}
|
|
|
|
// Allow checks whether a call for the given key is within the rate limit.
|
|
// If allowed, it records the call and returns true. Otherwise returns false.
|
|
func (rl *RateLimiter) Allow(key string) bool {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
cutoff := now.Add(-rl.window)
|
|
|
|
// Trim expired entries
|
|
calls := rl.buckets[key]
|
|
start := 0
|
|
for start < len(calls) && calls[start].Before(cutoff) {
|
|
start++
|
|
}
|
|
calls = calls[start:]
|
|
|
|
if len(calls) >= rl.maxCalls {
|
|
rl.buckets[key] = calls
|
|
return false
|
|
}
|
|
|
|
rl.buckets[key] = append(calls, now)
|
|
return true
|
|
}
|
|
|
|
// Cleanup removes stale entries for keys that have no recent calls.
|
|
// Should be called periodically to prevent memory growth.
|
|
func (rl *RateLimiter) Cleanup() {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
cutoff := time.Now().Add(-rl.window)
|
|
for key, calls := range rl.buckets {
|
|
start := 0
|
|
for start < len(calls) && calls[start].Before(cutoff) {
|
|
start++
|
|
}
|
|
if start >= len(calls) {
|
|
delete(rl.buckets, key)
|
|
} else {
|
|
rl.buckets[key] = calls[start:]
|
|
}
|
|
}
|
|
}
|