Skip to content

fix: let daemon finalize disputes without solver row#746

Merged
grunch merged 3 commits into
mainfrom
fix/admin-finalize-daemon-bypass
May 22, 2026
Merged

fix: let daemon finalize disputes without solver row#746
grunch merged 3 commits into
mainfrom
fix/admin-finalize-daemon-bypass

Conversation

@arkanoider
Copy link
Copy Markdown
Collaborator

@arkanoider arkanoider commented May 22, 2026

Summary

admin-settle and admin-cancel rejected the Mostro daemon with NotAuthorized after it took a dispute, because solver_has_write_permission required a users row with is_solver and category = 2. That was inconsistent with admin_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

  • Add ensure_dispute_finalize_permission in src/db.rs
  • Use it from admin_settle_action and admin_cancel_action
  • Update RPC service comment (no longer requires registering daemon as solver)

Summary by CodeRabbit

  • Refactor

    • Centralized authorization for dispute finalization (cancel and settle); admin daemon identity bypasses solver checks while other callers must meet solver write permission.
  • Documentation

    • Clarified how admin RPC authorization is enforced for dispute cancellation.
  • Tests

    • Added unit tests covering the new permission behavior, including daemon bypass and denial scenarios.

Review Change Stack

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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Walkthrough

Adds ensure_dispute_finalize_permission to centralize dispute-finalization authorization, updates admin_cancel_action and admin_settle_action to call it, and adjusts RPC documentation to describe the downstream enforcement and daemon/admin bypass.

Changes

Dispute Finalization Authorization Refactoring

Layer / File(s) Summary
Centralized permission helper definition
src/db.rs
ensure_dispute_finalize_permission gates dispute finalization by requiring assigned-solver status, allowing the admin/daemon pubkey to bypass solver-category/write checks, otherwise requiring solver_has_write_permission; includes tests and in-test DB seeding helpers.
Admin dispute action authorization integration
src/app/admin_cancel.rs, src/app/admin_settle.rs
admin_cancel_action and admin_settle_action now call ensure_dispute_finalize_permission(...).await? instead of inline solver_has_write_permission checks; imports updated accordingly.
RPC documentation clarification
src/rpc/service.rs
Reworded comment in call_admin_cancel to state authorization is enforced downstream via assigned-solver requirement and daemon/admin pubkey bypass.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • MostroP2P/mostro#708: Adds/adjusts solver write-permission enforcement and DB authorization logic related to dispute finalization.
  • MostroP2P/mostro#463: Modifies dispute-finalization authorization checks in admin_cancel_action/admin_settle_action.
  • MostroP2P/mostro#497: Edits src/rpc/service.rs; related to RPC-side documentation/comments about admin cancel flows.

Suggested reviewers

  • Catrya
  • grunch

Poem

🐰 I hopped through code to mend the gate,
One helper now decides the fate.
Daemon key skips a cautious test,
Assigned solver still bears the rest.
A tidy hop — disputes checked, at best.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: let daemon finalize disputes without solver row' directly and concisely describes the main change—enabling the daemon to finalize disputes without requiring a solver database entry.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/admin-finalize-daemon-bypass

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/db.rs (1)

1005-1026: 🏗️ Heavy lift

Make this helper enforce the full finalization invariant.

ensure_dispute_finalize_permission short-circuits on caller_pubkey == admin_pubkey, so the "assigned solver first" rule still lives entirely in every caller. Since this is pub and 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 focused mod tests coverage for the daemon-without-user-row and human-solver-denied cases.

As per coding guidelines, "Co-locate tests in their modules under mod tests blocks 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d13db6 and 38861da.

📒 Files selected for processing (4)
  • src/app/admin_cancel.rs
  • src/app/admin_settle.rs
  • src/db.rs
  • src/rpc/service.rs

Copy link
Copy Markdown

@ermeme ermeme Bot left a comment

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c64488e3-f311-46f8-a567-b072ccd4308e

📥 Commits

Reviewing files that changed from the base of the PR and between 38861da and 4475a50.

📒 Files selected for processing (1)
  • src/db.rs

Comment thread src/db.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/db.rs (1)

1980-2019: ⚡ Quick win

Consider adding test cases for complete coverage.

The current tests verify the daemon bypass and read-only solver denial. For completeness, consider adding:

  1. Human solver with write permission succeeds — a solver with category=2 should be allowed to finalize.
  2. 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69a8730c-b954-45c4-a6fc-bc3a59ddeff0

📥 Commits

Reviewing files that changed from the base of the PR and between 4475a50 and 1e4e5d8.

📒 Files selected for processing (1)
  • src/db.rs

@arkanoider arkanoider requested review from Catrya and grunch May 22, 2026 20:51
Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

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

Strict review on current head 1e4e5d8309f5dbbb6b3b4fa31518db8b5ba93f93.

I re-ran the relevant checks on this head:

  • cargo test ensure_dispute_finalize_permission -- --nocapture
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo 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 = 2 solver-row requirement, matching the existing admin_take_dispute model 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.

@grunch grunch merged commit d3f5bfa into main May 22, 2026
8 checks passed
@grunch grunch deleted the fix/admin-finalize-daemon-bypass branch May 22, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants