Skip to content

fix(engine): normalize actionClass once and read policies/buckets own-property in write-rate-limit - #10073

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
RealDiligent:fix/critical-issue-governor-normalize-9999
Jul 31, 2026
Merged

fix(engine): normalize actionClass once and read policies/buckets own-property in write-rate-limit#10073
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
RealDiligent:fix/critical-issue-governor-normalize-9999

Conversation

@RealDiligent

Copy link
Copy Markdown
Contributor

Summary

writeRateLimitRepoKey normalizes actionClass (.trim()) for the per-repo bucket key, but evaluateWriteRateLimit, recordWriteRateLimitAllowed, recordWriteRateLimitDenied and clearWriteRateLimitBackoff used the raw value for the global bucket key and both policyFor lookups. Consequences:

  • One stray whitespace character defeats the limit. " open_pr " resolves the per-repo key trimmed, but the global bucket and both policies fall through to PERMISSIVE_CONFIG's 1_000_000/min ceiling on the raw string — so the write is effectively unlimited on both scopes.
  • A decision and its recorded state diverge. recordWriteRateLimitAllowed keyed the global bucket under the raw " open_pr " while evaluateWriteRateLimit read it under… whatever it was passed, so the two scopes' bookkeeping split permanently for that action class.
  • Prototype-chain lookup. policies.global/buckets.global are plain records, so table[actionClass] with actionClass === "constructor" (or "toString"/"valueOf") resolves an inherited member — policyFor returned Object.prototype.constructor and the write was scored against a fabricated limit: 0 (denied) instead of PERMISSIVE_CONFIG.

Fix

  • Each of the four functions normalizes actionClass once at the top and uses that single value for the global bucket key, the per-repo key, and both policy lookups — the same .trim() writeRateLimitRepoKey already applies.
  • recordWriteRateLimitAllowed keys the global bucket by the normalized class, so a decision and the state written for it always address the same bucket.
  • policyFor and both bucket reads go through an ownValue(table, key) helper (Object.hasOwn(...) ? table[key] : undefined), so an actionClass naming an Object.prototype member resolves the intended fallback.

writeRateLimitRepoKey's output, DEFAULT_WRITE_RATE_LIMIT_POLICIES, PERMISSIVE_CONFIG's fallback role, incrementBucket's window math, the blockedBy precedence, retryAfterMs composition, and the ledger-event shape are all unchanged.

Tests

A regression test in packages/loopover-engine/test/write-rate-limit-enforcement.test.ts covering all four bullets: " open_pr " resolves the same 30/3 limits as "open_pr"; recordWriteRateLimitAllowed(_, " open_pr ", …) keys the global bucket under "open_pr" and a later evaluate observes the count; actionClass: "constructor" returns allowed: true / under_limit with PERMISSIVE_CONFIG limits (today: allowed: false, limit: 0); and a backoff recorded with " open_pr " is cleared by "open_pr". Three of the four bullets fail against the current raw-lookup code; existing assertions pass unchanged. Engine-flag coverage of the changed lines verified (source-mapped lcov).

Closes #9999

@RealDiligent
RealDiligent requested a review from JSONbored as a code owner July 31, 2026 06:23
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-31 06:47:06 UTC

2 files · 1 AI reviewer · no blockers · readiness 100/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This fixes a real bug in write-rate-limit.ts: actionClass was trimmed for the per-repo key but used raw for the global bucket key and policy lookups, letting a whitespace-padded actionClass fall through to the 1,000,000/min PERMISSIVE_CONFIG, and letting decisions/recorded state diverge across scopes. The fix normalizes actionClass once per function and adds an ownValue() helper to prevent prototype-chain lookups (e.g. actionClass='constructor') from resolving inherited Object.prototype members instead of the intended fallback. The change is correct, narrowly scoped to the four affected functions, and is backed by a solid regression test covering all three failure modes plus a mirrored vitest suite in the existing test tree.

Nits — 5 non-blocking

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9999
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 239 registered-repo PR(s), 89 merged, 38 issue(s).
Contributor context ✅ Confirmed Gittensor contributor RealDiligent; Gittensor profile; 239 PR(s), 38 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff normalizes actionClass exactly once at the top of each of the four functions, uses that single value consistently for global bucket key, per-repo key, and both policy lookups, and adds an ownValue helper using Object.hasOwn to avoid prototype-chain lookups in policyFor and both bucket reads. It also adds regression tests covering the padded-whitespace case, decision/recorded-state bucket

Review context
  • Author: RealDiligent
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 239 PR(s), 38 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

…-property in write-rate-limit

writeRateLimitRepoKey trimmed actionClass for the per-repo key, but evaluateWriteRateLimit,
recordWriteRateLimitAllowed, recordWriteRateLimitDenied and clearWriteRateLimitBackoff used
the RAW value for the global bucket key and both policy lookups. So " open_pr " resolved the
per-repo key trimmed while the global bucket and both policies fell through to
PERMISSIVE_CONFIG's 1_000_000 ceiling, and recordWriteRateLimitAllowed keyed the global
bucket under the raw string — permanently diverging a decision from the state written for it.
Separately, policyFor and the bucket reads used bare table[actionClass], so an actionClass
naming an Object.prototype member ("constructor"/"toString") resolved an inherited value
(a fabricated limit:0) instead of the intended fallback.

Normalize actionClass exactly once at the top of each function and use it for every lookup,
and read policies/buckets via an ownValue(Object.hasOwn) helper. writeRateLimitRepoKey's
output, DEFAULT_WRITE_RATE_LIMIT_POLICIES, PERMISSIVE_CONFIG's fallback role, and the verdict
shape are all unchanged.

Closes JSONbored#9999
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.88%. Comparing base (a7673e2) to head (1b0875c).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10073   +/-   ##
=======================================
  Coverage   91.88%   91.88%           
=======================================
  Files         930      930           
  Lines      113827   113843   +16     
  Branches    27466    27467    +1     
=======================================
+ Hits       104588   104610   +22     
+ Misses       7940     7930   -10     
- Partials     1299     1303    +4     
Flag Coverage Δ
backend 95.66% <100.00%> (-0.01%) ⬇️
engine 72.41% <100.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...s/loopover-engine/src/governor/write-rate-limit.ts 92.76% <100.00%> (+5.09%) ⬆️

... and 1 file with indirect coverage changes

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LoopOver approves — the gate is satisfied and CI is green.

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

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine(governor): evaluateWriteRateLimit normalizes actionClass for the per-repo key but not for the policy or global-bucket lookup

1 participant