pwn 0.5.784 → 0.5.785

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 (242) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +6 -6
  4. data/README.md +37 -0
  5. data/bin/pwn +2 -1
  6. data/bin/pwn_web_cache_deception +35 -7
  7. data/documentation/AI-Integration.md +7 -5
  8. data/documentation/Agent-Tool-Registry.md +2 -2
  9. data/documentation/Backup-Retention.md +70 -15
  10. data/documentation/Configuration.md +10 -6
  11. data/documentation/Contributing.md +1 -1
  12. data/documentation/Cron.md +2 -2
  13. data/documentation/Diagrams.md +2 -2
  14. data/documentation/General-PWN-Usage.md +4 -3
  15. data/documentation/Home.md +6 -5
  16. data/documentation/How-PWN-Works.md +6 -5
  17. data/documentation/Installation.md +12 -9
  18. data/documentation/Mission-Cancellation.md +134 -0
  19. data/documentation/Operability.md +17 -0
  20. data/documentation/Persistence.md +7 -5
  21. data/documentation/Plugin-Degradation.md +12 -0
  22. data/documentation/Plugins.md +3 -2
  23. data/documentation/Runtime-Source-Protection.md +162 -0
  24. data/documentation/Transparent-Browser.md +11 -0
  25. data/documentation/Troubleshooting.md +3 -2
  26. data/documentation/What-is-PWN.md +2 -2
  27. data/documentation/YubiKey-Vaults.md +183 -0
  28. data/documentation/YubiKey.md +339 -0
  29. data/documentation/diagrams/cron-scheduling.svg +130 -130
  30. data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
  31. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  32. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  33. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  34. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  35. data/documentation/pwn-ai-Agent.md +10 -4
  36. data/etc/default_cron/session_cleanup.rb +43 -27
  37. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  38. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  39. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  40. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  41. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  42. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  43. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  44. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  45. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  46. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  47. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  48. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  49. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  50. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  51. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  52. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  57. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  58. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  59. data/lib/pwn/ai/agent/learning.rb +14 -18
  60. data/lib/pwn/ai/agent/loop.rb +17 -1
  61. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  62. data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
  63. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  64. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  65. data/lib/pwn/ai/agent/reward.rb +5 -3
  66. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  67. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  68. data/lib/pwn/ai/agent/swarm.rb +4 -0
  69. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  70. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  71. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  72. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  73. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  74. data/lib/pwn/ai/agent.rb +1 -0
  75. data/lib/pwn/ai/anthropic.rb +8 -33
  76. data/lib/pwn/ai/cli.rb +34 -5
  77. data/lib/pwn/ai/gemini.rb +1 -0
  78. data/lib/pwn/ai/grok.rb +39 -28
  79. data/lib/pwn/ai/ollama.rb +4 -2
  80. data/lib/pwn/ai/open_ai.rb +11 -32
  81. data/lib/pwn/ai/open_web_ui.rb +62 -4
  82. data/lib/pwn/ai/router.rb +8 -1
  83. data/lib/pwn/ai/server_tools.rb +231 -0
  84. data/lib/pwn/ai.rb +1 -0
  85. data/lib/pwn/backups.rb +347 -0
  86. data/lib/pwn/config.rb +70 -39
  87. data/lib/pwn/migrate.rb +85 -280
  88. data/lib/pwn/module_skills.rb +7 -0
  89. data/lib/pwn/plugins/ai_sandbox.rb +538 -1
  90. data/lib/pwn/plugins/authentication_helper.rb +74 -4
  91. data/lib/pwn/plugins/basic_auth.rb +2 -1
  92. data/lib/pwn/plugins/google_workspace.rb +4 -21
  93. data/lib/pwn/plugins/jobs.rb +1 -1
  94. data/lib/pwn/plugins/log.rb +3 -1
  95. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  96. data/lib/pwn/plugins/recon.rb +4 -1
  97. data/lib/pwn/plugins/repl/ai.rb +293 -26
  98. data/lib/pwn/plugins/repl/ai_console.rb +268 -88
  99. data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
  100. data/lib/pwn/plugins/repl/mesh.rb +3 -16
  101. data/lib/pwn/plugins/repl/vault.rb +4 -15
  102. data/lib/pwn/plugins/repl.rb +1 -216
  103. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  104. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  105. data/lib/pwn/plugins/vault.rb +412 -135
  106. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  107. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  108. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  109. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  110. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  111. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  112. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  113. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  114. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  115. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  116. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  117. data/lib/pwn/plugins/yubikey.rb +844 -0
  118. data/lib/pwn/plugins.rb +1 -0
  119. data/lib/pwn/redaction.rb +69 -12
  120. data/lib/pwn/session_trace.rb +4 -0
  121. data/lib/pwn/setup.rb +114 -24
  122. data/lib/pwn/version.rb +1 -1
  123. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  124. data/lib/pwn/www/bug_crowd.rb +16 -5
  125. data/lib/pwn/www/coinbase_pro.rb +16 -5
  126. data/lib/pwn/www/github.rb +30 -14
  127. data/lib/pwn/www/paypal.rb +15 -4
  128. data/lib/pwn/www/synack.rb +16 -5
  129. data/lib/pwn/www/twitter.rb +17 -6
  130. data/lib/pwn.rb +1 -0
  131. data/spec/documentation/installation_md_spec.rb +1 -1
  132. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  133. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  134. data/spec/integration/controller_spill_spec.rb +48 -0
  135. data/spec/integration/harness_process_spec.rb +97 -0
  136. data/spec/integration/mission_cancel_spec.rb +79 -0
  137. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  138. data/spec/integration/readme_security_spec.rb +47 -0
  139. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  140. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  141. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  142. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  143. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  144. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  145. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  146. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  147. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  148. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  149. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  150. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  151. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  152. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  153. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  154. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  155. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  156. data/spec/lib/pwn/ai/cli_spec.rb +6 -9
  157. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  158. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  159. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  160. data/spec/lib/pwn/ai/router_spec.rb +1 -1
  161. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  162. data/spec/lib/pwn/backups_spec.rb +114 -0
  163. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  164. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  165. data/spec/lib/pwn/migrate_spec.rb +29 -81
  166. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  167. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  168. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  169. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  170. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  171. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  172. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  173. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  174. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  175. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  176. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  177. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
  178. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  179. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
  180. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  181. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  182. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  183. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  184. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  185. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  186. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  187. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  188. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  189. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  190. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  191. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  192. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  193. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  194. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  195. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  196. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  197. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  198. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  199. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  200. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  201. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  202. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  203. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  204. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  205. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  206. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  207. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  208. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  209. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  210. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  211. data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
  212. data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
  213. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
  214. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  215. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  216. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  217. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  218. data/spec/spec_helper.rb +15 -0
  219. data/spec/support/harness/cli_faults.rb +13 -9
  220. data/spec/support/harness/controller_spill.rb +26 -0
  221. data/spec/support/harness/controller_state.rb +54 -0
  222. data/spec/support/harness/evaluate.rb +12 -1
  223. data/spec/support/harness/experiments.rb +1 -1
  224. data/spec/support/harness/fake_provider.rb +22 -6
  225. data/spec/support/harness/fault_preload.rb +3 -1
  226. data/spec/support/harness/mission_console.rb +54 -0
  227. data/spec/support/harness/mission_curses_pty.rb +41 -0
  228. data/spec/support/harness/mission_dispatch.rb +30 -0
  229. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  230. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  231. data/spec/support/harness/mission_legacy.rb +25 -0
  232. data/spec/support/harness/mission_normal_exit.rb +34 -0
  233. data/spec/support/harness/mission_swarm.rb +23 -0
  234. data/spec/support/harness/mission_unsupported.rb +29 -0
  235. data/spec/support/harness/package_acceptance.rb +33 -5
  236. data/spec/support/harness/session_acceptance.rb +4 -4
  237. data/spec/support/harness/swarm_acceptance.rb +1 -1
  238. data/spec/support/harness/swarm_control.rb +44 -0
  239. data/spec/support/mission_namespace.rb +14 -0
  240. data/spec/support/mission_pipe_queue.rb +33 -0
  241. data/third_party/pwn_rdoc.jsonl +589 -19
  242. metadata +129 -13
@@ -9,135 +9,135 @@
9
9
  <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(43.2 636.7)">
10
10
  <title>PWN_Plugins</title>
11
11
  <polygon fill="#0f172a" stroke="none" points="-43.2,43.2 -43.2,-636.7 634.45,-636.7 634.45,43.2 -43.2,43.2"/>
12
- <text xml:space="preserve" text-anchor="start" x="8" y="-571.5" font-family="sans-Serif" font-weight="bold" font-size="20.00" fill="#e2e8f0">PWN::Plugins &#45; Selected categories from 107 plugin modules</text>
12
+ <text xml:space="preserve" text-anchor="start" x="8" y="-571.5" font-family="sans-Serif" font-weight="bold" font-size="20.00" fill="#e2e8f0">PWN::Plugins &#45; Selected categories from 108 plugin modules</text>
13
13
  <g id="clust1" class="cluster">
14
14
  <title>cluster_cat</title>
15
15
  </g>
16
16
  <!-- Root -->
17
17
  <g id="node1" class="node">
18
18
  <title>Root</title>
19
- <path fill="#7dd3fc" stroke="#334155" stroke-width="2" d="M115.12,-303.25C115.12,-303.25 39.12,-303.25 39.12,-303.25 33.12,-303.25 27.12,-297.25 27.12,-291.25 27.12,-291.25 27.12,-279.25 27.12,-279.25 27.12,-273.25 33.12,-267.25 39.12,-267.25 39.12,-267.25 115.12,-267.25 115.12,-267.25 121.12,-267.25 127.12,-273.25 127.12,-279.25 127.12,-279.25 127.12,-291.25 127.12,-291.25 127.12,-297.25 121.12,-303.25 115.12,-303.25"/>
20
- <text xml:space="preserve" text-anchor="middle" x="77.12" y="-279.82" font-family="sans-Serif" font-size="14.00" fill="#0f172a">PWN::Plugins</text>
19
+ <path fill="#7dd3fc" stroke="#334155" stroke-width="2" d="M108.38,-303.25C108.38,-303.25 32.38,-303.25 32.38,-303.25 26.38,-303.25 20.38,-297.25 20.38,-291.25 20.38,-291.25 20.38,-279.25 20.38,-279.25 20.38,-273.25 26.38,-267.25 32.38,-267.25 32.38,-267.25 108.38,-267.25 108.38,-267.25 114.38,-267.25 120.38,-273.25 120.38,-279.25 120.38,-279.25 120.38,-291.25 120.38,-291.25 120.38,-297.25 114.38,-303.25 108.38,-303.25"/>
20
+ <text xml:space="preserve" text-anchor="middle" x="70.38" y="-279.82" font-family="sans-Serif" font-size="14.00" fill="#0f172a">PWN::Plugins</text>
21
21
  </g>
22
22
  <!-- Web -->
23
23
  <g id="node2" class="node">
24
24
  <title>Web</title>
25
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M287.88,-546.25C287.88,-546.25 247.12,-546.25 247.12,-546.25 241.12,-546.25 235.12,-540.25 235.12,-534.25 235.12,-534.25 235.12,-522.25 235.12,-522.25 235.12,-516.25 241.12,-510.25 247.12,-510.25 247.12,-510.25 287.88,-510.25 287.88,-510.25 293.88,-510.25 299.88,-516.25 299.88,-522.25 299.88,-522.25 299.88,-534.25 299.88,-534.25 299.88,-540.25 293.88,-546.25 287.88,-546.25"/>
26
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-524.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Web / Proxy</text>
25
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M281.12,-546.25C281.12,-546.25 240.38,-546.25 240.38,-546.25 234.38,-546.25 228.38,-540.25 228.38,-534.25 228.38,-534.25 228.38,-522.25 228.38,-522.25 228.38,-516.25 234.38,-510.25 240.38,-510.25 240.38,-510.25 281.12,-510.25 281.12,-510.25 287.12,-510.25 293.12,-516.25 293.12,-522.25 293.12,-522.25 293.12,-534.25 293.12,-534.25 293.12,-540.25 287.12,-546.25 281.12,-546.25"/>
26
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-524.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Web / Proxy</text>
27
27
  </g>
28
28
  <!-- Root&#45;&gt;Web -->
29
29
  <g id="edge1" class="edge">
30
30
  <title>Root&#45;&gt;Web</title>
31
- <path fill="none" stroke="#94a3b8" d="M87.93,-303.98C109.32,-343.65 163.42,-437.72 227.12,-501.25 228.56,-502.68 230.07,-504.08 231.64,-505.44"/>
32
- <polygon fill="#94a3b8" stroke="#94a3b8" points="230,-506.82 235.98,-508.98 232.66,-503.56 230,-506.82"/>
31
+ <path fill="none" stroke="#94a3b8" d="M81.18,-303.98C102.57,-343.65 156.67,-437.72 220.38,-501.25 221.81,-502.68 223.32,-504.08 224.89,-505.44"/>
32
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="223.25,-506.82 229.23,-508.98 225.91,-503.56 223.25,-506.82"/>
33
33
  </g>
34
34
  <!-- Net -->
35
35
  <g id="node3" class="node">
36
36
  <title>Net</title>
37
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M282.5,-492.25C282.5,-492.25 252.5,-492.25 252.5,-492.25 246.5,-492.25 240.5,-486.25 240.5,-480.25 240.5,-480.25 240.5,-468.25 240.5,-468.25 240.5,-462.25 246.5,-456.25 252.5,-456.25 252.5,-456.25 282.5,-456.25 282.5,-456.25 288.5,-456.25 294.5,-462.25 294.5,-468.25 294.5,-468.25 294.5,-480.25 294.5,-480.25 294.5,-486.25 288.5,-492.25 282.5,-492.25"/>
38
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-470.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Network</text>
37
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M275.75,-492.25C275.75,-492.25 245.75,-492.25 245.75,-492.25 239.75,-492.25 233.75,-486.25 233.75,-480.25 233.75,-480.25 233.75,-468.25 233.75,-468.25 233.75,-462.25 239.75,-456.25 245.75,-456.25 245.75,-456.25 275.75,-456.25 275.75,-456.25 281.75,-456.25 287.75,-462.25 287.75,-468.25 287.75,-468.25 287.75,-480.25 287.75,-480.25 287.75,-486.25 281.75,-492.25 275.75,-492.25"/>
38
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-470.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Network</text>
39
39
  </g>
40
40
  <!-- Root&#45;&gt;Net -->
41
41
  <g id="edge2" class="edge">
42
42
  <title>Root&#45;&gt;Net</title>
43
- <path fill="none" stroke="#94a3b8" d="M92.99,-304.01C118.51,-335.58 173.13,-400.45 227.12,-447.25 229.38,-449.2 231.78,-451.14 234.24,-453.03"/>
44
- <polygon fill="#94a3b8" stroke="#94a3b8" points="232.75,-454.55 238.83,-456.43 235.25,-451.17 232.75,-454.55"/>
43
+ <path fill="none" stroke="#94a3b8" d="M86.24,-304.01C111.76,-335.58 166.38,-400.45 220.38,-447.25 222.63,-449.2 225.03,-451.14 227.49,-453.03"/>
44
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="226,-454.55 232.08,-456.43 228.5,-451.17 226,-454.55"/>
45
45
  </g>
46
46
  <!-- Xpl -->
47
47
  <g id="node4" class="node">
48
48
  <title>Xpl</title>
49
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M287.5,-438.25C287.5,-438.25 247.5,-438.25 247.5,-438.25 241.5,-438.25 235.5,-432.25 235.5,-426.25 235.5,-426.25 235.5,-414.25 235.5,-414.25 235.5,-408.25 241.5,-402.25 247.5,-402.25 247.5,-402.25 287.5,-402.25 287.5,-402.25 293.5,-402.25 299.5,-408.25 299.5,-414.25 299.5,-414.25 299.5,-426.25 299.5,-426.25 299.5,-432.25 293.5,-438.25 287.5,-438.25"/>
50
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-416.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Exploitation</text>
49
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M280.75,-438.25C280.75,-438.25 240.75,-438.25 240.75,-438.25 234.75,-438.25 228.75,-432.25 228.75,-426.25 228.75,-426.25 228.75,-414.25 228.75,-414.25 228.75,-408.25 234.75,-402.25 240.75,-402.25 240.75,-402.25 280.75,-402.25 280.75,-402.25 286.75,-402.25 292.75,-408.25 292.75,-414.25 292.75,-414.25 292.75,-426.25 292.75,-426.25 292.75,-432.25 286.75,-438.25 280.75,-438.25"/>
50
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-416.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Exploitation</text>
51
51
  </g>
52
52
  <!-- Root&#45;&gt;Xpl -->
53
53
  <g id="edge3" class="edge">
54
54
  <title>Root&#45;&gt;Xpl</title>
55
- <path fill="none" stroke="#94a3b8" d="M103.82,-304.19C133.4,-325.91 183.52,-362.5 227.12,-393.25 229.09,-394.64 231.11,-396.05 233.16,-397.48"/>
56
- <polygon fill="#94a3b8" stroke="#94a3b8" points="231.95,-399.2 238.08,-400.89 234.35,-395.74 231.95,-399.2"/>
55
+ <path fill="none" stroke="#94a3b8" d="M97.07,-304.19C126.65,-325.91 176.77,-362.5 220.38,-393.25 222.34,-394.64 224.36,-396.05 226.41,-397.48"/>
56
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="225.2,-399.2 231.33,-400.89 227.6,-395.74 225.2,-399.2"/>
57
57
  </g>
58
58
  <!-- Osint -->
59
59
  <g id="node5" class="node">
60
60
  <title>Osint</title>
61
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M282.5,-384.25C282.5,-384.25 252.5,-384.25 252.5,-384.25 246.5,-384.25 240.5,-378.25 240.5,-372.25 240.5,-372.25 240.5,-360.25 240.5,-360.25 240.5,-354.25 246.5,-348.25 252.5,-348.25 252.5,-348.25 282.5,-348.25 282.5,-348.25 288.5,-348.25 294.5,-354.25 294.5,-360.25 294.5,-360.25 294.5,-372.25 294.5,-372.25 294.5,-378.25 288.5,-384.25 282.5,-384.25"/>
62
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-362.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">OSINT</text>
61
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M275.75,-384.25C275.75,-384.25 245.75,-384.25 245.75,-384.25 239.75,-384.25 233.75,-378.25 233.75,-372.25 233.75,-372.25 233.75,-360.25 233.75,-360.25 233.75,-354.25 239.75,-348.25 245.75,-348.25 245.75,-348.25 275.75,-348.25 275.75,-348.25 281.75,-348.25 287.75,-354.25 287.75,-360.25 287.75,-360.25 287.75,-372.25 287.75,-372.25 287.75,-378.25 281.75,-384.25 275.75,-384.25"/>
62
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-362.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">OSINT</text>
63
63
  </g>
64
64
  <!-- Root&#45;&gt;Osint -->
65
65
  <g id="edge4" class="edge">
66
66
  <title>Root&#45;&gt;Osint</title>
67
- <path fill="none" stroke="#94a3b8" d="M122.29,-304.24C156.26,-318.85 202.46,-338.71 233.32,-351.98"/>
68
- <polygon fill="#94a3b8" stroke="#94a3b8" points="232.31,-353.84 238.66,-354.28 233.97,-349.98 232.31,-353.84"/>
67
+ <path fill="none" stroke="#94a3b8" d="M115.54,-304.24C149.51,-318.85 195.71,-338.71 226.57,-351.98"/>
68
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="225.56,-353.84 231.91,-354.28 227.22,-349.98 225.56,-353.84"/>
69
69
  </g>
70
70
  <!-- Data -->
71
71
  <g id="node6" class="node">
72
72
  <title>Data</title>
73
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M286.38,-330.25C286.38,-330.25 248.62,-330.25 248.62,-330.25 242.62,-330.25 236.62,-324.25 236.62,-318.25 236.62,-318.25 236.62,-306.25 236.62,-306.25 236.62,-300.25 242.62,-294.25 248.62,-294.25 248.62,-294.25 286.38,-294.25 286.38,-294.25 292.38,-294.25 298.38,-300.25 298.38,-306.25 298.38,-306.25 298.38,-318.25 298.38,-318.25 298.38,-324.25 292.38,-330.25 286.38,-330.25"/>
74
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-308.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Data / DAO</text>
73
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M279.62,-330.25C279.62,-330.25 241.88,-330.25 241.88,-330.25 235.88,-330.25 229.88,-324.25 229.88,-318.25 229.88,-318.25 229.88,-306.25 229.88,-306.25 229.88,-300.25 235.88,-294.25 241.88,-294.25 241.88,-294.25 279.62,-294.25 279.62,-294.25 285.62,-294.25 291.62,-300.25 291.62,-306.25 291.62,-306.25 291.62,-318.25 291.62,-318.25 291.62,-324.25 285.62,-330.25 279.62,-330.25"/>
74
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-308.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Data / DAO</text>
75
75
  </g>
76
76
  <!-- Root&#45;&gt;Data -->
77
77
  <g id="edge5" class="edge">
78
78
  <title>Root&#45;&gt;Data</title>
79
- <path fill="none" stroke="#94a3b8" d="M128.11,-292.41C159.76,-296.95 200.03,-302.72 228.95,-306.87"/>
80
- <polygon fill="#94a3b8" stroke="#94a3b8" points="228.32,-308.9 234.55,-307.67 228.91,-304.74 228.32,-308.9"/>
79
+ <path fill="none" stroke="#94a3b8" d="M121.36,-292.41C153.01,-296.95 193.28,-302.72 222.2,-306.87"/>
80
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="221.57,-308.9 227.8,-307.67 222.16,-304.74 221.57,-308.9"/>
81
81
  </g>
82
82
  <!-- Auth -->
83
83
  <g id="node7" class="node">
84
84
  <title>Auth</title>
85
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M282.5,-276.25C282.5,-276.25 252.5,-276.25 252.5,-276.25 246.5,-276.25 240.5,-270.25 240.5,-264.25 240.5,-264.25 240.5,-252.25 240.5,-252.25 240.5,-246.25 246.5,-240.25 252.5,-240.25 252.5,-240.25 282.5,-240.25 282.5,-240.25 288.5,-240.25 294.5,-246.25 294.5,-252.25 294.5,-252.25 294.5,-264.25 294.5,-264.25 294.5,-270.25 288.5,-276.25 282.5,-276.25"/>
86
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-254.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Auth</text>
85
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M275.75,-276.25C275.75,-276.25 245.75,-276.25 245.75,-276.25 239.75,-276.25 233.75,-270.25 233.75,-264.25 233.75,-264.25 233.75,-252.25 233.75,-252.25 233.75,-246.25 239.75,-240.25 245.75,-240.25 245.75,-240.25 275.75,-240.25 275.75,-240.25 281.75,-240.25 287.75,-246.25 287.75,-252.25 287.75,-252.25 287.75,-264.25 287.75,-264.25 287.75,-270.25 281.75,-276.25 275.75,-276.25"/>
86
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-254.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Auth</text>
87
87
  </g>
88
88
  <!-- Root&#45;&gt;Auth -->
89
89
  <g id="edge6" class="edge">
90
90
  <title>Root&#45;&gt;Auth</title>
91
- <path fill="none" stroke="#94a3b8" d="M128.11,-278.09C161.27,-273.33 203.88,-267.23 233.01,-263.05"/>
92
- <polygon fill="#94a3b8" stroke="#94a3b8" points="232.97,-265.18 238.61,-262.25 232.37,-261.02 232.97,-265.18"/>
91
+ <path fill="none" stroke="#94a3b8" d="M121.36,-278.09C154.52,-273.33 197.13,-267.23 226.26,-263.05"/>
92
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="226.22,-265.18 231.86,-262.25 225.62,-261.02 226.22,-265.18"/>
93
93
  </g>
94
94
  <!-- HW -->
95
95
  <g id="node8" class="node">
96
96
  <title>HW</title>
97
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M283.75,-222.25C283.75,-222.25 251.25,-222.25 251.25,-222.25 245.25,-222.25 239.25,-216.25 239.25,-210.25 239.25,-210.25 239.25,-198.25 239.25,-198.25 239.25,-192.25 245.25,-186.25 251.25,-186.25 251.25,-186.25 283.75,-186.25 283.75,-186.25 289.75,-186.25 295.75,-192.25 295.75,-198.25 295.75,-198.25 295.75,-210.25 295.75,-210.25 295.75,-216.25 289.75,-222.25 283.75,-222.25"/>
98
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-200.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Hardware</text>
97
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M277,-222.25C277,-222.25 244.5,-222.25 244.5,-222.25 238.5,-222.25 232.5,-216.25 232.5,-210.25 232.5,-210.25 232.5,-198.25 232.5,-198.25 232.5,-192.25 238.5,-186.25 244.5,-186.25 244.5,-186.25 277,-186.25 277,-186.25 283,-186.25 289,-192.25 289,-198.25 289,-198.25 289,-210.25 289,-210.25 289,-216.25 283,-222.25 277,-222.25"/>
98
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-200.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Hardware</text>
99
99
  </g>
100
100
  <!-- Root&#45;&gt;HW -->
101
101
  <g id="edge7" class="edge">
102
102
  <title>Root&#45;&gt;HW</title>
103
- <path fill="none" stroke="#94a3b8" d="M122.29,-266.26C155.75,-251.87 201.09,-232.38 231.93,-219.11"/>
104
- <polygon fill="#94a3b8" stroke="#94a3b8" points="232.6,-221.11 237.28,-216.81 230.94,-217.26 232.6,-221.11"/>
103
+ <path fill="none" stroke="#94a3b8" d="M115.54,-266.26C149,-251.87 194.34,-232.38 225.18,-219.11"/>
104
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="225.85,-221.11 230.53,-216.81 224.19,-217.26 225.85,-221.11"/>
105
105
  </g>
106
106
  <!-- CI -->
107
107
  <g id="node9" class="node">
108
108
  <title>CI</title>
109
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M287.88,-168.25C287.88,-168.25 247.12,-168.25 247.12,-168.25 241.12,-168.25 235.12,-162.25 235.12,-156.25 235.12,-156.25 235.12,-144.25 235.12,-144.25 235.12,-138.25 241.12,-132.25 247.12,-132.25 247.12,-132.25 287.88,-132.25 287.88,-132.25 293.88,-132.25 299.88,-138.25 299.88,-144.25 299.88,-144.25 299.88,-156.25 299.88,-156.25 299.88,-162.25 293.88,-168.25 287.88,-168.25"/>
110
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-146.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">CI / DevOps</text>
109
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M281.12,-168.25C281.12,-168.25 240.38,-168.25 240.38,-168.25 234.38,-168.25 228.38,-162.25 228.38,-156.25 228.38,-156.25 228.38,-144.25 228.38,-144.25 228.38,-138.25 234.38,-132.25 240.38,-132.25 240.38,-132.25 281.12,-132.25 281.12,-132.25 287.12,-132.25 293.12,-138.25 293.12,-144.25 293.12,-144.25 293.12,-156.25 293.12,-156.25 293.12,-162.25 287.12,-168.25 281.12,-168.25"/>
110
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-146.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">CI / DevOps</text>
111
111
  </g>
112
112
  <!-- Root&#45;&gt;CI -->
113
113
  <g id="edge8" class="edge">
114
114
  <title>Root&#45;&gt;CI</title>
115
- <path fill="none" stroke="#94a3b8" d="M103.82,-266.31C133.4,-244.59 183.52,-208 227.12,-177.25 229.09,-175.86 231.11,-174.45 233.16,-173.02"/>
116
- <polygon fill="#94a3b8" stroke="#94a3b8" points="234.35,-174.76 238.08,-169.61 231.95,-171.3 234.35,-174.76"/>
115
+ <path fill="none" stroke="#94a3b8" d="M97.07,-266.31C126.65,-244.59 176.77,-208 220.38,-177.25 222.34,-175.86 224.36,-174.45 226.41,-173.02"/>
116
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="227.6,-174.76 231.33,-169.61 225.2,-171.3 227.6,-174.76"/>
117
117
  </g>
118
118
  <!-- Comm -->
119
119
  <g id="node10" class="node">
120
120
  <title>Comm</title>
121
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M282.5,-114.25C282.5,-114.25 252.5,-114.25 252.5,-114.25 246.5,-114.25 240.5,-108.25 240.5,-102.25 240.5,-102.25 240.5,-90.25 240.5,-90.25 240.5,-84.25 246.5,-78.25 252.5,-78.25 252.5,-78.25 282.5,-78.25 282.5,-78.25 288.5,-78.25 294.5,-84.25 294.5,-90.25 294.5,-90.25 294.5,-102.25 294.5,-102.25 294.5,-108.25 288.5,-114.25 282.5,-114.25"/>
122
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-92.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Comms</text>
121
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M275.75,-114.25C275.75,-114.25 245.75,-114.25 245.75,-114.25 239.75,-114.25 233.75,-108.25 233.75,-102.25 233.75,-102.25 233.75,-90.25 233.75,-90.25 233.75,-84.25 239.75,-78.25 245.75,-78.25 245.75,-78.25 275.75,-78.25 275.75,-78.25 281.75,-78.25 287.75,-84.25 287.75,-90.25 287.75,-90.25 287.75,-102.25 287.75,-102.25 287.75,-108.25 281.75,-114.25 275.75,-114.25"/>
122
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-92.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Comms</text>
123
123
  </g>
124
124
  <!-- Root&#45;&gt;Comm -->
125
125
  <g id="edge9" class="edge">
126
126
  <title>Root&#45;&gt;Comm</title>
127
- <path fill="none" stroke="#94a3b8" d="M92.99,-266.49C118.51,-234.92 173.13,-170.05 227.12,-123.25 229.38,-121.3 231.78,-119.36 234.24,-117.47"/>
128
- <polygon fill="#94a3b8" stroke="#94a3b8" points="235.25,-119.33 238.83,-114.07 232.75,-115.95 235.25,-119.33"/>
127
+ <path fill="none" stroke="#94a3b8" d="M86.24,-266.49C111.76,-234.92 166.38,-170.05 220.38,-123.25 222.63,-121.3 225.03,-119.36 227.49,-117.47"/>
128
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="228.5,-119.33 232.08,-114.07 226,-115.95 228.5,-119.33"/>
129
129
  </g>
130
130
  <!-- Util -->
131
131
  <g id="node11" class="node">
132
132
  <title>Util</title>
133
- <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M282.5,-60.25C282.5,-60.25 252.5,-60.25 252.5,-60.25 246.5,-60.25 240.5,-54.25 240.5,-48.25 240.5,-48.25 240.5,-36.25 240.5,-36.25 240.5,-30.25 246.5,-24.25 252.5,-24.25 252.5,-24.25 282.5,-24.25 282.5,-24.25 288.5,-24.25 294.5,-30.25 294.5,-36.25 294.5,-36.25 294.5,-48.25 294.5,-48.25 294.5,-54.25 288.5,-60.25 282.5,-60.25"/>
134
- <text xml:space="preserve" text-anchor="middle" x="267.5" y="-38.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Utility</text>
133
+ <path fill="#6ee7b7" stroke="#334155" stroke-width="1.2" d="M275.75,-60.25C275.75,-60.25 245.75,-60.25 245.75,-60.25 239.75,-60.25 233.75,-54.25 233.75,-48.25 233.75,-48.25 233.75,-36.25 233.75,-36.25 233.75,-30.25 239.75,-24.25 245.75,-24.25 245.75,-24.25 275.75,-24.25 275.75,-24.25 281.75,-24.25 287.75,-30.25 287.75,-36.25 287.75,-36.25 287.75,-48.25 287.75,-48.25 287.75,-54.25 281.75,-60.25 275.75,-60.25"/>
134
+ <text xml:space="preserve" text-anchor="middle" x="260.75" y="-38.95" font-family="sans-Serif" font-size="9.00" fill="#0f172a">Utility</text>
135
135
  </g>
136
136
  <!-- Root&#45;&gt;Util -->
137
137
  <g id="edge10" class="edge">
138
138
  <title>Root&#45;&gt;Util</title>
139
- <path fill="none" stroke="#94a3b8" d="M87.93,-266.52C109.32,-226.85 163.42,-132.78 227.12,-69.25 229.3,-67.08 231.68,-64.97 234.15,-62.95"/>
140
- <polygon fill="#94a3b8" stroke="#94a3b8" points="235.36,-64.66 238.83,-59.33 232.8,-61.34 235.36,-64.66"/>
139
+ <path fill="none" stroke="#94a3b8" d="M81.18,-266.52C102.57,-226.85 156.67,-132.78 220.38,-69.25 222.55,-67.08 224.93,-64.97 227.4,-62.95"/>
140
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="228.61,-64.66 232.08,-59.33 226.05,-61.34 228.61,-64.66"/>
141
141
  </g>
142
142
  <!-- w -->
143
143
  <g id="node12" class="node">
@@ -150,8 +150,8 @@
150
150
  <!-- Web&#45;&gt;w -->
151
151
  <g id="edge11" class="edge">
152
152
  <title>Web&#45;&gt;w</title>
153
- <path fill="none" stroke="#94a3b8" d="M300.45,-528.55C329.56,-528.81 373.65,-529.22 411.44,-529.57"/>
154
- <polygon fill="#94a3b8" stroke="#94a3b8" points="411.37,-531.67 417.39,-529.63 411.41,-527.47 411.37,-531.67"/>
153
+ <path fill="none" stroke="#94a3b8" d="M293.26,-528.53C323.93,-528.81 371.64,-529.23 411.79,-529.59"/>
154
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="411.67,-531.69 417.68,-529.65 411.7,-527.49 411.67,-531.69"/>
155
155
  </g>
156
156
  <!-- n -->
157
157
  <g id="node13" class="node">
@@ -163,8 +163,8 @@
163
163
  <!-- Net&#45;&gt;n -->
164
164
  <g id="edge12" class="edge">
165
165
  <title>Net&#45;&gt;n</title>
166
- <path fill="none" stroke="#94a3b8" d="M295.05,-474.25C327.18,-474.25 382.26,-474.25 424.86,-474.25"/>
167
- <polygon fill="#94a3b8" stroke="#94a3b8" points="424.61,-476.35 430.61,-474.25 424.61,-472.15 424.61,-476.35"/>
166
+ <path fill="none" stroke="#94a3b8" d="M288.24,-474.25C321.72,-474.25 380.35,-474.25 424.93,-474.25"/>
167
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="424.73,-476.35 430.73,-474.25 424.73,-472.15 424.73,-476.35"/>
168
168
  </g>
169
169
  <!-- x -->
170
170
  <g id="node14" class="node">
@@ -176,8 +176,8 @@
176
176
  <!-- Xpl&#45;&gt;x -->
177
177
  <g id="edge13" class="edge">
178
178
  <title>Xpl&#45;&gt;x</title>
179
- <path fill="none" stroke="#94a3b8" d="M299.98,-420.25C335.61,-420.25 394.08,-420.25 435.76,-420.25"/>
180
- <polygon fill="#94a3b8" stroke="#94a3b8" points="435.56,-422.35 441.56,-420.25 435.56,-418.15 435.56,-422.35"/>
179
+ <path fill="none" stroke="#94a3b8" d="M293.26,-420.25C330.3,-420.25 392.22,-420.25 435.64,-420.25"/>
180
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="435.47,-422.35 441.47,-420.25 435.47,-418.15 435.47,-422.35"/>
181
181
  </g>
182
182
  <!-- o -->
183
183
  <g id="node15" class="node">
@@ -189,8 +189,8 @@
189
189
  <!-- Osint&#45;&gt;o -->
190
190
  <g id="edge14" class="edge">
191
191
  <title>Osint&#45;&gt;o</title>
192
- <path fill="none" stroke="#94a3b8" d="M295.05,-366.25C328.56,-366.25 387.06,-366.25 430.31,-366.25"/>
193
- <polygon fill="#94a3b8" stroke="#94a3b8" points="430.14,-368.35 436.14,-366.25 430.14,-364.15 430.14,-368.35"/>
192
+ <path fill="none" stroke="#94a3b8" d="M288.24,-366.25C323.03,-366.25 384.99,-366.25 430.11,-366.25"/>
193
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="429.97,-368.35 435.97,-366.25 429.97,-364.15 429.97,-368.35"/>
194
194
  </g>
195
195
  <!-- d -->
196
196
  <g id="node16" class="node">
@@ -202,21 +202,21 @@
202
202
  <!-- Data&#45;&gt;d -->
203
203
  <g id="edge15" class="edge">
204
204
  <title>Data&#45;&gt;d</title>
205
- <path fill="none" stroke="#94a3b8" d="M298.6,-312.25C326.38,-312.25 368.85,-312.25 406.25,-312.25"/>
205
+ <path fill="none" stroke="#94a3b8" d="M291.86,-312.25C321.1,-312.25 366.76,-312.25 406.34,-312.25"/>
206
206
  <polygon fill="#94a3b8" stroke="#94a3b8" points="406.14,-314.35 412.14,-312.25 406.14,-310.15 406.14,-314.35"/>
207
207
  </g>
208
208
  <!-- a -->
209
209
  <g id="node17" class="node">
210
210
  <title>a</title>
211
- <path fill="#fcd34d" stroke="#334155" stroke-width="1.2" d="M538.62,-276.25C538.62,-276.25 433.38,-276.25 433.38,-276.25 427.38,-276.25 421.38,-270.25 421.38,-264.25 421.38,-264.25 421.38,-252.25 421.38,-252.25 421.38,-246.25 427.38,-240.25 433.38,-240.25 433.38,-240.25 538.62,-240.25 538.62,-240.25 544.62,-240.25 550.62,-246.25 550.62,-252.25 550.62,-252.25 550.62,-264.25 550.62,-264.25 550.62,-270.25 544.62,-276.25 538.62,-276.25"/>
211
+ <path fill="#fcd34d" stroke="#334155" stroke-width="1.2" d="M558.88,-276.25C558.88,-276.25 413.12,-276.25 413.12,-276.25 407.12,-276.25 401.12,-270.25 401.12,-264.25 401.12,-264.25 401.12,-252.25 401.12,-252.25 401.12,-246.25 407.12,-240.25 413.12,-240.25 413.12,-240.25 558.88,-240.25 558.88,-240.25 564.88,-240.25 570.88,-246.25 570.88,-252.25 570.88,-252.25 570.88,-264.25 570.88,-264.25 570.88,-270.25 564.88,-276.25 558.88,-276.25"/>
212
212
  <text xml:space="preserve" text-anchor="middle" x="486" y="-260.2" font-family="sans-Serif" font-size="9.00" fill="#0f172a">OAuth2 · BasicAuth</text>
213
- <text xml:space="preserve" text-anchor="middle" x="486" y="-249.7" font-family="sans-Serif" font-size="9.00" fill="#0f172a">AuthenticationHelper · Vault</text>
213
+ <text xml:space="preserve" text-anchor="middle" x="486" y="-249.7" font-family="sans-Serif" font-size="9.00" fill="#0f172a">AuthenticationHelper · Vault · YubiKey</text>
214
214
  </g>
215
215
  <!-- Auth&#45;&gt;a -->
216
216
  <g id="edge16" class="edge">
217
217
  <title>Auth&#45;&gt;a</title>
218
- <path fill="none" stroke="#94a3b8" d="M295.05,-258.25C324.39,-258.25 372.87,-258.25 413.5,-258.25"/>
219
- <polygon fill="#94a3b8" stroke="#94a3b8" points="413.48,-260.35 419.48,-258.25 413.48,-256.15 413.48,-260.35"/>
218
+ <path fill="none" stroke="#94a3b8" d="M288.24,-258.25C314.28,-258.25 355.54,-258.25 393.57,-258.25"/>
219
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="393.11,-260.35 399.11,-258.25 393.11,-256.15 393.11,-260.35"/>
220
220
  </g>
221
221
  <!-- h -->
222
222
  <g id="node18" class="node">
@@ -228,8 +228,8 @@
228
228
  <!-- HW&#45;&gt;h -->
229
229
  <g id="edge17" class="edge">
230
230
  <title>HW&#45;&gt;h</title>
231
- <path fill="none" stroke="#94a3b8" d="M295.92,-204.25C325.06,-204.25 372.34,-204.25 412.34,-204.25"/>
232
- <polygon fill="#94a3b8" stroke="#94a3b8" points="412.2,-206.35 418.2,-204.25 412.2,-202.15 412.2,-206.35"/>
231
+ <path fill="none" stroke="#94a3b8" d="M289.57,-204.25C320.12,-204.25 370.39,-204.25 412.29,-204.25"/>
232
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="412.23,-206.35 418.23,-204.25 412.23,-202.15 412.23,-206.35"/>
233
233
  </g>
234
234
  <!-- c -->
235
235
  <g id="node19" class="node">
@@ -241,8 +241,8 @@
241
241
  <!-- CI&#45;&gt;c -->
242
242
  <g id="edge18" class="edge">
243
243
  <title>CI&#45;&gt;c</title>
244
- <path fill="none" stroke="#94a3b8" d="M300.45,-150.25C332.45,-150.25 382.55,-150.25 422.48,-150.25"/>
245
- <polygon fill="#94a3b8" stroke="#94a3b8" points="422.3,-152.35 428.3,-150.25 422.3,-148.15 422.3,-152.35"/>
244
+ <path fill="none" stroke="#94a3b8" d="M293.26,-150.25C326.6,-150.25 380.08,-150.25 422.1,-150.25"/>
245
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="422.03,-152.35 428.03,-150.25 422.03,-148.15 422.03,-152.35"/>
246
246
  </g>
247
247
  <!-- m -->
248
248
  <g id="node20" class="node">
@@ -254,8 +254,8 @@
254
254
  <!-- Comm&#45;&gt;m -->
255
255
  <g id="edge19" class="edge">
256
256
  <title>Comm&#45;&gt;m</title>
257
- <path fill="none" stroke="#94a3b8" d="M295.05,-96.25C321.19,-96.25 362.54,-96.25 400,-96.25"/>
258
- <polygon fill="#94a3b8" stroke="#94a3b8" points="399.92,-98.35 405.92,-96.25 399.92,-94.15 399.92,-98.35"/>
257
+ <path fill="none" stroke="#94a3b8" d="M288.24,-96.25C315.75,-96.25 360.24,-96.25 399.97,-96.25"/>
258
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="399.81,-98.35 405.81,-96.25 399.81,-94.15 399.81,-98.35"/>
259
259
  </g>
260
260
  <!-- u -->
261
261
  <g id="node21" class="node">
@@ -270,8 +270,8 @@
270
270
  <!-- Util&#45;&gt;u -->
271
271
  <g id="edge20" class="edge">
272
272
  <title>Util&#45;&gt;u</title>
273
- <path fill="none" stroke="#94a3b8" d="M295.05,-40.78C323.22,-39.22 369.03,-36.68 408.61,-34.48"/>
274
- <polygon fill="#94a3b8" stroke="#94a3b8" points="408.54,-36.59 414.41,-34.16 408.3,-32.4 408.54,-36.59"/>
273
+ <path fill="none" stroke="#94a3b8" d="M288.24,-40.83C317.76,-39.24 366.86,-36.6 408.62,-34.36"/>
274
+ <polygon fill="#94a3b8" stroke="#94a3b8" points="408.67,-36.46 414.55,-34.04 408.44,-32.26 408.67,-36.46"/>
275
275
  </g>
276
276
  </g>
277
277
  </svg>
@@ -5,6 +5,9 @@ the goal; the agent plans a sequence of tool calls (`pwn_eval`, `shell`,
5
5
  `memory_*`, `skill_*`, `mistakes_*`, `reward_*`, `curriculum_*`, `extro_*`,
6
6
  `agent_*`, ...), executes them against the live process, observes the results,
7
7
  and checks completion against the original request and available host evidence.
8
+ Unspecified tool writes go to `~/.pwn/workspaces/runtime`. A path outside that
9
+ directory is allowed only when the original request names it. See
10
+ [Operability](Operability.md).
8
11
  Learning records failures and corrections; it cannot guarantee nonrecurrence.
9
12
 
10
13
  ## Two ways to run it
@@ -336,7 +339,9 @@ anthropic` opens that nested mapping, and `/vault edit ai anthropic model`
336
339
  opens a scalar. Tokens identify one nested key path, never filenames or multiple
337
340
  disjoint scopes. The target and decryptor are the same `driver_opts.pwn_env_path`
338
341
  and `driver_opts.pwn_dec_path` used by `pwn-vault`, defaulting to
339
- `~/.pwn/pwn.yaml` and its existing `.decryptor` sidecar. Unknown paths are rejected; edit the parent mapping
342
+ `~/.pwn/pwn.yaml` and its existing legacy `.decryptor` sidecar. Hardware-backed
343
+ [YubiKey vaults](YubiKey-Vaults.md) are detected without requiring a sidecar.
344
+ Unknown paths are rejected; edit the parent mapping
340
345
  to add a new key. Arrays are edited as a whole, not by numeric path indices.
341
346
  Existing non-null types must be retained; an existing null may remain null or
342
347
  be replaced by a YAML value. The root must retain an `ai` mapping, a supported
@@ -355,8 +360,9 @@ only shipped schema key names, never reads/decrypts the vault, and never offers
355
360
  configuration values; existing custom keys can be typed explicitly.
356
361
 
357
362
  Plaintext is decrypted/edited in memory. Only encrypted bytes enter the private
358
- temporary file, which is atomically renamed over the vault using the same
359
- AES-256-CBC format/key/IV as `pwn-vault`; the decryptor is not regenerated.
363
+ temporary file, which is atomically renamed over the vault using its existing
364
+ backend: legacy AES-256-CBC key/IV or a fresh authenticated YubiKey envelope.
365
+ The device is never provisioned and the legacy decryptor is not regenerated.
360
366
  Save re-reads the current document, preserves changes outside the selected scope,
361
367
  and refuses a changed selected scope (the whole document for unscoped edits).
362
368
  Editor instances serialize saves with a directory lock; a final ciphertext check
@@ -642,7 +648,7 @@ After the reply, `Learning.auto_introspect` calls `rsi_tick`. That tick reads ES
642
648
 
643
649
  ## What the agent can call
644
650
 
645
- 16 toolsets · **150 tools** - full table at
651
+ 16 toolsets · **152 tools** - full table at
646
652
  [Agent Tool Registry](Agent-Tool-Registry.md).
647
653
 
648
654
  The two that matter most:
@@ -20,7 +20,7 @@ module PWN
20
20
  'learning' => %w[learning.jsonl learning lessons.json memory.json memory.archive.json memory.idx mistakes.json policy policy.json policy_traj.jsonl curriculum curriculum_kpi.jsonl extrospection.json finetune preferences.jsonl reward_sentinel.json skill_review.jsonl operator-learning-review.jsonl],
21
21
  'artifacts' => %w[artifacts exports findings.jsonl engagements corpora binws redb ghidra sandbox_snapshots],
22
22
  'state' => %w[swarm cron agents.yml open_goal.json jobs missions runs metrics metrics.json capabilities.json],
23
- 'logs' => %w[logs], 'cache' => %w[cache], 'tmp' => %w[tmp],
23
+ 'logs' => %w[logs], 'cache' => %w[cache], 'tmp' => %w[workspaces/runtime],
24
24
  'maintenance' => %w[maintenance session_cleanup_archive],
25
25
  'backups' => %w[backup]
26
26
  }.freeze
@@ -38,16 +38,17 @@ module PWN
38
38
  raise IOError, 'Symlinked maintenance policy' if File.symlink?(path)
39
39
 
40
40
  publish(path, JSON.pretty_generate(POLICY)) unless File.exist?(path)
41
- %w[cache tmp].each { |name| private_directory(File.join(root, name)) unless File.exist?(File.join(root, name)) }
41
+ private_directory(File.join(root, 'cache')) unless File.exist?(File.join(root, 'cache'))
42
+ private_directory(File.join(root, 'workspaces', 'runtime')) unless File.exist?(File.join(root, 'workspaces', 'runtime'))
42
43
  layout = File.join(directory, 'layout.json')
43
44
  raise IOError, 'Unsafe maintenance inventory' if File.symlink?(layout)
44
45
 
45
46
  previous = File.file?(layout) ? JSON.parse(File.binread(layout)) : nil
46
- raise IOError, 'Unknown maintenance inventory version' if previous && ![1, 2].include?(previous['version'])
47
+ raise IOError, 'Unknown maintenance inventory version' if previous && ![1, 2, 3].include?(previous['version'])
47
48
 
48
- publish(layout, JSON.generate(version: 2, categories: STORAGE)) unless previous && previous['version'] == 2
49
+ publish(layout, JSON.generate(version: 3, categories: STORAGE)) unless previous && previous['version'] == 3
49
50
  end
50
- { version: 2, root: directory }
51
+ { version: 3, root: directory }
51
52
  end
52
53
 
53
54
  # Startup performs only fixed-path checks and one coalesced request write.
@@ -368,7 +369,7 @@ module PWN
368
369
  def self.register_disposable(path:, category:, expires_at: nil)
369
370
  raise ArgumentError, 'Not a disposable category' unless %w[cache tmp logs].include?(category)
370
371
 
371
- root = File.join(Dir.home, '.pwn', category)
372
+ root = disposable_directory(category)
372
373
  path = File.expand_path(path)
373
374
  raise IOError, 'Disposable path escapes its category' unless path.start_with?("#{root}/") && File.realpath(path) == path
374
375
  raise IOError, 'Disposable must be an owned regular file' unless File.file?(path) && File.stat(path).uid == Process.uid
@@ -421,7 +422,7 @@ module PWN
421
422
  # Known disposable input producer. The permanent lease covers publication,
422
423
  # consumption and unlink; a killed consumer leaves a registered exact file.
423
424
  def self.temporary(bytes:)
424
- directory = private_directory(File.join(Dir.home, '.pwn', 'tmp'))
425
+ directory = private_directory(disposable_directory('tmp'))
425
426
  path = File.join(directory, "input-#{SecureRandom.hex(12)}.txt")
426
427
  File.open("#{path}.lease", File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lease|
427
428
  lease.flock(File::LOCK_EX)
@@ -465,6 +466,14 @@ module PWN
465
466
  path
466
467
  end
467
468
 
469
+ # Disposable category tmp lives in the runtime workspace, not ~/.pwn/tmp.
470
+ def self.disposable_directory(category)
471
+ raise ArgumentError, 'Not a disposable category' unless %w[cache tmp logs].include?(category)
472
+
473
+ base = File.join(Dir.home, '.pwn')
474
+ category == 'tmp' ? File.join(base, 'workspaces', 'runtime') : File.join(base, category)
475
+ end
476
+
468
477
  def self.private_directory(path)
469
478
  ancestor = File.expand_path(path)
470
479
  loop do
@@ -499,7 +508,7 @@ module PWN
499
508
  return 'unknown_schema' unless row.is_a?(Hash) && row['version'] == 1 && %w[cache tmp logs].include?(row['category']) && row['expires_at'].is_a?(Integer)
500
509
 
501
510
  path = row['path']
502
- root = File.join(Dir.home, '.pwn', row['category'])
511
+ root = disposable_directory(row['category'])
503
512
  return 'unsafe_path' unless path.is_a?(String) && path.start_with?("#{root}/") && File.expand_path(path) == path
504
513
  return 'missing' unless File.exist?(path)
505
514
  return 'unsafe_path' unless File.realpath(path) == path && File.file?(path) && File.stat(path).uid == Process.uid
@@ -574,7 +583,7 @@ module PWN
574
583
  next if dry_run
575
584
 
576
585
  unless result[:ledger]
577
- directory = private_directory(File.join(root, 'quarantine', SecureRandom.hex(12)))
586
+ directory = PWN::Backups.directory(label: 'disposable')
578
587
  result[:ledger] = File.join(directory, 'ledger.json')
579
588
  end
580
589
  backup_path = File.join(File.dirname(result[:ledger]), "#{Digest::SHA256.hexdigest(path)}.bin")
@@ -788,11 +797,14 @@ module PWN
788
797
  # children. Restoring a ledger alone never undoes deletion or migration.
789
798
  def self.recovery_status(ledger:)
790
799
  root = File.join(Dir.home, '.pwn')
791
- prefixes = %w[maintenance session_cleanup_archive learning/migrations].map { |name| "#{File.join(root, name)}/" }
800
+ prefixes = %w[backup maintenance session_cleanup_archive learning/migrations].map { |name| "#{File.join(root, name)}/" }
792
801
  raise IOError, 'Unsafe recovery ledger' unless prefixes.any? { |prefix| ledger.start_with?(prefix) } && File.realpath(ledger) == ledger
793
802
 
794
803
  data = JSON.parse(File.binread(ledger))
795
804
  operation = data['operation']
805
+ operation = 'session_cleanup' if ledger.include?('/backup/') && data['records'].is_a?(Array) && data['deleted_ids'].is_a?(Array)
806
+ operation = 'continuation_retirement' if ledger.include?('/backup/') && data['backup'] == File.join(File.dirname(ledger), 'personas.json')
807
+ operation = 'session_layout' if ledger.include?('/backup/') && File.basename(ledger) == 'layout.json'
796
808
  operation = 'session_layout' if File.basename(ledger) == 'layout.json' && ledger.start_with?("#{File.join(root, 'session_cleanup_archive')}/") && data['records'].is_a?(Array)
797
809
  operation = 'learning_migration' if ledger.start_with?("#{File.join(root, 'learning', 'migrations')}/") && data['version'] == 1 && data['before_sha256'] == File.basename(ledger, '.json')
798
810
  operation = 'session_cleanup' if ledger.start_with?("#{File.join(root, 'session_cleanup_archive')}/") && data['dry_run'] == false && data['records'].is_a?(Array) && data['deleted_ids'].is_a?(Array)
@@ -841,7 +853,7 @@ module PWN
841
853
  end
842
854
  end
843
855
  root = File.join(Dir.home, '.pwn', 'maintenance', 'quarantine')
844
- raise IOError, 'Unsafe recovery ledger' unless ledger.start_with?("#{root}/") && File.realpath(ledger) == ledger
856
+ raise IOError, 'Unsafe recovery ledger' unless [root, File.join(Dir.home, '.pwn', 'backup')].any? { |base| ledger.start_with?("#{base}/") } && File.realpath(ledger) == ledger
845
857
 
846
858
  data = JSON.parse(File.binread(ledger))
847
859
  raise IOError, 'Unknown recovery ledger' unless data['operation'] == 'routine' && data['records'].is_a?(Array)
@@ -851,7 +863,7 @@ module PWN
851
863
  data['records'].each do |row|
852
864
  path = row.fetch('path')
853
865
  category = row.fetch('category')
854
- target_root = File.join(Dir.home, '.pwn', category)
866
+ target_root = disposable_directory(category)
855
867
  backup_path = row.fetch('backup')
856
868
  raise IOError, 'Unsafe recovery path' unless %w[cache tmp logs].include?(category) && path.start_with?("#{target_root}/") && File.expand_path(path) == path && File.realpath(File.dirname(path)) == File.dirname(path)
857
869
  raise IOError, 'Unsafe recovery backup' unless File.dirname(backup_path) == File.dirname(ledger) && File.realpath(backup_path) == backup_path && Digest::SHA256.file(backup_path).hexdigest == row['sha256']
@@ -1021,7 +1033,7 @@ module PWN
1021
1033
  File.open(PWN::Sessions.support_path(kind: :lease, session_id: id), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lease|
1022
1034
  raise IOError, 'Swarm continuation active' unless lease.flock(File::LOCK_EX | File::LOCK_NB)
1023
1035
 
1024
- directory = private_directory(File.join(Dir.home, '.pwn', 'maintenance', 'retirements', SecureRandom.hex(12)))
1036
+ directory = PWN::Backups.directory(label: 'continuation')
1025
1037
  backup_path = File.join(directory, 'personas.json')
1026
1038
  publish(backup_path, bytes)
1027
1039
  raise IOError, 'Continuation backup mismatch' unless File.binread(backup_path) == bytes
@@ -1141,7 +1153,7 @@ module PWN
1141
1153
  def self.publish(path, bytes)
1142
1154
  temporary = "#{path}.#{SecureRandom.hex(8)}.tmp"
1143
1155
  root = File.join(Dir.home, '.pwn')
1144
- category = %w[cache tmp].find { |name| path.start_with?("#{File.join(root, name)}/") }
1156
+ category = %w[cache tmp].find { |name| path.start_with?("#{disposable_directory(name)}/") }
1145
1157
  if category
1146
1158
  directory = private_directory(File.join(root, 'maintenance', 'atomic'))
1147
1159
  journal = File.join(directory, "#{SecureRandom.hex(12)}.json")
@@ -1183,7 +1195,7 @@ module PWN
1183
1195
  next if days.nil? || (expired_only && row.fetch('created_at') > Time.now.to_i - (days * 86_400))
1184
1196
 
1185
1197
  path, target = row.values_at('path', 'target')
1186
- base = "#{File.join(root, row['category'])}/"
1198
+ base = "#{disposable_directory(row['category'])}/"
1187
1199
  raise IOError, 'Unsafe atomic path' unless [path, target].all? { |name| name.is_a?(String) && name.start_with?(base) && File.expand_path(name) == name } && File.dirname(path) == File.dirname(target) && path.match?(/\A#{Regexp.escape(target)}\.[a-f0-9]{16}\.tmp\z/)
1188
1200
  next unless File.exist?(path) || File.symlink?(path)
1189
1201
  raise IOError, 'Changed atomic bytes' unless verified_backup?(path, row['sha256']) && File.stat(path).ino == row['inode'] && File.stat(path).dev == row['device']
@@ -1210,16 +1222,7 @@ module PWN
1210
1222
  end
1211
1223
 
1212
1224
  def self.backup(path, expected, result)
1213
- unless result[:archive_dir]
1214
- base = File.join(Dir.home, '.pwn', 'session_cleanup_archive')
1215
- raise 'Unsafe archive directory' if File.symlink?(base)
1216
-
1217
- FileUtils.mkdir_p(base, mode: 0o700)
1218
- raise 'Unsafe archive permissions' unless File.stat(base).uid == Process.uid && (File.stat(base).mode & 0o777) == 0o700
1219
-
1220
- result[:archive_dir] = File.join(base, "#{Time.now.utc.strftime('%Y%m%dT%H%M%S')}-#{SecureRandom.hex(8)}")
1221
- Dir.mkdir(result[:archive_dir], 0o700)
1222
- end
1225
+ result[:archive_dir] ||= PWN::Backups.directory(label: 'session-cleanup')
1223
1226
  expected.each do |name, record|
1224
1227
  next unless record
1225
1228
 
@@ -1273,6 +1276,9 @@ module PWN
1273
1276
  locks = []
1274
1277
  begin
1275
1278
  paths.each do |path|
1279
+ owner = Thread.current[:pwn_learning_write_lock]
1280
+ next if File.basename(path) == '.learning-write.lock' && owner && owner[:pid] == Process.pid && owner[:thread] == Thread.current
1281
+
1276
1282
  lock = File.open(path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) # rubocop:disable Style/FileOpen -- closed together in ensure
1277
1283
  locks << lock
1278
1284
  raise 'Dependency writer active' unless lock.flock(File::LOCK_EX | File::LOCK_NB)
@@ -1327,7 +1333,7 @@ module PWN
1327
1333
  # Legacy writers block all mutations, including evidence migration.
1328
1334
  # One permanent inode serializes CLI, curses and cron maintenance writers.
1329
1335
  # Read-only previews never acquire a writer lock or create lock files.
1330
- def self.with_maintenance_lock(dry_run:)
1336
+ def self.with_maintenance_lock(dry_run:, &block)
1331
1337
  return yield if dry_run
1332
1338
 
1333
1339
  root = PWN::Sessions.sessions_dir
@@ -1342,8 +1348,16 @@ module PWN
1342
1348
 
1343
1349
  begin
1344
1350
  Thread.current[:pwn_session_maintenance_lock] = { pid: Process.pid, thread: Thread.current, path: path, file: file }
1345
- yield
1351
+ learning = PWN::AI::Agent::Learning.send(:learning_lock, nonblocking: true)
1352
+ if learning == :busy
1353
+ Thread.current[:pwn_backup_learning_busy] = true
1354
+ yield
1355
+ else
1356
+ PWN::Backups.transaction(&block)
1357
+ end
1346
1358
  ensure
1359
+ PWN::AI::Agent::Learning.send(:learning_unlock, lock: learning) unless learning == :busy
1360
+ Thread.current[:pwn_backup_learning_busy] = nil
1347
1361
  Thread.current[:pwn_session_maintenance_lock] = owner
1348
1362
  end
1349
1363
  end
@@ -1452,6 +1466,8 @@ module PWN
1452
1466
 
1453
1467
  def self.maintenance_transaction(dry_run:, current_session_id:, proc_root:)
1454
1468
  preflight = sweep(dry_run: true, current_session_id: current_session_id, proc_root: proc_root)
1469
+ return preflight.merge(apply_blocked: true, evidence_migration: { deferred: 'learning_writer_active', migrated_rows: 0 }) if Thread.current[:pwn_backup_learning_busy] && !dry_run
1470
+
1455
1471
  blocked = stopping? || !preflight[:legacy_owners].empty? || Array(preflight[:unresolved_store_remediation]).any?
1456
1472
  unless dry_run || blocked
1457
1473
  # Private metadata-only preflight survives an interrupted migration.