fix: let daemon finalize disputes without solver row#746
Conversation
Admin settle/cancel now bypass read-write category checks when event.identity matches the Mostro daemon key, consistent with admin_take_dispute. Human solvers still require category=2. Co-authored-by: Cursor <cursoragent@cursor.com>
WalkthroughAdds ChangesDispute Finalization Authorization Refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/db.rs (1)
1005-1026: 🏗️ Heavy liftMake this helper enforce the full finalization invariant.
ensure_dispute_finalize_permissionshort-circuits oncaller_pubkey == admin_pubkey, so the "assigned solver first" rule still lives entirely in every caller. Since this ispuband reads like the complete dispute-finalization gate, that split contract is easy to misuse later and would silently widen the daemon bypass. I'd either fold the assignment check into this helper or rename it to make the precondition explicit, and add focusedmod testscoverage for the daemon-without-user-row and human-solver-denied cases.As per coding guidelines, "Co-locate tests in their modules under
mod testsblocks with descriptive names."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/db.rs` around lines 1005 - 1026, The helper ensure_dispute_finalize_permission currently short-circuits for admin_pubkey and skips the "assigned solver" invariant; change it to first verify assignment by calling is_assigned_solver(pool, caller_pubkey, order_id) (or the existing assignment-check helper) and only then allow the admin bypass of solver-category checks (i.e., require assignment for both human solvers and the daemon), then fall back to solver_has_write_permission for human permission checks; also add co-located tests in a mod tests block covering at least (1) daemon identity without a user row (daemon-without-user-row) and (2) a human solver that is assigned but denied write permission (human-solver-denied) to validate the new invariant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/db.rs`:
- Around line 1005-1026: The helper ensure_dispute_finalize_permission currently
short-circuits for admin_pubkey and skips the "assigned solver" invariant;
change it to first verify assignment by calling is_assigned_solver(pool,
caller_pubkey, order_id) (or the existing assignment-check helper) and only then
allow the admin bypass of solver-category checks (i.e., require assignment for
both human solvers and the daemon), then fall back to
solver_has_write_permission for human permission checks; also add co-located
tests in a mod tests block covering at least (1) daemon identity without a user
row (daemon-without-user-row) and (2) a human solver that is assigned but denied
write permission (human-solver-denied) to validate the new invariant.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 054d4062-49b2-48af-ad40-e90ab177671b
📒 Files selected for processing (4)
src/app/admin_cancel.rssrc/app/admin_settle.rssrc/db.rssrc/rpc/service.rs
There was a problem hiding this comment.
Code Review
Looks good overall.
What changed here is consistent with the existing authorization model: the daemon can now finalize disputes without needing a solver row in users, while human solvers still need the same write permission as before.
One suggestion for follow-up: add a regression test that covers admin-settle / admin-cancel with the daemon identity when no solver row exists, so this auth path stays protected.
Reviewed by Hermes Agent
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/db.rs`:
- Line 1994: Run rustfmt (cargo fmt) and reformat the failing assertion so it
matches project style; specifically update the assert! invocation using the
symbol assert!(result.is_ok(), "assigned daemon must finalize without a users
row") to the rustfmt-canonical formatting produced by cargo fmt and commit that
change so CI passes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/db.rs (1)
1980-2019: ⚡ Quick winConsider adding test cases for complete coverage.
The current tests verify the daemon bypass and read-only solver denial. For completeness, consider adding:
- Human solver with write permission succeeds — a solver with
category=2should be allowed to finalize.- Unassigned caller is rejected — should return
IsNotYourDispute.🧪 Suggested additional test cases
async fn insert_write_solver(pool: &SqlitePool, pubkey: &str) { sqlx::query( "INSERT INTO users (pubkey, is_solver, category, created_at) VALUES (?1, 1, 2, 1700000000)", ) .bind(pubkey) .execute(pool) .await .unwrap(); } #[tokio::test] async fn ensure_dispute_finalize_permission_human_solver_with_write_ok() { let pool = setup_finalize_permission_db().await.unwrap(); let order_id = uuid::Uuid::new_v4(); insert_write_solver(&pool, HUMAN_SOLVER_PUBKEY).await; insert_assigned_dispute(&pool, order_id, HUMAN_SOLVER_PUBKEY).await; let result = super::ensure_dispute_finalize_permission( &pool, HUMAN_SOLVER_PUBKEY, DAEMON_PUBKEY, order_id, ) .await; assert!( result.is_ok(), "assigned solver with write permission must finalize" ); } #[tokio::test] async fn ensure_dispute_finalize_permission_unassigned_caller_denied() { let pool = setup_finalize_permission_db().await.unwrap(); let order_id = uuid::Uuid::new_v4(); // Dispute assigned to daemon, but caller is human solver insert_assigned_dispute(&pool, order_id, DAEMON_PUBKEY).await; let result = super::ensure_dispute_finalize_permission( &pool, HUMAN_SOLVER_PUBKEY, DAEMON_PUBKEY, order_id, ) .await; assert!(matches!( result, Err(MostroError::MostroCantDo(CantDoReason::IsNotYourDispute)) )); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/db.rs` around lines 1980 - 2019, Add two tests to cover the missing branches of ensure_dispute_finalize_permission: create insert_write_solver (as suggested) and add ensure_dispute_finalize_permission_human_solver_with_write_ok which inserts a users row with category=2, assigns the dispute to HUMAN_SOLVER_PUBKEY via insert_assigned_dispute, calls ensure_dispute_finalize_permission with HUMAN_SOLVER_PUBKEY as caller and asserts Ok; and add ensure_dispute_finalize_permission_unassigned_caller_denied which assigns the dispute to DAEMON_PUBKEY, calls ensure_dispute_finalize_permission with HUMAN_SOLVER_PUBKEY as caller and asserts Err(MostroError::MostroCantDo(CantDoReason::IsNotYourDispute)). Ensure both tests use setup_finalize_permission_db(), new uuid order_id, and the same helper names insert_assigned_dispute and HUMAN_SOLVER_PUBKEY/DAEMON_PUBKEY to match existing tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/db.rs`:
- Around line 1980-2019: Add two tests to cover the missing branches of
ensure_dispute_finalize_permission: create insert_write_solver (as suggested)
and add ensure_dispute_finalize_permission_human_solver_with_write_ok which
inserts a users row with category=2, assigns the dispute to HUMAN_SOLVER_PUBKEY
via insert_assigned_dispute, calls ensure_dispute_finalize_permission with
HUMAN_SOLVER_PUBKEY as caller and asserts Ok; and add
ensure_dispute_finalize_permission_unassigned_caller_denied which assigns the
dispute to DAEMON_PUBKEY, calls ensure_dispute_finalize_permission with
HUMAN_SOLVER_PUBKEY as caller and asserts
Err(MostroError::MostroCantDo(CantDoReason::IsNotYourDispute)). Ensure both
tests use setup_finalize_permission_db(), new uuid order_id, and the same helper
names insert_assigned_dispute and HUMAN_SOLVER_PUBKEY/DAEMON_PUBKEY to match
existing tests.
There was a problem hiding this comment.
Strict review on current head 1e4e5d8309f5dbbb6b3b4fa31518db8b5ba93f93.
I re-ran the relevant checks on this head:
cargo test ensure_dispute_finalize_permission -- --nocapturecargo clippy --all-targets --all-features -- -D warningscargo fmt --check
I don't see a remaining bug here.
The core behavior change is consistent across the stack:
- RPC admin cancel/settle synthesize the daemon pubkey as the caller identity.
ensure_dispute_finalize_permission()still requires the dispute to be assigned to that identity.- The daemon identity then bypasses the
users.category = 2solver-row requirement, matching the existingadmin_take_disputemodel instead of failing only because the daemon has no solver user row. - Human solvers remain gated by read-write solver permission, and the new tests cover both the daemon-allowed and human-read-only-denied paths.
APPROVE.
Summary
admin-settleandadmin-cancelrejected the Mostro daemon withNotAuthorizedafter it took a dispute, becausesolver_has_write_permissionrequired ausersrow withis_solverandcategory = 2. That was inconsistent withadmin_take_dispute, which already allows the daemon key without DB registration.This PR adds
ensure_dispute_finalize_permission: the daemon identity bypasses solver category checks; human solvers still need read-write permission on the assigned dispute.Changes
ensure_dispute_finalize_permissioninsrc/db.rsadmin_settle_actionandadmin_cancel_actionSummary by CodeRabbit
Refactor
Documentation
Tests