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,583 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CTF-Agent Workspace & Skill Update Engine
4
+ Synchronizes CTF-Agent skills, rules, agent personas, references, and scripts
5
+ into existing workspaces with SHA-256 diffing, lockfile verification, and conflict guards.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import shutil
11
+ import hashlib
12
+ import json
13
+ import argparse
14
+ from pathlib import Path
15
+ from typing import Dict, Any, List, Optional, Tuple
16
+
17
+ # Safe UTF-8 output on Windows consoles
18
+ if hasattr(sys.stdout, "reconfigure"):
19
+ try:
20
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
21
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
22
+ except Exception:
23
+ pass
24
+
25
+ REPO_ROOT = Path(__file__).resolve().parent.parent
26
+
27
+ ESSENTIAL_DIRS = ["skills", "rules", "agents", "scripts", "references"]
28
+ ESSENTIAL_FILES = ["AGENTS.md", "mcp_config.json", "skills.json", "README.md"]
29
+ WORKSPACE_ROOT_FILES = ["AGENTS.md", "mcp_config.json", "skills.json"]
30
+
31
+
32
+ class SkillDiff:
33
+ """Represents the diff state of a specific skill between upstream and workspace."""
34
+
35
+ STATUS_NEW = "NEW"
36
+ STATUS_UPDATED = "UPDATED"
37
+ STATUS_MODIFIED = "MODIFIED"
38
+ STATUS_CUSTOM = "CUSTOM"
39
+ STATUS_UP_TO_DATE = "UP_TO_DATE"
40
+
41
+ def __init__(
42
+ self,
43
+ name: str,
44
+ status: str,
45
+ upstream_hash: Optional[str] = None,
46
+ local_hash: Optional[str] = None,
47
+ locked_hash: Optional[str] = None,
48
+ detail: str = "",
49
+ ):
50
+ self.name = name
51
+ self.status = status
52
+ self.upstream_hash = upstream_hash
53
+ self.local_hash = local_hash
54
+ self.locked_hash = locked_hash
55
+ self.detail = detail
56
+
57
+ def to_dict(self) -> Dict[str, Any]:
58
+ return {
59
+ "name": self.name,
60
+ "status": self.status,
61
+ "upstream_hash": self.upstream_hash,
62
+ "local_hash": self.local_hash,
63
+ "locked_hash": self.locked_hash,
64
+ "detail": self.detail,
65
+ }
66
+
67
+
68
+ class WorkspaceUpdater:
69
+ """Orchestrates workspace and skill synchronization with conflict protection."""
70
+
71
+ @staticmethod
72
+ def resolve_workspace(target_input: Optional[str]) -> Path:
73
+ """Resolves target workspace directory from input string or current working directory."""
74
+ if target_input:
75
+ path = Path(target_input).resolve()
76
+ else:
77
+ path = Path.cwd().resolve()
78
+
79
+ if path.name in (".agents", ".agent") and path.parent.is_dir():
80
+ path = path.parent
81
+
82
+ return path
83
+
84
+ @staticmethod
85
+ def compute_file_hash(path: Path) -> Optional[str]:
86
+ """Calculates SHA-256 hash of a file if it exists."""
87
+ if not path.is_file():
88
+ return None
89
+ try:
90
+ return hashlib.sha256(path.read_bytes()).hexdigest()
91
+ except Exception:
92
+ return None
93
+
94
+ @classmethod
95
+ def inspect_skills(cls, workspace_path: Path) -> List[SkillDiff]:
96
+ """Compares upstream skills with workspace .agents/skills/ and skills-lock.json."""
97
+ diffs: List[SkillDiff] = []
98
+
99
+ dot_agents = workspace_path / ".agents"
100
+ if not dot_agents.exists() and (workspace_path / ".agent").exists():
101
+ dot_agents = workspace_path / ".agent"
102
+
103
+ local_skills_dir = dot_agents / "skills"
104
+ upstream_skills_dir = REPO_ROOT / "skills"
105
+
106
+ # Load skills-lock.json if available
107
+ lock_file = workspace_path / "skills-lock.json"
108
+ if not lock_file.exists():
109
+ lock_file = dot_agents / "skills-lock.json"
110
+
111
+ locked_skills: Dict[str, str] = {}
112
+ if lock_file.exists():
113
+ try:
114
+ data = json.loads(lock_file.read_text(encoding="utf-8"))
115
+ for s_name, s_meta in data.get("skills", {}).items():
116
+ if isinstance(s_meta, dict) and "computedHash" in s_meta:
117
+ locked_skills[s_name] = s_meta["computedHash"]
118
+ except Exception:
119
+ pass
120
+
121
+ upstream_skill_names = set()
122
+ if upstream_skills_dir.exists():
123
+ for item in sorted(upstream_skills_dir.iterdir()):
124
+ if item.is_dir():
125
+ upstream_skill_names.add(item.name)
126
+ up_md = item / "SKILL.md"
127
+ up_hash = cls.compute_file_hash(up_md)
128
+
129
+ loc_dir = local_skills_dir / item.name
130
+ loc_md = loc_dir / "SKILL.md"
131
+ loc_hash = cls.compute_file_hash(loc_md)
132
+ lock_hash = locked_skills.get(item.name)
133
+
134
+ if not loc_dir.exists() or not loc_md.exists():
135
+ diffs.append(
136
+ SkillDiff(
137
+ name=item.name,
138
+ status=SkillDiff.STATUS_NEW,
139
+ upstream_hash=up_hash,
140
+ local_hash=None,
141
+ locked_hash=lock_hash,
142
+ detail="New skill available upstream",
143
+ )
144
+ )
145
+ elif loc_hash == up_hash:
146
+ diffs.append(
147
+ SkillDiff(
148
+ name=item.name,
149
+ status=SkillDiff.STATUS_UP_TO_DATE,
150
+ upstream_hash=up_hash,
151
+ local_hash=loc_hash,
152
+ locked_hash=lock_hash,
153
+ detail="Up to date with upstream",
154
+ )
155
+ )
156
+ else:
157
+ if lock_hash and loc_hash == lock_hash:
158
+ diffs.append(
159
+ SkillDiff(
160
+ name=item.name,
161
+ status=SkillDiff.STATUS_UPDATED,
162
+ upstream_hash=up_hash,
163
+ local_hash=loc_hash,
164
+ locked_hash=lock_hash,
165
+ detail="Upstream update available (clean local copy)",
166
+ )
167
+ )
168
+ elif lock_hash is None:
169
+ diffs.append(
170
+ SkillDiff(
171
+ name=item.name,
172
+ status=SkillDiff.STATUS_UPDATED,
173
+ upstream_hash=up_hash,
174
+ local_hash=loc_hash,
175
+ locked_hash=None,
176
+ detail="Upstream update available",
177
+ )
178
+ )
179
+ else:
180
+ diffs.append(
181
+ SkillDiff(
182
+ name=item.name,
183
+ status=SkillDiff.STATUS_MODIFIED,
184
+ upstream_hash=up_hash,
185
+ local_hash=loc_hash,
186
+ locked_hash=lock_hash,
187
+ detail="Local modifications detected (different from lockfile)",
188
+ )
189
+ )
190
+
191
+ # Check for custom workspace skills not in upstream
192
+ if local_skills_dir.exists():
193
+ for loc_item in sorted(local_skills_dir.iterdir()):
194
+ if loc_item.is_dir() and loc_item.name not in upstream_skill_names:
195
+ loc_hash = cls.compute_file_hash(loc_item / "SKILL.md")
196
+ diffs.append(
197
+ SkillDiff(
198
+ name=loc_item.name,
199
+ status=SkillDiff.STATUS_CUSTOM,
200
+ upstream_hash=None,
201
+ local_hash=loc_hash,
202
+ locked_hash=locked_skills.get(loc_item.name),
203
+ detail="Custom workspace skill (preserved untouched)",
204
+ )
205
+ )
206
+
207
+ return diffs
208
+
209
+ @classmethod
210
+ def sync_directory_with_guard(
211
+ cls,
212
+ src_dir: Path,
213
+ dest_dir: Path,
214
+ label: str,
215
+ force: bool = False,
216
+ dry_run: bool = False,
217
+ ) -> List[str]:
218
+ """
219
+ Surgically synchronizes a directory file-by-file with SHA-256 diffing.
220
+ - Installs new files from upstream
221
+ - Updates modified files while creating <file>.bak backups
222
+ - Preserves user custom files completely untouched
223
+ """
224
+ actions: List[str] = []
225
+ if not src_dir.exists():
226
+ return actions
227
+
228
+ if not dry_run:
229
+ dest_dir.mkdir(parents=True, exist_ok=True)
230
+ upstream_rel_paths = set()
231
+
232
+ # 1. Inspect upstream files
233
+ for src_file in src_dir.rglob("*"):
234
+ if src_file.is_file():
235
+ if src_file.suffix == ".pyc" or "__pycache__" in src_file.parts:
236
+ continue
237
+ rel = src_file.relative_to(src_dir)
238
+ upstream_rel_paths.add(rel)
239
+ target_file = dest_dir / rel
240
+
241
+ if not target_file.exists():
242
+ if not dry_run:
243
+ target_file.parent.mkdir(parents=True, exist_ok=True)
244
+ shutil.copy2(src_file, target_file)
245
+ actions.append(f"{'Would install' if dry_run else 'Installed'} new {label}: {rel}")
246
+ else:
247
+ up_hash = cls.compute_file_hash(src_file)
248
+ loc_hash = cls.compute_file_hash(target_file)
249
+ if up_hash != loc_hash:
250
+ if not dry_run:
251
+ bak_file = target_file.with_name(target_file.name + ".bak")
252
+ shutil.copy2(target_file, bak_file)
253
+ shutil.copy2(src_file, target_file)
254
+ actions.append(f"{'Would update' if dry_run else 'Updated'} {label}{' with backup' if not dry_run else ''}: {rel}{' (saved .bak)' if not dry_run else ''}")
255
+
256
+ # 2. Inspect local directory for custom files
257
+ if dest_dir.exists():
258
+ for loc_file in dest_dir.rglob("*"):
259
+ if loc_file.is_file():
260
+ if loc_file.suffix == ".pyc" or "__pycache__" in loc_file.parts:
261
+ continue
262
+ rel = loc_file.relative_to(dest_dir)
263
+ if not str(rel).endswith(".bak") and rel not in upstream_rel_paths:
264
+ actions.append(f"Preserved custom {label}: {rel}")
265
+
266
+ return actions
267
+
268
+ @classmethod
269
+ def update_workspace(
270
+ cls,
271
+ workspace_path: Path,
272
+ skills_only: bool = False,
273
+ dry_run: bool = False,
274
+ force: bool = False,
275
+ ) -> Dict[str, Any]:
276
+ """Executes synchronization of skills and support files into target workspace."""
277
+ dot_agents = workspace_path / ".agents"
278
+ if not dot_agents.exists() and (workspace_path / ".agent").exists():
279
+ dot_agents = workspace_path / ".agent"
280
+
281
+ if not dot_agents.exists():
282
+ raise FileNotFoundError(
283
+ f"No .agents/ or .agent/ directory found in {workspace_path}. "
284
+ f"Please run 'ctf-agent init' first to initialize the workspace."
285
+ )
286
+
287
+ diffs = cls.inspect_skills(workspace_path)
288
+ new_skills = [d for d in diffs if d.status == SkillDiff.STATUS_NEW]
289
+ updated_skills = [d for d in diffs if d.status == SkillDiff.STATUS_UPDATED]
290
+ modified_skills = [d for d in diffs if d.status == SkillDiff.STATUS_MODIFIED]
291
+ custom_skills = [d for d in diffs if d.status == SkillDiff.STATUS_CUSTOM]
292
+ uptodate_skills = [d for d in diffs if d.status == SkillDiff.STATUS_UP_TO_DATE]
293
+
294
+ result: Dict[str, Any] = {
295
+ "workspace": str(workspace_path.resolve()),
296
+ "dot_agents": str(dot_agents.resolve()),
297
+ "dry_run": dry_run,
298
+ "skills_only": skills_only,
299
+ "skills_summary": {
300
+ "new": len(new_skills),
301
+ "updated": len(updated_skills),
302
+ "modified": len(modified_skills),
303
+ "custom": len(custom_skills),
304
+ "up_to_date": len(uptodate_skills),
305
+ },
306
+ "skills_detail": [d.to_dict() for d in diffs],
307
+ "actions_taken": [],
308
+ "planned_actions": [],
309
+ }
310
+
311
+ action_sink = result["planned_actions"] if dry_run else result["actions_taken"]
312
+
313
+ local_skills_dir = dot_agents / "skills"
314
+ if not dry_run:
315
+ local_skills_dir.mkdir(parents=True, exist_ok=True)
316
+ upstream_skills_dir = REPO_ROOT / "skills"
317
+
318
+ # 1. Synchronize Skills
319
+ for diff in diffs:
320
+ if diff.status in (SkillDiff.STATUS_NEW, SkillDiff.STATUS_UPDATED):
321
+ if not dry_run:
322
+ src_skill = upstream_skills_dir / diff.name
323
+ dest_skill = local_skills_dir / diff.name
324
+ if dest_skill.exists():
325
+ shutil.rmtree(dest_skill)
326
+ shutil.copytree(src_skill, dest_skill)
327
+ action_str = f"{'Would install' if dry_run else 'Installed'} new skill: {diff.name}" if diff.status == SkillDiff.STATUS_NEW else f"{'Would update' if dry_run else 'Updated'} skill: {diff.name}"
328
+ action_sink.append(action_str)
329
+
330
+ elif diff.status == SkillDiff.STATUS_MODIFIED:
331
+ dest_skill = local_skills_dir / diff.name
332
+ src_skill = upstream_skills_dir / diff.name
333
+ if not dry_run:
334
+ # Backup local modified SKILL.md before overwrite
335
+ local_md = dest_skill / "SKILL.md"
336
+ local_bytes = local_md.read_bytes() if local_md.exists() else None
337
+ if dest_skill.exists():
338
+ shutil.rmtree(dest_skill)
339
+ shutil.copytree(src_skill, dest_skill)
340
+ # Re-place backup for user inspection
341
+ if local_bytes is not None:
342
+ (dest_skill / "SKILL.md.bak").write_bytes(local_bytes)
343
+ action_str = f"{'Would update' if dry_run else 'Updated'} modified skill with backup: {diff.name}{' (saved SKILL.md.bak)' if not dry_run else ''}"
344
+ action_sink.append(action_str)
345
+
346
+ elif diff.status == SkillDiff.STATUS_CUSTOM:
347
+ custom_skill_dir = local_skills_dir / diff.name
348
+ audit_suffix = ""
349
+ try:
350
+ from scripts.skill_validator import SkillEnvelopeValidator
351
+ val_rep = SkillEnvelopeValidator.validate_skill_dir(custom_skill_dir)
352
+ if not val_rep.is_valid:
353
+ audit_suffix = f" (envelope issues: {len(val_rep.errors)} errors, run 'ctf-agent validate-skill {custom_skill_dir}')"
354
+ elif val_rep.warnings:
355
+ audit_suffix = f" (envelope warnings: {len(val_rep.warnings)})"
356
+ else:
357
+ audit_suffix = " (envelope verified PASS)"
358
+ except Exception:
359
+ pass
360
+
361
+ action_sink.append(f"Preserved custom skill: {diff.name}{audit_suffix}")
362
+
363
+ # 2. Synchronize non-skill directories & files unless skills_only
364
+ if not skills_only:
365
+ for dir_name in ["rules", "agents", "references", "scripts"]:
366
+ src_d = REPO_ROOT / dir_name
367
+ dest_d = dot_agents / dir_name
368
+ dir_actions = cls.sync_directory_with_guard(src_d, dest_d, label=f".agents/{dir_name}", force=force, dry_run=dry_run)
369
+ action_sink.extend(dir_actions)
370
+
371
+ # Essential files in .agents/
372
+ for f_name in ESSENTIAL_FILES:
373
+ src_file = REPO_ROOT / f_name
374
+ dest_file = dot_agents / f_name
375
+ if src_file.exists():
376
+ up_h = cls.compute_file_hash(src_file)
377
+ loc_h = cls.compute_file_hash(dest_file)
378
+ if not dest_file.exists():
379
+ if not dry_run:
380
+ shutil.copy2(src_file, dest_file)
381
+ action_sink.append(f"{'Would install' if dry_run else 'Installed'} .agents/{f_name}")
382
+ elif up_h != loc_h:
383
+ if not dry_run:
384
+ shutil.copy2(dest_file, dest_file.with_name(f_name + ".bak"))
385
+ shutil.copy2(src_file, dest_file)
386
+ action_sink.append(f"{'Would update' if dry_run else 'Updated'} .agents/{f_name}{' (saved .bak)' if not dry_run else ''}")
387
+
388
+ # Workspace root files (AGENTS.md, mcp_config.json, skills.json)
389
+ for f_name in WORKSPACE_ROOT_FILES:
390
+ src_file = REPO_ROOT / f_name
391
+ dest_file = workspace_path / f_name
392
+ if src_file.exists():
393
+ if f_name == "AGENTS.md" and workspace_path != REPO_ROOT:
394
+ content = src_file.read_text(encoding="utf-8")
395
+ content = content.replace("](references/", "](.agents/references/")
396
+ content = content.replace("](rules/", "](.agents/rules/")
397
+ dest_content = dest_file.read_text(encoding="utf-8") if dest_file.exists() else None
398
+ if dest_content != content:
399
+ if not dry_run:
400
+ dest_file.write_text(content, encoding="utf-8", newline="\n")
401
+ action_sink.append(f"{'Would update' if dry_run else 'Updated'} workspace root: {f_name}")
402
+ else:
403
+ up_h = cls.compute_file_hash(src_file)
404
+ loc_h = cls.compute_file_hash(dest_file)
405
+ if up_h != loc_h:
406
+ if not dry_run:
407
+ shutil.copy2(src_file, dest_file)
408
+ action_sink.append(f"{'Would update' if dry_run else 'Updated'} workspace root: {f_name}")
409
+
410
+ # Workspace scripts/ synchronization if folder exists
411
+ ws_scripts = workspace_path / "scripts"
412
+ if ws_scripts.is_dir():
413
+ src_scripts = REPO_ROOT / "scripts"
414
+ ws_actions = cls.sync_directory_with_guard(src_scripts, ws_scripts, label="scripts", force=force, dry_run=dry_run)
415
+ action_sink.extend(ws_actions)
416
+
417
+ # 3. Regenerate skills-lock.json with updated SHA-256 hashes
418
+ skills_lock: Dict[str, Any] = {"version": 1, "skills": {}}
419
+ if local_skills_dir.exists():
420
+ for s_dir in sorted(local_skills_dir.iterdir()):
421
+ if s_dir.is_dir():
422
+ s_md = s_dir / "SKILL.md"
423
+ if s_md.exists():
424
+ h = cls.compute_file_hash(s_md)
425
+ skills_lock["skills"][s_dir.name] = {
426
+ "source": "CTF-Agent (local)",
427
+ "sourceType": "local",
428
+ "skillPath": f".agents/skills/{s_dir.name}/SKILL.md",
429
+ "computedHash": h or "",
430
+ }
431
+
432
+ lock_target = workspace_path / "skills-lock.json"
433
+ lock_content = json.dumps(skills_lock, indent=2) + "\n"
434
+ existing_lock = lock_target.read_text(encoding="utf-8") if lock_target.exists() else None
435
+ needs_lock_update = (existing_lock is None) or (existing_lock.strip() != lock_content.strip())
436
+ if needs_lock_update:
437
+ if not dry_run:
438
+ lock_target.write_text(lock_content, encoding="utf-8", newline="\n")
439
+ action_sink.append(f"Regenerated {lock_target.name}")
440
+ else:
441
+ action_sink.append(f"Would regenerate {lock_target.name}")
442
+
443
+ return result
444
+
445
+ @classmethod
446
+ def update_globally(cls, dry_run: bool = False, force: bool = False) -> Dict[str, Any]:
447
+ """Updates global Antigravity configuration in ~/.gemini/config/."""
448
+ global_config = Path.home() / ".gemini" / "config"
449
+ result: Dict[str, Any] = {
450
+ "global_config": str(global_config.resolve()),
451
+ "dry_run": dry_run,
452
+ "actions_taken": [],
453
+ }
454
+
455
+ if dry_run:
456
+ return result
457
+
458
+ global_config.mkdir(parents=True, exist_ok=True)
459
+
460
+ # 1. Update Global Skills
461
+ global_skills = global_config / "skills"
462
+ global_skills.mkdir(exist_ok=True)
463
+ for s_path in (REPO_ROOT / "skills").iterdir():
464
+ if s_path.is_dir():
465
+ dest = global_skills / s_path.name
466
+ if dest.exists() and force:
467
+ shutil.rmtree(dest)
468
+ if not dest.exists():
469
+ shutil.copytree(s_path, dest)
470
+ result["actions_taken"].append(f"Installed global skill: {s_path.name}")
471
+ elif dest.exists():
472
+ shutil.rmtree(dest)
473
+ shutil.copytree(s_path, dest)
474
+ result["actions_taken"].append(f"Updated global skill: {s_path.name}")
475
+
476
+ # 2. Update Global Agents
477
+ global_agents = global_config / "agents"
478
+ global_agents.mkdir(exist_ok=True)
479
+ for a_path in (REPO_ROOT / "agents").iterdir():
480
+ if a_path.is_file() and a_path.suffix == ".md":
481
+ dest = global_agents / a_path.name
482
+ shutil.copy2(a_path, dest)
483
+ result["actions_taken"].append(f"Updated global agent: {a_path.name}")
484
+
485
+ # 3. Update Global Rules
486
+ global_rules = global_config / "rules"
487
+ global_rules.mkdir(exist_ok=True)
488
+ for r_path in (REPO_ROOT / "rules").iterdir():
489
+ if r_path.is_file() and r_path.suffix == ".md":
490
+ dest = global_rules / r_path.name
491
+ shutil.copy2(r_path, dest)
492
+ result["actions_taken"].append(f"Updated global rule: {r_path.name}")
493
+
494
+ return result
495
+
496
+
497
+ def print_update_report(result: Dict[str, Any]):
498
+ """Displays user-friendly terminal update report."""
499
+ print("\n=================================================================")
500
+ print("CTF-AGENT WORKSPACE & SKILL UPDATE REPORT")
501
+ print("=================================================================")
502
+ print(f"Target Workspace: {result.get('workspace', result.get('global_config'))}")
503
+ if result.get("dry_run"):
504
+ print("Mode : DRY RUN (No changes written)")
505
+ else:
506
+ print("Mode : LIVE SYNCHRONIZATION")
507
+ print("-----------------------------------------------------------------")
508
+
509
+ summary = result.get("skills_summary", {})
510
+ if summary:
511
+ print(f"Skills Overview:")
512
+ print(f" [+] New available : {summary.get('new', 0)}")
513
+ print(f" [*] Updated : {summary.get('updated', 0)}")
514
+ print(f" [!] Locally modified : {summary.get('modified', 0)}")
515
+ print(f" [=] Up to date : {summary.get('up_to_date', 0)}")
516
+ print(f" [#] Custom (preserved): {summary.get('custom', 0)}")
517
+ print("-----------------------------------------------------------------")
518
+
519
+ actions = result.get("actions_taken", [])
520
+ planned = result.get("planned_actions", [])
521
+ if actions:
522
+ print("Actions Executed:")
523
+ for act in actions:
524
+ print(f" [+] {act}")
525
+ print("-----------------------------------------------------------------")
526
+ elif result.get("dry_run"):
527
+ print("Planned Changes:")
528
+ for s in result.get("skills_detail", []):
529
+ st = s["status"]
530
+ name = s["name"]
531
+ detail = s["detail"]
532
+ if st == SkillDiff.STATUS_NEW:
533
+ print(f" [+] {name:30} -> {detail}")
534
+ elif st == SkillDiff.STATUS_UPDATED:
535
+ print(f" [*] {name:30} -> {detail}")
536
+ elif st == SkillDiff.STATUS_MODIFIED:
537
+ print(f" [!] {name:30} -> {detail}")
538
+ elif st == SkillDiff.STATUS_CUSTOM:
539
+ print(f" [#] {name:30} -> {detail}")
540
+ for act in planned:
541
+ if not act.startswith("Would install new skill") and not act.startswith("Would update skill") and not act.startswith("Would update modified skill"):
542
+ print(f" [*] {act}")
543
+ print("-----------------------------------------------------------------")
544
+
545
+ print("[OK] Workspace synchronization complete.")
546
+ print("=================================================================\n")
547
+
548
+
549
+ def main():
550
+ parser = argparse.ArgumentParser(description="CTF-Agent Workspace & Skill Updater")
551
+ parser.add_argument("target", nargs="?", help="Target CTF workspace directory (default: current directory)")
552
+ parser.add_argument("--global", dest="is_global", action="store_true", help="Update global Antigravity config (~/.gemini/config/)")
553
+ parser.add_argument("--skills-only", action="store_true", help="Only update skills, preserving rules, agents, and scripts")
554
+ parser.add_argument("--dry-run", action="store_true", help="Inspect and display planned changes without modifying files")
555
+ parser.add_argument("--force", "-f", action="store_true", help="Overwrite modified skills without interactive prompt")
556
+ parser.add_argument("--json", action="store_true", help="Output update report as JSON")
557
+
558
+ args = parser.parse_args()
559
+
560
+ try:
561
+ if args.is_global:
562
+ result = WorkspaceUpdater.update_globally(dry_run=args.dry_run, force=args.force)
563
+ else:
564
+ workspace = WorkspaceUpdater.resolve_workspace(args.target)
565
+ result = WorkspaceUpdater.update_workspace(
566
+ workspace,
567
+ skills_only=args.skills_only,
568
+ dry_run=args.dry_run,
569
+ force=args.force,
570
+ )
571
+
572
+ if args.json:
573
+ print(json.dumps(result, indent=2))
574
+ else:
575
+ print_update_report(result)
576
+
577
+ except Exception as e:
578
+ print(f"\n[!] Error during update: {e}", file=sys.stderr)
579
+ sys.exit(1)
580
+
581
+
582
+ if __name__ == "__main__":
583
+ main()
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CTFd Platform Automation Client (v2.0)
4
+ Handles authentication, pagination, challenge files, hints, and flag submission.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import time
11
+ import urllib.request
12
+ import urllib.error
13
+ from typing import Optional, Dict, Any, List
14
+
15
+ class CTFdClient:
16
+ def __init__(self, base_url: str, api_token: str):
17
+ self.base_url = base_url.rstrip("/")
18
+ self.headers = {
19
+ "Authorization": f"Token {api_token}",
20
+ "Content-Type": "application/json",
21
+ "User-Agent": "CTF-Agent-PlatformClient/2.0"
22
+ }
23
+
24
+ def _request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
25
+ url = f"{self.base_url}/api/v1/{endpoint.lstrip('/')}"
26
+ payload = json.dumps(data).encode("utf-8") if data else None
27
+ req = urllib.request.Request(url, data=payload, headers=self.headers, method=method)
28
+
29
+ for attempt in range(3):
30
+ try:
31
+ with urllib.request.urlopen(req, timeout=10) as resp:
32
+ return json.loads(resp.read().decode("utf-8"))
33
+ except urllib.error.HTTPError as e:
34
+ if e.code == 429:
35
+ time.sleep(2 ** attempt)
36
+ continue
37
+ return None
38
+ except Exception:
39
+ return None
40
+ return None
41
+
42
+ def get_challenges(self) -> List[Dict[str, Any]]:
43
+ """Retrieve all challenges across all paginated pages."""
44
+ all_challenges = []
45
+ page = 1
46
+ while True:
47
+ res = self._request("GET", f"challenges?page={page}")
48
+ if not res or not res.get("success") or not res.get("data"):
49
+ break
50
+ all_challenges.extend(res["data"])
51
+ pagination = res.get("meta", {}).get("pagination", {})
52
+ if page >= pagination.get("pages", 1):
53
+ break
54
+ page += 1
55
+ return all_challenges
56
+
57
+ def get_challenge_details(self, challenge_id: int) -> Optional[Dict[str, Any]]:
58
+ res = self._request("GET", f"challenges/{challenge_id}")
59
+ return res.get("data") if res else None
60
+
61
+ def get_challenge_files(self, challenge_id: int) -> List[str]:
62
+ """Fetch distinct attachment URLs for a challenge."""
63
+ res = self._request("GET", f"challenges/{challenge_id}/files")
64
+ if res and res.get("success"):
65
+ return [f.get("location") for f in res.get("data", [])]
66
+ return []
67
+
68
+ def get_challenge_hints(self, challenge_id: int) -> List[Dict[str, Any]]:
69
+ res = self._request("GET", f"challenges/{challenge_id}/hints")
70
+ return res.get("data", []) if res else []
71
+
72
+ def submit_flag(self, challenge_id: int, flag: str) -> Dict[str, Any]:
73
+ data = {"challenge_id": challenge_id, "submission": flag}
74
+ res = self._request("POST", "challenges/attempt", data=data)
75
+ if res and res.get("success"):
76
+ status = res.get("data", {}).get("status", "incorrect")
77
+ return {"status": status, "message": res.get("data", {}).get("message", "")}
78
+ return {"status": "error", "message": "Failed to communicate with CTFd"}
79
+
80
+ if __name__ == "__main__":
81
+ print("CTFdClient module initialized. Import in solve scripts or CTF automation tools.")