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.
- ata_coder/__init__.py +1 -0
- ata_coder/agent.py +874 -0
- ata_coder/agent_compact.py +190 -0
- ata_coder/agent_controller.py +218 -0
- ata_coder/agent_extension.py +69 -0
- ata_coder/agent_routing.py +105 -0
- ata_coder/agent_subsystems.py +72 -0
- ata_coder/agent_tools.py +318 -0
- ata_coder/agent_undo.py +63 -0
- ata_coder/anthropic_client.py +465 -0
- ata_coder/change_tracker.py +368 -0
- ata_coder/clawd_integration.py +574 -0
- ata_coder/commands/__init__.py +128 -0
- ata_coder/commands/_core.py +184 -0
- ata_coder/commands/_safety.py +95 -0
- ata_coder/commands/_settings.py +241 -0
- ata_coder/commands/_workflow.py +451 -0
- ata_coder/commands.py +974 -0
- ata_coder/config.py +257 -0
- ata_coder/core/__init__.py +35 -0
- ata_coder/core/events.py +73 -0
- ata_coder/core/queue.py +85 -0
- ata_coder/core/state.py +17 -0
- ata_coder/event_queue.py +5 -0
- ata_coder/extension.py +654 -0
- ata_coder/extensions/__init__.py +1 -0
- ata_coder/extensions/hello_skill.py +47 -0
- ata_coder/fool_proof.py +295 -0
- ata_coder/git_workflow.py +371 -0
- ata_coder/gui.py +511 -0
- ata_coder/llm_client.py +543 -0
- ata_coder/main.py +814 -0
- ata_coder/mcp_client.py +1095 -0
- ata_coder/memory.py +539 -0
- ata_coder/model_registry.py +134 -0
- ata_coder/model_router.py +105 -0
- ata_coder/permissions.py +274 -0
- ata_coder/privilege.py +464 -0
- ata_coder/project.py +273 -0
- ata_coder/prompt_template.py +423 -0
- ata_coder/prompts/auto-mode.md +7 -0
- ata_coder/prompts/coding-rules.md +40 -0
- ata_coder/prompts/execution-guardrails.md +14 -0
- ata_coder/prompts/memory-system.md +24 -0
- ata_coder/prompts/output-style.md +23 -0
- ata_coder/prompts/safety.md +17 -0
- ata_coder/prompts/slash-commands.md +24 -0
- ata_coder/prompts/sub-agents.md +38 -0
- ata_coder/prompts/system-reminders.md +17 -0
- ata_coder/prompts/system.md +105 -0
- ata_coder/prompts/tool-policy.md +46 -0
- ata_coder/repl_theme.py +99 -0
- ata_coder/repl_tracker.py +89 -0
- ata_coder/repl_ui.py +1214 -0
- ata_coder/safety_guard.py +434 -0
- ata_coder/self_correct.py +346 -0
- ata_coder/server.py +882 -0
- ata_coder/server_session.py +159 -0
- ata_coder/server_shell.py +129 -0
- ata_coder/session.py +431 -0
- ata_coder/settings.py +439 -0
- ata_coder/setup_wizard.py +136 -0
- ata_coder/skill_extension.py +92 -0
- ata_coder/skills/architect/SKILL.md +42 -0
- ata_coder/skills/code-reviewer/SKILL.md +37 -0
- ata_coder/skills/codecraft/SKILL.md +452 -0
- ata_coder/skills/debugger/SKILL.md +45 -0
- ata_coder/skills/doc-writer/SKILL.md +36 -0
- ata_coder/skills/general-coder/SKILL.md +76 -0
- ata_coder/skills/math-calculator/README.md +40 -0
- ata_coder/skills/math-calculator/SKILL.md +59 -0
- ata_coder/skills/math-calculator/handler.py +103 -0
- ata_coder/skills/math-calculator/prompts/system.md +8 -0
- ata_coder/skills/math-calculator/requirements.txt +2 -0
- ata_coder/skills/math-calculator/resources/constants.json +8 -0
- ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
- ata_coder/skills/security-auditor/SKILL.md +40 -0
- ata_coder/skills/test-writer/SKILL.md +36 -0
- ata_coder/skills/weather-skill/README.md +45 -0
- ata_coder/skills/weather-skill/handler.py +76 -0
- ata_coder/skills/weather-skill/manifest.json +48 -0
- ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
- ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
- ata_coder/skills/weather-skill/requirements.txt +1 -0
- ata_coder/skills/weather-skill/resources/city_list.json +17 -0
- ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
- ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
- ata_coder/skills/weather-skill/weather_utils.py +50 -0
- ata_coder/skills.py +1014 -0
- ata_coder/sub_agent.py +273 -0
- ata_coder/sub_agent_manager.py +203 -0
- ata_coder/system_prompt_builder.py +146 -0
- ata_coder/task_planner.py +391 -0
- ata_coder/terminal.py +318 -0
- ata_coder/test_runner.py +219 -0
- ata_coder/thread_supervisor.py +195 -0
- ata_coder/tool_defs.py +335 -0
- ata_coder/tools/__init__.py +11 -0
- ata_coder/tools/definitions.py +335 -0
- ata_coder/tools/executor.py +1036 -0
- ata_coder/tools/result.py +26 -0
- ata_coder/tools/subagent.py +332 -0
- ata_coder/tools/web.py +361 -0
- ata_coder/tools.py +1576 -0
- ata_coder/types.py +92 -0
- ata_coder/utils.py +113 -0
- ata_coder/web/css/style.css +180 -0
- ata_coder/web/index.html +84 -0
- ata_coder/web/js/app.js +489 -0
- ata_coder/web/package-lock.json +25 -0
- ata_coder/web/package.json +10 -0
- ata_coder/web/tsconfig.json +13 -0
- ata_coder-2.4.2.dist-info/METADATA +799 -0
- ata_coder-2.4.2.dist-info/RECORD +118 -0
- ata_coder-2.4.2.dist-info/WHEEL +5 -0
- ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
- ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
- 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
|