- Home
- Skills
- Code Quality & Review
- Rival: Adversarial Second-Opinion Code Review
Rival: Adversarial Second-Opinion Code Review
Adversarial code reviews using a panel of structurally diverse models via OpenRouter or local Ollama.
Free
Works with the AI Agents you already use
Rival: Adversarial Second-Opinion Code Review
Example session with this skill installed
/rival --panel
Review billing.py:
import time
_cache = {}
def get_invoices(db, customer_id, page=1, per_page=50):
offset = page * per_page
rows = db.execute(
f"SELECT * FROM invoices WHERE customer_id = '{customer_id}' "
f"ORDER BY created_at LIMIT {per_page} OFFSET {offset}"
).fetchall()
return rows
def apply_discount(total, percent):
if percent > 100:
percent = 100
return round(total - total * percent / 100)
def cached_balance(db, customer_id, ttl=60):
hit = _cache.get(customer_id)
if hit and time.time() - hit[0] < ttl:
return hit[1]
balance = sum(r["amount"] for r in get_invoices(db, customer_id))
_cache[customer_id] = (time.time(), balance)
return balance
- Read your context and instructions
- Compiled the rival
Rival Panel Review (2 models, sequential chain)
Panel reduced to 2 models: google/gemma-4-31b-it:free was rate-limited (HTTP 429 after 5 retries), so it was skipped.
Models: nvidia/nemotron-3-ultra-550b-a55b:free → nvidia/nemotron-3-super-120b-a12b:free
Final reviewer: nvidia/nemotron-3-super-120b-a12b:free
Independent review of the original code
Below is a fresh walk‑through of the supplied snippet, grouped by the same categories the first reviewer used. I note where I agree, where I see a different severity or nuance, and I add issues that were not mentioned at all.
✅ Issues I agree with (same finding, same or similar severity)
| # | Issue (as noted by reviewer) | Why I agree |
|---|---|---|
| 1 | SQL Injection – direct interpolation of customer_id, per_page, offset | Classic injection; any control over those strings yields arbitrary SQL. |
| 2 | Pagination offset off‑by‑one (offset = page * per_page) | Page 1 skips the first per_page rows; the usual 1‑based formula is (page‑1)*per_page. |
| 3 | cached_balance only sums the first page | The helper is called with its defaults (page=1, per_page=50); >50 invoices are ignored. |
| 4 | Global cache not thread‑safe | Concurrent get/set on a plain dict can race, corrupt entries or lose updates. |
| 5 | Floating‑point money in apply_discount | Binary float cannot represent cents exactly; rounding errors accumulate. |
| 6 | Unbounded cache growth | Every distinct customer_id stays forever → OOM in long‑running services. |
| 7 | No pagination input validation | Negative or huge page/per_page produce bad offsets, DoS or wrong data. |
| 8 | apply_discount accepts negative percent | -50 → 150 % of total (a surcharge). |
| 9 | TTL logic inconsistent across callers | TTL is only a read‑time check; the stored entry has no expiry, so different callers see different “freshness”. |
| dermal | SELECT * fragility | If the DB driver returns tuples, r["amount"] fails; schema changes can break silently. |
⚠️ Issues where I see a different nuance or severity
| # | Reviewer’s claim | My take / severity adjustment |
|---|---|---|
| 5 | Floating‑point money – marked P1 | I would elevate this to P0 in any financial‑context code because a rounding error can lead to incorrect balances, failed audits, and regulatory penalties. |
| 9 | TTL inconsistency – marked P2 | While not a security bug, this is a logic bug that can cause stale data to be served indefinitely (if a caller with a large TTL wins the race). I’d keep it P2 but note that it can manifest as a subtle correctness issue in multi‑tenant services. |
| 10 | SELECT * – marked P3 | I agree it’s low‑priority if the DB driver always returns row‑objects with named columns (e.g., SQLAlchemy Core, sqlite3 Row). However, many DB‑API drivers return plain tuples; the code as written would raise TypeError. I’d keep it P3 but add a note that the code relies on a specific driver behavior. |
| 2 | Pagination offset – marked P1 | The bug is indeed a correctness issue (missing first page), but it does not lead to a crash or security exposure. I’d keep it P1. |
| 7 | No pagination validation – marked P2 | I agree; missing validation can cause DoS via huge offsets or negative offsets that error. |
🆕 New issues the first reviewer did not mention
| Category | Issue | Details & Impact | Suggested fix |
|---|---|---|---|
| Arithmetic / rounding | apply_discount uses round(val) (no ndigits) → rounds to nearest integer, discarding cents. | For money this is catastrophic: a $10.50 invoice with a 10 % discount becomes round(10.50 - 1.05) = round(9.45) = 9 → loses $0.45. | Use round(value, 2) or, preferably, Decimal quantisation (see floating‑point fix). |
| Type safety | No validation that total and percent are numeric (int/float/Decimal). | Passing a string (total="100") raises TypeError at runtime; passing None raises TypeError on arithmetic. | Add if not isinstance(total, (int, float, Decimal)): raise TypeError (or convert via Decimal(str(total))). |
| Negative totals | apply_discount does not guard against a negative total. | A negative total would produce a negative discount (i.e., increase the amount owed), which is likely unintended. | Clamp total to max(total, 0) or validate business rules upstream. |
| Non‑integer pagination args | If page or per_page are strings, the expression page * per_page repeats the string (e.g., "2"*50 → "222…") before being injected into SQL, causing a SQL syntax error or unexpected behaviour. | Leads to runtime exceptions and potentially exposes internal errors to users. | Convert to int early: page = int(page); per_page = int(per_page) and catch ValueError. |
| Extreme offset values | Even after clamping per_page, a huge page (e.g., page=10**9) yields an enormous OFFSET that forces the DB to scan and skip billions of rows → severe performance degradation / DoS. | Not caught by the existing validation if only per_page is bounded. | Add an upper bound on page (e.g., if page > 1_000_000: page = 1_000_000) or calculate offset safely with checks for overflow. |
| Cache stampede (thundering herd) | On a cache miss, all concurrent threads will compute cached_balance afresh, potentially hammering the DB. | Under high load this can spike DB latency and exacerbate the miss‑rate. | Consider a locking pattern (double‑checked locking or cachetool's Lock) or use functools.lru_cache with a threading.Lock wrapper. |
| Global cache visibility / test pollution | _cache is a module‑level mutable; unit tests that call cached_balance leave state that can affect subsequent tests. | Leads to flaky tests and hard‑to‑debug CI failures. | Expose a clear_cache() function for tests, or switch to a per‑instance cache (e.g., pass a cache object in). |
| Missing docstrings / type hints | The code lacks any documentation or type annotations, making IDE assistance and static analysis harder. | Lowers maintainability, especially in larger codebases. | Add docstrings and PEP‑484 type hints (def get_invoices(db: Connection, customer_id: str, page: int = 1, per_page: int = 50) -> List[sqlite3.Row]: …). |
| Assumption about row type | The code accesses r["amount"], assuming the driver returns a mapping‑like row (e.g., sqlite3.Row or SQLAlchemy Row). | If the driver returns a plain tuple, this raises TypeError: tuple indices must be integers or slices, not str. | Either enforce a row factory that returns mappings (db.row_factory = sqlite3.Row) or use positional indexing (r[1] after verifying column order). |
| No error handling for DB failures | If db.execute raises (e.g., timeout, constraint violation), the exception propagates outward uncaught, possibly exposing stack traces. | In a web service this could leak internal details or cause 500 responses. | Wrap the DB call in a try/except and translate to a domain‑specific error or log and return a safe default. |
| Cache entry stores a float balance | The cached value is a Python float (the result of sum). Combined with the floating‑point issue above, the cached value can drift from the true decimal amount over time. | Even if the TTL expires, the stored value is already imprecise. | Store the balance as a Decimal (or integer cents) in the cache. |
| TTL of zero or negative | If ttl <= 0, the condition time.time() - hit[0] < ttl is never true, so the cache is effectively disabled but still stores entries that are never used. | Wastes memory. | Guard: if ttl <= 0: return sum(...) or skip caching altogether. |
Hard‑coded default per_page=50 in cached_balance | The balance function cannot be tuned without changing the source; callers who need a different page size must reimplement the sum. | Limits flexibility. | Add per_page (and maybe page) as parameters to cached_balance or provide a dedicated get_all_invoices helper. |
| Potential division by zero in discount calculation | Not present directly, but if percent is a very large number, total * percent / 100 could overflow to inf (float) before the if percent > 100 clamp runs (because the clamp is after the multiplication in the original line? Actually the clamp is before the multiplication, so safe). | Minor, but worth noting if the order ever changes. | Keep the clamp before any arithmetic, as already done. |
Missing __all__ / public‑API definition | The module exposes _cache and helper functions that may be imported unintentionally. | Increases surface area for misuse. | Define __all__ = ["get_invoices", "apply_discount", "cached_balance"] and keep _cache private (already is). |
📌 Summary
Core security & correctness problems (SQL injection, pagination offset, incomplete balance, thread‑unsafe cache, floating‑point money) are all correctly identified by the first reviewer.
I agree with all of those findings, though I would raise the
floating‑point money issue to
P0 and note a few severity adjustments.
Newly discovered problems include the integer‑rounding bug in apply_discount, missing type/range checks for inputs, potential cache‑stampede, test pollution due to the global cache, and assumptions about row‑type that could cause runtime exceptions.
- Addressing the new items—especially fixing the rounding to cents, validating/coercing pagination arguments, and making the cache thread‑safe with a bounded size—will bring the code to a production‑ready state for a financial‑oriented service.
Feel free to let me know if you’d like a concrete patch that incorporates all of the above fixes.
Individual reviewer outputs
Reviewer 1: nvidia/nemotron-3-ultra-550b-a55b:free
1. SQL Injection — Critical (Lines 7-9)
f"SELECT * FROM invoices WHERE customer_id = '{customer_id}' "
f"ORDER BY created_at LIMIT {per_page} OFFSET {offset}"
Impact: Arbitrary SQL execution. customer_id, per_page, offset are interpolated directly. An attacker controlling customer_id (e.g., "1' OR '1'='1") bypasses authorization, reads all invoices, or drops tables.
Fix: Use parameterized queries. Most DB-API drivers support ? or %s placeholders for values. For LIMIT/OFFSET, validate as integers first.
# Example fix (sqlite3 style)
offset = (page - 1) * per_page # also fixes bug #2
rows = db.execute(
"SELECT * FROM invoices WHERE customer_id = ? ORDER BY created_at LIMIT ? OFFSET ?",
(customer_id, per_page, offset)
).fetchall()
2. Pagination Offset Calculation Wrong (Line 6)
offset = page * per_page
Impact: Page 1 returns records 51–100 (offset=50), skipping the first page entirely. Page 0 would work but is non-standard.
Fix: offset = (page - 1) * per_page for 1-based paging. Validate page >= 1.
3. cached_balance Only Sums First Page (Line 25)
balance = sum(r["amount"] for r in get_invoices(db, customer_id))
Impact: get_invoices defaults to page=1, per_page=50. Customers with >50 invoices return an incorrect (low) balance. Silent data corruption.
Fix: Either paginate through all pages in cached_balance, or add a dedicated get_all_invoices / sum_invoices query:
balance = db.execute(
"SELECT COALESCE(SUM(amount), 0) FROM invoices WHERE customer_id = ?",
(customer_id,)
).fetchone()[0]
4. Global Cache Not Thread-Safe (Lines 1, 22, 27)
_cache = {} # module-level global
Impact: In a multi-threaded server (Flask, FastAPI, gunicorn workers with threads), concurrent reads/writes to _cache cause race conditions: corrupted entries, KeyError, or stale data returned indefinitely.
Fix: Use threading.Lock for the cache, or better, a thread-safe cache like functools.lru_cache (if single-process) or Redis/memcached for multi-process. At minimum:
import threading
_cache = {}
_cache_lock = threading.Lock()
def cached_balance(...):
with _cache_lock:
hit = _cache.get(customer_id)
...
_cache[customer_id] = (time.time(), balance)
5. Floating-Point Money in apply_discount (Line 16)
return round(total - total * percent / 100)
Impact: Binary floating-point cannot represent decimal fractions exactly. apply_discount(100.00, 33.33) → 66.67000000000001 → round() → 66.67 (sometimes), but edge cases like 2.675 round to 2.67 (banker's rounding). Financial audits will fail.
Fix: Use decimal.Decimal with fixed precision:
from decimal import Decimal, ROUND_HALF_UP
def apply_discount(total, percent):
total = Decimal(str(total))
percent = Decimal(str(percent))
if percent > 100:
percent = Decimal('100')
discount = total * percent / Decimal('100')
return (total - discount).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
6. No Cache Size Limit / Eviction (Line 1)
_cache = {}
Impact: Unbounded memory growth. Every unique customer_id ever queried stays in memory forever. Long-running process → OOM kill.
Fix: Use functools.lru_cache(maxsize=...) on a helper, or cachetools.TTLCache(maxsize=10000, ttl=60).
7. No Input Validation on Pagination (Lines 4-5)
def get_invoices(db, customer_id, page=1, per_page=50):
Impact: page=0 → negative offset (some DBs error, some return wrong data). page=-1 → huge positive offset (DoS via full table scan). per_page=1000000 → memory exhaustion.
Fix:
if page < 1: page = 1
if per_page < 1 or per_page > 1000: per_page = 50 # clamp
8. apply_discount Accepts Negative Percent (Line 13)
if percent > 100:
percent = 100
Impact: percent=-50 → 150% of total returned (surcharge). Likely unintended.
Fix: Clamp lower bound: percent = max(0, min(100, percent)).
9. Cache TTL Logic Inconsistent Across Callers (Lines 22-24)
hit = _cache.get(customer_id)
if hit and time.time() - hit[0] < ttl:
Impact: Caller A uses ttl=60, Caller B uses ttl=300. Entry cached at T=0. At T=100, Caller B sees 100 < 300 → returns 100s-stale data. Cache entry has no TTL metadata; TTL is a read-time policy.
Fix: Store TTL with entry, or use a cache library that handles this.
10. SELECT * Fragility (Line 7)
SELECT * FROM invoices ...
Impact: Schema change (column added/removed/reordered) breaks r["amount"] if amount is not explicitly selected.
Fix: SELECT id, amount, created_at, ... explicitly.
Summary of Fixes Priority
| Priority | Issue |
|---|---|
| P0 | SQL Injection (1) |
| P0 | cached_balance sums only first page (3) |
| P0 | Thread-unsafe global cache (4) |
| P1 | Pagination offset off-by-one (2) |
| P1 | Floating-point money (5) |
| P1 | Unbounded cache memory (6) |
| P2 | No pagination validation (7) |
| P2 | Negative discount (8) |
| P2 | TTL inconsistency (9) |
| P3 | SELECT * (10) |
Connects securely to your tools. The creator never sees your data.
About this skill
Code reviews often suffer from "LLM optimism," where agents overlook critical bugs or security flaws. Rival forces an adversarial perspective by routing your code through structurally diverse model families (whichever free families are healthy that day, such as Nemotron, Gemma, Qwen or Llama) to find what you missed.
What it does
- Adversarial Review identifies logic errors, off-by-one mistakes, and security vulnerabilities instead of giving generic praise.
- Multi-Model Panels chains up to three different models to build on each other's findings or provide independent blind reviews.
- Automated Roster Discovery ranks and caches available free-tier models by size and family diversity every 24 hours.
- Local Compute Support enables private reviews using local Ollama models when code cannot leave the machine.
- Task Delegation routes non-review requests to specific models without adversarial prompting for general implementation work.
How it works
- Rival Discovery runs a script that ranks healthy OpenRouter free models by size, one per family, and caches the roster for 24 hours (pinned paid models rank first). With --local it uses your Ollama model instead.
- Context Gathering pulls the current
git diffor specific file contents to prepare the review target. - Panel Execution calls models sequentially with built-in delays to respect free-tier rate limits, passing findings from one to the next.
- Consolidation presents a severity-ranked summary (CRITICAL to LOW) alongside the raw outputs from each reviewer.
Frameworks & tools
Uses Bash scripts, curl, and jq to interface with the OpenRouter API or local Ollama instances.
Why this beats prompting it yourself
Manual prompting rarely forces the model to disagree with itself or its peers. Rival automates the cross-family validation logic and handles the rate-limit delays required to get deep, multi-perspective reviews for free.
Use cases
- Pre-merge Audit to find edge cases in a complex PR before human review.
- Security Check to scan for injection or auth bypasses using diverse model logic.
- Offline Review to maintain productivity and code privacy using Ollama.
- Consensus Building to resolve technical disagreements via a three-model blind panel.
Known limitations
Limited to one file or diff at a time; for a whole-repo audit see Crucible. Free-tier models get rate-limited: panels space calls 8-10 seconds apart, and a model that stays at HTTP 429 is skipped and the reduced panel is announced. Code under review is sent to openrouter.ai unless you use --local.
How to use
Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill passes our trust checks below.
Security scanned
Passed our 8-point scan before listing
Fresh listing
Recently published to Agensi
Free forever
No account required to browse
Trust & safety
Security scanned
Verified clean 7 days ago
- Free to download with an account
Requires