ata-coder 2.4.2__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 (118) hide show
  1. ata_coder/__init__.py +1 -0
  2. ata_coder/agent.py +874 -0
  3. ata_coder/agent_compact.py +190 -0
  4. ata_coder/agent_controller.py +218 -0
  5. ata_coder/agent_extension.py +69 -0
  6. ata_coder/agent_routing.py +105 -0
  7. ata_coder/agent_subsystems.py +72 -0
  8. ata_coder/agent_tools.py +318 -0
  9. ata_coder/agent_undo.py +63 -0
  10. ata_coder/anthropic_client.py +465 -0
  11. ata_coder/change_tracker.py +368 -0
  12. ata_coder/clawd_integration.py +574 -0
  13. ata_coder/commands/__init__.py +128 -0
  14. ata_coder/commands/_core.py +184 -0
  15. ata_coder/commands/_safety.py +95 -0
  16. ata_coder/commands/_settings.py +241 -0
  17. ata_coder/commands/_workflow.py +451 -0
  18. ata_coder/commands.py +974 -0
  19. ata_coder/config.py +257 -0
  20. ata_coder/core/__init__.py +35 -0
  21. ata_coder/core/events.py +73 -0
  22. ata_coder/core/queue.py +85 -0
  23. ata_coder/core/state.py +17 -0
  24. ata_coder/event_queue.py +5 -0
  25. ata_coder/extension.py +654 -0
  26. ata_coder/extensions/__init__.py +1 -0
  27. ata_coder/extensions/hello_skill.py +47 -0
  28. ata_coder/fool_proof.py +295 -0
  29. ata_coder/git_workflow.py +371 -0
  30. ata_coder/gui.py +511 -0
  31. ata_coder/llm_client.py +543 -0
  32. ata_coder/main.py +814 -0
  33. ata_coder/mcp_client.py +1095 -0
  34. ata_coder/memory.py +539 -0
  35. ata_coder/model_registry.py +134 -0
  36. ata_coder/model_router.py +105 -0
  37. ata_coder/permissions.py +274 -0
  38. ata_coder/privilege.py +464 -0
  39. ata_coder/project.py +273 -0
  40. ata_coder/prompt_template.py +423 -0
  41. ata_coder/prompts/auto-mode.md +7 -0
  42. ata_coder/prompts/coding-rules.md +40 -0
  43. ata_coder/prompts/execution-guardrails.md +14 -0
  44. ata_coder/prompts/memory-system.md +24 -0
  45. ata_coder/prompts/output-style.md +23 -0
  46. ata_coder/prompts/safety.md +17 -0
  47. ata_coder/prompts/slash-commands.md +24 -0
  48. ata_coder/prompts/sub-agents.md +38 -0
  49. ata_coder/prompts/system-reminders.md +17 -0
  50. ata_coder/prompts/system.md +105 -0
  51. ata_coder/prompts/tool-policy.md +46 -0
  52. ata_coder/repl_theme.py +99 -0
  53. ata_coder/repl_tracker.py +89 -0
  54. ata_coder/repl_ui.py +1214 -0
  55. ata_coder/safety_guard.py +434 -0
  56. ata_coder/self_correct.py +346 -0
  57. ata_coder/server.py +882 -0
  58. ata_coder/server_session.py +159 -0
  59. ata_coder/server_shell.py +129 -0
  60. ata_coder/session.py +431 -0
  61. ata_coder/settings.py +439 -0
  62. ata_coder/setup_wizard.py +136 -0
  63. ata_coder/skill_extension.py +92 -0
  64. ata_coder/skills/architect/SKILL.md +42 -0
  65. ata_coder/skills/code-reviewer/SKILL.md +37 -0
  66. ata_coder/skills/codecraft/SKILL.md +452 -0
  67. ata_coder/skills/debugger/SKILL.md +45 -0
  68. ata_coder/skills/doc-writer/SKILL.md +36 -0
  69. ata_coder/skills/general-coder/SKILL.md +76 -0
  70. ata_coder/skills/math-calculator/README.md +40 -0
  71. ata_coder/skills/math-calculator/SKILL.md +59 -0
  72. ata_coder/skills/math-calculator/handler.py +103 -0
  73. ata_coder/skills/math-calculator/prompts/system.md +8 -0
  74. ata_coder/skills/math-calculator/requirements.txt +2 -0
  75. ata_coder/skills/math-calculator/resources/constants.json +8 -0
  76. ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
  77. ata_coder/skills/security-auditor/SKILL.md +40 -0
  78. ata_coder/skills/test-writer/SKILL.md +36 -0
  79. ata_coder/skills/weather-skill/README.md +45 -0
  80. ata_coder/skills/weather-skill/handler.py +76 -0
  81. ata_coder/skills/weather-skill/manifest.json +48 -0
  82. ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
  83. ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
  84. ata_coder/skills/weather-skill/requirements.txt +1 -0
  85. ata_coder/skills/weather-skill/resources/city_list.json +17 -0
  86. ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
  87. ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
  88. ata_coder/skills/weather-skill/weather_utils.py +50 -0
  89. ata_coder/skills.py +1014 -0
  90. ata_coder/sub_agent.py +273 -0
  91. ata_coder/sub_agent_manager.py +203 -0
  92. ata_coder/system_prompt_builder.py +146 -0
  93. ata_coder/task_planner.py +391 -0
  94. ata_coder/terminal.py +318 -0
  95. ata_coder/test_runner.py +219 -0
  96. ata_coder/thread_supervisor.py +195 -0
  97. ata_coder/tool_defs.py +335 -0
  98. ata_coder/tools/__init__.py +11 -0
  99. ata_coder/tools/definitions.py +335 -0
  100. ata_coder/tools/executor.py +1036 -0
  101. ata_coder/tools/result.py +26 -0
  102. ata_coder/tools/subagent.py +332 -0
  103. ata_coder/tools/web.py +361 -0
  104. ata_coder/tools.py +1576 -0
  105. ata_coder/types.py +92 -0
  106. ata_coder/utils.py +113 -0
  107. ata_coder/web/css/style.css +180 -0
  108. ata_coder/web/index.html +84 -0
  109. ata_coder/web/js/app.js +489 -0
  110. ata_coder/web/package-lock.json +25 -0
  111. ata_coder/web/package.json +10 -0
  112. ata_coder/web/tsconfig.json +13 -0
  113. ata_coder-2.4.2.dist-info/METADATA +799 -0
  114. ata_coder-2.4.2.dist-info/RECORD +118 -0
  115. ata_coder-2.4.2.dist-info/WHEEL +5 -0
  116. ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
  117. ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
  118. ata_coder-2.4.2.dist-info/top_level.txt +1 -0
ata_coder/privilege.py ADDED
@@ -0,0 +1,464 @@
1
+ """
2
+ OS-aware privilege escalation system.
3
+
4
+ Handles platform-specific authorization for dangerous operations:
5
+
6
+ Windows → UAC elevation detection + admin check
7
+ macOS → osascript admin prompt + sudo biometric
8
+ Linux → sudo with password + root detection
9
+
10
+ Dangerous mode features:
11
+ - Must be explicitly activated by user (/dangerous on)
12
+ - Time-limited (auto-disable after configurable timeout)
13
+ - Visual indicators (red UI, warnings)
14
+ - Full audit logging of all privileged operations
15
+ - Even in dangerous mode, critical patterns remain blocked
16
+ - Platform-specific privilege escalation commands
17
+ """
18
+
19
+ import base64
20
+ import logging
21
+ import os
22
+ import platform
23
+ import shlex
24
+ import subprocess
25
+ import time
26
+ from dataclasses import dataclass, field
27
+ from enum import Enum
28
+ from pathlib import Path
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # ═══════════════════════════════════════════════════════════════════════════════
34
+ # OS detection
35
+ # ═══════════════════════════════════════════════════════════════════════════════
36
+
37
+ class OSFamily(Enum):
38
+ WINDOWS = "windows"
39
+ MACOS = "macos"
40
+ LINUX = "linux"
41
+ UNKNOWN = "unknown"
42
+
43
+
44
+ def detect_os() -> OSFamily:
45
+ system = platform.system().lower()
46
+ if system == "windows":
47
+ return OSFamily.WINDOWS
48
+ elif system == "darwin":
49
+ return OSFamily.MACOS
50
+ elif system == "linux":
51
+ return OSFamily.LINUX
52
+ return OSFamily.UNKNOWN
53
+
54
+
55
+ def os_display() -> str:
56
+ """Human-readable OS info."""
57
+ return f"{platform.system()} {platform.release()} ({platform.machine()})"
58
+
59
+
60
+ # ═══════════════════════════════════════════════════════════════════════════════
61
+ # Privilege level detection
62
+ # ═══════════════════════════════════════════════════════════════════════════════
63
+
64
+ class PrivilegeLevel(Enum):
65
+ USER = "user" # Normal user
66
+ ADMIN = "admin" # Administrator / sudoer
67
+ ROOT = "root" # Running as root (Linux) / SYSTEM (Windows)
68
+
69
+
70
+ def detect_privilege() -> PrivilegeLevel:
71
+ """Detect the current process's privilege level."""
72
+ os_family = detect_os()
73
+
74
+ if os_family == OSFamily.WINDOWS:
75
+ try:
76
+ import ctypes
77
+ return PrivilegeLevel.ADMIN if ctypes.windll.shell32.IsUserAnAdmin() else PrivilegeLevel.USER
78
+ except Exception:
79
+ logger.debug("IsUserAnAdmin check failed, defaulting to USER")
80
+ return PrivilegeLevel.USER
81
+
82
+ elif os_family in (OSFamily.LINUX, OSFamily.MACOS):
83
+ if os.geteuid() == 0:
84
+ return PrivilegeLevel.ROOT
85
+ # Check if user can sudo
86
+ try:
87
+ result = subprocess.run(
88
+ ["sudo", "-n", "true"],
89
+ capture_output=True, timeout=5,
90
+ )
91
+ if result.returncode == 0:
92
+ return PrivilegeLevel.ADMIN
93
+ except Exception:
94
+ pass
95
+ return PrivilegeLevel.USER
96
+
97
+ return PrivilegeLevel.USER
98
+
99
+
100
+ # ═══════════════════════════════════════════════════════════════════════════════
101
+ # Platform-specific privilege escalation
102
+ # ═══════════════════════════════════════════════════════════════════════════════
103
+
104
+ def get_elevation_prefix() -> str | None:
105
+ """
106
+ Get the platform-specific command prefix for privilege escalation.
107
+ Returns None if elevation is not possible.
108
+ """
109
+ os_family = detect_os()
110
+ priv = detect_privilege()
111
+
112
+ if priv == PrivilegeLevel.ROOT:
113
+ return None # Already root, no prefix needed
114
+
115
+ if os_family == OSFamily.LINUX:
116
+ # Check if sudo is available
117
+ try:
118
+ subprocess.run(["which", "sudo"], capture_output=True, timeout=3, check=True)
119
+ return "sudo"
120
+ except Exception:
121
+ pass
122
+ # Try pkexec
123
+ try:
124
+ subprocess.run(["which", "pkexec"], capture_output=True, timeout=3, check=True)
125
+ return "pkexec"
126
+ except Exception:
127
+ pass
128
+ return None
129
+
130
+ elif os_family == OSFamily.MACOS:
131
+ # macOS: osascript can be used for admin privileges
132
+ return "osascript"
133
+
134
+ elif os_family == OSFamily.WINDOWS:
135
+ # Windows: PowerShell Start-Process -Verb RunAs for elevation
136
+ return "powershell"
137
+
138
+ return None
139
+
140
+
141
+ def wrap_privileged_command(command: str) -> str:
142
+ """
143
+ Wrap a command with platform-specific privilege elevation.
144
+ Uses shlex.quote() to prevent shell injection.
145
+ """
146
+ os_family = detect_os()
147
+ priv = detect_privilege()
148
+
149
+ if priv == PrivilegeLevel.ROOT:
150
+ return command
151
+
152
+ if os_family == OSFamily.LINUX:
153
+ return f"sudo -- {command}" # -- stops option parsing, command is safe
154
+
155
+ elif os_family == OSFamily.MACOS:
156
+ # osascript: double-quote the command, shlex.quote for inner safety
157
+ return (
158
+ "osascript -e "
159
+ + shlex.quote(f'do shell script {shlex.quote(command)}'
160
+ ' with administrator privileges')
161
+ )
162
+
163
+ elif os_family == OSFamily.WINDOWS:
164
+ # Encode command as base64 (utf-16-le) to avoid escaping nightmares
165
+ # in PowerShell's nested quoting. Use subprocess-style argument list
166
+ # rather than string interpolation to prevent command injection.
167
+ encoded = base64.b64encode(command.encode("utf-16-le")).decode()
168
+ inner_cmd = f"/c powershell -EncodedCommand {encoded}"
169
+ return (
170
+ 'powershell -Command "'
171
+ 'Start-Process -Verb RunAs -Wait -FilePath cmd.exe '
172
+ f'-ArgumentList {shlex.quote(inner_cmd)}"'
173
+ )
174
+
175
+ return command
176
+
177
+
178
+ # ═══════════════════════════════════════════════════════════════════════════════
179
+ # Dangerous mode manager
180
+ # ═══════════════════════════════════════════════════════════════════════════════
181
+
182
+ @dataclass
183
+ class DangerousModeState:
184
+ """Current state of dangerous mode."""
185
+ enabled: bool = False
186
+ activated_at: float = 0.0 # Unix timestamp
187
+ timeout_minutes: int = 15 # Auto-disable after N minutes
188
+ confirmed_by: str = "" # How user confirmed
189
+ level: str = "standard" # "standard" | "elevated" | "full"
190
+ audit_log: list[str] = field(default_factory=list)
191
+
192
+
193
+ class PrivilegeManager:
194
+ """
195
+ Manages dangerous mode and privilege escalation.
196
+
197
+ Usage:
198
+ pm = PrivilegeManager()
199
+ pm.enable_dangerous_mode("user-typed-confirm")
200
+ if pm.is_dangerous:
201
+ elevated_cmd = pm.wrap_command("apt install nginx")
202
+ """
203
+
204
+ def __init__(self, workspace: str | Path | None = None):
205
+ self.os_family = detect_os()
206
+ self.privilege = detect_privilege()
207
+ self.workspace = Path(workspace) if workspace else Path.cwd()
208
+
209
+ self._dangerous = DangerousModeState()
210
+
211
+ # Operations that are STILL blocked even in dangerous mode
212
+ self._hard_blocks: list[str] = [
213
+ "rm -rf /", "mkfs.", "dd if=/dev/zero of=/dev/",
214
+ "> /dev/sda", "> /dev/nvme",
215
+ "chmod 777 /", ":(){ :|:& };:",
216
+ ]
217
+
218
+ # ── Dangerous mode ─────────────────────────────────────────────────
219
+
220
+ @property
221
+ def is_dangerous(self) -> bool:
222
+ """Check if dangerous mode is active (and not expired)."""
223
+ if not self._dangerous.enabled:
224
+ return False
225
+
226
+ # Check timeout
227
+ if self._dangerous.timeout_minutes > 0:
228
+ elapsed = (time.time() - self._dangerous.activated_at) / 60
229
+ if elapsed > self._dangerous.timeout_minutes:
230
+ logger.warning("Dangerous mode expired after %.1f minutes", elapsed)
231
+ self._dangerous.enabled = False
232
+ return False
233
+
234
+ return True
235
+
236
+ def enable_dangerous_mode(self, confirmed_by: str = "",
237
+ timeout_minutes: int = 15,
238
+ level: str = "standard") -> str:
239
+ """
240
+ Activate dangerous mode. Requires explicit confirmation.
241
+ Returns a confirmation message.
242
+ """
243
+ self._dangerous.enabled = True
244
+ self._dangerous.activated_at = time.time()
245
+ self._dangerous.timeout_minutes = timeout_minutes
246
+ self._dangerous.confirmed_by = confirmed_by
247
+ self._dangerous.level = level
248
+
249
+ self._audit("DANGEROUS_MODE_ENABLED", {
250
+ "level": level,
251
+ "timeout": timeout_minutes,
252
+ "os": os_display(),
253
+ "privilege": self.privilege.value,
254
+ "confirmed_by": confirmed_by,
255
+ })
256
+
257
+ msg = f"""
258
+ ╔══════════════════════════════════════════════════════════╗
259
+ ║ ⚠️ DANGEROUS MODE ACTIVATED ║
260
+ ╠══════════════════════════════════════════════════════════╣
261
+ ║ Level: {level:<44}║
262
+ ║ Timeout: {timeout_minutes} minutes{'':<37}║
263
+ ║ OS: {os_display():<44}║
264
+ ║ Privilege: {self.privilege.value:<44}║
265
+ ╠══════════════════════════════════════════════════════════╣
266
+ ║ ALL privileged operations will be AUDIT LOGGED. ║
267
+ ║ Critical system-destroying commands remain BLOCKED. ║
268
+ ║ Use /dangerous off to disable. ║
269
+ ╚══════════════════════════════════════════════════════════╝
270
+ """
271
+ return msg
272
+
273
+ def disable_dangerous_mode(self) -> str:
274
+ """Deactivate dangerous mode."""
275
+ was_enabled = self._dangerous.enabled
276
+ self._dangerous = DangerousModeState()
277
+ if was_enabled:
278
+ self._audit("DANGEROUS_MODE_DISABLED", {})
279
+ return "Dangerous mode disabled. Normal safety rules restored."
280
+ return "Dangerous mode was not active."
281
+
282
+ def status(self) -> str:
283
+ """Get status message."""
284
+ if self.is_dangerous:
285
+ remaining = self._dangerous.timeout_minutes - (time.time() - self._dangerous.activated_at) / 60
286
+ return (
287
+ f"DANGEROUS MODE ACTIVE | "
288
+ f"Level: {self._dangerous.level} | "
289
+ f"OS: {self.os_family.value} | "
290
+ f"Privilege: {self.privilege.value} | "
291
+ f"Remaining: {remaining:.0f}min | "
292
+ f"Audit entries: {len(self._dangerous.audit_log)}"
293
+ )
294
+ return (
295
+ f"Safe mode | "
296
+ f"OS: {self.os_family.value} | "
297
+ f"Privilege: {self.privilege.value}"
298
+ )
299
+
300
+ # ── Command wrapping ──────────────────────────────────────────────
301
+
302
+ def wrap_command(self, command: str, force_elevation: bool = False) -> str:
303
+ """
304
+ Wrap a command for execution, potentially with privilege escalation.
305
+ Only elevates if dangerous mode is active AND elevation is requested.
306
+ """
307
+ if force_elevation and self.is_dangerous:
308
+ return wrap_privileged_command(command)
309
+ return command
310
+
311
+ def check_dangerous_command(self, command: str) -> tuple[bool, str]:
312
+ """
313
+ Check if a command is allowed in dangerous mode.
314
+ Returns (allowed, reason).
315
+ """
316
+ # Hard blocks — always denied (safety-critical, use substring match)
317
+ cmd_clean = command.strip()
318
+ for pattern in self._hard_blocks:
319
+ if pattern in cmd_clean:
320
+ return False, f"CRITICAL BLOCK (even in dangerous mode): pattern '{pattern}'"
321
+
322
+ # In dangerous mode, most things are allowed
323
+ if self.is_dangerous:
324
+ return True, ""
325
+
326
+ # Outside dangerous mode — check if command needs elevation
327
+ needs_elev = self.needs_elevation(command)
328
+ if needs_elev:
329
+ return False, (
330
+ f"This command requires elevated privileges. "
331
+ f"Enable dangerous mode first: /dangerous on\n"
332
+ f" Detected OS: {os_display()}\n"
333
+ f" Current privilege: {self.privilege.value}\n"
334
+ f" Elevation command: {wrap_privileged_command(command)[:100]}..."
335
+ )
336
+
337
+ return True, ""
338
+
339
+ def needs_elevation(self, command: str) -> bool:
340
+ """Check if a command needs privilege elevation."""
341
+ cmd_lower = command.lower().strip()
342
+
343
+ # Package management
344
+ package_managers = [
345
+ "apt ", "apt-get ", "yum ", "dnf ", "pacman ", "zypper ",
346
+ "brew ", "port ", "choco ", "winget ",
347
+ "pip install", "pip3 install",
348
+ "npm install -g", "npm i -g",
349
+ "gem install",
350
+ ]
351
+ for pm in package_managers:
352
+ if cmd_lower.startswith(pm):
353
+ return True
354
+
355
+ # System service management
356
+ service_patterns = [
357
+ "systemctl ", "service ", "launchctl ",
358
+ "sc start", "sc stop", "net start", "net stop",
359
+ ]
360
+ for sp in service_patterns:
361
+ if sp in cmd_lower:
362
+ return True
363
+
364
+ # File operations in protected areas
365
+ if any(p in command for p in ["/etc/", "/usr/", "/opt/", "/var/",
366
+ "C:\\Program Files", "C:\\Windows\\System32"]):
367
+ return True
368
+
369
+ # Permission changes
370
+ if any(p in cmd_lower for p in ["chmod ", "chown ", "chgrp ", "setfacl "]):
371
+ return True
372
+
373
+ # Network config
374
+ if any(p in cmd_lower for p in ["ifconfig ", "ip link", "ip addr",
375
+ "netsh ", "iptables ", "ufw ", "firewall-cmd"]):
376
+ return True
377
+
378
+ # Docker (often needs sudo)
379
+ if cmd_lower.startswith("docker ") and "docker ps" not in cmd_lower:
380
+ return True
381
+
382
+ return False
383
+
384
+ # ── Audit ───────────────────────────────────────────────────────────
385
+
386
+ def _audit(self, event: str, details: dict) -> None:
387
+ """Record an audited event."""
388
+ entry = f"[{time.strftime('%Y-%m-%dT%H:%M:%SZ')}] {event}"
389
+ if details:
390
+ entry += " | " + " ".join(f"{k}={v}" for k, v in details.items())
391
+ self._dangerous.audit_log.append(entry)
392
+ logger.info("AUDIT: %s", entry)
393
+
394
+ def audit_operation(self, tool_name: str, arguments: dict) -> None:
395
+ """Audit a privileged operation."""
396
+ if self.is_dangerous:
397
+ details = {"tool": tool_name}
398
+ if "command" in arguments:
399
+ details["command"] = arguments["command"][:200]
400
+ elif "file_path" in arguments:
401
+ details["file"] = arguments["file_path"]
402
+ self._audit("PRIVILEGED_OP", details)
403
+
404
+ def get_audit_log(self) -> str:
405
+ """Get the full audit log."""
406
+ if not self._dangerous.audit_log:
407
+ return "(no privileged operations logged)"
408
+ return "\n".join(self._dangerous.audit_log)
409
+
410
+ # ── OS-specific helpers ────────────────────────────────────────────
411
+
412
+ def get_elevation_instructions(self) -> str:
413
+ """Get human-readable instructions for gaining privileges on this OS."""
414
+ os_family = detect_os()
415
+ priv = detect_privilege()
416
+
417
+ if priv == PrivilegeLevel.ROOT:
418
+ return "Already running as root. Full system access available."
419
+ if priv == PrivilegeLevel.ADMIN:
420
+ return f"Running with admin privileges on {os_display()}. Use /dangerous on to enable."
421
+
422
+ if os_family == OSFamily.WINDOWS:
423
+ return (
424
+ "To gain admin privileges on Windows:\n"
425
+ " 1. Right-click Terminal/PowerShell → Run as Administrator\n"
426
+ " 2. Or: Start-Process -Verb RunAs python main.py\n"
427
+ " 3. Confirm the UAC prompt"
428
+ )
429
+ elif os_family == OSFamily.MACOS:
430
+ return (
431
+ "To gain admin privileges on macOS:\n"
432
+ " 1. Prefix commands with 'sudo'\n"
433
+ " 2. The system will prompt for your password / Touch ID\n"
434
+ " 3. Or run the agent with: sudo python main.py"
435
+ )
436
+ elif os_family == OSFamily.LINUX:
437
+ return (
438
+ "To gain admin privileges on Linux:\n"
439
+ " 1. Prefix commands with 'sudo'\n"
440
+ " 2. Or run the agent with: sudo python main.py\n"
441
+ " 3. To allow passwordless sudo for specific commands, edit /etc/sudoers"
442
+ )
443
+ return "Unknown OS. Cannot determine elevation method."
444
+
445
+ @property
446
+ def can_elevate(self) -> bool:
447
+ """Check if privilege escalation is possible on this system."""
448
+ if self.privilege in (PrivilegeLevel.ADMIN, PrivilegeLevel.ROOT):
449
+ return True
450
+ return get_elevation_prefix() is not None
451
+
452
+
453
+ # ═══════════════════════════════════════════════════════════════════════════════
454
+ # Global
455
+ # ═══════════════════════════════════════════════════════════════════════════════
456
+
457
+ _privilege_manager: PrivilegeManager | None = None
458
+
459
+
460
+ def get_privilege_manager(workspace: str | None = None) -> PrivilegeManager:
461
+ global _privilege_manager
462
+ if _privilege_manager is None:
463
+ _privilege_manager = PrivilegeManager(workspace)
464
+ return _privilege_manager