fix(accounts): harden removeAccount pointer normalization - #413
Conversation
Addresses oracle audit HIGH-3 finding (flagged as pre-existing during PR #399 review). When removing the currently active account while other accounts remain in the pool, pointers now advance to the next enabled account instead of defaulting to -1. Normalizes activeIndex, currentAccountIndexByFamily, and cursorByFamily consistently via a shared findNextEnabled helper. The helper walks forward (with modulo wrap) from a search origin and only returns -1 when every remaining account is disabled or the pool is empty. Tests cover: remove-at-last, remove-at-middle, remove-with-all-others-disabled, remove-until-empty, and multi-family isolation. Source: .sisyphus/notepads/phase1-audit/reports/pr399.json HIGH-3
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 37 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Move release notes from draft to docs/releases/v1.3.0.md - Bump version 1.2.7 -> 1.3.0 - Update README release-notes pointer (if applicable) - Update CHANGELOG.md (if present) Full post-audit Phase 1: 20 PRs + follow-up #413 + 7 audit fixes. Staging-merge validated: 3527 tests green, 8-check battery passing.
PR #413 hardened removeAccount pointer normalization so the current pointer no longer defaults to -1 when the active account is removed while other accounts remain in the pool. It did so by retargeting the pointer onto the successor account at the post-splice slot, which silently substitutes a different account for the caller's "current" without any audit trail. HI-04 (from .sisyphus/notepads/deep-audit/reports/accounts-rotation.json) flagged this as a correctness issue: getCurrentAccountForFamily() returns an account the caller never selected, with no lastSwitchReason indicating that the pool chose it. This masks pool-driven retargets in logs, dashboards, and session-affinity consumers that key on lastSwitchReason to distinguish user-selected from pool-selected identities. Fix: when removeAccount retargets the active pointer off the removed slot onto a new successor (priorActive[family] === idx), stamp lastSwitchReason="rotation" on that successor. This mirrors the existing convention already used by setActiveIndex() and markSwitched() for pool-driven selection events, so downstream observers see a consistent retarget signal instead of the successor's stale prior reason. Successors are deduped across families (lastSwitchReason is per-account, not per-family) via a Set, and the signal is only applied when we actually retargeted off the removed slot. If the pool collapses to no routable account (every remaining peer disabled), no successor is stamped and the pointer falls to -1 — matching the "no routable account" contract PR #413 established. Tests (test/accounts.test.ts "removed-current retarget signal (HI-04)"): - removing the currently active (middle) account stamps rotation on the successor and leaves untouched peers with their prior reason - removing the last enabled account when every remaining peer is disabled yields pointer=-1 and does NOT stamp any disabled peer - removing the currently active account with exactly one other enabled peer (plus a disabled peer that findNextEnabled must skip) lands on the enabled successor and stamps rotation only on it Source: .sisyphus/notepads/deep-audit/reports/accounts-rotation.json HI-04
Follow-up to PR #399 addressing the pre-existing HIGH-3 finding from the oracle audit.
PR #399 addressed all-disabled and cursorByFamily drift but intentionally deferred the removeAccount dangle (flagged as pre-existing, out of scope for PR #399). This PR closes that remaining HIGH.
See
.sisyphus/notepads/phase1-audit/reports/pr399.jsonfinding HIGH-3.Summary
When
removeAccountremoves the currently active account (e.g. the user's active index was at the last slot of the pool), the active pointer could collapse to-1even when other enabled accounts still remained. Rotation paths paper over this at runtime, but any caller reading the pointer directly would see "no active account" immediately after the remove.This PR adds a shared
findNextEnabledhelper and routes bothactiveIndexandcursorByFamilynormalization through it. Pointers now advance to the next enabled account after removal and only fall back to-1when the pool is empty or every remaining account is disabled.Changes
lib/accounts.tsfindNextEnabled(start)private helper (wraps via modulo).removeAccountsnapshots prior per-family pointer state, applies the splice, then usesfindNextEnabledto re-seat any pointer that was pointing at the removed slot or now dangles off the end.test/accounts.test.tsdescribe("active-account pointer dangle (audit HIGH-3)")with 5 regression cases:getCurrentAccountForFamilyreturnsnull.-1.Validation
npm test→ all 3423 tests pass (225 files).npm run typecheck→ exit 0.npm run lint→ exit 0.No changes outside
lib/accounts.tsandtest/accounts.test.ts. No type escape hatches (as any/@ts-ignore/@ts-expect-error).note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
closes audit HIGH-3:
removeAccountnow usesfindNextEnabled(modulo wrap, returns -1 only on empty/all-disabled pool) to re-seat both the active and cursor pointers when the removed slot was the active account, instead of unconditionally collapsing to -1. no type escapes, all 3423 tests pass.remaining findings are all P2: an unreachable defensive branch in the active-pointer normalization, a silent-return in the multi-family test that passes vacuously on single-family builds, and a missing
getActiveIndexForFamilyassertion in the all-disabled case that would reveal the pre-existing inconsistency betweengetActiveIndexForFamily's clamping fallback and the raw-1stored afterfindNextEnabledreturns no results. no property-based test was added totest/property/for the new walk logic despite fast-check being available.Confidence Score: 5/5
safe to merge — the core logic is correct and all remaining findings are P2 style/test gaps
all three comments are P2: one unreachable defensive branch, one silent-pass test risk on single-family builds, one missing assertion to pin internal state. none affect runtime correctness of the fix. the HIGH-3 regression is properly closed, no type escapes, 3423 tests pass.
test/accounts.test.ts — silent early-return and missing pointer assertion worth tightening before the test suite grows
Important Files Changed
findNextEnabledhelper and updatesremoveAccountto re-seat active pointers via modulo walk instead of defaulting to -1; logic is correct for the stated cases, one unreachable defensive branch noted (P2)Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[removeAccount called] --> B[indexOf account] B -- not found --> C[return false] B -- found idx --> D[snapshot priorCursor + priorActive per family] D --> E[splice accounts at idx] E --> F{pool empty?} F -- yes --> G[set all family pointers to -1, cursor to 0\nreturn true] F -- no --> H[for each MODEL_FAMILY] H --> I[cursor normalization\nshift-down if cursor > idx\nclamp to 0..length-1] I --> J{priorActive vs idx} J -- active > idx --> K[active -= 1\ntrack same account] J -- active === idx --> L[findNextEnabled start=min idx, length-1\nwrap-around walk] J -- active < idx --> M[active unchanged] L --> N{found enabled?} N -- yes --> O[active = candidate index] N -- no all-disabled --> P[active = -1] K --> Q{active >= length?} O --> Q M --> Q P --> Q Q -- yes defensive guard --> R[findNextEnabled 0] Q -- no --> S[currentAccountIndexByFamily = active] R --> S S --> T[return true]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(accounts): harden removeAccount poin..." | Re-trigger Greptile