-
Notifications
You must be signed in to change notification settings - Fork 48
Add read and read-write dispute solver permissions #708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5a6769a
cfac7ad
08fe6aa
c4c43fe
1d4261a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # Solver Permission Levels | ||
|
|
||
| This document specifies solver permission levels for dispute resolution. | ||
|
|
||
| ## Summary | ||
|
|
||
| Mostro supports two solver permission levels: | ||
|
|
||
| - `read`: solver can take a dispute, receive dispute context, and communicate with users | ||
| - `read-write`: solver can do everything above and can also execute `admin-settle` and `admin-cancel` | ||
|
|
||
| This split is intended to support automated dispute assistants, including AI-based agents, without giving them authority to move funds. | ||
|
|
||
| ## Goals | ||
|
|
||
| - allow non-human dispute assistants to participate safely | ||
| - enforce authorization at the daemon level, not in prompts or UI | ||
| - preserve backward compatibility for existing solver registration flows | ||
|
|
||
| ## Data Model | ||
|
|
||
| The `users.category` field is used to represent solver permissions: | ||
|
|
||
| - `0`: regular user / no solver permissions | ||
| - `1`: solver with `read` permission only | ||
| - `2`: solver with `read-write` permission | ||
|
|
||
| The legacy `is_solver` flag still indicates whether the user is a solver at all. | ||
|
|
||
| ## Authorization Rules | ||
|
|
||
| ### `admin-take-dispute` | ||
| Allowed for: | ||
| - Mostro daemon admin key while dispute status is `initiated` or `in-progress` | ||
| - solvers with `is_solver = true` while dispute status is `initiated` | ||
|
|
||
| Both `read` and `read-write` solvers may take a dispute. | ||
|
|
||
| ### `admin-settle` | ||
| Allowed only when: | ||
| - the caller is the solver assigned to the dispute | ||
| - and the assigned solver has `category = 2` | ||
|
|
||
| If the caller is assigned but only has `read` permission, Mostro returns: | ||
| - `CantDoReason::NotAuthorized` | ||
|
|
||
| ### `admin-cancel` | ||
| Allowed only when: | ||
| - the caller is the solver assigned to the dispute | ||
| - and the assigned solver has `category = 2` | ||
|
|
||
| If the caller is assigned but only has `read` permission, Mostro returns: | ||
| - `CantDoReason::NotAuthorized` | ||
|
|
||
| ## AdminAddSolver payload | ||
|
|
||
| `admin-add-solver` continues using `Payload::TextMessage`, but now supports an optional permission suffix. | ||
|
|
||
| Formats: | ||
|
|
||
| - `npub1...` → defaults to `read-write` | ||
| - `npub1...:read` → registers solver as read-only | ||
| - `npub1...:read-write` → registers solver as read-write | ||
| - `npub1...:write` → alias for read-write | ||
|
|
||
| Invalid suffixes must be rejected with `CantDoReason::InvalidParameters`. | ||
|
|
||
| ## RPC impact | ||
|
|
||
| The current RPC `AddSolverRequest` still only exposes `solver_pubkey`. | ||
|
|
||
| That means RPC registration remains backward compatible and defaults to `read-write` until the protobuf/API is extended. | ||
|
|
||
| ## Dependency | ||
|
|
||
| This feature requires `mostro-core >= 0.8.4` because it uses `CantDoReason::NotAuthorized`. | ||
|
|
||
| ## Security rationale | ||
|
|
||
| The key security property is that read-only solvers can never execute dispute-closing actions, even if: | ||
|
|
||
| - a UI exposes the wrong button | ||
| - an operator misconfigures an agent prompt | ||
| - a remote tool attempts to call `admin-settle` or `admin-cancel` directly | ||
|
|
||
| The daemon enforces the permission boundary. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| -- Backfill legacy solver records so pre-existing solvers keep their historical | ||
| -- settle/cancel authority after solver permission categories are enforced. | ||
| -- | ||
| -- Before PR #708, solver capability was represented only by `is_solver = 1`. | ||
| -- After PR #708, settle/cancel paths require `category = 2` (read-write). | ||
| -- | ||
| -- Operators should not need to patch this manually, so migrate any legacy solver | ||
| -- rows that still have the default/legacy category value to read-write. | ||
| UPDATE users | ||
| SET category = 2 | ||
| WHERE is_solver = 1 | ||
| AND category = 0; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| use crate::app::context::AppContext; | ||
| use crate::db::{find_dispute_by_order_id, is_assigned_solver, is_dispute_taken_by_admin}; | ||
| use crate::db::{ | ||
| find_dispute_by_order_id, is_assigned_solver, is_dispute_taken_by_admin, | ||
| solver_has_write_permission, | ||
| }; | ||
| use crate::lightning::LndConnector; | ||
| use crate::nip33::{create_platform_tag_values, new_dispute_event}; | ||
| use crate::util::{enqueue_order_msg, get_order, settle_seller_hold_invoice, update_order_event}; | ||
|
|
@@ -47,6 +50,10 @@ pub async fn admin_settle_action( | |
| _ => {} | ||
| } | ||
|
|
||
| if !solver_has_write_permission(pool, &event.sender.to_string(), order.id).await? { | ||
| return Err(MostroCantDo(CantDoReason::NotAuthorized)); | ||
| } | ||
|
|
||
| // Was order cooperatively cancelled? | ||
| if order.check_status(Status::CooperativelyCanceled).is_ok() { | ||
| enqueue_order_msg( | ||
|
|
@@ -206,7 +213,12 @@ mod tests { | |
| let admin_error = CantDoReason::DisputeTakenByAdmin; | ||
| assert_eq!(format!("{:?}", admin_error), "DisputeTakenByAdmin"); | ||
|
|
||
| // New error for authenticated callers lacking enough permissions | ||
| let unauthorized_error = CantDoReason::NotAuthorized; | ||
| assert_eq!(format!("{:?}", unauthorized_error), "NotAuthorized"); | ||
|
|
||
| // Verify they are different error types | ||
| assert_ne!(regular_error, admin_error); | ||
| assert_ne!(admin_error, unauthorized_error); | ||
| } | ||
|
Comment on lines
+216
to
223
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n --type rust -C3 '\b(admin_settle_action|admin_cancel_action|solver_has_write_permission|NotAuthorized)\b'Repository: MostroP2P/mostro Length of output: 6405 🏁 Script executed: #!/bin/bash
# Get the full test module from admin_settle.rs
fd "admin_settle.rs" -type f | head -1 | xargs wc -lRepository: MostroP2P/mostro Length of output: 249 🏁 Script executed: # Read the test module section from admin_settle.rs
fd "admin_settle.rs" -type f | head -1 | xargs tail -100Repository: MostroP2P/mostro Length of output: 230 🏁 Script executed: # Also check admin_cancel.rs tests
fd "admin_cancel.rs" -type f | head -1 | xargs tail -100Repository: MostroP2P/mostro Length of output: 230 🏁 Script executed: #!/bin/bash
# Find and display the test module from admin_settle.rs
find . -name "admin_settle.rs" | head -1 | xargs tail -150Repository: MostroP2P/mostro Length of output: 5708 🏁 Script executed: # Find and display the test module from admin_cancel.rs
find . -name "admin_cancel.rs" | head -1 | xargs tail -150Repository: MostroP2P/mostro Length of output: 5759 🏁 Script executed: #!/bin/bash
# Check full length and tail of admin_cancel.rs to see if there's a test module
find . -name "admin_cancel.rs" | head -1 | xargs wc -lRepository: MostroP2P/mostro Length of output: 89 🏁 Script executed: # Search for test functions that might exercise the handlers with different permissions
rg -n --type rust '(#\[test\]|fn test_.*admin_(settle|cancel))' src/app/Repository: MostroP2P/mostro Length of output: 1168 🏁 Script executed: # Look for any integration tests or tests that call admin_settle_action or admin_cancel_action
rg -n --type rust 'admin_(settle|cancel)_action' src/ | grep -i testRepository: MostroP2P/mostro Length of output: 42 🏁 Script executed: #!/bin/bash
# Check the end of admin_cancel.rs to confirm whether it has a test module
find . -name "admin_cancel.rs" | head -1 | xargs tail -50Repository: MostroP2P/mostro Length of output: 2086 🏁 Script executed: # Search for any tests or fixtures that might use solver permissions
rg -n --type rust 'solver.*permission|read.*only.*solver|write.*permission' src/ -iRepository: MostroP2P/mostro Length of output: 679 🏁 Script executed: #!/bin/bash
# Final confirmation: check if solver_has_write_permission is actually tested anywhere
rg -n --type rust 'solver_has_write_permission' src/ -A 5 | grep -A 5 'test\|#\[test\]'Repository: MostroP2P/mostro Length of output: 42 Add behavioral tests for the new solver permission gate. The current test at lines 216-223 only verifies that Since this PR introduces write-permission enforcement (lines 53-54 in
Repeat the same for 🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not implement the suggested admin_settle_action / admin_cancel_action success-path behavioral tests in this pass, because after verifying the current code they are not a direct bug fix and would require building non-trivial DB/LND fixtures to make the tests meaningful rather than synthetic. I’d prefer to handle that as a separate focused test-coverage improvement if needed.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a follow-up GitHub issue to track adding proper behavioral coverage for the solver write-permission gate in 🧠 Learnings used
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes please
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 46
It captures the full context — the deferred behavioral tests for the read-only rejection path and read-write success path in both 🧠 Learnings used
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I implemented the behavior change so a Fix commit: Note: I pushed it to branch
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.