Add POST /arena/vote write endpoint - #150
Conversation
Labeler-gated via X-Labeler-Key; voter_type is pinned server-side so a caller cannot claim to be a labeler. Records votes through ArenaStore, which upserts to keep one vote per identity per battle.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a ChangesArena Labeler Vote Endpoint
Sequence Diagram(s)sequenceDiagram
participant Client
participant ArenaRouter as POST /arena/vote
participant ArenaStore
participant Posthog
Client->>ArenaRouter: request + X-Labeler-Key header + VoteIn body
ArenaRouter->>ArenaRouter: _is_authenticated_labeler (hmac.compare_digest)
alt key mismatch or missing
ArenaRouter-->>Client: 403 Forbidden
end
ArenaRouter->>ArenaStore: get_battle(battle_id)
alt battle not found
ArenaRouter-->>Client: 404 Not Found
end
ArenaRouter->>ArenaStore: upsert_vote(VoterType.LABELER, VoteOutcome, voter_id)
ArenaStore-->>ArenaRouter: persisted Vote row
ArenaRouter->>Posthog: capture("vote_cast", {...})
ArenaRouter-->>Client: 201 VoteOut
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Stacks on the merged read endpoints (#147) to complete the arena vote loop's write side.
What
POST /v1/arena/voterecords a vote on a battle.X-Labeler-Keyheader (constant-time compare againstARENA_LABELER_KEY). A valid key votes aslabeler; missing/invalid key → 403 ("external voting is not enabled"). Fail-closed: if no key is configured, all votes are rejected.voter_typeis pinned server-side, never read from the body — a caller cannot claim to be a trusted labeler.ArenaStore.upsert_vote:ON CONFLICT (battle_id, voter_type, voter_id) DO UPDATEkeeps one current vote per identity (re-vote updates the row; theBEFORE UPDATEtrigger bumpsupdated_at).outcome→ 422;60/minuterate limit, matching the read endpoints.arena_vote_castcarries only{outcome, voter_type}— no identifiers, consistent with the other arena events.Scope (MVP, labeler-only)
externalvoting is intentionally not enabled yet — it needs a stable per-voter identity (session) for the dedup to be meaningful, plus its own rate strategy. Deferred to the voting-UI PR.voter_idis not validated; acceptable while the only caller is the trusted internal labeling tool.Note
ARENA_LABELER_KEYmust be set as a runner secret (Cloud Run / Secret Manager) before this works in a deployed env, or every vote returns 403.Summary by CodeRabbit
Release Notes
New Features
Tests
Greptile Summary
This PR completes the arena vote write path by adding
POST /v1/arena/vote, gated behind anX-Labeler-Keyheader verified with a constant-time compare and defaulting to 403 when unconfigured. Dedup is handled via anON CONFLICT DO UPDATEupsert, andvoter_typeis pinned server-side so callers can never self-promote to labeler._is_authenticated_labeler): useshmac.compare_digestagainst aSecretStr; the same 403 message is returned for a missing or wrong key, leaking no information about which condition was triggered.ArenaStore.upsert_voteperforms a singleINSERT … ON CONFLICT DO UPDATE RETURNING, with the battle-existence pre-check handled by a separateget_battlequery; the PostHog event carries only{outcome, voter_type}with no voter identifiers.voter_id, unknown battle, and invalid outcome.Confidence Score: 4/5
The write path is well-structured and the auth logic is correct; the two minor concerns do not affect normal operation.
The auth guard, dedup logic, and test coverage are solid. The two flagged items — a TOCTOU window between the battle existence check and the upsert, and duplicate outcome definitions across VoteIn and VoteOutcome — are both benign under current usage (battles are not deleted, the two sets are identical), but either could surface as a 500 in a future change rather than a clean 404 or 422.
runner/src/coval_bench/api/routers/arena.py around the two-query battle check and the VoteOutcome conversion; runner/src/coval_bench/api/schemas.py for the loose str types in VoteOut.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant C as Client participant R as POST /v1/arena/vote participant Auth as _is_authenticated_labeler participant Store as ArenaStore participant DB as PostgreSQL C->>R: "POST /v1/arena/vote {battle_id, outcome, voter_id} X-Labeler-Key" R->>Auth: check(x_labeler_key, settings) Auth-->>R: hmac.compare_digest result alt key missing or invalid R-->>C: 403 external voting is not enabled else key valid R->>Store: get_battle(battle_id) Store->>DB: "SELECT arena.battles WHERE id = ?" DB-->>Store: row or None alt battle not found R-->>C: 404 battle not found else battle exists R->>Store: upsert_vote(battle_id, outcome, LABELER, voter_id) Store->>DB: INSERT ON CONFLICT DO UPDATE RETURNING DB-->>Store: Vote row Store-->>R: Vote model R->>R: capture_api_event arena_vote_cast R-->>C: 201 VoteOut end end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant C as Client participant R as POST /v1/arena/vote participant Auth as _is_authenticated_labeler participant Store as ArenaStore participant DB as PostgreSQL C->>R: "POST /v1/arena/vote {battle_id, outcome, voter_id} X-Labeler-Key" R->>Auth: check(x_labeler_key, settings) Auth-->>R: hmac.compare_digest result alt key missing or invalid R-->>C: 403 external voting is not enabled else key valid R->>Store: get_battle(battle_id) Store->>DB: "SELECT arena.battles WHERE id = ?" DB-->>Store: row or None alt battle not found R-->>C: 404 battle not found else battle exists R->>Store: upsert_vote(battle_id, outcome, LABELER, voter_id) Store->>DB: INSERT ON CONFLICT DO UPDATE RETURNING DB-->>Store: Vote row Store-->>R: Vote model R->>R: capture_api_event arena_vote_cast R-->>C: 201 VoteOut end endReviews (1): Last reviewed commit: "Add POST /arena/vote write endpoint" | Re-trigger Greptile