ctf-agent 1.2.0

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 (152) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +192 -0
  3. package/README.md +522 -0
  4. package/agents/ctf-analyzer.md +58 -0
  5. package/agents/ctf-controller.md +151 -0
  6. package/agents/ctf-speedrun.md +74 -0
  7. package/bin/ctf-agent.js +119 -0
  8. package/docker-compose.yml +13 -0
  9. package/mcp_config.json +19 -0
  10. package/package.json +44 -0
  11. package/references/ctf-triage-ladder.md +68 -0
  12. package/references/exploit-databases.md +575 -0
  13. package/references/llm-safety-and-policy-compliance.md +173 -0
  14. package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
  15. package/references/security-events-and-intelligence.md +566 -0
  16. package/references/version-matrix.md +59 -0
  17. package/rules/ctf-execution-rules.md +44 -0
  18. package/rules/ctf-safety-framing-rules.md +140 -0
  19. package/rules/flag-validation-rules.md +25 -0
  20. package/scripts/__init__.py +1 -0
  21. package/scripts/ctf-tools.lock +205 -0
  22. package/scripts/ctf_init.py +1026 -0
  23. package/scripts/ctf_update.py +583 -0
  24. package/scripts/ctfd_client.py +81 -0
  25. package/scripts/cve_lookup.py +453 -0
  26. package/scripts/extract_flags.py +155 -0
  27. package/scripts/install_as_agent.py +278 -0
  28. package/scripts/install_ctf_tools.sh +603 -0
  29. package/scripts/parallel_triage.py +395 -0
  30. package/scripts/prompt_policy_sanitizer.py +283 -0
  31. package/scripts/scope_guard.py +444 -0
  32. package/scripts/skill_validator.py +504 -0
  33. package/scripts/workspace_cleaner.py +141 -0
  34. package/skills/ctf-ai-ml/SKILL.md +120 -0
  35. package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
  36. package/skills/ctf-ai-ml/llm-attacks.md +487 -0
  37. package/skills/ctf-ai-ml/model-attacks.md +422 -0
  38. package/skills/ctf-crypto/SKILL.md +320 -0
  39. package/skills/ctf-crypto/advanced-math.md +798 -0
  40. package/skills/ctf-crypto/classic-ciphers.md +651 -0
  41. package/skills/ctf-crypto/ecc-attacks.md +347 -0
  42. package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
  43. package/skills/ctf-crypto/exotic-crypto.md +528 -0
  44. package/skills/ctf-crypto/historical.md +113 -0
  45. package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
  46. package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
  47. package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
  48. package/skills/ctf-crypto/modern-ciphers.md +649 -0
  49. package/skills/ctf-crypto/prng-attacks.md +257 -0
  50. package/skills/ctf-crypto/prng.md +664 -0
  51. package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
  52. package/skills/ctf-crypto/rsa-attacks.md +487 -0
  53. package/skills/ctf-crypto/stream-ciphers.md +390 -0
  54. package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
  55. package/skills/ctf-forensics/3d-printing.md +121 -0
  56. package/skills/ctf-forensics/SKILL.md +379 -0
  57. package/skills/ctf-forensics/disk-advanced.md +497 -0
  58. package/skills/ctf-forensics/disk-and-memory.md +491 -0
  59. package/skills/ctf-forensics/disk-recovery.md +699 -0
  60. package/skills/ctf-forensics/linux-forensics.md +511 -0
  61. package/skills/ctf-forensics/network-advanced.md +583 -0
  62. package/skills/ctf-forensics/network.md +645 -0
  63. package/skills/ctf-forensics/peripheral-capture.md +287 -0
  64. package/skills/ctf-forensics/signals-and-hardware.md +713 -0
  65. package/skills/ctf-forensics/steganography.md +694 -0
  66. package/skills/ctf-forensics/stego-advanced-2.md +475 -0
  67. package/skills/ctf-forensics/stego-advanced.md +481 -0
  68. package/skills/ctf-forensics/stego-image.md +691 -0
  69. package/skills/ctf-forensics/windows.md +625 -0
  70. package/skills/ctf-malware/SKILL.md +181 -0
  71. package/skills/ctf-malware/c2-and-protocols.md +274 -0
  72. package/skills/ctf-malware/pe-and-dotnet.md +108 -0
  73. package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
  74. package/skills/ctf-misc/SKILL.md +498 -0
  75. package/skills/ctf-misc/bashjails.md +323 -0
  76. package/skills/ctf-misc/ctfd-navigation.md +465 -0
  77. package/skills/ctf-misc/dns.md +255 -0
  78. package/skills/ctf-misc/encodings-advanced.md +504 -0
  79. package/skills/ctf-misc/encodings.md +431 -0
  80. package/skills/ctf-misc/games-and-vms-2.md +254 -0
  81. package/skills/ctf-misc/games-and-vms-3.md +690 -0
  82. package/skills/ctf-misc/games-and-vms-4.md +229 -0
  83. package/skills/ctf-misc/games-and-vms.md +529 -0
  84. package/skills/ctf-misc/linux-privesc.md +333 -0
  85. package/skills/ctf-misc/pyjails.md +671 -0
  86. package/skills/ctf-misc/rf-sdr.md +91 -0
  87. package/skills/ctf-osint/SKILL.md +198 -0
  88. package/skills/ctf-osint/geolocation-and-media.md +464 -0
  89. package/skills/ctf-osint/social-media.md +312 -0
  90. package/skills/ctf-osint/web-and-dns.md +341 -0
  91. package/skills/ctf-pwn/SKILL.md +214 -0
  92. package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
  93. package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
  94. package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
  95. package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
  96. package/skills/ctf-pwn/advanced-exploits.md +773 -0
  97. package/skills/ctf-pwn/advanced.md +326 -0
  98. package/skills/ctf-pwn/field-notes.md +245 -0
  99. package/skills/ctf-pwn/format-string.md +694 -0
  100. package/skills/ctf-pwn/heap-fsop.md +285 -0
  101. package/skills/ctf-pwn/heap-techniques-2.md +333 -0
  102. package/skills/ctf-pwn/heap-techniques.md +513 -0
  103. package/skills/ctf-pwn/kernel-bypass.md +421 -0
  104. package/skills/ctf-pwn/kernel-techniques.md +366 -0
  105. package/skills/ctf-pwn/kernel.md +636 -0
  106. package/skills/ctf-pwn/overflow-basics.md +611 -0
  107. package/skills/ctf-pwn/rop-advanced.md +725 -0
  108. package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
  109. package/skills/ctf-pwn/sandbox-escape.md +313 -0
  110. package/skills/ctf-reverse/SKILL.md +163 -0
  111. package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
  112. package/skills/ctf-reverse/anti-analysis.md +693 -0
  113. package/skills/ctf-reverse/field-notes.md +376 -0
  114. package/skills/ctf-reverse/languages-compiled.md +666 -0
  115. package/skills/ctf-reverse/languages-platforms.md +592 -0
  116. package/skills/ctf-reverse/languages.md +553 -0
  117. package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
  118. package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
  119. package/skills/ctf-reverse/patterns-ctf.md +670 -0
  120. package/skills/ctf-reverse/patterns-runtime.md +274 -0
  121. package/skills/ctf-reverse/patterns.md +572 -0
  122. package/skills/ctf-reverse/platforms-hardware.md +387 -0
  123. package/skills/ctf-reverse/platforms.md +664 -0
  124. package/skills/ctf-reverse/tools-advanced-2.md +421 -0
  125. package/skills/ctf-reverse/tools-advanced.md +407 -0
  126. package/skills/ctf-reverse/tools-dynamic.md +679 -0
  127. package/skills/ctf-reverse/tools-emulation.md +319 -0
  128. package/skills/ctf-reverse/tools.md +573 -0
  129. package/skills/ctf-web/SKILL.md +153 -0
  130. package/skills/ctf-web/auth-and-access-2.md +82 -0
  131. package/skills/ctf-web/auth-and-access.md +783 -0
  132. package/skills/ctf-web/auth-infra.md +321 -0
  133. package/skills/ctf-web/auth-jwt.md +186 -0
  134. package/skills/ctf-web/client-side-advanced.md +739 -0
  135. package/skills/ctf-web/client-side.md +529 -0
  136. package/skills/ctf-web/cves.md +373 -0
  137. package/skills/ctf-web/field-notes.md +482 -0
  138. package/skills/ctf-web/node-and-prototype.md +200 -0
  139. package/skills/ctf-web/server-side-2.md +337 -0
  140. package/skills/ctf-web/server-side-advanced-2.md +559 -0
  141. package/skills/ctf-web/server-side-advanced-3.md +125 -0
  142. package/skills/ctf-web/server-side-advanced-4.md +480 -0
  143. package/skills/ctf-web/server-side-advanced.md +378 -0
  144. package/skills/ctf-web/server-side-deser.md +443 -0
  145. package/skills/ctf-web/server-side-exec-2.md +799 -0
  146. package/skills/ctf-web/server-side-exec.md +457 -0
  147. package/skills/ctf-web/server-side.md +629 -0
  148. package/skills/ctf-web/sql-injection.md +790 -0
  149. package/skills/ctf-web/web3.md +374 -0
  150. package/skills/ctf-writeup/SKILL.md +90 -0
  151. package/skills/solve-challenge/SKILL.md +269 -0
  152. package/skills.json +16 -0
@@ -0,0 +1,1026 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CTF-Agent Preflight Environment Detector & Workspace Initializer (v1.0)
4
+ Analyzes host capabilities, scores execution backends (WSL vs Docker),
5
+ selects workload profiles, provisions workspaces, and verifies health.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import platform
11
+ import shutil
12
+ import argparse
13
+ import subprocess
14
+ import json
15
+ import socket
16
+ from pathlib import Path
17
+ from typing import Dict, Any, List, Optional, Tuple
18
+
19
+ # Ensure safe UTF-8 output on Windows consoles
20
+ if hasattr(sys.stdout, "reconfigure"):
21
+ try:
22
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
23
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
24
+ except Exception:
25
+ pass
26
+
27
+ REPO_ROOT = Path(__file__).resolve().parent.parent
28
+
29
+ # Import deployment logic from install_as_agent
30
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
31
+ try:
32
+ import install_as_agent
33
+ except ImportError:
34
+ install_as_agent = None
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Workload & Profile Definitions
39
+ # ---------------------------------------------------------------------------
40
+
41
+ PURPOSE_PROFILES = {
42
+ "live-ctf": {
43
+ "title": "Live CTF Competition (Blitz / Speedrun)",
44
+ "description": "Fastest time-to-flag, minimal documentation overhead, stop-on-flag workflow.",
45
+ "profiles": "core,pwn,web,crypto",
46
+ "agent_mode": "@ctf-speedrun (--blitz)",
47
+ },
48
+ "security-lab": {
49
+ "title": "Security Lab & Research (Deep Analysis / RCA)",
50
+ "description": "Exhaustive root-cause analysis, standardized writeup.md, resources/ preservation.",
51
+ "profiles": "core,pwn,rev,crypto,forensics,web,cloud,ad,kernel,web3",
52
+ "agent_mode": "@ctf-analyzer (--deep)",
53
+ },
54
+ "rev-pwn": {
55
+ "title": "Reverse Engineering & Binary Exploitation",
56
+ "description": "Dedicated disassemblers, decompilers, ROP, heap, and symbolic execution.",
57
+ "profiles": "core,pwn,rev,kernel,wasm",
58
+ "agent_mode": "@ctf-speedrun / @ctf-analyzer",
59
+ },
60
+ "full": {
61
+ "title": "Full Comprehensive CTF Workstation",
62
+ "description": "All 15 category profiles installed into the isolated backend.",
63
+ "profiles": "all",
64
+ "agent_mode": "@ctf-controller",
65
+ },
66
+ }
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Environment Preflight Detector
71
+ # ---------------------------------------------------------------------------
72
+
73
+ class EnvironmentDetector:
74
+ """Detects host platform, resources, WSL distro state, Docker state, and network."""
75
+
76
+ @staticmethod
77
+ def detect_os() -> Dict[str, Any]:
78
+ os_type = platform.system()
79
+ return {
80
+ "system": os_type,
81
+ "platform": sys.platform,
82
+ "release": platform.release(),
83
+ "arch": platform.machine(),
84
+ "cpu_cores": os.cpu_count() or 1,
85
+ "is_windows": os_type == "Windows",
86
+ "is_linux": os_type == "Linux",
87
+ "is_macos": os_type == "Darwin",
88
+ }
89
+
90
+ @staticmethod
91
+ def detect_memory() -> Dict[str, float]:
92
+ total_gb = 0.0
93
+ avail_gb = 0.0
94
+
95
+ if platform.system() == "Windows":
96
+ try:
97
+ import ctypes
98
+ class MEMORYSTATUSEX(ctypes.Structure):
99
+ _fields_ = [
100
+ ("dwLength", ctypes.c_ulong),
101
+ ("dwMemoryLoad", ctypes.c_ulong),
102
+ ("ullTotalPhys", ctypes.c_ulonglong),
103
+ ("ullAvailPhys", ctypes.c_ulonglong),
104
+ ("ullTotalPageFile", ctypes.c_ulonglong),
105
+ ("ullAvailPageFile", ctypes.c_ulonglong),
106
+ ("ullTotalVirtual", ctypes.c_ulonglong),
107
+ ("ullAvailVirtual", ctypes.c_ulonglong),
108
+ ("sullAvailExtendedVirtual", ctypes.c_ulonglong),
109
+ ]
110
+ stat = MEMORYSTATUSEX()
111
+ stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
112
+ if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
113
+ total_gb = round(stat.ullTotalPhys / (1024 ** 3), 2)
114
+ avail_gb = round(stat.ullAvailPhys / (1024 ** 3), 2)
115
+ except Exception:
116
+ pass
117
+ elif platform.system() == "Linux":
118
+ try:
119
+ with open("/proc/meminfo", "r", encoding="utf-8") as f:
120
+ mem_data = {}
121
+ for line in f:
122
+ parts = line.split(":")
123
+ if len(parts) == 2:
124
+ mem_data[parts[0].strip()] = parts[1].strip()
125
+ if "MemTotal" in mem_data:
126
+ kb = float(mem_data["MemTotal"].split()[0])
127
+ total_gb = round(kb / (1024 ** 2), 2)
128
+ if "MemAvailable" in mem_data:
129
+ kb = float(mem_data["MemAvailable"].split()[0])
130
+ avail_gb = round(kb / (1024 ** 2), 2)
131
+ except Exception:
132
+ pass
133
+ elif platform.system() == "Darwin":
134
+ try:
135
+ out = subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True).strip()
136
+ bytes_mem = float(out)
137
+ total_gb = round(bytes_mem / (1024 ** 3), 2)
138
+ avail_gb = total_gb * 0.5
139
+ except Exception:
140
+ pass
141
+
142
+ return {"total_gb": total_gb, "avail_gb": avail_gb}
143
+
144
+ @staticmethod
145
+ def detect_disk(path: Path) -> Dict[str, float]:
146
+ target = path.resolve()
147
+ if not target.exists():
148
+ target = target.parent if target.parent.exists() else Path.cwd()
149
+ try:
150
+ total, used, free = shutil.disk_usage(str(target))
151
+ return {
152
+ "total_gb": round(total / (1024 ** 3), 2),
153
+ "free_gb": round(free / (1024 ** 3), 2),
154
+ "used_gb": round(used / (1024 ** 3), 2),
155
+ }
156
+ except Exception:
157
+ return {"total_gb": 0.0, "free_gb": 0.0, "used_gb": 0.0}
158
+
159
+ @staticmethod
160
+ def detect_wsl() -> Dict[str, Any]:
161
+ if platform.system() != "Windows":
162
+ return {
163
+ "available": False,
164
+ "reason": "WSL is only available on Windows",
165
+ "distros": [],
166
+ "kali_present": False,
167
+ "kali_running": False,
168
+ "wsl2_supported": False,
169
+ }
170
+
171
+ wsl_bin = shutil.which("wsl.exe") or shutil.which("wsl")
172
+ if not wsl_bin:
173
+ return {
174
+ "available": False,
175
+ "reason": "wsl.exe executable not found in PATH",
176
+ "distros": [],
177
+ "kali_present": False,
178
+ "kali_running": False,
179
+ "wsl2_supported": False,
180
+ }
181
+
182
+ try:
183
+ res = subprocess.run(
184
+ [wsl_bin, "-l", "-v"],
185
+ capture_output=True,
186
+ check=False,
187
+ timeout=5,
188
+ )
189
+ raw = res.stdout
190
+ text = ""
191
+ try:
192
+ decoded_utf16 = raw.decode("utf-16le", errors="ignore")
193
+ if "NAME" in decoded_utf16 or "Name" in decoded_utf16:
194
+ text = decoded_utf16
195
+ else:
196
+ text = raw.decode("utf-8", errors="replace")
197
+ except Exception:
198
+ text = raw.decode("utf-8", errors="replace")
199
+
200
+ distros = []
201
+ kali_present = False
202
+ kali_running = False
203
+ wsl2_supported = False
204
+
205
+ for line in text.splitlines():
206
+ line = line.strip()
207
+ if not line or "NAME" in line or "---" in line:
208
+ continue
209
+ is_default = line.startswith("*")
210
+ clean = line.lstrip("*").strip()
211
+ parts = clean.split()
212
+ if len(parts) >= 2:
213
+ distro_name = parts[0]
214
+ state = parts[1]
215
+ version = parts[2] if len(parts) >= 3 else "Unknown"
216
+ if version == "2":
217
+ wsl2_supported = True
218
+ is_kali = "kali" in distro_name.lower()
219
+ if is_kali:
220
+ kali_present = True
221
+ if state.lower() == "running":
222
+ kali_running = True
223
+ distros.append({
224
+ "name": distro_name,
225
+ "state": state,
226
+ "version": version,
227
+ "is_default": is_default,
228
+ "is_kali": is_kali,
229
+ })
230
+
231
+ return {
232
+ "available": True,
233
+ "reason": f"Found {len(distros)} registered distribution(s)",
234
+ "distros": distros,
235
+ "kali_present": kali_present,
236
+ "kali_running": kali_running,
237
+ "wsl2_supported": wsl2_supported,
238
+ }
239
+ except subprocess.TimeoutExpired:
240
+ return {
241
+ "available": False,
242
+ "reason": "WSL command timed out",
243
+ "distros": [],
244
+ "kali_present": False,
245
+ "kali_running": False,
246
+ "wsl2_supported": False,
247
+ }
248
+ except Exception as e:
249
+ return {
250
+ "available": False,
251
+ "reason": f"Error querying WSL: {e}",
252
+ "distros": [],
253
+ "kali_present": False,
254
+ "kali_running": False,
255
+ "wsl2_supported": False,
256
+ }
257
+
258
+ @staticmethod
259
+ def detect_docker() -> Dict[str, Any]:
260
+ docker_bin = shutil.which("docker")
261
+ if not docker_bin:
262
+ return {
263
+ "available": False,
264
+ "daemon_running": False,
265
+ "reason": "docker executable not found in PATH",
266
+ "version": "",
267
+ }
268
+
269
+ version_str = ""
270
+ daemon_running = False
271
+
272
+ try:
273
+ res_ver = subprocess.run(
274
+ [docker_bin, "--version"],
275
+ capture_output=True,
276
+ text=True,
277
+ check=False,
278
+ timeout=4,
279
+ )
280
+ if res_ver.returncode == 0:
281
+ version_str = res_ver.stdout.strip()
282
+ except Exception:
283
+ pass
284
+
285
+ try:
286
+ res_info = subprocess.run(
287
+ [docker_bin, "info", "--format", "{{.ServerVersion}}"],
288
+ capture_output=True,
289
+ text=True,
290
+ check=False,
291
+ timeout=5,
292
+ )
293
+ if res_info.returncode == 0 and res_info.stdout.strip():
294
+ daemon_running = True
295
+ except Exception:
296
+ daemon_running = False
297
+
298
+ return {
299
+ "available": True,
300
+ "daemon_running": daemon_running,
301
+ "reason": "Docker daemon running" if daemon_running else "Docker CLI present, but daemon is stopped",
302
+ "version": version_str,
303
+ }
304
+
305
+ @staticmethod
306
+ def detect_network() -> Dict[str, bool]:
307
+ connected = False
308
+ try:
309
+ socket.create_connection(("1.1.1.1", 53), timeout=1.5).close()
310
+ connected = True
311
+ except OSError:
312
+ try:
313
+ socket.create_connection(("8.8.8.8", 53), timeout=1.5).close()
314
+ connected = True
315
+ except OSError:
316
+ connected = False
317
+ return {"connected": connected}
318
+
319
+ @classmethod
320
+ def run_preflight(cls, workspace_path: Path) -> Dict[str, Any]:
321
+ dot_agents = workspace_path / ".agents"
322
+ dot_agent_legacy = workspace_path / ".agent"
323
+ return {
324
+ "os": cls.detect_os(),
325
+ "memory": cls.detect_memory(),
326
+ "disk": cls.detect_disk(workspace_path),
327
+ "wsl": cls.detect_wsl(),
328
+ "docker": cls.detect_docker(),
329
+ "network": cls.detect_network(),
330
+ "workspace_path": str(workspace_path.resolve()),
331
+ "existing_agents": dot_agents.exists() or dot_agent_legacy.exists(),
332
+ }
333
+
334
+
335
+ # ---------------------------------------------------------------------------
336
+ # Backend Capability Scoring Engine
337
+ # ---------------------------------------------------------------------------
338
+
339
+ class CapabilityScoringEngine:
340
+ """Calculates weighted scores for WSL vs Docker execution backends."""
341
+
342
+ @staticmethod
343
+ def calculate_scores(env: Dict[str, Any], workload: str = "live-ctf") -> Dict[str, Any]:
344
+ os_info = env["os"]
345
+ wsl_info = env["wsl"]
346
+ docker_info = env["docker"]
347
+ disk_info = env["disk"]
348
+ mem_info = env["memory"]
349
+
350
+ free_disk = disk_info.get("free_gb", 0.0)
351
+ total_ram = mem_info.get("total_gb", 0.0)
352
+
353
+ wsl_score = 0
354
+ wsl_reasons: List[str] = []
355
+
356
+ docker_score = 0
357
+ docker_reasons: List[str] = []
358
+
359
+ # WSL Evaluation
360
+ if os_info["is_windows"]:
361
+ wsl_score += 3
362
+ wsl_reasons.append("Windows native OS environment (+3)")
363
+
364
+ if wsl_info["available"]:
365
+ wsl_score += 3
366
+ wsl_reasons.append("WSL service active (+3)")
367
+
368
+ if wsl_info["kali_present"]:
369
+ wsl_score += 3
370
+ wsl_reasons.append("Kali Linux distribution installed (+3)")
371
+ if wsl_info["kali_running"]:
372
+ wsl_score += 1
373
+ wsl_reasons.append("Kali Linux instance currently running (+1)")
374
+ else:
375
+ wsl_reasons.append("Kali Linux distribution missing (0)")
376
+
377
+ if wsl_info["wsl2_supported"]:
378
+ wsl_score += 1
379
+ wsl_reasons.append("WSL2 virtualization architecture verified (+1)")
380
+ else:
381
+ wsl_reasons.append("WSL not available on host (0)")
382
+
383
+ if free_disk >= 25.0:
384
+ wsl_score += 2
385
+ wsl_reasons.append(f"Storage ample ({free_disk} GB free) (+2)")
386
+ elif free_disk >= 15.0:
387
+ wsl_score += 1
388
+ wsl_reasons.append(f"Storage sufficient ({free_disk} GB free) (+1)")
389
+ else:
390
+ wsl_score -= 2
391
+ wsl_reasons.append(f"Storage low ({free_disk} GB free) (-2)")
392
+
393
+ if total_ram >= 8.0:
394
+ wsl_score += 1
395
+ wsl_reasons.append(f"RAM sufficient ({total_ram} GB) (+1)")
396
+ else:
397
+ wsl_score = 0
398
+ wsl_reasons.append("Non-Windows OS: WSL not applicable (0)")
399
+
400
+ # Docker Evaluation
401
+ if docker_info["available"]:
402
+ docker_score += 2
403
+ docker_reasons.append("Docker CLI installed (+2)")
404
+
405
+ if docker_info["daemon_running"]:
406
+ docker_score += 3
407
+ docker_reasons.append("Docker daemon running and responsive (+3)")
408
+
409
+ if os_info["is_linux"]:
410
+ docker_score += 3
411
+ docker_reasons.append("Native Linux container execution (+3)")
412
+ elif os_info["is_macos"]:
413
+ docker_score += 2
414
+ docker_reasons.append("macOS container execution (+2)")
415
+ elif os_info["is_windows"]:
416
+ docker_score -= 1
417
+ docker_reasons.append("Windows Docker 9P filesystem interop penalty (-1)")
418
+
419
+ if free_disk >= 25.0:
420
+ docker_score += 2
421
+ docker_reasons.append(f"Storage ample ({free_disk} GB free) (+2)")
422
+ elif free_disk >= 15.0:
423
+ docker_score += 1
424
+ docker_reasons.append(f"Storage sufficient ({free_disk} GB free) (+1)")
425
+ else:
426
+ docker_score -= 2
427
+ docker_reasons.append(f"Storage low ({free_disk} GB free) (-2)")
428
+
429
+ if total_ram >= 8.0:
430
+ docker_score += 1
431
+ docker_reasons.append(f"RAM sufficient ({total_ram} GB) (+1)")
432
+ else:
433
+ docker_reasons.append("Docker daemon stopped (0)")
434
+ else:
435
+ docker_reasons.append("Docker CLI not installed (0)")
436
+
437
+ # Workload-Aware Capability Scoring adjustments
438
+ if workload == "rev-pwn":
439
+ if os_info["is_windows"] and wsl_info["available"]:
440
+ wsl_score += 2
441
+ wsl_reasons.append("Workload 'rev-pwn': native ELF debugging & GDB kernel syscalls (+2)")
442
+ elif os_info["is_linux"]:
443
+ docker_score += 1
444
+ docker_reasons.append("Workload 'rev-pwn': native Linux execution (+1)")
445
+ elif workload == "security-lab":
446
+ if docker_info["daemon_running"]:
447
+ docker_score += 2
448
+ docker_reasons.append("Workload 'security-lab': Docker containment sandbox for research (+2)")
449
+ elif workload == "live-ctf":
450
+ if wsl_info["available"] and wsl_info["kali_running"]:
451
+ wsl_score += 1
452
+ wsl_reasons.append("Workload 'live-ctf': Zero spin-up latency with active Kali instance (+1)")
453
+ elif workload == "full":
454
+ if free_disk < 30.0:
455
+ wsl_score -= 1
456
+ docker_score -= 1
457
+ wsl_reasons.append(f"Workload 'full': high storage footprint ({free_disk} GB free) (-1)")
458
+ docker_reasons.append(f"Workload 'full': high storage footprint ({free_disk} GB free) (-1)")
459
+
460
+ # Decision synthesis
461
+ if os_info["is_windows"]:
462
+ if wsl_score >= docker_score and wsl_info["available"]:
463
+ recommended = "wsl"
464
+ distro_target = "kali-linux" if wsl_info["kali_present"] else "kali-linux"
465
+ if wsl_info["kali_present"]:
466
+ reason = "Native Windows + Kali Linux distro verified in WSL2"
467
+ else:
468
+ reason = "Windows detected with WSL available; Kali Linux installation recommended"
469
+ elif docker_score > wsl_score and docker_info["daemon_running"]:
470
+ recommended = "docker"
471
+ distro_target = ""
472
+ reason = "Docker daemon is active and scored higher than WSL"
473
+ elif wsl_info["available"]:
474
+ recommended = "wsl"
475
+ distro_target = "kali-linux"
476
+ reason = "WSL is available and Docker daemon is stopped"
477
+ elif docker_info["available"]:
478
+ recommended = "docker"
479
+ distro_target = ""
480
+ reason = "Docker CLI is present; start daemon to proceed"
481
+ else:
482
+ recommended = "manual"
483
+ distro_target = ""
484
+ reason = "Neither WSL nor Docker is currently ready"
485
+ else:
486
+ if docker_info["daemon_running"]:
487
+ recommended = "docker"
488
+ distro_target = ""
489
+ reason = "Native container runtime active for Unix platform"
490
+ elif os_info["is_linux"]:
491
+ recommended = "host"
492
+ distro_target = ""
493
+ reason = "Native Linux host execution available directly"
494
+ else:
495
+ recommended = "docker"
496
+ distro_target = ""
497
+ reason = "Docker runtime recommended for macOS/Linux"
498
+
499
+ return {
500
+ "wsl_score": max(0, wsl_score),
501
+ "docker_score": max(0, docker_score),
502
+ "recommended": recommended,
503
+ "distro_target": distro_target,
504
+ "reason": reason,
505
+ "wsl_factors": wsl_reasons,
506
+ "docker_factors": docker_reasons,
507
+ "workload": workload,
508
+ }
509
+
510
+
511
+ # ---------------------------------------------------------------------------
512
+ # Workspace Provisioning & Initialization
513
+ # ---------------------------------------------------------------------------
514
+
515
+ class WorkspaceProvisioner:
516
+ """Configures workspace scaffolding, templates, and backend toolchain."""
517
+
518
+ @staticmethod
519
+ def setup_templates(workspace_path: Path):
520
+ """Creates standard resources/, solve.py, and .env.example files."""
521
+ resources_dir = workspace_path / "resources"
522
+ resources_dir.mkdir(parents=True, exist_ok=True)
523
+
524
+ notes_dir = workspace_path / "notes"
525
+ notes_dir.mkdir(parents=True, exist_ok=True)
526
+
527
+ env_example = workspace_path / ".env.example"
528
+ if not env_example.exists():
529
+ env_content = (
530
+ "# CTF-Agent Target & Competition Credentials\n"
531
+ "CTFD_URL=https://ctf.example.com\n"
532
+ "CTFD_TOKEN=your_ctfd_token_here\n"
533
+ "TARGET_HOST=127.0.0.1\n"
534
+ "TARGET_PORT=1337\n"
535
+ "TARGET_URL=http://127.0.0.1:8000\n"
536
+ )
537
+ env_example.write_text(env_content, encoding="utf-8")
538
+ print(" [+] Created .env.example template")
539
+
540
+ solve_py = workspace_path / "solve.py"
541
+ if not solve_py.exists():
542
+ solve_content = (
543
+ "#!/usr/bin/env python3\n"
544
+ "# ===========================================================================\n"
545
+ "# CTF Educational PoC & Vulnerability Verification Script\n"
546
+ "# Scope: Authorized CTF sandbox challenge\n"
547
+ "# ===========================================================================\n"
548
+ "import sys\n"
549
+ "import os\n"
550
+ "\n"
551
+ "def solve():\n"
552
+ " print('[*] Initializing verification sequence...')\n"
553
+ " # Implementation here\n"
554
+ "\n"
555
+ "if __name__ == '__main__':\n"
556
+ " solve()\n"
557
+ )
558
+ solve_py.write_text(solve_content, encoding="utf-8")
559
+ print(" [+] Created solve.py template")
560
+
561
+ @classmethod
562
+ def provision(
563
+ cls,
564
+ workspace_path: Path,
565
+ backend: str,
566
+ distro: str,
567
+ profiles: str,
568
+ use_symlink: bool = False,
569
+ force: bool = False,
570
+ skip_toolchain: bool = False,
571
+ ) -> bool:
572
+ print(f"\n[*] Initializing workspace at: {workspace_path.resolve()}")
573
+
574
+ # 1. Deploy .agents/ and configuration
575
+ if install_as_agent:
576
+ install_as_agent.deploy_to_workspace(
577
+ workspace_path,
578
+ use_symlink=use_symlink,
579
+ force=force,
580
+ setup_workspace=True,
581
+ install_wsl=(backend == "wsl" and not skip_toolchain),
582
+ wsl_profile=profiles,
583
+ wsl_distro=distro,
584
+ )
585
+ else:
586
+ print("[!] Error: install_as_agent module unavailable")
587
+ return False
588
+
589
+ # 2. Setup standard challenge templates
590
+ cls.setup_templates(workspace_path)
591
+
592
+ # 3. Handle Docker backend guidance if selected
593
+ if backend == "docker" and not skip_toolchain:
594
+ cls.provision_docker(workspace_path, profiles)
595
+
596
+ return True
597
+
598
+ @classmethod
599
+ def provision_docker(cls, workspace_path: Path, profiles: str, dry_run: bool = False):
600
+ print(f"\n[*] Provisioning Docker backend for workspace...")
601
+ compose_file = REPO_ROOT / "docker-compose.yml"
602
+ dockerfile = REPO_ROOT / "Dockerfile"
603
+ target_compose = workspace_path / "docker-compose.yml"
604
+
605
+ if compose_file.exists() and not target_compose.exists():
606
+ shutil.copy2(compose_file, target_compose)
607
+ print(f" [+] Copied docker-compose.yml -> {target_compose}")
608
+
609
+ if dockerfile.exists() and not (workspace_path / "Dockerfile").exists():
610
+ shutil.copy2(dockerfile, workspace_path / "Dockerfile")
611
+ print(f" [+] Copied Dockerfile -> {workspace_path / 'Dockerfile'}")
612
+
613
+ if dry_run:
614
+ print(" [*] Dry-run enabled: skipping container execution.")
615
+ return
616
+
617
+ docker_bin = shutil.which("docker")
618
+ if not docker_bin:
619
+ print(" [!] Docker CLI not detected on system PATH.")
620
+ return
621
+
622
+ # 1. Validate compose configuration
623
+ try:
624
+ res_cfg = subprocess.run(
625
+ [docker_bin, "compose", "-f", str(target_compose), "config"],
626
+ capture_output=True,
627
+ text=True,
628
+ check=False,
629
+ timeout=6,
630
+ )
631
+ if res_cfg.returncode == 0:
632
+ print(" [+] Validated docker compose configuration syntax")
633
+ else:
634
+ print(f" [!] Compose config warning: {res_cfg.stderr.strip()[:120]}")
635
+ except Exception:
636
+ pass
637
+
638
+ # 2. Check docker daemon and test container readiness
639
+ try:
640
+ res_info = subprocess.run(
641
+ [docker_bin, "info"],
642
+ capture_output=True,
643
+ check=False,
644
+ timeout=4,
645
+ )
646
+ if res_info.returncode == 0:
647
+ print(" [*] Docker daemon is active. Verifying container readiness...")
648
+ test_cmd = [
649
+ docker_bin, "compose", "-f", str(target_compose),
650
+ "run", "--rm", "ctf-agent", "python3", "--version"
651
+ ]
652
+ test_run = subprocess.run(test_cmd, capture_output=True, text=True, check=False, timeout=12)
653
+ if test_run.returncode == 0:
654
+ print(f" [OK] Docker CTF container ready: {test_run.stdout.strip()}")
655
+ else:
656
+ print(" [*] Container image not yet built. To build:")
657
+ print(f" docker compose -f '{target_compose}' build")
658
+ else:
659
+ print(" [!] Docker daemon is not running. Please start Docker to activate container sandbox.")
660
+ except Exception as e:
661
+ print(f" [!] Docker readiness check skipped: {e}")
662
+
663
+
664
+ # ---------------------------------------------------------------------------
665
+ # Health Check Runner
666
+ # ---------------------------------------------------------------------------
667
+
668
+ class HealthCheckRunner:
669
+ """Verifies workspace file integrity, agent components, and backend availability."""
670
+
671
+ @staticmethod
672
+ def check_workspace(workspace_path: Path, backend: str, distro: str) -> Dict[str, Any]:
673
+ results: Dict[str, Any] = {
674
+ "workspace": str(workspace_path.resolve()),
675
+ "backend": backend,
676
+ "checks": [],
677
+ "all_passed": True,
678
+ }
679
+
680
+ def record(check_name: str, passed: bool, detail: str):
681
+ results["checks"].append({
682
+ "name": check_name,
683
+ "passed": passed,
684
+ "detail": detail,
685
+ })
686
+ if not passed:
687
+ results["all_passed"] = False
688
+
689
+ dot_agents = workspace_path / ".agents"
690
+ record("dot_agents_directory", dot_agents.is_dir(), f"Checked {dot_agents}")
691
+
692
+ agents_md = workspace_path / "AGENTS.md"
693
+ record("workspace_agents_md", agents_md.is_file(), f"Checked {agents_md}")
694
+
695
+ skills_lock = workspace_path / "skills-lock.json"
696
+ record("skills_lock_file", skills_lock.is_file(), f"Checked {skills_lock}")
697
+
698
+ controller_md = dot_agents / "agents" / "ctf-controller.md"
699
+ record("agent_ctf_controller", controller_md.is_file(), f"Checked {controller_md}")
700
+
701
+ speedrun_md = dot_agents / "agents" / "ctf-speedrun.md"
702
+ record("agent_ctf_speedrun", speedrun_md.is_file(), f"Checked {speedrun_md}")
703
+
704
+ analyzer_md = dot_agents / "agents" / "ctf-analyzer.md"
705
+ record("agent_ctf_analyzer", analyzer_md.is_file(), f"Checked {analyzer_md}")
706
+
707
+ scripts_dir = dot_agents / "scripts"
708
+ record("scripts_present", scripts_dir.is_dir() and any(scripts_dir.iterdir()), f"Checked {scripts_dir}")
709
+
710
+ if backend == "wsl":
711
+ try:
712
+ test_cmd = ["wsl", "-d", distro, "bash", "-c", "python3 --version"]
713
+ res = subprocess.run(test_cmd, capture_output=True, text=True, check=False, timeout=5)
714
+ wsl_ok = res.returncode == 0
715
+ record("wsl_kali_python", wsl_ok, f"Output: {res.stdout.strip() if wsl_ok else res.stderr.strip()}")
716
+ except Exception as e:
717
+ record("wsl_kali_python", False, f"WSL execution error: {e}")
718
+
719
+ elif backend == "docker":
720
+ docker_bin = shutil.which("docker")
721
+ if docker_bin:
722
+ try:
723
+ res = subprocess.run([docker_bin, "info"], capture_output=True, text=True, check=False, timeout=5)
724
+ record("docker_daemon_active", res.returncode == 0, "Docker daemon connection verified" if res.returncode == 0 else "Daemon stopped")
725
+ except Exception as e:
726
+ record("docker_daemon_active", False, f"Docker query error: {e}")
727
+ else:
728
+ record("docker_daemon_active", False, "docker CLI not found in PATH")
729
+
730
+ return results
731
+
732
+
733
+ # ---------------------------------------------------------------------------
734
+ # Presentation & CLI Formatting Helpers
735
+ # ---------------------------------------------------------------------------
736
+
737
+ def print_banner(env: Dict[str, Any], scoring: Dict[str, Any]):
738
+ os_info = env["os"]
739
+ mem_info = env["memory"]
740
+ disk_info = env["disk"]
741
+ wsl_info = env["wsl"]
742
+ docker_info = env["docker"]
743
+
744
+ print("=================================================================")
745
+ print("CTF-AGENT ENVIRONMENT PREFLIGHT & WORKSPACE INITIALIZER")
746
+ print("=================================================================")
747
+ print(f"Platform : {os_info['system']} {os_info['release']} ({os_info['arch']})")
748
+ print(f"Hardware : {os_info['cpu_cores']} CPU cores | {mem_info['total_gb']} GB RAM")
749
+ print(f"Storage : {disk_info['free_gb']} GB Free / {disk_info['total_gb']} GB Total")
750
+ ws_status = "Existing .agents/ configuration found" if env.get("existing_agents", False) else "Fresh directory (clean)"
751
+ print(f"Workspace: {ws_status}")
752
+ print("-----------------------------------------------------------------")
753
+ print("Detected Backends:")
754
+
755
+ if os_info["is_windows"]:
756
+ if wsl_info["available"]:
757
+ kali_status = "READY & RUNNING" if wsl_info["kali_running"] else ("INSTALLED" if wsl_info["kali_present"] else "MISSING")
758
+ print(f" [+] WSL2 : Available ({len(wsl_info['distros'])} distros)")
759
+ print(f" [{'+' if wsl_info['kali_present'] else '!'}] Kali Linux : {kali_status}")
760
+ else:
761
+ print(f" [-] WSL2 : Unavailable ({wsl_info.get('reason', 'N/A')})")
762
+ else:
763
+ print(f" [*] Host Native : {os_info['system']} Linux/Unix Environment")
764
+
765
+ if docker_info["available"]:
766
+ doc_status = "RUNNING" if docker_info["daemon_running"] else "STOPPED"
767
+ print(f" [{'+' if docker_info['daemon_running'] else '!'}] Docker : {doc_status} ({docker_info.get('version', 'CLI found')})")
768
+ else:
769
+ print(" [-] Docker : Not Installed")
770
+
771
+ print("-----------------------------------------------------------------")
772
+ print("Backend Capability Evaluation:")
773
+ print(f" WSL Score : {scoring['wsl_score']:2d} / 12 (Factors: {len(scoring['wsl_factors'])})")
774
+ print(f" Docker Score : {scoring['docker_score']:2d} / 12 (Factors: {len(scoring['docker_factors'])})")
775
+ print(f" Recommended : {scoring['recommended'].upper()} {'(' + scoring['distro_target'] + ')' if scoring['distro_target'] else ''}")
776
+ print(f" Rationale : {scoring['reason']}")
777
+ print("=================================================================")
778
+
779
+
780
+ def print_health_report(report: Dict[str, Any]):
781
+ print("\n=================================================================")
782
+ print("WORKSPACE HEALTH VERIFICATION REPORT")
783
+ print("=================================================================")
784
+ for c in report["checks"]:
785
+ status = "[PASS]" if c["passed"] else "[FAIL]"
786
+ print(f" {status} {c['name']:<25} : {c['detail']}")
787
+ print("-----------------------------------------------------------------")
788
+ if report["all_passed"]:
789
+ print("[OK] Workspace initialized successfully! All components verified.")
790
+ else:
791
+ print("[!] Workspace initialized with warnings or missing components.")
792
+ print("=================================================================")
793
+
794
+
795
+ # ---------------------------------------------------------------------------
796
+ # Main CLI Entrypoint
797
+ # ---------------------------------------------------------------------------
798
+
799
+ def main():
800
+ parser = argparse.ArgumentParser(
801
+ description="CTF-Agent Preflight Environment Detector & Workspace Initializer"
802
+ )
803
+ parser.add_argument(
804
+ "workspace",
805
+ nargs="?",
806
+ default=".",
807
+ help="Target workspace directory (default: current directory .)",
808
+ )
809
+ parser.add_argument(
810
+ "--auto",
811
+ action="store_true",
812
+ help="Run non-interactively, automatically accepting recommended backend and profile",
813
+ )
814
+ parser.add_argument(
815
+ "--backend",
816
+ choices=["wsl", "docker", "host", "none"],
817
+ help="Explicitly override execution backend",
818
+ )
819
+ parser.add_argument(
820
+ "--distro",
821
+ default="kali-linux",
822
+ help="WSL distribution target (default: kali-linux)",
823
+ )
824
+ parser.add_argument(
825
+ "--purpose",
826
+ choices=list(PURPOSE_PROFILES.keys()),
827
+ default="live-ctf",
828
+ help="Workload purpose: live-ctf, security-lab, rev-pwn, full (default: live-ctf)",
829
+ )
830
+ parser.add_argument(
831
+ "--profile",
832
+ help="Explicit comma-separated toolchain profiles (overrides purpose preset)",
833
+ )
834
+ parser.add_argument(
835
+ "--symlink",
836
+ action="store_true",
837
+ help="Use symlinks/junctions for .agents/ deployment",
838
+ )
839
+ parser.add_argument(
840
+ "--force",
841
+ "-f",
842
+ action="store_true",
843
+ help="Overwrite existing workspace configuration",
844
+ )
845
+ parser.add_argument(
846
+ "--skip-toolchain",
847
+ action="store_true",
848
+ help="Deploy agent workspace without executing backend toolchain installations",
849
+ )
850
+ parser.add_argument(
851
+ "--dry-run",
852
+ action="store_true",
853
+ help="Perform preflight checks and output recommendation without modifying workspace",
854
+ )
855
+ parser.add_argument(
856
+ "--json",
857
+ action="store_true",
858
+ help="Output preflight detection, scoring, and plan as JSON",
859
+ )
860
+ parser.add_argument(
861
+ "--check-only",
862
+ action="store_true",
863
+ help="Run health check on existing workspace without modifying it",
864
+ )
865
+
866
+ args = parser.parse_args()
867
+ ws_path = Path(args.workspace).resolve()
868
+
869
+ # Preflight Detection
870
+ env = EnvironmentDetector.run_preflight(ws_path)
871
+ scoring = CapabilityScoringEngine.calculate_scores(env, workload=args.purpose)
872
+
873
+ # Health check only mode
874
+ if args.check_only:
875
+ backend_target = args.backend or scoring["recommended"]
876
+ distro_target = args.distro or scoring["distro_target"] or "kali-linux"
877
+ report = HealthCheckRunner.check_workspace(ws_path, backend_target, distro_target)
878
+ if args.json:
879
+ print(json.dumps(report, indent=2))
880
+ else:
881
+ print_health_report(report)
882
+ sys.exit(0 if report["all_passed"] else 1)
883
+
884
+ # JSON output mode
885
+ if args.json:
886
+ payload = {
887
+ "environment": env,
888
+ "scoring": scoring,
889
+ "purpose_profiles": PURPOSE_PROFILES,
890
+ "selected_purpose": args.purpose,
891
+ "selected_profiles": args.profile or PURPOSE_PROFILES[args.purpose]["profiles"],
892
+ }
893
+ print(json.dumps(payload, indent=2))
894
+ sys.exit(0)
895
+
896
+ # Print summary banner
897
+ print_banner(env, scoring)
898
+
899
+ # Dry-run mode
900
+ if args.dry_run:
901
+ target_backend = args.backend or scoring["recommended"]
902
+ target_distro = args.distro if target_backend == "wsl" else ""
903
+ selected_prof = args.profile or PURPOSE_PROFILES[args.purpose]["profiles"]
904
+ print("\n[*] Dry-run mode enabled. Planned actions:")
905
+ print(f" Target Workspace : {ws_path}")
906
+ print(f" Selected Backend : {target_backend}")
907
+ if target_distro:
908
+ print(f" WSL Distro : {target_distro}")
909
+ print(f" Workload Purpose : {args.purpose} ({PURPOSE_PROFILES[args.purpose]['title']})")
910
+ print(f" Tool Profiles : {selected_prof}")
911
+ print(f" Skip Toolchain : {args.skip_toolchain}")
912
+ if args.auto or not sys.stdin.isatty():
913
+ mode_desc = "Automated flag (--auto)" if args.auto else "Non-interactive environment (headless/non-TTY)"
914
+ print(f" Execution Mode : {mode_desc}")
915
+ print("\n[OK] Preflight checks passed without modifications.")
916
+ sys.exit(0)
917
+
918
+ # Interactive vs Non-interactive selection
919
+ chosen_backend = args.backend or scoring["recommended"]
920
+ chosen_distro = args.distro or scoring["distro_target"] or "kali-linux"
921
+ chosen_purpose = args.purpose
922
+ chosen_profiles = args.profile or PURPOSE_PROFILES[chosen_purpose]["profiles"]
923
+ chosen_skip_toolchain = args.skip_toolchain
924
+
925
+ if not args.auto and sys.stdin.isatty():
926
+ while True:
927
+ print("\nAvailable Actions:")
928
+ rec_label = f"Recommended ({scoring['recommended'].upper()})"
929
+ tool_status = "DISABLED (Agent workspace only)" if chosen_skip_toolchain else f"ENABLED ({chosen_profiles})"
930
+ print(f" [1] Continue with {rec_label} [{chosen_backend.upper()}]")
931
+ print(" [2] Switch backend (WSL <-> Docker)")
932
+ print(" [3] Select workload purpose (Live CTF, Lab, Rev/Pwn, Full)")
933
+ print(f" [4] Toggle backend toolchain installation (Currently: {tool_status})")
934
+ print(" [5] Exit")
935
+
936
+ try:
937
+ choice = input("\nSelect option [1-5] (default: 1): ").strip()
938
+ except (KeyboardInterrupt, EOFError):
939
+ print("\n[!] Operation cancelled by user.")
940
+ sys.exit(0)
941
+
942
+ if not choice or choice == "1":
943
+ break
944
+ elif choice == "2":
945
+ chosen_backend = "docker" if chosen_backend == "wsl" else "wsl"
946
+ print(f"[*] Backend switched to: {chosen_backend.upper()}")
947
+ elif choice == "3":
948
+ print("\nSelect Workload Purpose:")
949
+ purpose_keys = list(PURPOSE_PROFILES.keys())
950
+ for idx, k in enumerate(purpose_keys, 1):
951
+ p = PURPOSE_PROFILES[k]
952
+ print(f" [{idx}] {p['title']}")
953
+ print(f" Profiles: {p['profiles']}")
954
+ try:
955
+ p_choice = input(f"\nSelect purpose [1-{len(purpose_keys)}] (default: 1): ").strip()
956
+ if p_choice.isdigit() and 1 <= int(p_choice) <= len(purpose_keys):
957
+ chosen_purpose = purpose_keys[int(p_choice) - 1]
958
+ chosen_profiles = PURPOSE_PROFILES[chosen_purpose]["profiles"]
959
+ print(f"[*] Selected purpose: {PURPOSE_PROFILES[chosen_purpose]['title']}")
960
+ except (KeyboardInterrupt, EOFError):
961
+ pass
962
+ elif choice == "4":
963
+ chosen_skip_toolchain = not chosen_skip_toolchain
964
+ new_state = "DISABLED (Agent workspace only)" if chosen_skip_toolchain else "ENABLED"
965
+ print(f"[*] Toolchain installation is now: {new_state}")
966
+ elif choice == "5":
967
+ print("[*] Exiting without changes.")
968
+ sys.exit(0)
969
+ else:
970
+ env_mode = "Automated flag (--auto)" if args.auto else "Non-interactive environment detected (headless/non-TTY)"
971
+ print(f"\n[*] {env_mode}.")
972
+ print(f" Auto-selected backend : {chosen_backend.upper()} ({chosen_distro})")
973
+ print(f" Workload purpose : {chosen_purpose} ({PURPOSE_PROFILES[chosen_purpose]['title']})")
974
+ print(f" Toolchain installation : {'DISABLED' if chosen_skip_toolchain else 'ENABLED (' + chosen_profiles + ')'}")
975
+
976
+ # Check for existing workspace directory (.agents or legacy .agent)
977
+ dot_agents = ws_path / ".agents"
978
+ dot_agent_legacy = ws_path / ".agent"
979
+ existing_agents_found = dot_agents.exists() or dot_agent_legacy.exists()
980
+ effective_force = args.force
981
+
982
+ if existing_agents_found and not args.force:
983
+ if not args.auto and sys.stdin.isatty():
984
+ print("\n[!] CONFLICT / UPDATE DETECTED:")
985
+ target_name = ".agents/" if dot_agents.exists() else ".agent/ (legacy)"
986
+ print(f" Found existing {target_name} in target workspace: {ws_path}")
987
+ print(" Overwriting will refresh agent skills, rules, and scripts, while")
988
+ print(" preserving your existing challenge files (resources/, notes/, solve.py).")
989
+ try:
990
+ ov_choice = input(" Proceed to update/overwrite agent framework? [y/N]: ").strip().lower()
991
+ if ov_choice in ("y", "yes"):
992
+ effective_force = True
993
+ print(" [+] Overwrite confirmed. Updating agent framework...")
994
+ else:
995
+ print(" [*] Operation cancelled by user. Existing files left intact.")
996
+ sys.exit(0)
997
+ except (KeyboardInterrupt, EOFError):
998
+ print("\n[!] Operation cancelled by user.")
999
+ sys.exit(0)
1000
+ else:
1001
+ print(f"\n[!] Error: Target workspace already contains .agents/: {ws_path}")
1002
+ print(" Re-run with --force (-f) to overwrite or update agent configuration.")
1003
+ sys.exit(1)
1004
+
1005
+ # Execute Provisioning
1006
+ success = WorkspaceProvisioner.provision(
1007
+ workspace_path=ws_path,
1008
+ backend=chosen_backend,
1009
+ distro=chosen_distro,
1010
+ profiles=chosen_profiles,
1011
+ use_symlink=args.symlink,
1012
+ force=effective_force,
1013
+ skip_toolchain=chosen_skip_toolchain,
1014
+ )
1015
+
1016
+ if not success:
1017
+ print("[!] Workspace provisioning failed.")
1018
+ sys.exit(1)
1019
+
1020
+ # Post-provision Health Check
1021
+ health_report = HealthCheckRunner.check_workspace(ws_path, chosen_backend, chosen_distro)
1022
+ print_health_report(health_report)
1023
+
1024
+
1025
+ if __name__ == "__main__":
1026
+ main()