mcpower-proxy 0.0.58__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. main.py +112 -0
  2. mcpower_proxy-0.0.58.dist-info/METADATA +250 -0
  3. mcpower_proxy-0.0.58.dist-info/RECORD +43 -0
  4. mcpower_proxy-0.0.58.dist-info/WHEEL +5 -0
  5. mcpower_proxy-0.0.58.dist-info/entry_points.txt +2 -0
  6. mcpower_proxy-0.0.58.dist-info/licenses/LICENSE +201 -0
  7. mcpower_proxy-0.0.58.dist-info/top_level.txt +3 -0
  8. modules/__init__.py +1 -0
  9. modules/apis/__init__.py +1 -0
  10. modules/apis/security_policy.py +322 -0
  11. modules/logs/__init__.py +1 -0
  12. modules/logs/audit_trail.py +162 -0
  13. modules/logs/logger.py +128 -0
  14. modules/redaction/__init__.py +13 -0
  15. modules/redaction/constants.py +38 -0
  16. modules/redaction/gitleaks_rules.py +1268 -0
  17. modules/redaction/pii_rules.py +271 -0
  18. modules/redaction/redactor.py +599 -0
  19. modules/ui/__init__.py +1 -0
  20. modules/ui/classes.py +48 -0
  21. modules/ui/confirmation.py +200 -0
  22. modules/ui/simple_dialog.py +104 -0
  23. modules/ui/xdialog/__init__.py +249 -0
  24. modules/ui/xdialog/constants.py +13 -0
  25. modules/ui/xdialog/mac_dialogs.py +190 -0
  26. modules/ui/xdialog/tk_dialogs.py +78 -0
  27. modules/ui/xdialog/windows_custom_dialog.py +426 -0
  28. modules/ui/xdialog/windows_dialogs.py +250 -0
  29. modules/ui/xdialog/windows_structs.py +183 -0
  30. modules/ui/xdialog/yad_dialogs.py +236 -0
  31. modules/ui/xdialog/zenity_dialogs.py +156 -0
  32. modules/utils/__init__.py +1 -0
  33. modules/utils/cli.py +46 -0
  34. modules/utils/config.py +193 -0
  35. modules/utils/copy.py +36 -0
  36. modules/utils/ids.py +160 -0
  37. modules/utils/json.py +120 -0
  38. modules/utils/mcp_configs.py +48 -0
  39. wrapper/__init__.py +1 -0
  40. wrapper/__version__.py +6 -0
  41. wrapper/middleware.py +750 -0
  42. wrapper/schema.py +227 -0
  43. wrapper/server.py +78 -0
@@ -0,0 +1,200 @@
1
+ """
2
+ User Confirmation Dialog for Security Policy Enforcement
3
+
4
+ Provides simple, lightweight modal dialogs for explicit user confirmation
5
+ when security policies require user approval for MCP operations.
6
+ """
7
+
8
+ from datetime import datetime, timezone
9
+ from typing import Optional
10
+
11
+ from mcpower_shared.mcp_types import UserDecision
12
+
13
+ from modules.logs.audit_trail import AuditTrailLogger
14
+ from modules.logs.logger import MCPLogger
15
+ from . import xdialog
16
+ from .classes import ConfirmationRequest, ConfirmationResponse, UserConfirmationError, DialogOptions
17
+ from .simple_dialog import show_explicit_user_confirmation_dialog, show_blocking_dialog
18
+
19
+
20
+ # noinspection PyMethodMayBeStatic
21
+ class UserConfirmationDialog:
22
+ """
23
+ Simple user confirmation dialog using tkinter messagebox
24
+
25
+ Provides lightweight modal dialogs for explicit user approval of MCP operations
26
+ when security policies require confirmation at request or response stages.
27
+ """
28
+
29
+ def __init__(self, logger: MCPLogger, audit_logger: AuditTrailLogger):
30
+ self.logger = logger
31
+ self.audit_logger = audit_logger
32
+
33
+ def request_confirmation(self, request: ConfirmationRequest, prompt_id: str,
34
+ call_type: Optional[str] = None, options: DialogOptions = None) -> ConfirmationResponse:
35
+ """
36
+ Display a confirmation dialog and wait for the user decision
37
+
38
+ Args:
39
+ request: Confirmation request with all necessary context
40
+ call_type: Optional call type from inspect decision ("read", "write")
41
+ options: Optional dialog options for controlling button visibility
42
+ prompt_id: Prompt ID for audit trail grouping (mandatory for tool calls)
43
+
44
+ Returns:
45
+ ConfirmationResponse with user decision
46
+
47
+ Raises:
48
+ UserConfirmationError: If a user denies or the dialog fails
49
+ """
50
+ if options is None:
51
+ options = DialogOptions()
52
+ self.logger.info(f"Requesting user confirmation for "
53
+ f"{'request' if request.is_request else 'response'} "
54
+ f"operation on tool '{request.tool_name}' (event: {request.event_id})")
55
+
56
+ # AUDIT: Log user interaction
57
+ self.audit_logger.log_event(
58
+ "user_interaction",
59
+ {
60
+ "type": "dialog",
61
+ "interaction": "explicit user confirmation"
62
+ },
63
+ event_id=request.event_id,
64
+ prompt_id=prompt_id
65
+ )
66
+
67
+ result = show_explicit_user_confirmation_dialog(request, options, self.logger)
68
+ direction = "request" if request.is_request else "response"
69
+
70
+ # Convert dialog result to UserDecision enum
71
+ match result:
72
+ case xdialog.YES:
73
+ user_decision = UserDecision.ALLOW
74
+ case xdialog.YES_ALWAYS:
75
+ user_decision = UserDecision.ALWAYS_ALLOW
76
+ case xdialog.NO_ALWAYS:
77
+ user_decision = UserDecision.ALWAYS_BLOCK
78
+ case _: # NO or any other result
79
+ user_decision = UserDecision.BLOCK
80
+
81
+ response = ConfirmationResponse(
82
+ user_decision=user_decision,
83
+ timestamp=datetime.now(timezone.utc),
84
+ event_id=request.event_id,
85
+ direction=direction,
86
+ call_type=call_type
87
+ )
88
+ self._log_confirmation_decision(request, response)
89
+
90
+ # AUDIT: Log user interaction result
91
+ self.audit_logger.log_event(
92
+ "user_interaction_result",
93
+ {
94
+ "decision": user_decision.value
95
+ },
96
+ event_id=request.event_id,
97
+ prompt_id=prompt_id
98
+ )
99
+
100
+ # Process user decision
101
+ if user_decision in (UserDecision.BLOCK, UserDecision.ALWAYS_BLOCK):
102
+ # User denied confirmation
103
+ raise UserConfirmationError(
104
+ f"User denied {'request' if request.is_request else 'response'} operation "
105
+ f"for tool '{request.tool_name}'",
106
+ event_id=request.event_id,
107
+ is_request=request.is_request,
108
+ tool_name=request.tool_name
109
+ )
110
+
111
+ # User approved
112
+ return response
113
+
114
+ def request_blocking_confirmation(self, request: ConfirmationRequest, prompt_id: str,
115
+ call_type: Optional[str] = None) -> ConfirmationResponse:
116
+ """
117
+ Display a blocking dialog and wait for the user decision
118
+ Shows "Block" vs "Allow Anyway" options for policy-blocked requests
119
+
120
+ Args:
121
+ request: Confirmation request with all necessary context
122
+ prompt_id: Prompt ID for audit trail grouping (mandatory for tool calls)
123
+ call_type: Optional call type from inspect decision ("read", "write")
124
+
125
+ Returns:
126
+ ConfirmationResponse with user decision
127
+
128
+ Raises:
129
+ UserConfirmationError: If user chooses to block or dialog fails
130
+ """
131
+ self.logger.info(f"Requesting user blocking confirmation for "
132
+ f"{'request' if request.is_request else 'response'} "
133
+ f"operation on tool '{request.tool_name}' (event: {request.event_id})")
134
+
135
+ # AUDIT: Log user interaction
136
+ self.audit_logger.log_event(
137
+ "user_interaction",
138
+ {
139
+ "type": "dialog",
140
+ "interaction": "block recommendation"
141
+ },
142
+ event_id=request.event_id,
143
+ prompt_id=prompt_id
144
+ )
145
+
146
+ result = show_blocking_dialog(request, self.logger)
147
+ direction = "request" if request.is_request else "response"
148
+
149
+ # Convert dialog result to UserDecision enum
150
+ match result:
151
+ case xdialog.YES: # Allow Anyway
152
+ user_decision = UserDecision.ALLOW
153
+ case _: # NO (Block) or any other result
154
+ user_decision = UserDecision.BLOCK
155
+
156
+ response = ConfirmationResponse(
157
+ user_decision=user_decision,
158
+ timestamp=datetime.now(timezone.utc),
159
+ event_id=request.event_id,
160
+ direction=direction,
161
+ call_type=call_type
162
+ )
163
+ self._log_confirmation_decision(request, response)
164
+
165
+ # AUDIT: Log user interaction result
166
+ self.audit_logger.log_event(
167
+ "user_interaction_result",
168
+ {
169
+ "decision": user_decision.value
170
+ },
171
+ event_id=request.event_id,
172
+ prompt_id=prompt_id
173
+ )
174
+
175
+ # Process user decision - only allow if user explicitly chose "Allow Anyway"
176
+ if user_decision == UserDecision.BLOCK:
177
+ # User chose to block
178
+ raise UserConfirmationError(
179
+ f"User blocked {'request' if request.is_request else 'response'} operation "
180
+ f"for tool '{request.tool_name}'",
181
+ event_id=request.event_id,
182
+ is_request=request.is_request,
183
+ tool_name=request.tool_name
184
+ )
185
+
186
+ # User chose "Allow Anyway"
187
+ return response
188
+
189
+ def _log_confirmation_decision(self, request: ConfirmationRequest, response: ConfirmationResponse):
190
+ """Log user confirmation decision for audit trail"""
191
+ self.logger.debug(
192
+ f"User confirmation decision: "
193
+ f"event_id={response.event_id}, "
194
+ f"direction={response.direction}, "
195
+ f"tool={request.tool_name}, "
196
+ f"user_decision={response.user_decision.value}, "
197
+ f"call_type={response.call_type}, "
198
+ f"timed_out={response.timed_out}, "
199
+ f"timestamp={response.timestamp.isoformat()}"
200
+ )
@@ -0,0 +1,104 @@
1
+ """
2
+ Simple, lightweight confirmation dialog using native OS dialogs
3
+
4
+ This provides cross-platform native dialogs
5
+ """
6
+
7
+ from modules.logs.logger import MCPLogger
8
+
9
+ from . import xdialog
10
+ from .classes import ConfirmationRequest, DialogOptions
11
+
12
+
13
+ def show_explicit_user_confirmation_dialog(request: ConfirmationRequest, options: DialogOptions,
14
+ logger: MCPLogger) -> int:
15
+ """
16
+ Show a native OS confirmation dialog using xdialog
17
+
18
+ Returns:
19
+ int: xdialog constant (YES, NO, YES_ALWAYS, NO_ALWAYS)
20
+ """
21
+ message = (f"Server: {request.server_name}"
22
+ f"\nTool: {request.tool_name}"
23
+ f"\n\nPolicy Alert ({request.severity.title()}):"
24
+ f"\n{request.policy_reasons[0]}")
25
+
26
+ try:
27
+ # Build custom button array: Block, Always Block (optional), Allow, Always Allow (optional)
28
+ buttons = ["Block"]
29
+
30
+ if options.show_always_block:
31
+ buttons.append("Always Block")
32
+
33
+ buttons.append("Allow")
34
+
35
+ if options.show_always_allow:
36
+ buttons.append("Always Allow")
37
+
38
+ # Use generic dialog with Block/Allow buttons
39
+ result_index = xdialog.generic_dialog(
40
+ title="MCPower Security Confirmation Required",
41
+ message=message,
42
+ buttons=buttons,
43
+ default_button=buttons.index("Allow"), # Default to "Allow"
44
+ icon=xdialog.ICON_WARNING
45
+ )
46
+
47
+ # Map button indices back to xdialog constants for compatibility
48
+ # Button order: Block, [Always Block], Allow, [Always Allow]
49
+ if result_index == 0:
50
+ return xdialog.NO # Block -> NO
51
+ elif options.show_always_block and result_index == 1:
52
+ return xdialog.NO_ALWAYS # Always Block -> NO_ALWAYS
53
+ elif not options.show_always_block and result_index == 1:
54
+ return xdialog.YES # Allow -> YES (when no Always Block)
55
+ elif options.show_always_block and result_index == 2:
56
+ return xdialog.YES # Allow -> YES (when Always Block is present)
57
+ elif not options.show_always_block and result_index == 2:
58
+ return xdialog.YES_ALWAYS # Always Allow -> YES_ALWAYS (when no Always Block)
59
+ elif options.show_always_block and result_index == 3:
60
+ return xdialog.YES_ALWAYS # Always Allow -> YES_ALWAYS (when Always Block is present)
61
+ else:
62
+ raise Exception(f"Unexpected result index {result_index}")
63
+
64
+ except Exception as e:
65
+ logger.error(f"Native dialog error: {e}")
66
+ raise e
67
+
68
+
69
+ def show_blocking_dialog(request: ConfirmationRequest, logger: MCPLogger) -> int:
70
+ """
71
+ Show a blocking dialog with red error styling and "Block"/"Allow Anyway" buttons
72
+
73
+ Returns:
74
+ int: xdialog constant (YES=Allow Anyway, NO=Block)
75
+ """
76
+ message = (f"Server: {request.server_name}"
77
+ f"\nTool: {request.tool_name}"
78
+ f"\n\nPolicy Alert ({request.severity.title()}):"
79
+ f"\n{request.policy_reasons[0]}")
80
+
81
+ try:
82
+ # Build button array: Block (default), Allow Anyway
83
+ buttons = ["Block", "Allow Anyway"]
84
+
85
+ # Use generic dialog with error icon and Block as default
86
+ result_index = xdialog.generic_dialog(
87
+ title="MCPower Security Request Blocked",
88
+ message=message,
89
+ buttons=buttons,
90
+ default_button=0, # Default to "Block" button (first button)
91
+ icon=xdialog.ICON_ERROR # Red error icon
92
+ )
93
+
94
+ # Map button indices to xdialog constants
95
+ if result_index == 0:
96
+ return xdialog.NO # Block -> NO
97
+ elif result_index == 1:
98
+ return xdialog.YES # Allow Anyway -> YES
99
+ else:
100
+ raise Exception(f"Unexpected result index {result_index}")
101
+
102
+ except Exception as e:
103
+ logger.error(f"Blocking dialog error: {e}")
104
+ raise e
@@ -0,0 +1,249 @@
1
+ import platform
2
+ import subprocess
3
+
4
+ from .constants import *
5
+ from typing import Iterable, Union, Tuple, Optional, Literal, overload
6
+
7
+ __all__ = [
8
+ "open_file", "save_file", "directory",
9
+ "info", "warning", "error",
10
+ "yesno", "yesno_always", "yesnocancel", "retrycancel", "okcancel",
11
+ "generic_dialog",
12
+ "YES", "NO", "CANCEL", "RETRY", "OK", "YES_ALWAYS", "NO_ALWAYS",
13
+ "ICON_QUESTION", "ICON_WARNING", "ICON_ERROR", "ICON_INFO"
14
+ ]
15
+
16
+ SYSTEM = platform.system()
17
+
18
+ # Find the best dialog for this platform. Default to tkinter.
19
+ # Using import keyword instead of __import__ for extra compatibility.
20
+ def get_dialogs():
21
+ if SYSTEM == 'Windows':
22
+ from . import windows_dialogs
23
+ return windows_dialogs
24
+ elif SYSTEM == "Darwin":
25
+ from . import mac_dialogs
26
+ return mac_dialogs
27
+ else:
28
+ def cmd_exists(cmd):
29
+ proc = subprocess.Popen(('which', cmd), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=False)
30
+ proc.communicate()
31
+ return not proc.returncode
32
+
33
+ if cmd_exists('yad'):
34
+ from . import yad_dialogs
35
+ return yad_dialogs
36
+ if cmd_exists('zenity'):
37
+ from . import zenity_dialogs
38
+ return zenity_dialogs
39
+
40
+ try:
41
+ from . import tk_dialogs
42
+ return tk_dialogs
43
+ except ModuleNotFoundError: pass
44
+
45
+ raise ModuleNotFoundError('No dialog type is supported on this machine. Install tkinter to guarantee dialogs.')
46
+
47
+
48
+
49
+ dialogs = get_dialogs()
50
+
51
+ @overload
52
+ def open_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")], multiple: Literal[False] = False) -> str: ...
53
+ @overload
54
+ def open_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")], multiple: Literal[True] = True) -> Iterable[str]: ...
55
+
56
+ def open_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")], multiple: bool = False) -> Union[str, Iterable[str]]:
57
+ '''Shows a dialog box for selecting one or more files to be opened.
58
+
59
+ Arguments:
60
+ title: Text to show on the header of the dialog box.
61
+ Omitting it has system-dependent results.
62
+
63
+ filetypes: A list of tuples specifying which filetypes to show.
64
+ The first string is a readable name of that filetype, and
65
+ the second string is one or more glob (e.g., * or *.txt) extension.
66
+
67
+ Each glob in the second string is separated by spaces.
68
+
69
+ Each tuple will normally appear in a dropdown of file types to select from.
70
+ If this argument is not specified, all file types are visible.
71
+
72
+ MacOS will ignore the first string in each tuple (as it doesn't
73
+ display it anywhere), and will instead enable selection of all
74
+ file extensions provided.
75
+
76
+ multiple: If False (default), only one file may be selected. If True, multiple files may be selected.
77
+
78
+ Returns:
79
+ If `multiple` is True, an iterable of selected files or an empty iterable.
80
+ If `multiple` is False, the file that was selected or an empty string.
81
+ '''
82
+ return dialogs.open_file(title, filetypes, multiple)
83
+
84
+ def save_file(title: Optional[str] = None, filetypes: Iterable[Tuple[str, str]] = [("All Files", "*")]) -> str:
85
+ '''Shows a dialog box for selecting one or more files to be opened.
86
+
87
+ Arguments:
88
+ title: Text to show on the header of the dialog box.
89
+ Omitting it has system-dependent results.
90
+
91
+ filetypes: A list of tuples specifying which filetypes to show.
92
+ The first string is a readable name of that filetype, and
93
+ the second string is one or more glob (e.g., * or *.txt) expression.
94
+
95
+ Each glob in the second string is separated by spaces.
96
+
97
+ Each tuple will appear in a dropdown of file types to select from.
98
+ If this argument is not specified, all file types are visible.
99
+
100
+ MacOS does not support this option, but will instead use the first
101
+ tuple's glob to populate the default name.
102
+
103
+ Returns:
104
+ The file that was selected or an empty string.
105
+ '''
106
+ return dialogs.save_file(title, filetypes)
107
+
108
+ def directory(title: Optional[str] = None) -> str:
109
+ '''Shows a dialog box for selecting a directory. The directory must exist to be selected.
110
+
111
+ Arguments:
112
+ title: Text to show on the header of the dialog box.
113
+ Omitting it has system-dependent results.
114
+
115
+ Returns:
116
+ The directory that was selected or an empty string.
117
+ '''
118
+ return dialogs.directory(title)
119
+
120
+ def info(title: Optional[str] = None, message: str = '') -> None:
121
+ '''Shows an info dialog box.
122
+
123
+ Arguments:
124
+ title: Text to show on the header of the dialog box.
125
+ Omitting it has system-dependent results.
126
+
127
+ message: Text to show in the middle of the dialog box.
128
+ '''
129
+ dialogs.info(title, message)
130
+
131
+ def warning(title: Optional[str] = None, message: str = '') -> None:
132
+ '''Shows a warning dialog box.
133
+
134
+ Arguments:
135
+ title: Text to show on the header of the dialog box.
136
+ Omitting it has system-dependent results.
137
+
138
+ message: Text to show in the middle of the dialog box.
139
+ '''
140
+ dialogs.warning(title, message)
141
+
142
+ def error(title: Optional[str] = None, message: str = '') -> None:
143
+ '''Shows an error dialog box.
144
+
145
+ Arguments:
146
+ title: Text to show on the header of the dialog box.
147
+ Omitting it has system-dependent results.
148
+
149
+ message: Text to show in the middle of the dialog box.
150
+ '''
151
+ dialogs.error(title, message)
152
+
153
+ def yesno(title: Optional[str] = None, message: str = '') -> int:
154
+ '''Shows a question dialog box with the buttons "Yes" and "No".
155
+
156
+ Arguments:
157
+ title: Text to show on the header of the dialog box.
158
+ Omitting it has system-dependent results.
159
+
160
+ message: Text to show in the middle of the dialog box.
161
+
162
+ Returns:
163
+ `xdialog.YES` or `xdialog.NO`. Closing the box results in `xdialog.NO`.
164
+ '''
165
+ return dialogs.yesno(title, message)
166
+
167
+ def yesno_always(title: Optional[str] = None, message: str = '', yes_always: bool = False, no_always: bool = False) -> int:
168
+ '''Shows a question dialog box with Yes/No and optional Always buttons.
169
+
170
+ Arguments:
171
+ title: Text to show on the header of the dialog box.
172
+ Omitting it has system-dependent results.
173
+
174
+ message: Text to show in the middle of the dialog box.
175
+
176
+ yes_always: If True, shows "Yes Always" button.
177
+
178
+ no_always: If True, shows "No Always" button.
179
+
180
+ Returns:
181
+ `xdialog.YES`, `xdialog.NO`, `xdialog.YES_ALWAYS`, or `xdialog.NO_ALWAYS`.
182
+ Button order: No, No Always (if enabled), Yes, Yes Always (if enabled).
183
+ '''
184
+ return dialogs.yesno_always(title, message, yes_always, no_always)
185
+
186
+ def yesnocancel(title: Optional[str] = None, message: str = '') -> int:
187
+ '''Shows a question dialog box with the buttons "Yes", "No", and "Cancel".
188
+
189
+ Arguments:
190
+ title: Text to show on the header of the dialog box.
191
+ Omitting it has system-dependent results.
192
+
193
+ message: Text to show in the middle of the dialog box.
194
+
195
+ Returns:
196
+ `xdialog.YES`, `xdialog.NO`, or `xdialog.CANCEL`. Closing the box results in `xdialog.CANCEL`.
197
+ '''
198
+ return dialogs.yesnocancel(title, message)
199
+
200
+ def retrycancel(title: Optional[str] = None, message: str = '') -> int:
201
+ '''Shows a question dialog box with the buttons "Retry" and "Cancel".
202
+
203
+ Arguments:
204
+ title: Text to show on the header of the dialog box.
205
+ Omitting it has system-dependent results.
206
+
207
+ message: Text to show in the middle of the dialog box.
208
+
209
+ Returns:
210
+ `xdialog.RETRY` or `xdialog.CANCEL`. Closing the box results in `xdialog.CANCEL`.
211
+ '''
212
+ return dialogs.retrycancel(title, message)
213
+
214
+ def okcancel(title: Optional[str] = None, message: str = '') -> int:
215
+ '''Shows a question dialog box with the buttons "Ok" and "Cancel".
216
+
217
+ Arguments:
218
+ title: Text to show on the header of the dialog box.
219
+ Omitting it has system-dependent results.
220
+
221
+ message: Text to show in the middle of the dialog box.
222
+
223
+ Returns:
224
+ `xdialog.OK` or `xdialog.CANCEL`. Closing the box results in `xdialog.CANCEL`.
225
+ '''
226
+ return dialogs.okcancel(title, message)
227
+
228
+ def generic_dialog(title: str, message: str, buttons: list[str], default_button: int, icon: str) -> int:
229
+ '''Shows a generic dialog box with custom buttons and icon.
230
+
231
+ Arguments:
232
+ title: Text to show on the header of the dialog box.
233
+
234
+ message: Text to show in the middle of the dialog box.
235
+
236
+ buttons: List of button text strings to display.
237
+
238
+ default_button: Index of default button (0-based). Used when dialog is dismissed.
239
+
240
+ icon: Icon type to display. Use ICON_QUESTION, ICON_WARNING, ICON_ERROR, or ICON_INFO.
241
+
242
+ Returns:
243
+ Index of clicked button (0-based). If dialog is dismissed, returns default_button.
244
+
245
+ Raises:
246
+ NotImplementedError: On platforms that don't support custom buttons (Zenity, Tkinter).
247
+ ValueError: If parameters are invalid (icon not supported, buttons empty, default_button out of range).
248
+ '''
249
+ return dialogs.generic_dialog(title, message, buttons, default_button, icon)
@@ -0,0 +1,13 @@
1
+ YES = 0
2
+ RETRY = 0
3
+ OK = 0
4
+ NO = 1
5
+ CANCEL = 2
6
+ YES_ALWAYS = 3
7
+ NO_ALWAYS = 4
8
+
9
+ # Icon constants for generic_dialog
10
+ ICON_QUESTION = "question"
11
+ ICON_WARNING = "warning"
12
+ ICON_ERROR = "error"
13
+ ICON_INFO = "info"