internal/sshd/ratelimit.go throttles authentication failures only, so once a
key authenticated nothing bounded how many commands it ran. The same
issue create was limited through the JSON API and unlimited over SSH — the
primary interface.
The limiter goes in Dispatch, not in a surface, for the reason the registry
itself exists: SSH, the API and the web spend one per-account budget and a caller
cannot refresh it by changing how it connects. limits.api_rate stays in front
of it, bounding a network source rather than an account.
limits.write_rate defaults to 60 a minute, burst 60; a negative value disables
it. A command is one token whatever it writes, so account import-bundle replays
a whole bundle for one, and only a loop of separate commands spends the budget.
Exempt, each for a reason: read-only commands; the runner protocol, since a build
streams its log in many small runner log calls and throttling those would
throttle CI; and the host CLI on the server, which has no account to key on and
is already root-equivalent. Pushes are unaffected — the hook path runs as the
host.
Refusals exit 4 and name both the rate and the wait.
TestWriteLimiter* cover the burst, the refill and per-account isolation.
TestWriteRateLimit spends a budget of four over SSH and checks the fifth write
is refused while reads still work and another account is unaffected;
TestWriteRateLimitSparesTheRunner runs a real build under a budget of two.
On the issue's open question — whether a pending account should write at all —
it already cannot: pendingAllowed (control.go:168) confines an unverified
account to email verify, email add, whoami and help. Confirmed rather
than assumed; no change needed.
Documented in the wiki's Admin page beside ssh_auth_rate.
Closes #148