[Bug]: Social rate-limiter's configuration.json override bypasses the documented hard ceiling
Description
SocialRateLimiter in core/framework/rate_limiter.py exists specifically to stop LLM-driven browser automation (LinkedIn invites, X DMs/posts, Instagram DMs, etc.) from getting real user accounts banned. The module docstring is explicit about the guarantee:
Limits are conservative defaults with hard ceilings. Users can raise defaults via env vars ..., but never past the ceiling.
That guarantee doesn't hold for the configuration.json override path — a value written there is honored uncapped, and the enforcement function (SocialRateLimiter.check()) actually allows actions past the documented hard ceiling as a result.
Location
# core/framework/rate_limiter.py
def _resolve_limit(platform: str, action: str, window: str) -> int | None:
"""Read the effective limit for *window* (``hourly``, ``daily``, or ``weekly``).
Priority: env var > configuration.json > default. All clamped to ceiling.
"""
...
env_val = os.environ.get(env_name)
if env_val is not None:
try:
return _clamp(int(env_val), ceiling) # <-- env var: clamped
except ValueError:
pass
cfg = _get_config_overrides()
cfg_key = f"{platform.lower()}.{action.lower()}.{window}"
if cfg_key in cfg:
try:
return max(1, int(cfg[cfg_key])) # <-- configuration.json: NOT clamped
except (ValueError, TypeError):
pass
return defaultThe env-var branch calls _clamp(value, ceiling). The configuration.json branch, right below it, only enforces a floor of 1 and never calls _clamp — despite the function's own docstring saying "All clamped to ceiling."
A second, related gap: PUT /api/config/rate-limits (core/framework/server/routes_config.py::handle_update_rate_limits) is the documented way to write these overrides. Its docstring also says "Values are clamped to the hard ceiling", but the implementation only appends an advisory warning when a value exceeds the ceiling — it still persists the uncapped value to configuration.json:
if ceiling is not None and val > ceiling:
warnings.append(f"... exceeds recommended max of {ceiling} ...")
cleaned[key] = val # persisted uncapped regardlessSo there's no enforcement point in this whole path — not on write, not on read.
Steps to Reproduce
Verified directly against the real module (unmodified core/framework/rate_limiter.py), not a mock:
import os, time
os.environ["HIVE_HOME"] = "/tmp/hivehome"
os.makedirs("/tmp/hivehome", exist_ok=True)
with open("/tmp/hivehome/configuration.json", "w") as f:
f.write('{"rate_limits": {"linkedin.invite.daily": 999999}}')
from framework.rate_limiter import _resolve_limit, SocialRateLimiter, _LIMITS
ceiling = _LIMITS[("linkedin", "invite")]["daily_max"] # 125
print(_resolve_limit("linkedin", "invite", "daily")) # 999999 -- not clamped
limiter = SocialRateLimiter(db_path="/tmp/hivehome/social_rate_limits.db")
# seed 150 LinkedIn invites already sent in the last 24h (above the ceiling of 125)
con = limiter._connect()
now = time.time()
for i in range(150):
con.execute(
"INSERT INTO actions (platform, account_id, action_type, target_id, performed_at, session_id) VALUES (?,?,?,?,?,?)",
("linkedin", "acct1", "invite", None, now - 7200 - i, "seed"),
)
con.commit(); con.close()
print(limiter.check("linkedin", "acct1", "invite"))Actual output:
999999
{'allowed': True, 'hourly_count': 0, 'hourly_limit': 10,
'daily_count': 150, 'daily_limit': 999999, 'weekly_count': 150, 'weekly_limit': 200}allowed: True with 150 invites already sent today — 25 over the documented hard ceiling of 125 — and the 151st+ would be allowed too, since the effective limit is now 999999.
Impact
This is the guardrail whose entire job is preventing account-ban-triggering over-automation on social platforms. It can be silently defeated through the framework's own documented config surface (configuration.json / PUT /api/config/rate-limits), with both relevant docstrings ("never past the ceiling", "clamped to the hard ceiling") contradicted by the actual behavior. No privilege escalation or unusual access needed — just a normal config write that the API itself claims to sanitize.
Expected Behavior
_resolve_limit()'s configuration.json branch should call _clamp(value, ceiling) exactly like the env-var branch does, so the hard ceiling in _LIMITS is actually enforced regardless of where an override came from. handle_update_rate_limits() should clamp cleaned[key] to ceiling before persisting (keeping the warning as an informational note, not a silent no-op), so its own docstring becomes true.
Environment
- OS: Windows 11 (repro is platform-independent — pure logic bug, no OS-specific behavior involved)
- Python: 3.14.4
Additional Context
Checked core/tests/test_social_rate_limiter.py: it covers test_env_var_clamped_to_ceiling but has no equivalent test for the configuration.json path, consistent with this being an overlooked asymmetry between the two override branches rather than intentional behavior.
Happy to open a PR with the one-line fix in _resolve_limit(), the corresponding fix in handle_update_rate_limits(), and regression tests covering both paths, if this is a direction the maintainers want.
Source: aden-hive/hive