Skip to content

fix(go-policy): check rate-limit threshold before incrementing counter - #2021

Closed
Ken Tannenbaum (finnoybu) wants to merge 1 commit into
microsoft:mainfrom
aegis-initiative:fix/go-policy-rate-limit-check-then-increment
Closed

Ken Tannenbaum (finnoybu) wants to merge 1 commit into
microsoft:mainfrom
aegis-initiative:fix/go-policy-rate-limit-check-then-increment

Conversation

@finnoybu

Copy link
Copy Markdown
Contributor

Summary

checkRateLimit in agent-governance-golang/packages/agentmesh/policy.go:153-156 incremented the counter first and then compared:

state.count++
if state.count > rule.MaxCalls {
    return RateLimit
}
return Allow

Once the limit was reached, every subsequent call within the same window continued to increment the counter on its way to returning RateLimit. The counter grew unboundedly until the window expired.

The growth was internally harmless — the next window reset to 1 — but the counter no longer represented the admit/reject state, so any test or observability hook reading state.count after a deny would see values larger than MaxCalls.

Change

Reorder to check-then-increment, using >= so MaxCalls is the inclusive bound:

if state.count >= rule.MaxCalls {
    return RateLimit
}
state.count++
return Allow

A 3-call limit now goes: counter 0→1→2→3 across three allowed calls; the 4th call sees count >= 3, returns RateLimit, leaves count at 3.

Tests

New TestRateLimitCounterDoesNotGrowAfterDeny: log 3 allowed calls then 3 denied calls; assert state.count == 3 after each phase. The existing TestRateLimiting continues to pass — its semantics ("admit N, then deny") are unchanged.

go test -run TestRateLimit ./... -v
  --- PASS: TestRateLimiting (0.00s)
  --- PASS: TestRateLimitCounterDoesNotGrowAfterDeny (0.00s)

Note on REVIEW.md item #7

This is part 1 of REVIEW.md item #7. Part 2 — keying the rate limiter by (action, agent_id, tenant) so one noisy caller doesn't starve all others — ships as a separate PR because it requires threading context through to checkRateLimit and updating the in-tree caller (middleware.go:256 currently passes nil context).

Test plan

  • CI passes
  • Existing rate-limit tests continue to pass

Surfaced as part of the AEGIS Initiative review of the agent-governance-toolkit (HIGH severity, Go section). Filed by Ken Tannenbaum (AEGIS Initiative).

@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

TL;DR: 1 blocker, 0 warnings. The counter increment logic in checkRateLimit was incorrect, leading to unbounded growth after reaching the limit.

# Sev Issue Where
1 CRITICAL Counter incremented before limit check, causing incorrect state representation agent-governance-golang/packages/agentmesh/policy.go

Action items: Reorder the counter increment logic in checkRateLimit to check the limit before incrementing.

Warnings: No warnings found.

@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

Docs Sync

  • checkRateLimit() in policy.go -- missing docstring
  • README.md -- no section appears to require an update
  • CHANGELOG.md -- missing entry for behavioral change in rate-limiting logic

@github-actions github-actions Bot added the size/S Small PR (< 50 lines) label May 11, 2026
@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `policy.go`

policy.go

  • TestRateLimitCounterDoesNotGrowAfterDeny -- validates that the counter does not exceed MaxCalls after reaching the limit.

policy_test.go

  • No gaps identified. Test coverage looks good.

@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

No security issues found.

@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

API Compatibility

Severity Change Impact
High Modified behavior of checkRateLimit to check before incrementing state.count. This change alters the behavior of the rate-limiting logic. Code relying on the previous behavior (where state.count could grow unboundedly after reaching the limit) may break or behave differently.

@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ❌ Failed Issues detected
🛡️ Security Scan ✅ Passed No issues found
🔄 Breaking Changes ✅ Completed Analysis complete
📝 Docs Sync ✅ Completed Analysis complete
🧪 Test Coverage ✅ Completed Analysis complete

Verdict: ❌ Changes needed

@github-actions

Copy link
Copy Markdown

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential NONE
Overall MEDIUM

Automated check by AGT Contributor Check.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Security fix from finnoybu audit.

@imran-siddique

Copy link
Copy Markdown
Collaborator

Merge conflict after sibling PRs landed. Please rebase on main and force-push to resolve.

checkRateLimit incremented `state.count++` and then compared the new
value against rule.MaxCalls. Once the limit was reached, subsequent
calls within the same window continued to increment the counter on
their way to the RateLimit decision, growing it unboundedly for the
remainder of the window.

The growth was internally harmless (the counter is reset when the
window expires), but it meant the counter no longer reflected the
admit/reject state — a test or observability hook reading
state.count after a deny would see values larger than MaxCalls.

Reorder: compare BEFORE incrementing. Use `>=` so MaxCalls itself is
the inclusive bound (3 allowed → counter reaches 3 → next call gets
RateLimit without increment).

Adds TestRateLimitCounterDoesNotGrowAfterDeny: log 3 allowed calls,
3 denied calls, assert counter stays at 3.

Note: This is part 1 of REVIEW.md item #7. Part 2 — keying the rate
limiter by (action, agent_id, tenant) so one noisy caller doesn't
starve others — ships as a separate PR because it requires threading
context through to checkRateLimit and updating callers.
@imran-siddique

Copy link
Copy Markdown
Collaborator

Superseded by #2232 which applies this fix (check-before-increment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk size/S Small PR (< 50 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants