omnimancer-cli 0.1.0__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 (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,511 @@
1
+ """Permission controller for managing agent access permissions."""
2
+
3
+ import os
4
+ import re
5
+ import tempfile
6
+ from datetime import datetime, timedelta
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Set
10
+
11
+
12
+ class PermissionLevel(Enum):
13
+ """Permission levels for different operations."""
14
+
15
+ READ = "read"
16
+ WRITE = "write"
17
+ EXECUTE = "execute"
18
+ ADMIN = "admin"
19
+
20
+
21
+ class PermissionOperation:
22
+ """Represents an operation that needs permission validation."""
23
+
24
+ def __init__(
25
+ self,
26
+ operation_type: str,
27
+ path: Optional[str] = None,
28
+ command: Optional[str] = None,
29
+ **kwargs,
30
+ ):
31
+ self.operation_type = operation_type
32
+ self.path = path
33
+ self.command = command
34
+ self.metadata = kwargs
35
+
36
+
37
+ class PermissionController:
38
+ """Controls and validates permissions for agent operations."""
39
+
40
+ def __init__(self):
41
+ self.restricted_paths = self._get_default_restricted_paths()
42
+ self.allowed_commands = self._get_default_allowed_commands()
43
+ self.permission_rules = self._get_default_permission_rules()
44
+
45
+ # Initialize approval memory storage
46
+ self._approval_memory: Dict[str, Dict[str, Any]] = {}
47
+
48
+ def _get_default_restricted_paths(self) -> Set[str]:
49
+ """Get default set of restricted file paths."""
50
+ return {
51
+ # SSH and security keys
52
+ ".ssh",
53
+ "~/.ssh",
54
+ "/home/*/.ssh",
55
+ "/Users/*/.ssh",
56
+ # Environment and config files
57
+ ".env",
58
+ ".env.local",
59
+ ".env.production",
60
+ # System directories
61
+ "/etc",
62
+ "/System",
63
+ "/sys",
64
+ "/proc",
65
+ "/boot",
66
+ "/root",
67
+ # Package manager directories
68
+ "/usr/bin",
69
+ "/usr/sbin",
70
+ "/sbin",
71
+ # Database files
72
+ "*.db",
73
+ "*.sqlite",
74
+ "*.sqlite3",
75
+ # Credential files
76
+ "*credentials*",
77
+ "*password*",
78
+ "*secret*",
79
+ "*token*",
80
+ "*key*",
81
+ ".aws/credentials",
82
+ ".config/gcloud",
83
+ }
84
+
85
+ def _get_default_allowed_commands(self) -> Set[str]:
86
+ """Get default set of allowed shell commands."""
87
+ return {
88
+ # File operations
89
+ "ls",
90
+ "cat",
91
+ "head",
92
+ "tail",
93
+ "grep",
94
+ "find",
95
+ "locate",
96
+ "cp",
97
+ "mv",
98
+ "mkdir",
99
+ "rmdir",
100
+ "touch",
101
+ "chmod",
102
+ "chown",
103
+ # Text processing
104
+ "sed",
105
+ "awk",
106
+ "sort",
107
+ "uniq",
108
+ "wc",
109
+ "tr",
110
+ "cut",
111
+ # Basic commands
112
+ "echo",
113
+ "printf",
114
+ "true",
115
+ "false",
116
+ "sleep",
117
+ # Development tools
118
+ "git",
119
+ "npm",
120
+ "pip",
121
+ "python",
122
+ "node",
123
+ "go",
124
+ "cargo",
125
+ "make",
126
+ "cmake",
127
+ "gcc",
128
+ "clang",
129
+ # System info (read-only)
130
+ "ps",
131
+ "top",
132
+ "df",
133
+ "du",
134
+ "free",
135
+ "uptime",
136
+ "whoami",
137
+ "pwd",
138
+ "which",
139
+ "whereis",
140
+ "uname",
141
+ # Network (limited)
142
+ "curl",
143
+ "wget",
144
+ "ping",
145
+ }
146
+
147
+ def _get_default_permission_rules(self) -> Dict[str, PermissionLevel]:
148
+ """Get default permission rules for operation types."""
149
+ return {
150
+ "file_read": PermissionLevel.READ,
151
+ "file_write": PermissionLevel.WRITE,
152
+ "file_delete": PermissionLevel.WRITE,
153
+ "file_execute": PermissionLevel.EXECUTE,
154
+ "command_execute": PermissionLevel.EXECUTE,
155
+ "network_request": PermissionLevel.READ,
156
+ "system_info": PermissionLevel.READ,
157
+ "package_install": PermissionLevel.ADMIN,
158
+ "service_control": PermissionLevel.ADMIN,
159
+ }
160
+
161
+ def validate_path_access(self, path: str, operation: str = "read") -> bool:
162
+ """Validate if path access is allowed."""
163
+ try:
164
+ # Normalize path
165
+ normalized_path = str(Path(path).resolve())
166
+
167
+ # Check against restricted paths
168
+ for restricted in self.restricted_paths:
169
+ if self._path_matches_pattern(normalized_path, restricted):
170
+ return False
171
+
172
+ # Additional checks for write operations
173
+ if operation in ["write", "delete", "execute"]:
174
+ # Don't allow writes to system directories
175
+ system_prefixes = ["/usr", "/etc", "/System", "/sys", "/proc"]
176
+ if any(
177
+ normalized_path.startswith(prefix) for prefix in system_prefixes
178
+ ):
179
+ return False
180
+
181
+ # Allow writes to project directory and safe temporary directories
182
+ project_root = os.getcwd()
183
+ safe_temp_prefixes = [
184
+ "/tmp",
185
+ "/var/tmp",
186
+ tempfile.gettempdir(),
187
+ ]
188
+
189
+ # Check if path is in project directory or safe temp directory
190
+ is_in_project = normalized_path.startswith(project_root)
191
+ is_in_safe_temp = any(
192
+ normalized_path.startswith(prefix) for prefix in safe_temp_prefixes
193
+ )
194
+
195
+ if not (is_in_project or is_in_safe_temp):
196
+ return False
197
+
198
+ return True
199
+
200
+ except (OSError, ValueError):
201
+ # If path resolution fails, deny access
202
+ return False
203
+
204
+ def validate_command(self, command: str) -> bool:
205
+ """Validate if command execution is allowed."""
206
+ # Extract base command (first word)
207
+ base_command = command.strip().split()[0]
208
+
209
+ # Remove path prefixes
210
+ base_command = os.path.basename(base_command)
211
+
212
+ # Check if command is in allowed list
213
+ if base_command not in self.allowed_commands:
214
+ return False
215
+
216
+ # Additional security checks
217
+ if self._contains_dangerous_patterns(command):
218
+ return False
219
+
220
+ return True
221
+
222
+ def validate_operation(self, operation: PermissionOperation) -> bool:
223
+ """Validate if an operation is allowed."""
224
+ op_type = operation.operation_type
225
+
226
+ # Check if operation type is allowed
227
+ if op_type not in self.permission_rules:
228
+ return False
229
+
230
+ # Validate path if provided
231
+ if operation.path:
232
+ required_level = self.permission_rules[op_type]
233
+ access_type = self._permission_level_to_access_type(required_level)
234
+ if not self.validate_path_access(operation.path, access_type):
235
+ return False
236
+
237
+ # Validate command if provided
238
+ if operation.command:
239
+ if not self.validate_command(operation.command):
240
+ return False
241
+
242
+ return True
243
+
244
+ def _path_matches_pattern(self, path: str, pattern: str) -> bool:
245
+ """Check if path matches a pattern (supports wildcards)."""
246
+ # Expand user home directory
247
+ pattern = os.path.expanduser(pattern)
248
+
249
+ # Handle relative patterns (like .ssh, .env) by checking if they appear in the path
250
+ if not pattern.startswith("/") and not pattern.startswith("~"):
251
+ # For relative patterns, check if the pattern matches any part of the path
252
+ path_parts = path.split("/")
253
+ for i, part in enumerate(path_parts):
254
+ if "*" not in pattern:
255
+ # Exact match for directory/file name
256
+ if part == pattern:
257
+ return True
258
+ # Check if any part of the path ends with the pattern (for files in directories)
259
+ remaining_path = "/".join(path_parts[i:])
260
+ if (
261
+ remaining_path.startswith(pattern + "/")
262
+ or remaining_path == pattern
263
+ ):
264
+ return True
265
+ else:
266
+ # Wildcard pattern matching
267
+ regex_pattern = pattern.replace("*", ".*")
268
+ if re.match(f"^{regex_pattern}$", part, re.IGNORECASE):
269
+ return True
270
+ return False
271
+
272
+ # For absolute patterns, use the original logic
273
+ regex_pattern = pattern.replace("*", ".*")
274
+
275
+ # For exact matches (no wildcards), check if path starts with pattern
276
+ if "*" not in pattern:
277
+ # Check if path starts with the pattern (directory matching)
278
+ if path.startswith(pattern):
279
+ # Ensure it's a proper directory match (either exact or followed by /)
280
+ if len(path) == len(pattern) or path[len(pattern)] == "/":
281
+ return True
282
+ # Also check exact match
283
+ regex_pattern = f"^{re.escape(pattern)}$"
284
+ else:
285
+ # For wildcard patterns, use full regex matching
286
+ regex_pattern = f"^{regex_pattern}$"
287
+
288
+ return bool(re.match(regex_pattern, path, re.IGNORECASE))
289
+
290
+ def _contains_dangerous_patterns(self, command: str) -> bool:
291
+ """Check if command contains dangerous patterns."""
292
+ dangerous_patterns = [
293
+ r"[;&|]", # Command chaining
294
+ r"`", # Command substitution
295
+ r"\$\(", # Command substitution
296
+ r">\s*/dev/", # Device access
297
+ r"rm\s+-rf", # Recursive deletion
298
+ r"sudo", # Privilege escalation
299
+ r"su\s", # User switching
300
+ r"chmod\s+[0-7]{3,4}", # Permission changes
301
+ ]
302
+
303
+ for pattern in dangerous_patterns:
304
+ if re.search(pattern, command):
305
+ return True
306
+
307
+ return False
308
+
309
+ def _permission_level_to_access_type(self, level: PermissionLevel) -> str:
310
+ """Convert permission level to access type string."""
311
+ if level == PermissionLevel.READ:
312
+ return "read"
313
+ elif level == PermissionLevel.WRITE:
314
+ return "write"
315
+ elif level == PermissionLevel.EXECUTE:
316
+ return "execute"
317
+ else:
318
+ return "admin"
319
+
320
+ def add_restricted_path(self, path: str) -> None:
321
+ """Add a path to the restricted list."""
322
+ self.restricted_paths.add(path)
323
+
324
+ def remove_restricted_path(self, path: str) -> None:
325
+ """Remove a path from the restricted list."""
326
+ self.restricted_paths.discard(path)
327
+
328
+ def add_allowed_command(self, command: str) -> None:
329
+ """Add a command to the allowed list."""
330
+ self.allowed_commands.add(command)
331
+
332
+ def remove_allowed_command(self, command: str) -> None:
333
+ """Remove a command from the allowed list."""
334
+ self.allowed_commands.discard(command)
335
+
336
+ def get_restricted_paths(self) -> List[str]:
337
+ """Get list of restricted paths."""
338
+ return list(self.restricted_paths)
339
+
340
+ def get_allowed_commands(self) -> List[str]:
341
+ """Get list of allowed commands."""
342
+ return list(self.allowed_commands)
343
+
344
+ async def check_operation_permission(
345
+ self,
346
+ operation_type: str,
347
+ operation_signature: str,
348
+ operation_data: Dict[str, Any],
349
+ ) -> bool:
350
+ """
351
+ Check if operation has stored permission (remembered approval).
352
+
353
+ Args:
354
+ operation_type: Type of operation
355
+ operation_signature: Operation signature for matching
356
+ operation_data: Operation data for validation
357
+
358
+ Returns:
359
+ True if operation has stored permission, False otherwise
360
+ """
361
+ try:
362
+ # Check if we have stored approval for this signature
363
+ if operation_signature in self._approval_memory:
364
+ approval_data = self._approval_memory[operation_signature]
365
+
366
+ # Check if approval has expired
367
+ if "expires_at" in approval_data:
368
+ expires_at = datetime.fromisoformat(approval_data["expires_at"])
369
+ if datetime.now() > expires_at:
370
+ # Remove expired approval
371
+ del self._approval_memory[operation_signature]
372
+ return False
373
+
374
+ # Verify operation type matches
375
+ if approval_data.get("operation_type") == operation_type:
376
+ return True
377
+
378
+ return False
379
+
380
+ except Exception:
381
+ # If there's any error in checking, don't auto-approve
382
+ return False
383
+
384
+ async def grant_operation_permission(
385
+ self,
386
+ operation_type: str,
387
+ operation_signature: str,
388
+ metadata: Dict[str, Any],
389
+ expires_hours: int = 24,
390
+ ) -> None:
391
+ """
392
+ Store operation permission for future auto-approval.
393
+
394
+ Args:
395
+ operation_type: Type of operation
396
+ operation_signature: Operation signature for matching
397
+ metadata: Additional metadata about the approval
398
+ expires_hours: Hours until approval expires (default 24)
399
+ """
400
+ try:
401
+ expires_at = datetime.now() + timedelta(hours=expires_hours)
402
+
403
+ self._approval_memory[operation_signature] = {
404
+ "operation_type": operation_type,
405
+ "stored_at": datetime.now().isoformat(),
406
+ "expires_at": expires_at.isoformat(),
407
+ "metadata": metadata,
408
+ }
409
+
410
+ except Exception:
411
+ # Log error but don't fail the operation
412
+ pass
413
+
414
+ def get_stored_approvals(self) -> Dict[str, Dict[str, Any]]:
415
+ """Get all stored approvals with their metadata."""
416
+ return self._approval_memory.copy()
417
+
418
+ def get_permission_rules(self) -> List[Dict[str, Any]]:
419
+ """Get current permission rules in dictionary format."""
420
+ rules = []
421
+ rule_id = 1
422
+
423
+ # Convert permission rules to display format
424
+ for op_type, level in self.permission_rules.items():
425
+ rule = {
426
+ "id": str(rule_id),
427
+ "pattern": op_type.replace("_", " ").title(),
428
+ "type": "operation",
429
+ "level": level.value,
430
+ }
431
+ rules.append(rule)
432
+ rule_id += 1
433
+
434
+ # Add path-based rules
435
+ for path in self.restricted_paths:
436
+ rule = {
437
+ "id": str(rule_id),
438
+ "pattern": path,
439
+ "type": "file",
440
+ "level": "blocked",
441
+ }
442
+ rules.append(rule)
443
+ rule_id += 1
444
+
445
+ # Add command rules
446
+ for command in list(self.allowed_commands)[:10]: # Limit for display
447
+ rule = {
448
+ "id": str(rule_id),
449
+ "pattern": command,
450
+ "type": "command",
451
+ "level": "allowed",
452
+ }
453
+ rules.append(rule)
454
+ rule_id += 1
455
+
456
+ return rules
457
+
458
+ def get_learned_permissions(self) -> List[Dict[str, Any]]:
459
+ """Get learned permissions in display format."""
460
+ learned = []
461
+ for signature, approval_data in self._approval_memory.items():
462
+ learned.append(
463
+ {
464
+ "pattern": signature[:50] + ("..." if len(signature) > 50 else ""),
465
+ "decision": "approved",
466
+ "count": 1, # Could track actual usage count in the future
467
+ "last_used": approval_data.get("stored_at", ""),
468
+ }
469
+ )
470
+ return learned
471
+
472
+ def revoke_approval(self, operation_signature: str) -> bool:
473
+ """
474
+ Revoke a stored approval.
475
+
476
+ Args:
477
+ operation_signature: Signature of approval to revoke
478
+
479
+ Returns:
480
+ True if approval was found and revoked, False otherwise
481
+ """
482
+ if operation_signature in self._approval_memory:
483
+ del self._approval_memory[operation_signature]
484
+ return True
485
+ return False
486
+
487
+ def cleanup_expired_approvals(self) -> int:
488
+ """
489
+ Remove expired approvals from memory.
490
+
491
+ Returns:
492
+ Number of approvals that were removed
493
+ """
494
+ expired_signatures = []
495
+ now = datetime.now()
496
+
497
+ for signature, approval_data in self._approval_memory.items():
498
+ if "expires_at" in approval_data:
499
+ try:
500
+ expires_at = datetime.fromisoformat(approval_data["expires_at"])
501
+ if now > expires_at:
502
+ expired_signatures.append(signature)
503
+ except Exception:
504
+ # If we can't parse the date, consider it expired
505
+ expired_signatures.append(signature)
506
+
507
+ # Remove expired approvals
508
+ for signature in expired_signatures:
509
+ del self._approval_memory[signature]
510
+
511
+ return len(expired_signatures)