Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Changed
- **Rate limiter replaced with `governor` (GCRA, lock-free)** (`src/middleware/rate_limit.rs`): replaced the custom `Mutex<HashMap<String, Vec<Instant>>>` sliding-window implementation with the `governor` crate (GCRA algorithm). Enforcement is now lock-free (atomics) and O(1) per check — no more O(n) `Vec::retain` on every request, no more background cleanup task, no more TOCTOU race (closes #82 root cause). Adds optional `rate_limit_burst` field on `AgentPolicy` (defaults to `rate_limit` for full backward compatibility). Closes #98.

### Added
- **Kubernetes ConfigMap watcher** (`src/kubernetes.rs`, `src/reload.rs`, `src/config.rs`): Arbitus can now watch a Kubernetes ConfigMap via the K8s API and hot-reload its configuration on every `Apply` event — no `SIGUSR1` or 30-second polling required. Set `kubernetes.configmap_name` in `gateway.yml` to enable. Namespace defaults to the pod's own namespace (read from the projected service-account token). Built as an optional feature (`kubernetes`, enabled by default in the official image) using `kube 0.98` + `kube-runtime`. The Helm chart gains a `kubernetesWatcher` section that automatically provisions the Role + RoleBinding and injects the `kubernetes:` config block. Closes #132.

Expand Down
113 changes: 113 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ sha2 = "0.10"
hex = "0.4"
which = "7"
lru = "0.16.3"
governor = "0.6"
kube = { version = "0.98", features = ["client", "runtime"], optional = true }
k8s-openapi = { version = "0.24", features = ["v1_31"], optional = true }
regorus = "0.2"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Agent (Cursor, Claude, etc.)
- **Multiple upstreams** — route different agents to different MCP servers
- **Circuit breaker** — automatic upstream failure isolation with half-open recovery
- **Config hot-reload** — reload on `SIGUSR1`, automatically every 30 seconds, or event-driven via Kubernetes ConfigMap watcher
- **Lock-free rate limiting** — GCRA algorithm via `governor`; O(1) per check, configurable burst allowance
- **Metrics** — Prometheus-compatible `/metrics` endpoint with cost/token estimation
- **OpenTelemetry** — export traces to any OTLP backend (Jaeger, Tempo, Honeycomb, Datadog)
- **Dashboard** — `/dashboard` audit viewer with per-agent filtering
Expand Down
1 change: 1 addition & 0 deletions src/a2a/interceptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down
1 change: 1 addition & 0 deletions src/bin/arbitus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,7 @@ audits: []
allowed_tools: None,
denied_tools: vec![],
rate_limit: 60,
rate_limit_burst: None,
tool_rate_limits: std::collections::HashMap::new(),
upstream: None,
api_key: None,
Expand Down
5 changes: 5 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ pub struct AgentPolicy {
pub denied_tools: Vec<String>,
#[serde(default = "default_rate_limit")]
pub rate_limit: usize,
/// Maximum burst size for the agent rate limiter.
/// Defaults to `rate_limit` (entire quota usable as burst, matching the old sliding-window behaviour).
#[serde(default)]
pub rate_limit_burst: Option<usize>,
/// Per-tool rate limits (calls/min). Checked in addition to the global rate_limit.
#[serde(default)]
pub tool_rate_limits: HashMap<String, usize>,
Expand Down Expand Up @@ -757,6 +761,7 @@ pub(crate) fn make_agent(
allowed_tools: allowed.map(|v| v.into_iter().map(String::from).collect()),
denied_tools: denied.into_iter().map(String::from).collect(),
rate_limit,
rate_limit_burst: None,
tool_rate_limits: std::collections::HashMap::new(),
upstream: None,
api_key: None,
Expand Down
4 changes: 4 additions & 0 deletions src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 100,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down Expand Up @@ -1231,6 +1232,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 100,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: Some(upstream_name.to_string()),
api_key: None,
Expand Down Expand Up @@ -1381,6 +1383,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 100,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down Expand Up @@ -1608,6 +1611,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 100,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down
3 changes: 3 additions & 0 deletions src/live_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 60,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: Some(key.to_string()),
Expand All @@ -66,6 +67,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 60,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down Expand Up @@ -197,6 +199,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 60,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down
1 change: 1 addition & 0 deletions src/middleware/hitl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod tests {
allowed_tools: None,
denied_tools: vec![],
rate_limit: 60,
rate_limit_burst: None,
tool_rate_limits: HashMap::new(),
upstream: None,
api_key: None,
Expand Down
Loading
Loading