diff --git a/api/app/throttle.py b/api/app/throttle.py index 660c076..90ec759 100644 --- a/api/app/throttle.py +++ b/api/app/throttle.py @@ -36,10 +36,28 @@ log = logging.getLogger(__name__) WINDOW = timedelta(days=1) -# action name -> the settings field holding its per-day base for a standard band +# Actions this module ACTUALLY enforces: they pass through our API, so we can +# both count and refuse them. ACTION_BASE: dict[str, str] = { "repo.create": "rate_repo_creates_per_day", "grant.create": "rate_grants_per_day", +} + +# ⚠️ DECLARED BUT NOT ENFORCEABLE HERE — and named, rather than quietly listed +# alongside the real ones. +# +# `git push` goes straight to Gitea over HTTPS and never touches this API, so +# nothing records a `push` action and a count of them would be zero forever. +# Listing these in ACTION_BASE (as this module first did) would make `enforce` +# look up a limit, count nothing, and allow everything — a silent no-op wearing +# the costume of a control. That is the same dead-code pattern this module was +# written to remove, and it is worse here because the config name implies the +# protection exists. +# +# Enforcing push velocity requires a Gitea-side hook (pre-receive or the push +# webhook) that reports into `agent_actions`. Until that exists these settings +# are inert, and saying so is the honest option. +NOT_ENFORCED_HERE: dict[str, str] = { "push": "rate_pushes_per_day", "push.force": "rate_force_pushes_per_day", } diff --git a/api/tests/test_ept_and_throttle.py b/api/tests/test_ept_and_throttle.py index 37361c2..33f707d 100644 --- a/api/tests/test_ept_and_throttle.py +++ b/api/tests/test_ept_and_throttle.py @@ -283,3 +283,30 @@ def test_routing_does_not_depend_on_signature_wellformedness(): assert not looks_like_ept("not-a-token") assert not looks_like_ept("") + + +def test_only_actions_that_route_through_this_api_are_claimed_enforced(): + """git push never touches this API, so a push limit here would count zero + forever and allow everything — a silent no-op wearing the costume of a + control. Push limits must stay in NOT_ENFORCED_HERE until a Gitea-side hook + reports pushes into agent_actions.""" + from api.app.throttle import ACTION_BASE, NOT_ENFORCED_HERE + + assert "push" not in ACTION_BASE + assert "push.force" not in ACTION_BASE + assert "push" in NOT_ENFORCED_HERE + assert not set(ACTION_BASE) & set(NOT_ENFORCED_HERE) + + +@pytest.mark.asyncio +async def test_enforce_refuses_an_action_it_cannot_actually_limit(): + """Asking to throttle 'push' must raise, not silently allow.""" + from api.app.auth import ActorType, Caller + from api.app.config import Settings + from api.app.errors import RepairPointer + from api.app.throttle import enforce + + caller = Caller(actor_type=ActorType.agent, passport="ET26-X", band="gold") + with pytest.raises(RepairPointer) as exc: + await enforce(None, Settings(), caller, "push") + assert exc.value.code == "throttle_unknown_action"